UNPKG

matterbridge-shelly

Version:
772 lines (771 loc) 41.7 kB
import { AnsiLogger, BLUE, CYAN, MAGENTA, RESET, db, debugStringify, er, hk, nf, wr, zb } from 'matterbridge/logger'; import coap, { parameters, globalAgent } 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; deviceDescription = new Map(); deviceSerial = new Map(); deviceValidityTimeout = 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) => { 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) => { 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) => { 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.parseDescription(payload); this.deviceDescription.set(host, desc); return desc; } if (msg.url === '/cit/s') { try { if (this.log.logLevel === "debug") this.saveResponse(this.deviceId.get(host) + '.coap.cits.json', payload); } catch { } this.deviceSerial.set(host, serial); let descriptions = this.deviceDescription.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}${deviceModel}${db}`); descriptions = this.parseDescription(SHDW_CITD); this.deviceDescription.set(host, descriptions); } else if (deviceModel === 'SHTRV-01') { this.log.debug(`*Set coap descriptions for host ${zb}${host}${db} deviceType ${CYAN}${deviceModel}${db}`); descriptions = this.parseDescription(SHTRV01_CITD, deviceModel); this.deviceDescription.set(host, descriptions); } else if (deviceModel === 'SHBTN-1' || deviceModel === 'SHBTN-2') { this.log.debug(`*Set coap descriptions for host ${zb}${host}${db} deviceType ${CYAN}${deviceModel}${db}`); descriptions = this.parseDescription(SHBTN_CITD); this.deviceDescription.set(host, descriptions); } else if (deviceModel === 'SHMOS-01') { this.log.debug(`*Set coap descriptions for host ${zb}${host}${db} deviceType ${CYAN}${deviceModel}${db}`); descriptions = this.parseDescription(SHMOS01_CITD); this.deviceDescription.set(host, descriptions); } else if (deviceModel === 'SHMOS-02') { this.log.debug(`*Set coap descriptions for host ${zb}${host}${db} deviceType ${CYAN}${deviceModel}${db}`); descriptions = this.parseDescription(SHMOS02_CITD); this.deviceDescription.set(host, descriptions); } else if (deviceModel === 'SHWT-1') { this.log.debug(`*Set coap descriptions for host ${zb}${host}${db} deviceType ${CYAN}${deviceModel}${db}`); descriptions = this.parseDescription(SHWT1_CITD); this.deviceDescription.set(host, descriptions); } else if (deviceModel === 'SHHT-1') { this.log.debug(`*Set coap descriptions for host ${zb}${host}${db} deviceType ${CYAN}${deviceModel}${db}`); descriptions = this.parseDescription(SHHT1_CITD); this.deviceDescription.set(host, descriptions); } else if (deviceModel === 'SHSM-01') { this.log.debug(`*Set coap descriptions for host ${zb}${host}${db} deviceType ${CYAN}${deviceModel}${db}`); descriptions = this.parseDescription(SHSM1_CITD); this.deviceDescription.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...`); const id = this.deviceId.get(host); if (id) this.registerDevice(host, id, false); } } try { const status = this.parseStatus(descriptions, payload); this.log.debug(`***Update status for device ${hk}${this.deviceId.get(host)}${db} host ${zb}${host}${db} payload:\n`, status); this.emit('coapupdate', host, status); return status; } catch { this.log.warn(`Error parsing values for host ${zb}${host}${wr}`); } } } parseStatus(descriptions, payload) { const status = {}; 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} => component ${CYAN}${desc.component}${db} property ${CYAN}${desc.property}${db} value ${CYAN}${desc.range === '0/1' ? v.value === 1 : v.value}${db}`); if (!desc.property.startsWith('input') && typeof desc.range === 'string' && desc.range === '0/1') { if (!status[desc.component]) status[desc.component] = {}; status[desc.component][desc.property] = v.value === 1; } else if (!desc.property.startsWith('input') && Array.isArray(desc.range) && desc.range[0] === '0/1' && desc.range[1] === '-1') { if (!status[desc.component]) status[desc.component] = {}; status[desc.component][desc.property] = v.value === -1 ? null : v.value === 1; } else { if (desc.property.includes('.')) { const [property, subproperty] = desc.property.split('.'); if (!status[desc.component]) status[desc.component] = {}; status[desc.component][property] = { [subproperty]: v.value }; } else { if (!status[desc.component]) status[desc.component] = {}; status[desc.component][desc.property] = v.value; } } } else this.log.debug(`No coap description found for id ${v.id}`); }); return status; } parseDescription(payload, model) { this.log.debug(`Parsing ${MAGENTA}blocks${db}:`); const desc = []; const blk = payload.blk; const sen = payload.sen; if (!blk || blk.length === 0 || !sen || sen.length === 0) { return desc; } blk.forEach((b) => { this.log.debug(`- block: ${CYAN}${b.I}${db} description ${CYAN}${b.D}${db}`); 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' && b.D === 'device') 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 === 'overpower') desc.push({ id: s.I, component: b.D.replace('_', ':').replace('device', 'sys'), property: 'overpower', range: s.R }); if (s.D === 'input' && !b.D.startsWith('sensor')) desc.push({ id: s.I, component: b.D.replace('relay', 'input').replace('_', ':').replace('device', 'input:0'), property: 'input', range: s.R }); if (s.D === 'inputEvent' && !b.D.startsWith('sensor')) desc.push({ id: s.I, component: b.D.replace('relay', 'input').replace('_', ':').replace('device', 'input:0'), property: 'event', range: s.R }); if (s.D === 'inputEventCnt' && !b.D.startsWith('sensor')) desc.push({ id: s.I, component: b.D.replace('relay', 'input').replace('_', ':').replace('device', 'input:0'), property: 'event_cnt', 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 === '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 === 'mode' && b.D !== 'device') desc.push({ id: s.I, component: b.D.replace('_', ':'), property: 'mode', 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 === 'effect') desc.push({ id: s.I, component: b.D.replace('_', ':'), property: 'effect', range: s.R }); if (s.D === 'power' && b.D.startsWith('light')) desc.push({ id: s.I, component: b.D.replace('_', ':').replace('light', 'meter'), property: 'power', range: s.R }); if (s.D === 'energy' && b.D.startsWith('light')) desc.push({ id: s.I, component: b.D.replace('_', ':').replace('light', 'meter'), 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 === 'motion' && b.D.startsWith('sensor')) desc.push({ id: s.I, component: 'sensor', property: 'motion', range: s.R }); if (s.D === 'dwIsOpened' && b.D.startsWith('sensor')) desc.push({ id: s.I, component: 'sensor', property: 'contact_open', range: s.R }); if (s.D === 'vibration' && b.D.startsWith('sensor')) desc.push({ id: s.I, component: 'vibration', property: 'vibration', range: s.R }); if (s.D === 'tilt' && b.D.startsWith('sensor')) desc.push({ id: s.I, component: 'vibration', property: 'tilt', range: s.R }); if (s.D === 'luminosity' && b.D.startsWith('sensor')) desc.push({ id: s.I, component: 'lux', property: 'value', range: s.R }); if (s.D === 'luminosityLevel' && b.D.startsWith('sensor')) desc.push({ id: s.I, component: 'lux', property: 'illumination', range: s.R }); if (s.D === 'flood' && b.D.startsWith('sensor')) desc.push({ id: s.I, component: 'flood', property: 'flood', range: s.R }); if (s.D === 'smoke' && b.D.startsWith('sensor')) desc.push({ id: s.I, component: 'smoke', property: 'alarm', range: s.R }); 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 === 'temp' && s.U === 'C' && b.D.startsWith('sensor') && model !== 'SHTRV-01') desc.push({ id: s.I, component: 'temperature', property: 'tC', range: s.R }); if (s.D === 'temp' && s.U === 'F' && b.D.startsWith('sensor') && model !== 'SHTRV-01') desc.push({ id: s.I, component: 'temperature', property: 'tF', range: s.R }); if (s.D === 'temp' && s.U === 'C' && b.D.startsWith('sensor') && model === 'SHTRV-01') desc.push({ id: s.I, component: 'thermostat:0', property: 'tmp.value', range: s.R }); if (s.D === 'targetTemp' && s.U === 'C' && b.D.startsWith('sensor')) desc.push({ id: s.I, component: 'thermostat:0', property: 'target_t.value', 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}`); }); return desc; } 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 on port 5683...'); } }); } 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]: `${ShellyDevice.normalizeId(id).type}#${ShellyDevice.normalizeId(id).mac}#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(`****Invalid response registering device ${hk}${id}${db} host ${zb}${host}${db} with fetch`); } }) .catch((err) => { this.log.debug(`****Error registering device ${hk}${id}${db} host ${zb}${host}${db} with fetch: ${err instanceof Error ? err.message : err}`); }); } 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 : ''}.`); }); globalAgent.close((err) => this.log.debug(`CoIoT (coap) agent closed${err ? ' with error ' + err.message : ''}.`)); this.deviceDescription.clear(); this.deviceId.clear(); this.deviceSerial.clear(); this.deviceValidityTimeout.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_CITD = { '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 SHBTN_CITD = { '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_CITD = { '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_CITD = { '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 SHWT1_CITD = { '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': 6106, 'T': 'A', 'D': 'flood', 'R': ['0/1', '-1'], '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 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 SHTRV01_CITD = { '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': 3103, 'T': 'T', 'D': 'targetTemp', 'U': 'C', 'R': ['4/31', '999'], 'L': 1 }, { 'I': 3104, 'T': 'T', 'D': 'targetTemp', 'U': 'F', 'R': ['39/88', '999'], 'L': 1 }, { 'I': 3115, 'T': 'S', 'D': 'sensorError', 'R': '0/1', 'L': 2 }, { 'I': 3116, 'T': 'S', 'D': 'valveError', 'R': '0/1', 'L': 2 }, { 'I': 3117, 'T': 'S', 'D': 'mode', 'R': '0/5', 'L': 2 }, { 'I': 3118, 'T': 'S', 'D': 'status', 'R': '0/1', 'L': 2 }, { 'I': 3111, 'T': 'B', 'D': 'battery', 'R': ['0/100', '-1'], 'L': 2 }, { 'I': 3121, 'T': 'S', 'D': 'valvePos', 'U': '%', 'R': ['0/100', '-1'], 'L': 2 }, { 'I': 3122, 'T': 'S', 'D': 'boostMinutes', 'U': '%', 'R': ['0/1440', '-1'], 'L': 2 }, { 'I': 9103, 'T': 'EVC', 'D': 'cfgChanged', 'R': 'U16', '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_CITD = { '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 }, ], }; const SHSM1_CITS = { 'G': [ [0, 9103, 0], [0, 3101, 999], [0, 3102, 999], [0, 6105, -1], [0, 3115, 0], [0, 3111, -1], [0, 9102, ['unknown']], ], }; const SHSM1_CITD = { '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': 6105, 'T': 'A', 'D': 'smoke', 'R': ['0/1', '-1'], '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 SHGS1_CITS = { 'G': [ [0, 9103, 0], [0, 3113, 'normal'], [0, 3114, 'not_completed'], [0, 6108, 'none'], [0, 3107, 0], [0, 1105, 'closed'], ], }; const SHGS1_CITD = { 'blk': [ { 'I': 1, 'D': 'sensor_0' }, { 'I': 2, 'D': 'valve_0' }, { 'I': 3, 'D': 'device' }, ], 'sen': [ { 'I': 9103, 'T': 'EVC', 'D': 'cfgChanged', 'R': 'U16', 'L': 3 }, { 'I': 3113, 'T': 'S', 'D': 'sensorOp', 'R': ['warmup/normal/fault', 'unknown'], 'L': 1 }, { 'I': 3114, 'T': 'S', 'D': 'selfTest', 'R': 'not_completed/completed/running/pending', 'L': 1 }, { 'I': 6108, 'T': 'A', 'D': 'gas', 'R': ['none/mild/heavy/test', 'unknown'], 'L': 1 }, { 'I': 3107, 'T': 'C', 'D': 'concentration', 'U': 'ppm', 'R': ['U16', '-1'], 'L': 1 }, { 'I': 1105, 'T': 'S', 'D': 'valve', 'R': ['closed/opened/not_connected/failure/closing/opening/checking', 'unknown'], 'L': 2 }, ], };