UNPKG

matterbridge-shelly

Version:
653 lines (652 loc) 38.2 kB
import { AnsiLogger, BLUE, CYAN, MAGENTA, RESET, db, debugStringify, er, hk, nf, wr, zb } from 'matterbridge/logger'; import coap, { parameters } from 'coap'; import EventEmitter from 'node:events'; import path from 'node:path'; import { promises as fs } from 'node:fs'; import { ShellyDevice } from './shellyDevice.js'; const COIOT_OPTION_GLOBAL_DEVID = '3332'; const COIOT_OPTION_STATUS_VALIDITY = '3412'; const COIOT_OPTION_STATUS_SERIAL = '3420'; const COAP_MULTICAST_ADDRESS = '224.0.1.187'; export class CoapServer extends EventEmitter { log; shelly; coapServer; _isListening = false; _isReady = false; devices = new Map(); deviceSerial = new Map(); deviceId = new Map(); _dataPath = 'temp'; constructor(shelly, logLevel = "info") { super(); this.shelly = shelly; this.log = new AnsiLogger({ logName: 'ShellyCoapServer', logTimestampFormat: 4, logLevel }); parameters.maxRetransmit = 3; if (parameters.refreshTiming) parameters.refreshTiming(); this.registerShellyOptions(); } emit(eventName, ...args) { return super.emit(eventName, ...args); } on(eventName, listener) { return super.on(eventName, listener); } set dataPath(path) { this._dataPath = path; } get isListening() { return this._isListening; } get isReady() { return this._isReady; } async getDeviceDescription(host, id) { this.log.debug(`Requesting CoIoT (coap) device description from ${hk}${id}${db} host ${zb}${host}${db}...`); return new Promise((resolve) => { coap .request({ host, method: 'GET', pathname: '/cit/d', retrySend: 0, }) .on('response', (msg) => { this.log.debug(`CoIoT (coap) received device description ("/cit/d") code ${BLUE}${msg.code}${db} url ${BLUE}${msg.url}${db} rsinfo ${debugStringify(msg.rsinfo)}:`); msg.url = '/cit/d'; this.parseShellyMessage(msg); resolve(msg); }) .on('timeout', (err) => { this.log.warn(`CoIoT (coap) timeout requesting device description ("/cit/d") from ${hk}${id}${wr} host ${zb}${host}${wr}: ${err instanceof Error ? err.message : err}`); resolve(null); }) .on('error', (err) => { this.log.warn(`CoIoT (coap) error requesting device description ("/cit/d") from ${hk}${id}${wr} host ${zb}${host}${wr}: ${err instanceof Error ? err.message : err}`); resolve(null); }) .end(); this.log.debug(`Sent CoIoT (coap) device description request to ${hk}${id}${db} host ${zb}${host}${db}.`); }); } async getDeviceStatus(host, id) { this.log.debug(`Requesting CoIoT (coap) device status from ${hk}${id}${db} host ${zb}${host}${db}...`); return new Promise((resolve) => { coap .request({ host, method: 'GET', pathname: '/cit/s', }) .on('response', (msg) => { this.log.debug(`CoIoT (coap) received device status ("/cit/s") code ${BLUE}${msg.code}${db} url ${BLUE}${msg.url}${db} rsinfo ${debugStringify(msg.rsinfo)}:`); this.parseShellyMessage(msg); resolve(msg); }) .on('timeout', (err) => { this.log.warn(`CoIoT (coap) timeout requesting device status ("/cit/s") from ${hk}${id}${wr} host ${zb}${host}${wr}: ${err instanceof Error ? err.message : err}`); resolve(null); }) .on('error', (err) => { this.log.warn(`CoIoT (coap) error requesting device status ("/cit/s") from ${hk}${id}${wr} host ${zb}${host}${wr}: ${err instanceof Error ? err.message : err}`); resolve(null); }) .end(); this.log.debug(`Sent CoIoT (coap) device status request to ${hk}${id}${db} host ${zb}${host}${db}.`); }); } async getMulticastDeviceStatus(timeout = 60) { this.log.debug('Requesting CoIoT (coap) multicast device status...'); return new Promise((resolve, reject) => { this.log.debug('Sending CoAP multicast request...'); const response = coap .request({ host: COAP_MULTICAST_ADDRESS, method: 'GET', pathname: '/cit/s', multicast: true, multicastTimeout: timeout * 1000, }) .on('response', (msg) => { this.log.debug(`Multicast device status code ${BLUE}${msg.code}${db} url ${BLUE}${msg.url}${db} rsinfo ${debugStringify(msg.rsinfo)}:`); this.parseShellyMessage(msg); resolve(msg); }) .on('timeout', (err) => { this.log.warn('CoIoT (coap) timeout requesting multicast device status ("/cit/s"):', err instanceof Error ? err.message : err); resolve(null); }) .on('error', (err) => { this.log.warn('CoIoT (coap) error requesting multicast device status ("/cit/s"):', err instanceof Error ? err.message : err); resolve(null); }) .end(); this.log.debug('Sent CoIoT (coap) multicast device status request'); }); } registerShellyOptions() { coap.registerOption(COIOT_OPTION_GLOBAL_DEVID, (str) => { this.log.debug('GLOBAL_DEVID str', str); if (typeof str === 'string' || (str && typeof str.toString === 'function')) { return Buffer.from(str.toString()); } throw new TypeError('Expected a string for GLOBAL_DEVID'); }, (buf) => buf.toString()); coap.registerOption(COIOT_OPTION_STATUS_VALIDITY, (str) => { this.log.debug('STATUS_VALIDITY str', str); if (typeof str === 'string') { const buffer = Buffer.alloc(2); buffer.writeUInt16LE(parseInt(str, 10), 0); return buffer; } throw new TypeError('Expected a string for STATUS_VALIDITY'); }, (buf) => buf.readUInt16LE(0)); coap.registerOption(COIOT_OPTION_STATUS_SERIAL, (str) => { this.log.debug('STATUS_SERIAL str', str); if (typeof str === 'string') { const buffer = Buffer.alloc(2); buffer.writeUInt16LE(parseInt(str, 10), 0); return buffer; } throw new TypeError('Expected a string for STATUS_SERIAL'); }, (buf) => buf.readUInt16LE(0)); } parseShellyMessage(msg) { if (!this.deviceId.get(msg.rsinfo.address)) return; this.log.debug(`Parsing CoIoT (coap) response from device ${hk}${this.deviceId.get(msg.rsinfo.address)}${db} host ${zb}${msg.rsinfo.address}${db}...`); const host = msg.rsinfo.address; const headers = msg.headers; const code = msg.code; const url = msg.url; let deviceModel = ''; let deviceMac = ''; let protocolRevision = ''; let validity = 0; let validFor = 0; let serial = 0; let payload; if (headers[COIOT_OPTION_GLOBAL_DEVID]) { const parts = headers[COIOT_OPTION_GLOBAL_DEVID].split('#'); deviceModel = parts[0]; deviceMac = parts[1]; protocolRevision = parts[2]; } if (headers[COIOT_OPTION_STATUS_VALIDITY]) { validity = headers[COIOT_OPTION_STATUS_VALIDITY]; if ((validity & 1) === 0) { validFor = Math.floor(validity / 10); } else { validFor = validity * 4; } } if (headers[COIOT_OPTION_STATUS_SERIAL]) { serial = headers[COIOT_OPTION_STATUS_SERIAL]; } if (url === '/cit/s' && this.deviceSerial.get(host) === serial && !['SHDW-1', 'SHDW-2'].includes(deviceModel)) { this.log.debug(`No updates (serial not changed) for device ${hk}${this.deviceId.get(host)}${db} host ${zb}${host}${db}`); return; } try { payload = JSON.parse(msg.payload.toString()); } catch { payload = msg.payload.toString(); } this.log.debug(`url: ${CYAN}${url}${db}`); this.log.debug(`code: ${CYAN}${code}${db}`); this.log.debug(`host: ${CYAN}${host}${db}`); this.log.debug(`deviceId: ${CYAN}${this.deviceId.get(host)}${db}`); this.log.debug(`deviceModel: ${CYAN}${deviceModel}${db}`); this.log.debug(`deviceMac: ${CYAN}${deviceMac}${db}`); this.log.debug(`protocolRevision: ${CYAN}${protocolRevision}${db}`); this.log.debug(`validFor (${validity}): ${CYAN}${validFor}${db} seconds`); this.log.debug(`serial (${this.deviceSerial.get(host) === serial ? 'not changed' : 'updated'}): ${CYAN}${serial}${db}`); this.log.debug(`payload:${RESET}\n`, payload); if (msg.url === '/cit/d') { try { if (this.log.logLevel === "debug") this.saveResponse(deviceModel + '-' + deviceMac + '.coap.citd.json', payload); } catch { } const desc = []; this.log.debug(`parsing ${MAGENTA}blocks${db}:`); const blk = payload.blk; if (!blk) { this.log.error(`No blk found for host ${zb}${host}${er} in ${msg.url}`); return; } blk.forEach((b) => { this.log.debug(`- block: ${CYAN}${b.I}${db} description ${CYAN}${b.D}${db}`); const sen = payload.sen; sen .filter((s) => s.L === b.I) .forEach((s) => { this.log.debug(` - id: ${CYAN}${s.I}${db} type ${CYAN}${s.T}${db} description ${CYAN}${s.D}${db} unit ${CYAN}${s.U}${db} range ${CYAN}${s.R}${db} block ${CYAN}${s.L}${db}`); if (s.D === 'mode') desc.push({ id: s.I, component: 'sys', property: 'profile', range: s.R }); if (s.D === 'deviceTemp' && s.U !== 'F' && b.D === 'device') desc.push({ id: s.I, component: 'sys', property: 'temperature', range: s.R }); if (s.D === 'overtemp' && b.D === 'device') desc.push({ id: s.I, component: 'sys', property: 'overtemperature', range: s.R }); if (s.D === 'voltage' && b.D === 'device') desc.push({ id: s.I, component: 'sys', property: 'voltage', range: s.R }); if (s.D === 'cfgChanged' && b.D === 'device') desc.push({ id: s.I, component: 'sys', property: 'cfg_rev', range: s.R }); if (s.D === 'wakeupEvent' && b.D === 'device') desc.push({ id: s.I, component: 'sys', property: 'act_reasons', range: s.R }); if (s.D === 'output') desc.push({ id: s.I, component: b.D.replace('_', ':'), property: 'state', range: s.R }); if (s.D === 'brightness') desc.push({ id: s.I, component: b.D.replace('_', ':'), property: 'brightness', range: s.R }); if (s.D === 'gain') desc.push({ id: s.I, component: b.D.replace('_', ':'), property: 'gain', range: s.R }); if (s.D === 'red') desc.push({ id: s.I, component: b.D.replace('_', ':'), property: 'red', range: s.R }); if (s.D === 'green') desc.push({ id: s.I, component: b.D.replace('_', ':'), property: 'green', range: s.R }); if (s.D === 'blue') desc.push({ id: s.I, component: b.D.replace('_', ':'), property: 'blue', range: s.R }); if (s.D === 'white') desc.push({ id: s.I, component: b.D.replace('_', ':'), property: 'white', range: s.R }); if (s.D === 'whiteLevel') desc.push({ id: s.I, component: b.D.replace('_', ':'), property: 'white', range: s.R }); if (s.D === 'colorTemp') desc.push({ id: s.I, component: b.D.replace('_', ':'), property: 'temp', range: s.R }); if (s.D === 'power' && b.D.startsWith('light')) desc.push({ id: s.I, component: 'meter:0', property: 'power', range: s.R }); if (s.D === 'energy' && b.D.startsWith('light')) desc.push({ id: s.I, component: 'meter:0', property: 'total', range: s.R }); if (s.D === 'power' && b.D.startsWith('relay')) desc.push({ id: s.I, component: b.D.replace('_', ':').replace('relay', 'meter'), property: 'power', range: s.R }); if (s.D === 'energy' && b.D.startsWith('relay')) desc.push({ id: s.I, component: b.D.replace('_', ':').replace('relay', 'meter'), property: 'total', range: s.R }); if (s.D === 'roller') desc.push({ id: s.I, component: b.D.replace('_', ':'), property: 'state', range: s.R }); if (s.D === 'rollerPos') desc.push({ id: s.I, component: b.D.replace('_', ':'), property: 'current_pos', range: s.R }); if (s.D === 'rollerStopReason') desc.push({ id: s.I, component: b.D.replace('_', ':'), property: 'stop_reason', range: s.R }); if (s.D === 'rollerPower') desc.push({ id: s.I, component: 'meter:0', property: 'power', range: s.R }); if (s.D === 'rollerEnergy') desc.push({ id: s.I, component: 'meter:0', property: 'total', range: s.R }); if (s.D === 'voltage' && b.D.startsWith('emeter')) desc.push({ id: s.I, component: b.D.replace('_', ':'), property: 'voltage', range: s.R }); if (s.D === 'power' && b.D.startsWith('emeter')) desc.push({ id: s.I, component: b.D.replace('_', ':'), property: 'power', range: s.R }); if (s.D === 'energy' && b.D.startsWith('emeter')) desc.push({ id: s.I, component: b.D.replace('_', ':'), property: 'total', range: s.R }); if (s.D === 'current' && b.D.startsWith('emeter')) desc.push({ id: s.I, component: b.D.replace('_', ':'), property: 'current', range: s.R }); if (s.D === 'inputEvent' && b.D.startsWith('sensor')) desc.push({ id: s.I, component: b.D.replace('_', ':').replace('sensor', 'input'), property: 'event', range: s.R }); if (s.D === 'inputEventCnt' && b.D.startsWith('sensor')) desc.push({ id: s.I, component: b.D.replace('_', ':').replace('sensor', 'input'), property: 'event_cnt', range: s.R }); if (s.D === 'motion' && b.D.startsWith('sensor')) desc.push({ id: s.I, component: 'sensor', property: 'motion', range: ['0/1', '-1'] }); if (s.D === 'dwIsOpened' && b.D.startsWith('sensor')) desc.push({ id: s.I, component: 'sensor', property: 'contact_open', range: ['0/1', '-1'] }); if (s.D === 'vibration' && b.D.startsWith('sensor')) desc.push({ id: s.I, component: 'vibration', property: 'vibration', range: ['0/1', '-1'] }); if (s.D === 'luminosity' && b.D.startsWith('sensor')) desc.push({ id: s.I, component: 'lux', property: 'value', range: ['U32', '-1'] }); if (s.D === 'flood' && b.D.startsWith('sensor')) desc.push({ id: s.I, component: 'flood', property: 'flood', range: ['0/1', '-1'] }); if (s.D === 'extTemp' && s.U === 'C' && b.D.startsWith('sensor')) desc.push({ id: s.I, component: 'temperature', property: 'tC', range: s.R }); if (s.D === 'extTemp' && s.U === 'F' && b.D.startsWith('sensor')) desc.push({ id: s.I, component: 'temperature', property: 'tF', range: s.R }); if (s.D === 'humidity' && b.D.startsWith('sensor')) desc.push({ id: s.I, component: 'humidity', property: 'value', range: s.R }); if (s.D === 'sensorOp' && b.D.startsWith('sensor')) desc.push({ id: s.I, component: 'gas', property: 'sensor_state', range: s.R }); if (s.D === 'gas' && b.D.startsWith('sensor')) desc.push({ id: s.I, component: 'gas', property: 'alarm_state', range: s.R }); if (s.D === 'concentration' && b.D.startsWith('sensor')) desc.push({ id: s.I, component: 'gas', property: 'ppm', range: s.R }); if (s.D === 'sensorError' && b.D.startsWith('sensor')) desc.push({ id: s.I, component: 'sys', property: 'sensor_error', range: s.R }); if (s.D === 'battery' && b.D === 'device') desc.push({ id: s.I, component: 'battery', property: 'level', range: s.R }); if (s.D === 'charger' && b.D === 'device') desc.push({ id: s.I, component: 'battery', property: 'charging', range: s.R }); }); }); this.log.debug(`parsing ${MAGENTA}decoding${db}:`); desc.forEach((d) => { this.log.debug(`- id ${CYAN}${d.id}${db} component ${CYAN}${d.component}${db} property ${CYAN}${d.property}${db} range ${CYAN}${d.range}${db}`); }); this.devices.set(host, desc); } if (msg.url === '/cit/s') { try { if (this.log.logLevel === "debug") this.saveResponse(deviceModel + '-' + deviceMac + '.coap.cits.json', payload); } catch { } this.deviceSerial.set(host, serial); const descriptions = this.devices.get(host) || []; if (!descriptions || descriptions.length === 0) { if (deviceModel === 'SHDW-1' || deviceModel === 'SHDW-2') { this.log.debug(`*Set coap descriptions for host ${zb}${host}${db} deviceType ${CYAN}SHDW-1/SHDW-2${db}`); descriptions.push({ id: 9102, component: 'sys', property: 'act_reasons', range: ['battery/button/periodic/poweron/sensor/alarm', 'unknown'] }); descriptions.push({ id: 9103, component: 'sys', property: 'cfg_rev', range: 'U16' }); descriptions.push({ id: 3111, component: 'battery', property: 'level', range: ['0/100', '-1'] }); descriptions.push({ id: 3108, component: 'sensor', property: 'contact_open', range: ['0/1', '-1'] }); descriptions.push({ id: 6110, component: 'vibration', property: 'vibration', range: ['0/1', '-1'] }); descriptions.push({ id: 3106, component: 'lux', property: 'value', range: ['U32', '-1'] }); descriptions.push({ id: 3101, component: 'temperature', property: 'value', range: ['-55/125', '999'] }); this.devices.set(host, descriptions); } else if (deviceModel === 'SHTRV-01') { this.log.debug(`*Set coap descriptions for host ${zb}${host}${db} deviceType ${CYAN}SHTRV-01${db}`); descriptions.push({ id: 3111, component: 'battery', property: 'level', range: ['0/100', '-1'] }); descriptions.push({ id: 3103, component: 'thermostat:0', property: 'target_t.value', range: ['4/31', '999'] }); descriptions.push({ id: 3101, component: 'thermostat:0', property: 'tmp.value', range: ['-55/125', '999'] }); descriptions.push({ id: 9103, component: 'sys', property: 'cfg_rev', range: 'U16' }); this.devices.set(host, descriptions); } else if (deviceModel === 'SHBTN-1' || deviceModel === 'SHBTN-2') { this.log.debug(`*Set coap descriptions for host ${zb}${host}${db} deviceType ${CYAN}SHBTN-1/SHBTN-2${db}`); descriptions.push({ id: 3111, component: 'battery', property: 'level', range: ['0/100', '-1'] }); descriptions.push({ id: 2102, component: 'input:0', property: 'event', range: ['S/L/SS/SSS', ''] }); descriptions.push({ id: 2103, component: 'input:0', property: 'event_cnt', range: 'U16' }); this.devices.set(host, descriptions); } else if (deviceModel === 'SHMOS-01') { this.log.debug(`*Set coap descriptions for host ${zb}${host}${db} deviceType ${CYAN}SHMOS-01${db}`); descriptions.push({ id: 3111, component: 'battery', property: 'level', range: ['0/100', '-1'] }); descriptions.push({ id: 6107, component: 'sensor', property: 'motion', range: ['0/1', '-1'] }); descriptions.push({ id: 6110, component: 'vibration', property: 'vibration', range: ['0/1', '-1'] }); descriptions.push({ id: 3106, component: 'lux', property: 'value', range: ['U32', '-1'] }); descriptions.push({ id: 9103, component: 'sys', property: 'cfg_rev', range: 'U16' }); this.devices.set(host, descriptions); } else if (deviceModel === 'SHMOS-02') { this.log.debug(`*Set coap descriptions for host ${zb}${host}${db} deviceType ${CYAN}SHMOS-02${db}`); descriptions.push({ id: 3111, component: 'battery', property: 'level', range: ['0/100', '-1'] }); descriptions.push({ id: 6107, component: 'sensor', property: 'motion', range: ['0/1', '-1'] }); descriptions.push({ id: 6110, component: 'vibration', property: 'vibration', range: ['0/1', '-1'] }); descriptions.push({ id: 3106, component: 'lux', property: 'value', range: ['U32', '-1'] }); descriptions.push({ id: 3101, component: 'temperature', property: 'value', range: ['-55/125', '999'] }); descriptions.push({ id: 9103, component: 'sys', property: 'cfg_rev', range: 'U16' }); this.devices.set(host, descriptions); } else if (deviceModel === 'SHWT-1') { this.log.debug(`*Set coap descriptions for host ${zb}${host}${db} deviceType ${CYAN}SHWT-1${db}`); descriptions.push({ id: 9102, component: 'sys', property: 'act_reasons', range: ['battery/button/periodic/poweron/sensor/alarm', 'unknown'] }); descriptions.push({ id: 3115, component: 'sys', property: 'sensor_error', range: ['0/1'] }); descriptions.push({ id: 3111, component: 'battery', property: 'level', range: ['0/100', '-1'] }); descriptions.push({ id: 6106, component: 'flood', property: 'flood', range: ['0/1', '-1'] }); descriptions.push({ id: 3101, component: 'temperature', property: 'value', range: ['-55/125', '999'] }); this.devices.set(host, descriptions); } else if (deviceModel === 'SHHT-1') { this.log.debug(`*Set coap descriptions for host ${zb}${host}${db} deviceType ${CYAN}SHHT-1${db}`); descriptions.push({ id: 9102, component: 'sys', property: 'act_reasons', range: ['battery/button/periodic/poweron/sensor/alarm', 'unknown'] }); descriptions.push({ id: 3115, component: 'sys', property: 'sensor_error', range: ['0/1'] }); descriptions.push({ id: 3111, component: 'battery', property: 'level', range: ['0/100', '-1'] }); descriptions.push({ id: 3101, component: 'temperature', property: 'value', range: ['-55/125', '999'] }); descriptions.push({ id: 3103, component: 'humidity', property: 'value', range: ['0/100', '-1'] }); this.devices.set(host, descriptions); } else { this.log.info(`No coap description found for ${hk}${deviceModel}${nf} id ${hk}${this.deviceId.get(host)}${nf} host ${zb}${host}${nf} fetching it...`); this.getDeviceDescription(host, deviceModel); } } try { const values = payload.G?.map((v) => ({ channel: v[0], id: v[1], value: v[2], })); this.log.debug(`parsing ${MAGENTA}values${db} (${values.length}):`); values.forEach((v) => { const desc = descriptions.find((d) => d.id === v.id); if (desc) { this.log.debug(`- channel ${CYAN}${v.channel}${db} id ${CYAN}${v.id}${db} value ${CYAN}${v.value}${db} => ${CYAN}${desc.component}${db} ${CYAN}${desc.property}${db} ${CYAN}${desc.range === '0/1' ? v.value === 1 : v.value}${db}`); if (typeof desc.range === 'string' && desc.range === '0/1') { this.log.debug(`sending update for component ${CYAN}${desc.component}${db} property ${CYAN}${desc.property}${db} value ${CYAN}${v.value === 1}${db}`); this.emit('update', host, desc.component, desc.property, v.value === 1); } else if (Array.isArray(desc.range) && desc.range[0] === '0/1' && desc.range[1] === '-1') { this.log.debug(`sending update for component ${CYAN}${desc.component}${db} property ${CYAN}${desc.property}${db} value ${CYAN}${v.value === -1 ? null : v.value === 1}${db}`); this.emit('update', host, desc.component, desc.property, v.value === -1 ? null : v.value === 1); } else { this.log.debug(`sending update for component ${CYAN}${desc.component}${db} property ${CYAN}${desc.property}${db} value ${CYAN}${v.value}${db}`); if (desc.property.includes('.')) { const [property, subproperty] = desc.property.split('.'); this.emit('update', host, desc.component, property, { [subproperty]: v.value }); } else this.emit('update', host, desc.component, desc.property, v.value); } } else this.log.debug(`No coap description found for id ${v.id}`); }); } catch { this.log.warn(`Error parsing values for host ${zb}${host}${wr}`); } } } listenForStatusUpdates() { this.coapServer = coap.createServer({ multicastAddress: COAP_MULTICAST_ADDRESS, }); this.coapServer.on('error', (err) => { this.log.error('CoIoT (coap) server error:', err instanceof Error ? err.message : err); }); this.coapServer.on('warning', (err) => { this.log.warn('CoIoT (coap) server warning:', err instanceof Error ? err.message : err); }); this.coapServer.on('request', (msg, res) => { this.log.debug(`CoIoT (coap) server recevived a messagge code ${BLUE}${msg.code}${db} url ${BLUE}${msg.url}${db} rsinfo ${debugStringify(msg.rsinfo)}`); if (msg.code === '0.30' && msg.url === '/cit/s') { this.parseShellyMessage(msg); } else { this.log.debug(`Coap server got a wrong messagge code ${BLUE}${msg.code}${db} url ${BLUE}${msg.url}${db} rsinfo ${db}${debugStringify(msg.rsinfo)}...`); } }); this.coapServer.listen((err) => { if (err) { this.log.error(`CoIoT (coap) server error: ${err instanceof Error ? err.message : err}`); } else { this._isReady = true; this.log.info('CoIoT (coap) server is listening...'); } }); } async registerDevice(host, id, registerOnly) { this.deviceId.set(host, id); if (registerOnly) return; this.log.debug(`*Registering device ${hk}${id}${db} host ${zb}${host}${db} with fetch...`); ShellyDevice.fetch(this.shelly, this.log, host, 'cit/d').then((msg) => { if (msg && msg.blk && msg.sen) { const coapMessage = { rsinfo: { address: host, port: 5683, family: 'IPv4' }, headers: { [COIOT_OPTION_GLOBAL_DEVID]: `${id}#XXXXXXXXX#2`, [COIOT_OPTION_STATUS_VALIDITY]: 0, [COIOT_OPTION_STATUS_SERIAL]: 0 }, url: '/cit/d', payload: Buffer.from(JSON.stringify(msg)), code: '2.05', }; this.parseShellyMessage(coapMessage); this.log.debug(`*Registered CoIoT (coap) ${CYAN}/cit/d${db} for device ${hk}${id}${db} host ${zb}${host}${db} with fetch`); } else { this.log.debug(`****No response registering device ${hk}${id}${db} host ${zb}${host}${db} with fetch`); } }); } start() { if (this._isListening) return; this.log.info('Starting CoIoT (coap) server for shelly devices...'); this._isListening = true; this.listenForStatusUpdates(); this.log.info('Started CoIoT (coap) server for shelly devices.'); } stop() { this.log.info('Stopping CoIoT (coap) server for shelly devices...'); this.removeAllListeners(); this._isListening = false; if (this.coapServer) this.coapServer.close((err) => { this._isReady = false; this.log.debug(`CoIoT (coap) server closed${err ? ' with error ' + err.message : ''}.`); }); this.devices.clear(); this.log.info('Stopped CoIoT (coap) server for shelly devices.'); } async saveResponse(fileName, payload) { const responseFile = path.join(this._dataPath, `${fileName}`); try { await fs.writeFile(responseFile, JSON.stringify(payload, null, 2), 'utf8'); this.log.debug(`*Saved shellyId ${hk}${fileName}${db} coap response file ${CYAN}${responseFile}${db}`); return Promise.resolve(); } catch (err) { this.log.error(`Error saving shellyId ${hk}${fileName}${er} coap response file ${CYAN}${responseFile}${er}: ${err instanceof Error ? err.message : err}`); return Promise.reject(err); } } } const SHDW = { 'blk': [ { 'I': 1, 'D': 'sensor_0' }, { 'I': 2, 'D': 'device' }, ], 'sen': [ { 'I': 9103, 'T': 'EVC', 'D': 'cfgChanged', 'R': 'U16', 'L': 2 }, { 'I': 3108, 'T': 'S', 'D': 'dwIsOpened', 'R': ['0/1', '-1'], 'L': 1 }, { 'I': 3119, 'T': 'S', 'D': 'dwStateChanged', 'R': ['0/1', '-1'], 'L': 1 }, { 'I': 3109, 'T': 'S', 'D': 'tilt', 'U': 'deg', 'R': ['0/180', '-1'], 'L': 1 }, { 'I': 6110, 'T': 'A', 'D': 'vibration', 'R': ['0/1', '-1'], 'L': 1 }, { 'I': 3106, 'T': 'L', 'D': 'luminosity', 'U': 'lux', 'R': ['U32', '-1'], 'L': 1 }, { 'I': 3110, 'T': 'S', 'D': 'luminosityLevel', 'R': ['dark/twilight/bright', 'unknown'], 'L': 1 }, { 'I': 3101, 'T': 'T', 'D': 'extTemp', 'U': 'C', 'R': ['-55/125', '999'], 'L': 1 }, { 'I': 3102, 'T': 'T', 'D': 'extTemp', 'U': 'F', 'R': ['-67/257', '999'], 'L': 1 }, { 'I': 3115, 'T': 'S', 'D': 'sensorError', 'R': '0/1', 'L': 1 }, { 'I': 3111, 'T': 'B', 'D': 'battery', 'R': ['0/100', '-1'], 'L': 2 }, { 'I': 9102, 'T': 'EV', 'D': 'wakeupEvent', 'R': ['battery/button/periodic/poweron/sensor/alarm', 'unknown'], 'L': 2 }, ], }; const SHBTN2 = { 'blk': [ { 'I': 1, 'D': 'sensor_0' }, { 'I': 2, 'D': 'device' }, ], 'sen': [ { 'I': 9103, 'T': 'EVC', 'D': 'cfgChanged', 'R': 'U16', 'L': 2 }, { 'I': 2102, 'T': 'EV', 'D': 'inputEvent', 'R': ['S/L/SS/SSS', ''], 'L': 1 }, { 'I': 2103, 'T': 'EVC', 'D': 'inputEventCnt', 'R': 'U16', 'L': 1 }, { 'I': 3115, 'T': 'S', 'D': 'sensorError', 'R': '0/1', 'L': 1 }, { 'I': 3112, 'T': 'S', 'D': 'charger', 'R': ['0/1', '-1'], 'L': 2 }, { 'I': 3111, 'T': 'B', 'D': 'battery', 'R': ['0/100', '-1'], 'L': 2 }, { 'I': 9102, 'T': 'EV', 'D': 'wakeupEvent', 'R': ['battery/button/periodic/poweron/sensor/ext_power', 'unknown'], 'L': 2 }, ], }; const SHMOS01 = { 'blk': [ { 'I': 1, 'D': 'sensor_0' }, { 'I': 2, 'D': 'device' }, ], 'sen': [ { 'I': 6107, 'T': 'A', 'D': 'motion', 'R': ['0/1', '-1'], 'L': 1 }, { 'I': 3119, 'T': 'S', 'D': 'timestamp', 'U': 's', 'R': ['U32', '-1'], 'L': 1 }, { 'I': 3120, 'T': 'S', 'D': 'motionActive', 'R': ['0/1', '-1'], 'L': 1 }, { 'I': 6110, 'T': 'A', 'D': 'vibration', 'R': ['0/1', '-1'], 'L': 1 }, { 'I': 3106, 'T': 'L', 'D': 'luminosity', 'R': ['U32', '-1'], 'L': 1 }, { 'I': 3111, 'T': 'B', 'D': 'battery', 'R': ['0/100', '-1'], 'L': 2 }, { 'I': 9103, 'T': 'EVC', 'D': 'cfgChanged', 'R': 'U16', 'L': 2 }, ], }; const SHMOS02 = { 'blk': [ { 'I': 1, 'D': 'sensor_0' }, { 'I': 2, 'D': 'device' }, ], 'sen': [ { 'I': 3101, 'T': 'T', 'D': 'temp', 'U': 'C', 'R': ['-55/125', '999'], 'L': 1 }, { 'I': 3102, 'T': 'T', 'D': 'temp', 'U': 'F', 'R': ['-67/257', '999'], 'L': 1 }, { 'I': 6107, 'T': 'A', 'D': 'motion', 'R': ['0/1', '-1'], 'L': 1 }, { 'I': 3119, 'T': 'S', 'D': 'timestamp', 'U': 's', 'R': ['U32', '-1'], 'L': 1 }, { 'I': 3120, 'T': 'A', 'D': 'motionActive', 'R': ['0/1', '-1'], 'L': 1 }, { 'I': 6110, 'T': 'A', 'D': 'vibration', 'R': ['0/1', '-1'], 'L': 1 }, { 'I': 3106, 'T': 'L', 'D': 'luminosity', 'R': ['U32', '-1'], 'L': 1 }, { 'I': 3111, 'T': 'B', 'D': 'battery', 'R': ['0/100', '-1'], 'L': 2 }, { 'I': 9103, 'T': 'EVC', 'D': 'cfgChanged', 'R': 'U16', 'L': 2 }, ], }; const SHRGBWW01 = { 'blk': [ { 'I': 1, 'D': 'light_0' }, { 'I': 2, 'D': 'device' }, ], 'sen': [ { 'I': 9103, 'T': 'EVC', 'D': 'cfgChanged', 'R': 'U16', 'L': 2 }, { 'I': 1101, 'T': 'S', 'D': 'output', 'R': '0/1', 'L': 1 }, { 'I': 5105, 'T': 'S', 'D': 'red', 'R': '0/255', 'L': 1 }, { 'I': 5106, 'T': 'S', 'D': 'green', 'R': '0/255', 'L': 1 }, { 'I': 5107, 'T': 'S', 'D': 'blue', 'R': '0/255', 'L': 1 }, { 'I': 5108, 'T': 'S', 'D': 'white', 'R': '0/255', 'L': 1 }, { 'I': 5102, 'T': 'S', 'D': 'gain', 'R': '0/100', 'L': 1 }, { 'I': 5109, 'T': 'S', 'D': 'effect', 'R': '0/3', 'L': 1 }, { 'I': 4101, 'T': 'P', 'D': 'power', 'U': 'W', 'R': ['0/288', '-1'], 'L': 1 }, { 'I': 4103, 'T': 'E', 'D': 'energy', 'U': 'Wmin', 'R': ['U32', '-1'], 'L': 1 }, { 'I': 6102, 'T': 'A', 'D': 'overpower', 'R': ['0/1', '-1'], 'L': 1 }, { 'I': 2101, 'T': 'S', 'D': 'input', 'R': '0/1', 'L': 2 }, { 'I': 2102, 'T': 'EV', 'D': 'inputEvent', 'R': ['S/L', ''], 'L': 2 }, { 'I': 2103, 'T': 'EVC', 'D': 'inputEventCnt', 'R': 'U16', 'L': 2 }, { 'I': 9101, 'T': 'S', 'D': 'mode', 'R': 'color/white', 'L': 2 }, ], }; const SHRGBW2 = { 'blk': [ { 'I': 1, 'D': 'light_0' }, { 'I': 2, 'D': 'device' }, ], 'sen': [ { 'I': 9103, 'T': 'EVC', 'D': 'cfgChanged', 'R': 'U16', 'L': 2 }, { 'I': 1101, 'T': 'S', 'D': 'output', 'R': '0/1', 'L': 1 }, { 'I': 5105, 'T': 'S', 'D': 'red', 'R': '0/255', 'L': 1 }, { 'I': 5106, 'T': 'S', 'D': 'green', 'R': '0/255', 'L': 1 }, { 'I': 5107, 'T': 'S', 'D': 'blue', 'R': '0/255', 'L': 1 }, { 'I': 5108, 'T': 'S', 'D': 'white', 'R': '0/255', 'L': 1 }, { 'I': 5102, 'T': 'S', 'D': 'gain', 'R': '0/100', 'L': 1 }, { 'I': 5109, 'T': 'S', 'D': 'effect', 'R': '0/3', 'L': 1 }, { 'I': 4101, 'T': 'P', 'D': 'power', 'U': 'W', 'R': ['0/288', '-1'], 'L': 1 }, { 'I': 4103, 'T': 'E', 'D': 'energy', 'U': 'Wmin', 'R': ['U32', '-1'], 'L': 1 }, { 'I': 6102, 'T': 'A', 'D': 'overpower', 'R': ['0/1', '-1'], 'L': 1 }, { 'I': 2101, 'T': 'S', 'D': 'input', 'R': '0/1', 'L': 2 }, { 'I': 2102, 'T': 'EV', 'D': 'inputEvent', 'R': ['S/L', ''], 'L': 2 }, { 'I': 2103, 'T': 'EVC', 'D': 'inputEventCnt', 'R': 'U16', 'L': 2 }, { 'I': 9101, 'T': 'S', 'D': 'mode', 'R': 'color/white', 'L': 2 }, ], }; const SHHT1 = { 'blk': [ { 'I': 1, 'D': 'sensor_0' }, { 'I': 2, 'D': 'device' }, ], 'sen': [ { 'I': 9103, 'T': 'EVC', 'D': 'cfgChanged', 'R': 'U16', 'L': 2 }, { 'I': 3101, 'T': 'T', 'D': 'extTemp', 'U': 'C', 'R': ['-55/125', '999'], 'L': 1 }, { 'I': 3102, 'T': 'T', 'D': 'extTemp', 'U': 'F', 'R': ['-67/257', '999'], 'L': 1 }, { 'I': 3103, 'T': 'H', 'D': 'humidity', 'R': ['0/100', '999'], 'L': 1 }, { 'I': 3115, 'T': 'S', 'D': 'sensorError', 'R': '0/1', 'L': 1 }, { 'I': 3111, 'T': 'B', 'D': 'battery', 'R': ['0/100', '-1'], 'L': 2 }, { 'I': 9102, 'T': 'EV', 'D': 'wakeupEvent', 'R': ['battery/button/periodic/poweron/sensor/alarm', 'unknown'], 'L': 2 }, ], };