UNPKG

matterbridge-shelly

Version:
772 lines (771 loc) 40.4 kB
import EventEmitter from 'node:events'; import { promises as fs } from 'node:fs'; import path from 'node:path'; import coap, { globalAgent, parameters } from 'coap'; import { AnsiLogger, BLUE, CYAN, db, debugStringify, er, hk, MAGENTA, nf, RESET, wr, zb } from 'matterbridge/logger'; 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(); } 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...'); this.emit('started'); } }); } 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`); } return; }) .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._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.emit('stopped', err); }); globalAgent.close((err) => { this.log.debug(`CoIoT (coap) agent closed${err ? ' with error ' + err.message : ''}.`); this.emit('agent_stopped', err); this.removeAllListeners(); }); 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 }, ], };