UNPKG

matterbridge-shelly

Version:
244 lines (243 loc) 10.9 kB
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}`); } } }