UNPKG

iobroker.signifylights

Version:
594 lines (499 loc) 22.3 kB
"use strict"; /* * Created with @iobroker/create-adapter v2.4.0 */ const utils = require("@iobroker/adapter-core"); // const objectHelper = require('@apollon/iobroker-tools').objectHelper; // Common adapter utils // const EventEmitter = require('events').EventEmitter; const uuid = require('uuid'); const ip = require("ip"); // const os = require("os"); const dns = require('dns'); const AllDeviceAttributes = require('./lib/AllDeviceAttributes.js'); // Load attribute library const ColorConv = require('./lib/colorconv.js'); // Load attribute library const dgram = require('dgram'); class Signifylights extends utils.Adapter { /** * @param {Partial<utils.AdapterOptions>} [options={}] */ constructor(options) { super({ ...options, name: "signifylights", }); this.HOST = ip.address();//'0.0.0.0'; this.PORTS = [38899, 38900]; // nobody knows if eth0 exists - skip this here and rely on user provided MAC // this.MAC = os.networkInterfaces()['eth0'][0]['mac'].replace(/:/g, '').toUpperCase(); //JSON.stringify(os.networkInterfaces());// this.IP = ip.address(); this.SOCKETS = {}; this.ISONLINE = {}; this.MESSAGEQUEUE = {}; this.ipmap = {}; this.maxAttempt = 10; this.sendTimeout = 1000; this.MESSAGEID = 1000; this.POLLER = null; this.timeoutList = {}; 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)); } async getIP(hostname) { const obj = await dns.promises.lookup(hostname).catch((error)=> { console.error(error); }); return obj?.address; } async open_udp_sockets() { for (const i in this.PORTS) { this.ISONLINE[this.PORTS[i]] = false; this.SOCKETS[this.PORTS[i]] = dgram.createSocket('udp4'); this.SOCKETS[this.PORTS[i]].bind(this.PORTS[i], this.HOST); this.SOCKETS[this.PORTS[i]].on('error', (err) => { this.log.debug(`server error:\n${err.stack}`); this.ISONLINE[this.PORTS[i]] = false; this.SOCKETS[this.PORTS[i]].close(); }); this.SOCKETS[this.PORTS[i]].on('message', (msg, client) => { this.ISONLINE[this.PORTS[i]] = true; //this.log.debug(`server got: ${msg} from ${client.address}:${client.port}`); this.WIZ__RECEIVE_MESSAGE(msg, client); }); this.SOCKETS[this.PORTS[i]].on('listening', () => { this.ISONLINE[this.PORTS[i]] = true; // const address = this.SOCKETS[this.PORTS[i]].address(); // this.log.debug(`server listening ${address.address}:${address.port}`); }); } } WIZ__RECEIVE_MESSAGE(msg, client) { const that = this; // QUEUE löschen try { msg = JSON.parse(msg); } catch (err) { this.log.error(err); } let objid = client.address; if (client.address in this.ipmap) { objid = this.ipmap[client.address]; } if ('result' in msg) { if (objid in this.MESSAGEQUEUE) { //this.log.debug(JSON.stringify(client)); for (const queueID in this.MESSAGEQUEUE[objid]) { const data = this.MESSAGEQUEUE[objid][queueID]; if (msg.method == data.message.method && client.port == data.port) { if (msg.method == 'getPilot' && ( msg.id == data.message.id || data.message.id == 0 ) ) { delete this.MESSAGEQUEUE[objid][queueID]; //this.log.debug(`[getPilot] ${client.address}:${client.port} success`); //msg.result = AllDeviceAttributes.override_with_null_not_exists() //Object.assign(AllDeviceAttributes.led_empty, msg.result); this.WIZ__UPDATE_STATES(objid, msg.result); } else if (msg.method == 'setPilot' && msg.id == data.message.id && msg.result.success == true ) { delete this.MESSAGEQUEUE[objid][queueID]; //this.log.debug(`[setPilot] ${client.address}:${client.port} success`); } else if (msg.method == 'getSystemConfig' && msg.id == data.message.id && 'result' in msg ) { delete this.MESSAGEQUEUE[objid][queueID]; //this.log.debug(`[getSystemConfig] ${client.address}:${client.port} success`); this.WIZ__UPDATE_STATES(objid, msg.result); } else if (msg.method == 'registration' && msg.id == data.message.id && 'result' in msg && msg.result.success == true ) { delete this.MESSAGEQUEUE[objid][queueID]; //this.log.debug(`[registration] ${client.address}:${client.port} success`); this.WIZ__UPDATE_STATES(objid, {'ip':client.address}); } } } } else { this.log.debug(`No QUEUE for Client ${client.address}:${client.port} found`); } } else if ('params' in msg && msg.method == 'syncPilot') { if (objid in this.MESSAGEQUEUE) { client.port = 38899; //this.log.debug(`[syncPilot] ${client.address}:${client.port} received`); this.WIZ__UPDATE_STATES(objid, msg.params); const message = new Buffer(`{"method":"syncPilot","result":{"mac":"${this.MAC}"}}`); //setTimeout(function() { that.SOCKETS[client.port].send(message, 0, message.length, client.port, client.address, (err) => { if (err) throw err; }); //that.log.debug(`[syncPilot] ${client.address}:${client.port} answerd`); //}, 5000); } } } async WIZ__UPDATE_STATES(ip, result){ try { const deviceId = ip.replace(/\./g, '_'); const convert = AllDeviceAttributes.conv_wiz_iob; result.ip = ip; if (!('online' in result)) { result.online = true; } for (const key in result) { if (key in convert) { if (['hsv','hsl','rgb','drvConf'].includes(key)) { result[key] = JSON.stringify(result[key]); } if (key == 'temp' && result[key] == 0) { // temp key 0 means unknown - which is invalid to set // skip it continue; } if (key =='online') { this.setState(deviceId+'.'+convert[key], {val: result[key], ack: true, expire: +this.config.listed_online}); } else { this.setState(deviceId+'.'+convert[key], {val: result[key], ack: true}); } } } } catch (err) { //this.log.debug(`__ERROR -> ${FUNCTION_NAME} [ ${ip} : ${name} ]`); this.log.error(err); } } WIZ__QUEUE_MESSAGE(method, id, params, ip, port) { const queueID = uuid.v4(); const data = { "ip": ip, "port": port, "attempt" : 0, "message": { "method": method, "id": id, "params": params }, "message_buffer": "" }; data['message_buffer'] = new Buffer(JSON.stringify(data.message)); try { this.MESSAGEQUEUE[ip][queueID] = data; this.WIZ__SEND_MESSAGE(ip, queueID, this); } catch(e) { this.log.warn("MSG QUEUE: IP: " + ip + "Error: " + e); } } async WIZ__SEND_MESSAGE(ip, queueID, that) { const realip = await that.getIP(ip); if (!realip) { that.log.error(`WIZ__SEND_MESSAGE: cannot find ip of ${ip}`); // by deleting the queue we skip steps below delete that.MESSAGEQUEUE[ip][queueID]; } if (ip in that.MESSAGEQUEUE && queueID in that.MESSAGEQUEUE[ip] && that.MESSAGEQUEUE[ip][queueID]['attempt'] < that.maxAttempt) { that.ipmap[realip] = ip; that.ipmap[ip] = realip; that.MESSAGEQUEUE[ip][queueID]['attempt'] = ++that.MESSAGEQUEUE[ip][queueID]['attempt']; that.log.debug(`Nachricht ${queueID} gesendet -> ${ip} ${realip} Versuch: ${that.MESSAGEQUEUE[ip][queueID]['attempt']}`); //that.log.warn(JSON.stringify(that.MESSAGEQUEUE[ip][queueID]['message'])) that.SOCKETS[that.MESSAGEQUEUE[ip][queueID]['port']].send(that.MESSAGEQUEUE[ip][queueID]['message_buffer'], 0, that.MESSAGEQUEUE[ip][queueID]['message_buffer'].length, that.MESSAGEQUEUE[ip][queueID]['port'], realip, (err) => { if (err) { that.log.warn(`Nachricht ${queueID} konnte nicht gesendet werden: ${err}`); } }); const timeoutid = that.setTimeout(function() { that.WIZ__SEND_MESSAGE(ip, queueID, that); delete that.timeoutList[timeoutid]; }, that.sendTimeout); if (!timeoutid) { that.log.error("ERROR ERROR NO timeoutid returned"); } that.timeoutList[timeoutid] = true; } else if (ip in that.MESSAGEQUEUE && queueID in that.MESSAGEQUEUE[ip] && that.MESSAGEQUEUE[ip][queueID]['attempt'] >= that.maxAttempt) { that.log.warn(`Nachricht ${queueID} ${ip} ${realip} hat keine Antwort erhalten`); delete that.MESSAGEQUEUE[ip][queueID]; that.WIZ__UPDATE_STATES(ip, {'online': false}); } } WIZ__GET_MESSAGEID() { const messageID = this.MESSAGEID; this.MESSAGEID = this.MESSAGEID + 1; if (this.MESSAGEID > 9999) { this.MESSAGEID = 1000; } return messageID; } WIZ__REGISTER(client_ip) { const that = this; this.WIZ__QUEUE_MESSAGE('registration',that.WIZ__GET_MESSAGEID(),{"phoneMac":this.MAC,"phoneIp":this.IP,"register":true},client_ip, 38899); } WIZ__GETPILOT(client_ip) { this.WIZ__QUEUE_MESSAGE('getPilot',0,{},client_ip, 38899); } WIZ__SETPILOT(client_ip, params) { this.WIZ__QUEUE_MESSAGE('setPilot',this.WIZ__GET_MESSAGEID(),params,client_ip, 38899); } WIZ__GETSYSTEMCONFIG(client_ip) { this.WIZ__QUEUE_MESSAGE('getSystemConfig',this.WIZ__GET_MESSAGEID(),{},client_ip, 38899); } WIZ__SET_STATE(client_ip, state) { this.WIZ__SETPILOT(client_ip,{'state':state}); } WIZ__SET_DIMMING(client_ip, state) { this.WIZ__SETPILOT(client_ip,{'dimming':state}); } WIZ__SET_COLORTEMP(client_ip, state) { // this.log.warn('colortemp '+client_ip+' '+state); this.WIZ__UPDATE_STATES(client_ip, {'sceneid':0}); this.WIZ__SETPILOT(client_ip,{'temp':state}); } async WIZ__SET_COLOR(client_ip) { const params = {}; params.r = await this.WIZ__GET_IOB_STATE(client_ip,'led.r'); params.g = await this.WIZ__GET_IOB_STATE(client_ip,'led.g'); params.b = await this.WIZ__GET_IOB_STATE(client_ip,'led.b'); params.w = await this.WIZ__GET_IOB_STATE(client_ip,'led.w'); params.c = await this.WIZ__GET_IOB_STATE(client_ip,'led.c'); for (const key in params) { if (params[key] == null ) { delete params[key]; } else { params[key] = params[key]['val']; } } const rgb = [params.r, params.g, params.b]; this.WIZ__SET_COLOR_RGB(client_ip, rgb); this.WIZ__UPDATE_STATES(client_ip, {'sceneid':0}); this.WIZ__SETPILOT(client_ip,params); } WIZ__SET_COLOR_HEX(client_ip, hex) { if (hex.startsWith('#')) { hex = hex.substring(1); } this.WIZ__SET_COLOR_RGB(client_ip,ColorConv.HEX2RGB(hex)); } WIZ__SET_COLOR_HSL(client_ip, hsl) { if (!Array.isArray(hsl)) { hsl = JSON.parse(hsl); } this.WIZ__SET_COLOR_RGB(client_ip,ColorConv.HSL2RGB(hsl[0],hsl[1],hsl[2])); } WIZ__SET_COLOR_HSV(client_ip, hsv) { if (!Array.isArray(hsv)) { hsv = JSON.parse(hsv); } this.WIZ__SET_COLOR_RGB(client_ip,ColorConv.HSV2RGB(hsv[0],hsv[1],hsv[2])); } WIZ__SET_COLOR_HUE(client_ip, hue) { this.WIZ__SET_COLOR_RGB(client_ip,ColorConv.HUE2RGB(hue)); } WIZ__SET_COLOR_RGB(client_ip, rgb) { if (!Array.isArray(rgb)) { rgb = JSON.parse(rgb); } const params = {'r':rgb[0],'g':rgb[1],'b':rgb[2]}; const hsv = ColorConv.RGB2HSV(params.r, params.g, params.b); const hsl = ColorConv.RGB2HSL(params.r, params.g, params.b); const hex = ColorConv.RGB2HEX(params.r, params.g, params.b); const hue = ColorConv.RGB2HUE(params.r, params.g, params.b); this.WIZ__UPDATE_STATES(client_ip, {'sceneid':0,'rgb':rgb,'hsv':hsv,'hsl':hsl,'hex':hex,'hue':hue,'r':rgb[0],'g':rgb[1],'b':rgb[2],'c':0,'w':0}); this.WIZ__SETPILOT(client_ip,params); } WIZ__SET_SPEED(client_ip, state) { this.WIZ__SETPILOT(client_ip,{'speed':state}); } WIZ__SET_SCENE(client_ip, state) { this.WIZ__SETPILOT(client_ip,{'sceneid':state}); } async WIZ__GET_IOB_STATE(ip, key) { const client_ip = ip.replace(/\./g, '_'); return await this.getStateAsync(client_ip+'.'+key); } /** * Is called when databases are connected and adapter received configuration. */ async onReady() { // const that = this; // Initialize your adapter here // Reset the connection indicator during startup this.setState("info.connection", false, true); if (!(this.config.bind_ip && this.config.bind_ip.length > 0)) { this.log.error("adapter is unconfigured, bind ip missing"); } else { this.HOST = this.config.bind_ip; this.log.info("config bind ip address: " + this.config.bind_ip); if (this.config.register_devices === true && !(this.config.udpip && this.config.udpip.length > 0 && this.config.udpmac && this.config.udpmac.length > 0)) { this.log.error("adapter is unconfigured, udp target ip and/or udp mac address for auto update missing"); } else { if (this.config.register_devices === true){ this.log.info("config register devices for auto update: enabled"); this.IP = this.config.udpip; this.log.info("config udp target IP for auto update: " + this.config.udpip); this.MAC = this.config.udpmac.replace(/:/g, '').toUpperCase(); this.log.info("config udp mac address for auto update: " + this.MAC); } await this.open_udp_sockets(); await this.WIZ__INIT_ALL_DEVICES(); this.setState('info.connection', true, true); } } } /** * Is called when adapter shuts down - callback has to be called under any circumstances! * @param {() => void} callback */ onUnload(callback) { try { for (const i in this.SOCKETS) { this.SOCKETS[i].close(); this.ISONLINE[i] = false; } if (this.POLLER) { clearInterval(this.POLLER); } for (const i in this.timeoutList) { clearTimeout(this.timeoutList[i]); } this.timeoutList = {}; callback(); } catch { callback(); } } /** * Is called if a subscribed state changes * @param {string} id * @param {ioBroker.State | null | undefined} state */ onStateChange(id, state) { if (state) { // The state was changed if (state.ack == false) { const state_name = id.split('.').slice(-2).join('.'); // eslint-disable-next-line no-unused-vars const client_ip = id.split('.').slice(2,3).join().replace(/_/g, '.'); // eslint-disable-next-line no-unused-vars const state_value = state.val; eval(AllDeviceAttributes.get_on_function(state_name)); //this.log.info(`state ${state_name} changed: ${state.val} (ack = ${state.ack}) ${JSON.stringify(state)}`); } } else { // The state was deleted // ToDo: register again ???? //this.log.info(`state ${id} deleted`); } } async WIZ__INIT_ALL_DEVICES() { const FUNCTION_NAME = 'WIZ__INIT_ALL_DEVICES'; //this.log.debug(`_START -> ${FUNCTION_NAME}`); try { const devices = this.config.devices; let ip; let name; for (const k in devices) { if (devices[k].active == true) { ip = devices[k].ip; name = devices[k].name; await this.WIZ__INIT_DEVICE(ip, name); } } const deviceStates = AllDeviceAttributes.defaults; if (deviceStates) { for (const statename in deviceStates) { const state = deviceStates[statename]; if ('on' in state) { this.subscribeStates('*.'+statename); } } } //this.log.debug(`_END -> ${FUNCTION_NAME}`); } catch (err) { this.log.debug(`_ERROR -> ${FUNCTION_NAME}`); this.log.error(err); } } async WIZ__INIT_DEVICE(ip, name) { const FUNCTION_NAME = 'WIZ__INIT_DEVICES'; const that = this; this.log.debug(`__START -> ${FUNCTION_NAME} [ ${ip} : ${name} ]`); try { const deviceId = ip.replace(/\./g, '_'); this.MESSAGEQUEUE[ip] = {}; this.log.debug(`-> CREATE Device: ${deviceId}`); let deviceStates = AllDeviceAttributes.MINIMAL(); let deviceType = "MINIMAL"; const obj = await this.getStateAsync(deviceId+'.system.moduleName'); if (obj && obj.val.length > 5) { deviceType = obj.val; //this.log.warn(deviceType); } this.log.debug(`-> CREATE DeviceType: ${deviceType}`); if (deviceType == "MINIMAL") { // reschedule until we know device type... const timeoutid = that.setTimeout(function() { that.WIZ__INIT_DEVICE(ip, name); delete that.timeoutList[timeoutid]; }, 5000); if (!timeoutid) { that.log.error("ERROR ERROR NO ${timeoutid} returned"); } that.timeoutList[timeoutid] = true; } if (eval('typeof AllDeviceAttributes.'+deviceType+'() !== "undefined"')) { this.log.debug(`-> CREATE DeviceType: EVAL ${deviceType}`); deviceStates = eval('AllDeviceAttributes.'+deviceType+'()'); } if (deviceStates) { await this.extendObjectAsync( deviceId, { type: 'device', common: { name: `Device: ${name} - ${ip}` }, native: { ip: ip, name: name, mac: '' // ToDo: } }); for (const statename in deviceStates) { const state = deviceStates[statename]; const channelId = statename.split('.').slice(0, 1).join(); if (channelId !== statename) { //this.log.debug(`-> CREATE CHANNEL: ${deviceId}.${channelId}`); await this.extendObjectAsync( deviceId + '.' + channelId, { type: 'channel', common: { name: `Channel: ${channelId}` }, native: {} }); } //this.log.debug(`-> CREATE STATE: ${deviceId}.${statename}`); await this.extendObjectAsync( deviceId + '.' + statename, { type: 'state', common: state.common }); } } const reg = await this.WIZ__GET_IOB_STATE(ip,'system.register'); if (this.config.register_devices == true && reg !== null && reg.val == true ) { this.WIZ__REGISTER(ip); } this.WIZ__GETSYSTEMCONFIG(ip); this.WIZ__GETPILOT(ip); if (this.config.polling_intervall > 0) { this.POLLER = this.setInterval(this.WIZ__GETPILOT.bind(this), this.config.polling_intervall*1000, ip); } //this.log.debug(`__END -> ${FUNCTION_NAME} [ ${ip} : ${name} ]`); } catch (err) { this.log.debug(`__ERROR -> ${FUNCTION_NAME} [ ${ip} : ${name} ]`); this.log.error(err); } } } if (require.main !== module) { // Export the constructor in compact mode /** * @param {Partial<utils.AdapterOptions>} [options={}] */ module.exports = (options) => new Signifylights(options); } else { // otherwise start the instance directly new Signifylights(); }