UNPKG

iobroker.ikettle2

Version:

Control your Smarter iKettle 2.0 with ioBroker.

370 lines (324 loc) 11.6 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("fs"); const socket = require('net'); let client, lastWaterLevel = [], lastWaterTemp = [], lastDataReceived = null; class Ikettle2 extends utils.Adapter { /** * @param {Partial<utils.AdapterOptions>} [options={}] */ 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 */ async onUnload(callback) { try { await this.setStateAsync(`${this.namespace}.info.connection`, {val: false, ack: true}); client.end( () => { callback(); }); } catch (e) { callback(); } } /** * Is called if a subscribed state changes * @param {string} id * @param {ioBroker.State | null | undefined} state */ 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={}] */ module.exports = (options) => new Ikettle2(options); } else { // otherwise start the instance directly new Ikettle2(); }