matterbridge-shelly
Version:
Matterbridge shelly plugin
244 lines (243 loc) • 10.9 kB
JavaScript
import { CYAN, MAGENTA, BRIGHT, hk, db, nf, wr, zb, er } from 'matterbridge/logger';
import { isValidArray, isValidObject } from 'matterbridge/utils';
import crypto from 'node:crypto';
import EventEmitter from 'node:events';
import { MdnsScanner } from './mdnsScanner.js';
import { CoapServer } from './coapServer.js';
import { WsClient } from './wsClient.js';
import { WsServer } from './wsServer.js';
export class Shelly extends EventEmitter {
_devices = new Map();
log;
fetchInterval;
mdnsScanner;
coapServer;
wsServer;
username;
password;
_dataPath = '';
_interfaceName;
_ipv4Address;
_ipv6Address;
constructor(log, username, password) {
super();
this.log = log;
this.username = username;
this.password = password;
this.mdnsScanner = new MdnsScanner();
this.coapServer = new CoapServer(this);
this.wsServer = new WsServer();
this.wsServer.on('wssupdate', async (shellyId, params) => {
const device = this.getDevice(shellyId);
if (!device) {
this.log.debug(`Received wssupdate from a not registered device id ${hk}${shellyId}${db}`);
return;
}
this.log.debug(`Received wssupdate from device id ${hk}${shellyId}${db} host ${zb}${device.host}${db}`);
if (device.sleepMode)
device.emit('awake');
if (!device.online) {
device.online = true;
device.emit('online');
this.log.debug(`Device ${hk}${device.id}${db} host ${zb}${device.host}${db} sent a WebSocket message: setting online to true`);
}
if (device.cached) {
device.cached = false;
this.log.debug(`Device ${hk}${device.id}${db} host ${zb}${device.host}${db} sent a WebSocket message: setting cached to false`);
}
if (isValidObject(params, 1))
device.onUpdate(params);
});
this.wsServer.on('wssevent', async (shellyId, params) => {
const device = this.getDevice(shellyId);
if (!device) {
this.log.debug(`Received wssevent from a not registered device id ${hk}${shellyId}${db}`);
return;
}
this.log.debug(`Received wssevent from device id ${hk}${shellyId}${db} host ${zb}${device.host}${db}`);
if (device.sleepMode)
device.emit('awake');
if (!device.online) {
device.online = true;
device.emit('online');
this.log.debug(`Device ${hk}${device.id}${db} host ${zb}${device.host}${db} sent a WebSocket message: setting online to true`);
}
if (device.cached) {
device.cached = false;
this.log.debug(`Device ${hk}${device.id}${db} host ${zb}${device.host}${db} sent a WebSocket message: setting cached to false`);
}
if (isValidObject(params, 1) && isValidArray(params.events, 1))
device.onEvent(params.events);
});
this.mdnsScanner.on('discovered', async (device) => {
this.log.info(`Discovered shelly gen ${CYAN}${device.gen}${nf} device id ${hk}${device.id}${nf} host ${zb}${device.host}${nf} port ${zb}${device.port}${nf} `);
this.emit('discovered', device);
});
this.coapServer.on('update', async (host, component, property, value) => {
const device = this.getDeviceByHost(host);
if (device) {
device.log.debug(`CoIoT update from device id ${hk}${device.id}${db} host ${zb}${host}${db} component ${CYAN}${component}${db} property ${CYAN}${property}${db} value ${CYAN}${value}${db}`);
if (!device.hasComponent(component))
this.log.error(`Device ${hk}${device.id}${er} host ${zb}${host}${er} does not have component ${CYAN}${component}${nf}`);
device.getComponent(component)?.setValue(property, value);
device.lastseen = Date.now();
if (device.sleepMode)
device.emit('awake');
if (!device.online) {
device.online = true;
device.emit('online');
this.log.debug(`Device ${hk}${device.id}${db} host ${zb}${host}${db} received a CoIoT message: setting online to true`);
}
if (device.cached) {
device.cached = false;
this.log.debug(`Device ${hk}${device.id}${db} host ${zb}${host}${db} received a CoIoT message: setting cached to false`);
}
}
});
this.fetchInterval = setInterval(() => {
this.devices.forEach((device) => {
if (device.fetchInterval === 0) {
const minMinutes = 55;
const maxMinutes = 65;
const randomFactor = crypto.randomBytes(4).readUInt32BE() / 0xffffffff;
device.fetchInterval = (minMinutes + randomFactor * (maxMinutes - minMinutes)) * 60 * 1000;
const fetchIntervalMinutes = Math.floor(device.fetchInterval / 1000 / 60);
const fetchIntervalSeconds = Math.round((device.fetchInterval / 1000) % 60);
this.log.debug(`Device ${hk}${device.id}${db} host ${zb}${device.host}${db} fetch interval ${CYAN}${fetchIntervalMinutes}${db} minutes and ${CYAN}${fetchIntervalSeconds}${db} seconds`);
}
if (device.sleepMode) {
if (Date.now() - device.lastseen > 24 * 60 * 60 * 1000) {
if (device.online) {
device.log.warn(`Device ${hk}${device.id}${wr} host ${zb}${device.host}${wr} has not reported in the last 24 hours.`);
device.online = false;
device.emit('offline');
}
}
return;
}
if (Date.now() - device.lastFetched > device.fetchInterval) {
const fetchIntervalMinutes = Math.floor(device.fetchInterval / 1000 / 60);
const fetchIntervalSeconds = Math.round((device.fetchInterval / 1000) % 60);
this.log.debug(`Fetching data from device ${hk}${device.id}${db} host ${zb}${device.host}${db} (fetch interval ${CYAN}${fetchIntervalMinutes}${db} minutes and ${CYAN}${fetchIntervalSeconds}${db} seconds)`);
device.fetchUpdate().then((data) => {
device.lastFetched = Date.now();
if (data)
device.saveDevicePayloads(this._dataPath);
});
}
});
}, 10 * 1000);
}
destroy() {
clearInterval(this.fetchInterval);
this.fetchInterval = undefined;
this.devices.forEach((device) => {
device.destroy();
this.removeDevice(device);
});
this.removeAllListeners();
this.wsServer.removeAllListeners();
this.wsServer.stop();
this.mdnsScanner.removeAllListeners();
this.mdnsScanner.stop();
this.coapServer.removeAllListeners();
this.coapServer.stop();
this._devices.clear();
}
set dataPath(path) {
this.log.debug(`Set shelly data path to ${CYAN}${path}${db}`);
this._dataPath = path;
this.mdnsScanner.dataPath = path;
this.coapServer.dataPath = path;
}
get dataPath() {
return this._dataPath;
}
get interfaceName() {
return this._interfaceName;
}
set interfaceName(value) {
this._interfaceName = value;
}
get ipv4Address() {
return this._ipv4Address;
}
set ipv4Address(value) {
this._ipv4Address = value;
}
get ipv6Address() {
return this._ipv6Address;
}
set ipv6Address(value) {
this._ipv6Address = value;
}
hasDevice(id) {
return this._devices.has(id);
}
hasDeviceHost(host) {
const devices = this.devices.filter((device) => device.host === host);
return devices.length > 0;
}
getDevice(id) {
return this._devices.get(id);
}
getDeviceByHost(host) {
const devices = this.devices.filter((device) => device.host === host);
if (devices.length === 0)
return undefined;
return this._devices.get(devices[0].id);
}
async addDevice(device) {
if (this.hasDevice(device.id)) {
this.log.warn(`Shelly device ${hk}${device.id}${wr}: name ${CYAN}${device.name}${wr} ip ${MAGENTA}${device.host}${wr} model ${CYAN}${device.model}${wr} already exists`);
return this;
}
this._devices.set(device.id, device);
if (device.gen === 1) {
this.coapServer.registerDevice(device.host, device.id, device.sleepMode);
}
else if (device.gen >= 2) {
if (!device.sleepMode && device.wsClient && device.wsClient.isConnected === false) {
device.log.info(`WebSocket client for device ${hk}${device.id}${nf} host ${zb}${device.host}${nf} is not connected. Starting connection...`);
device.wsClient.start();
}
}
this.emit('add', device);
return this;
}
removeDevice(device) {
const key = typeof device === 'string' ? device : device.id;
this._devices.delete(key);
return this;
}
get devices() {
return Array.from(this._devices.values());
}
*[Symbol.iterator]() {
for (const [id, device] of this._devices.entries()) {
yield [id, device];
}
}
setLogLevel(level, debugMdns, debugCoap, debugWs) {
this.log.logLevel = level;
this.mdnsScanner.log.logLevel = debugMdns ? "debug" : "info";
this.coapServer.log.logLevel = debugCoap ? "debug" : "info";
this.wsServer.log.logLevel = debugWs ? "debug" : "info";
WsClient.logLevel = debugWs ? "debug" : "info";
this.devices.forEach((device) => {
device.setLogLevel(level);
if (device.wsClient) {
if (debugWs)
device.wsClient.log.logLevel = "debug";
else
device.wsClient.log.logLevel = "info";
}
});
}
logDevices() {
this.log.debug(`${BRIGHT}Shellies${db} (${this.devices.length}):`);
for (const [id, device] of this) {
this.log.debug(`- ${hk}${id}${db}: name ${CYAN}${device.name}${db} ip ${MAGENTA}${device.host}${db} model ${CYAN}${device.model}${db} auth ${CYAN}${device.auth}${db}`);
}
}
}