matterbridge-shelly
Version:
Matterbridge shelly plugin
772 lines (771 loc) • 40.4 kB
JavaScript
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 },
],
};