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