UNPKG

iobroker.ikettle2

Version:

Control your Smarter iKettle 2.0 with ioBroker.

398 lines (358 loc) 12 kB
'use strict'; /* * Created with @iobroker/create-adapter v1.33.0 */ // The adapter-core module gives you access to the core ioBroker functions // you need to create an adapter const utils = require('@iobroker/adapter-core'); // Load your modules here, e.g.: // const fs = require("node:fs"); const socket = require('node:net'); let client, lastWaterLevel = [], lastWaterTemp = [], lastDataReceived = null; class Ikettle2 extends utils.Adapter { /** * @param {Partial<utils.AdapterOptions>} [options] - options object */ constructor(options) { super({ ...options, name: 'ikettle2', }); this.on('ready', this.onReady.bind(this)); this.on('stateChange', this.onStateChange.bind(this)); // this.on('objectChange', this.onObjectChange.bind(this)); // this.on('message', this.onMessage.bind(this)); this.on('unload', this.onUnload.bind(this)); } /** * Is called when databases are connected and adapter received configuration. */ async onReady() { this.subscribeStates('kettle.on'); this.subscribeStates('kettle.on_formula'); this.subscribeStates('kettle.calibrate'); this.subscribeStates('kettle.preset.set_preset'); client = new socket.Socket(); if (this.config.ip !== '') { client.connect({ port: 2081, host: this.config.ip }); } else { this.log.warn('No ip set in configuration! Please set the ip for the iKettle'); } client.on('ready', async () => { await this.setStateAsync(`${this.namespace}.info.connection`, { val: true, ack: true }); await this.getDeviceInfo(); await this.getPreset(); }); client.on('data', async data => { switch (data[0]) { case 0x14: await this.decodeStatus(data); /*if(data[4] > 200){ await this.calibrate(); }*/ break; } clearTimeout(lastDataReceived); lastDataReceived = setTimeout(() => { client.destroy(); client.connect({ port: 2081, host: this.config.ip }); }, 120000); }); client.on('timeout', async () => { await this.setStateAsync(`${this.namespace}.info.connection`, { val: false, ack: true }); client.destroy(); client.connect({ port: 2081, host: this.config.ip }); }); client.on('error', async error => { this.log.error(JSON.stringify(error)); client.destroy(); client.connect({ port: 2081, host: this.config.ip }); await this.setStateAsync(`${this.namespace}.info.connection`, { val: false, ack: true }); }); client.on('close', async () => { this.log.info('Connection closed by remote host'); await this.setStateAsync(`${this.namespace}.info.connection`, { val: false, ack: true }); }); client.on('end', async () => { this.log.info('Connection ended by remote host'); await this.setStateAsync(`${this.namespace}.info.connection`, { val: false, ack: true }); }); } /** * Is called when adapter shuts down - callback has to be called under any circumstances! * * @param {() => void} callback - callback function to indicate completion */ async onUnload(callback) { try { await this.setStateAsync(`${this.namespace}.info.connection`, { val: false, ack: true }); client.end(() => { callback(); }); // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (e) { callback(); } } /** * Is called if a subscribed state changes * * @param {string} id - id of the state that changed * @param {ioBroker.State | null | undefined} state - the state that changed */ async onStateChange(id, state) { if (state && state.ack === false) { // The state was changed this.log.info(`state ${id} changed: ${state.val} (ack = ${state.ack})`); switch (id) { case `${this.namespace}.kettle.calibrate`: await this.setStateAsync(`${this.namespace}.kettle.calibrate`, { val: state.val, ack: true }); if (state.val) { await this.calibrate(); } break; case `${this.namespace}.kettle.on`: await this.setStateAsync(`${this.namespace}.kettle.on`, { val: state.val, ack: true }); if (state.val) { await this.switchOn(); } else { await this.switchOff(); } break; case `${this.namespace}.kettle.on_formula`: await this.setStateAsync(`${this.namespace}.kettle.on_formula`, { val: state.val, ack: true }); if (state.val) { await this.switchOnFormula(); } else { await this.switchOff(); } break; case `${this.namespace}.kettle.preset.set_preset`: await this.setStateAsync(`${this.namespace}.kettle.preset.set_preset`, { val: state.val, ack: true, }); if (state.val) { await this.setPreset(); } break; case `${this.namespace}.kettle.preset.get_preset`: if (state.val) { await this.getPreset(); } break; } } else { // The state was deleted //this.log.info(`state ${id} deleted`); } } async decodeStatus(data) { let on = false, kettle = false, level = 0; if (data[1] === 0x01) { on = true; } if (data[3] === 0x08) { kettle = true; } let raw = data.slice(3, 5); raw = parseInt(raw.toString('hex'), 16); if (raw >= 2090) { if (lastWaterLevel.length < 5) { lastWaterLevel.push(raw); } else { const val = (lastWaterLevel[0] + lastWaterLevel[1] + lastWaterLevel[2] + lastWaterLevel[3] + lastWaterLevel[4]) / 5; const min = 2090, max = 2185, liter = 1.8; level = (liter / (max - min)) * (val - min); let result = Math.round(parseFloat(level.toFixed(1)) * 100) / 100; await this.setStateChangedAsync(`${this.namespace}.kettle.water_level`, { val: result, ack: true }); lastWaterLevel.splice(4, 1); lastWaterLevel.splice(0, 0, raw); } } if (lastWaterTemp.length < 3) { lastWaterTemp.push(data[2]); } else { const val = (lastWaterTemp[0] + lastWaterTemp[1] + lastWaterTemp[2]) / 3; const result = Math.round(parseFloat(val.toFixed(0)) * 100) / 100; await this.setStateChangedAsync(`${this.namespace}.kettle.water_temperature`, { val: result, ack: true }); lastWaterTemp.splice(2, 1); lastWaterTemp.splice(0, 0, data[2]); } await this.setStateChangedAsync(`${this.namespace}.kettle.on`, { val: on, ack: true }); await this.setStateChangedAsync(`${this.namespace}.kettle.on_formula`, { val: on, ack: true }); await this.setStateChangedAsync(`${this.namespace}.kettle.on_plate`, { val: kettle, ack: true }); } async calibrate() { const msg = Buffer.from([0x2c, 0x7e]); client.write(msg); const response = await this.waitForResponse(); if (response[1] === 0) { await this.setStateAsync(`${this.namespace}.kettle.calibrate`, { val: false, ack: true }); } else { await this.logError(response[1], 'Calibrate iKettle water level'); } } async switchOn() { const temp = await this.getStateAsync(`${this.namespace}.kettle.set_temperature`); const msg = Buffer.from([0x15, `0x${temp.val.toString(16)}`, 0x00, 0x7e]); client.write(msg); const response = await this.waitForResponse(); if (response[1] === 0) { await this.setStateAsync(`${this.namespace}.kettle.on`, { val: true, ack: true }); await this.setStateAsync(`${this.namespace}.kettle.set_temperature`, { val: temp.val, ack: true }); } else { await this.logError(response[1], 'Switch iKettle ON'); } } async switchOnFormula() { const temp = await this.getStateAsync(`${this.namespace}.kettle.formula_temperature`); const time = await this.getStateAsync(`${this.namespace}.kettle.warming_time`); if (time.val < 5 && time.val !== 0) { this.log.warn('Minimum warming time 5 minutes'); return; } if (time.val > 30) { this.log.warn('Maximum warming time 30 minutes'); return; } const msg = Buffer.from([0x19, `0x${temp.val.toString(16)}`, `0x${time.val.toString(16)}`, 0x7e]); console.log(msg); client.write(msg); const response = await this.waitForResponse(); if (response[1] === 0) { await this.setStateAsync(`${this.namespace}.kettle.on_formula`, { val: true, ack: true }); await this.setStateAsync(`${this.namespace}.kettle.formula_temperature`, { val: temp.val, ack: true }); await this.setStateAsync(`${this.namespace}.kettle.warming_time`, { val: time.val, ack: true }); } else { await this.logError(response[1], 'Switch iKettle ON formula'); } } async switchOff() { const msg = Buffer.from([0x16, 0x7e]); client.write(msg); const response = await this.waitForResponse(); if (response[1] === 0) { await this.setStateAsync(`${this.namespace}.kettle.on`, { val: false, ack: true }); } else { await this.logError(response[1], 'Switch iKettle OFF'); } } async setPreset() { const temp = await this.getStateAsync(`${this.namespace}.kettle.preset.temperature`); const formulaTemp = await this.getStateAsync(`${this.namespace}.kettle.preset.formula_temperature`); const warmingTime = await this.getStateAsync(`${this.namespace}.kettle.preset.warming_time`); const msg = Buffer.from([ 0x1f, `0x${warmingTime.val.toString(16)}`, `0x${temp.val.toString(16)}`, `0x${formulaTemp.val.toString(16)}`, 0x7e, ]); if (warmingTime.val < 5 && warmingTime.val !== 0) { this.log.warn('Minimum warming time 5 minutes'); return; } if (warmingTime.val > 30) { this.log.warn('Maximum warming time 30 minutes'); return; } client.write(msg); const response = await this.waitForResponse(); if (response[1] === 0) { await this.setStateAsync(`${this.namespace}.kettle.preset.set_preset`, { val: false, ack: true }); await this.setStateAsync(`${this.namespace}.kettle.preset.temperature`, { val: temp.val, ack: true }); await this.setStateAsync(`${this.namespace}.kettle.preset.formula_temperature`, { val: formulaTemp.val, ack: true, }); await this.setStateAsync(`${this.namespace}.kettle.preset.warming_time`, { val: warmingTime.val, ack: true, }); } else { await this.logError(response[1], 'Set presets for manual control'); } } async getPreset() { const msg = Buffer.from([0x2e, 0x7e]); client.write(msg); const response = await this.waitForResponse(); if (response[0] === 0x2f) { const temperature = response[1]; const warming_time = response[2]; const formula_temperature = response[3]; await this.setStateAsync(`${this.namespace}.kettle.preset.temperature`, { val: temperature, ack: true }); await this.setStateAsync(`${this.namespace}.kettle.preset.warming_time`, { val: warming_time, ack: true }); await this.setStateAsync(`${this.namespace}.kettle.preset.formula_temperature`, { val: formula_temperature, ack: true, }); await this.setStateAsync(`${this.namespace}.kettle.preset.get_preset`, { val: false, ack: true }); } } async getDeviceInfo() { const msg = Buffer.from([0x64, 0x7e]); client.write(msg); const response = await this.waitForResponse(); if (response[0] === 0x65) { const firmwareVersion = response[2]; await this.extendObjectAsync(`${this.namespace}.kettle`, { native: { firmwareVersion: firmwareVersion, }, }); } else { await this.logError(response[1], 'Get device info'); } } async getHistory() { const msg = Buffer.from([0x28, 0x7e]); client.write(msg); const response = await this.waitForResponse(); console.log(response); if (response[0] === 0x29) { // const counter = response[1]; //TODO: decode history } } waitForResponse() { return new Promise(resolve => { const listener = data => { resolve(data); client.off('data', listener); }; client.on('data', listener); }); } async logError(code, command) { const errorMessage = { 0x01: 'Busy and can not execute', 0x04: 'Command failed:', }; await this.log.error(`${errorMessage[code]} ${command}`); } } if (require.main !== module) { // Export the constructor in compact mode /** * @param {Partial<utils.AdapterOptions>} [options] - options object */ module.exports = options => new Ikettle2(options); } else { // otherwise start the instance directly new Ikettle2(); }