UNPKG

matterbridge-shelly

Version:
901 lines 67.1 kB
import { BLUE, CYAN, GREEN, GREY, MAGENTA, RESET, db, debugStringify, er, hk, nf, wr, zb, rs, YELLOW, idn, nt, rk, dn } from 'matterbridge/logger'; import { isValidNumber, isValidObject, isValidString } from 'matterbridge/utils'; import { EventEmitter } from 'node:events'; import crypto from 'node:crypto'; import { promises as fs } from 'node:fs'; import path from 'node:path'; import { parseDigestAuthenticateHeader, createDigestShellyAuth, createBasicShellyAuth, parseBasicAuthenticateHeader, getGen2BodyOptions, getGen1BodyOptions } from './auth.js'; import { WsClient } from './wsClient.js'; import { isCoverComponent, isLightComponent, isSwitchComponent, ShellyComponent } from './shellyComponent.js'; export class ShellyDevice extends EventEmitter { shelly; log; username; password; profile = undefined; host; id = ''; model = ''; mac = ''; firmware = ''; auth = false; name = ''; online = false; gen = 0; lastseen = 0; lastFetched = Date.now() - 50 * 60 * 1000; fetchInterval = 0; hasUpdate = false; sleepMode = false; cached = false; colorUpdateTimeout; colorCommandTimeout; thermostatSystemModeTimeout; thermostatSetpointTimeout; lastseenInterval; startWsClientTimeout; wsClient; _components = new Map(); shellyPayload = null; statusPayload = null; settingsPayload = null; componentsPayload = null; bthomeTrvs = new Map(); bthomeDevices = new Map(); bthomeSensors = new Map(); constructor(shelly, log, host) { super(); this.shelly = shelly; this.log = log; this.host = host; this.username = shelly.username; this.password = shelly.password; } emit(eventName, ...args) { return super.emit(eventName, ...args); } on(eventName, listener) { return super.on(eventName, listener); } destroy() { if (this.colorUpdateTimeout) clearInterval(this.colorUpdateTimeout); this.colorUpdateTimeout = undefined; if (this.colorCommandTimeout) clearInterval(this.colorCommandTimeout); this.colorCommandTimeout = undefined; if (this.thermostatSystemModeTimeout) clearInterval(this.thermostatSystemModeTimeout); this.thermostatSystemModeTimeout = undefined; if (this.thermostatSetpointTimeout) clearInterval(this.thermostatSetpointTimeout); this.thermostatSetpointTimeout = undefined; if (this.lastseenInterval) clearInterval(this.lastseenInterval); this.lastseenInterval = undefined; this.lastseen = 0; if (this.startWsClientTimeout) clearTimeout(this.startWsClientTimeout); this.startWsClientTimeout = undefined; this.wsClient?.stop(); this.wsClient?.removeAllListeners(); this.wsClient = undefined; this._components.clear(); this.shellyPayload = null; this.statusPayload = null; this.settingsPayload = null; this.componentsPayload = null; this.bthomeTrvs.clear(); this.bthomeDevices.clear(); this.bthomeSensors.clear(); this.removeAllListeners(); } setHost(value) { this.host = value; this.wsClient?.setHost(value); } setLogLevel(logLevel) { this.log.logLevel = logLevel; } hasComponent(id) { return this._components.has(id); } getComponent(id) { const component = this._components.get(id); if (!component) return undefined; if (isLightComponent(component)) return component; if (isSwitchComponent(component)) return component; if (isCoverComponent(component)) return component; return component; } getComponentIds() { return Array.from(this._components.keys()); } getComponentNames() { const names = Array.from(this._components.values()).map((component) => component.name); return Array.from(new Set(names)); } addComponent(component) { this._components.set(component.id, component); return component; } updateComponent(id, data) { const component = this.getComponent(id); if (component) { for (const prop in data) { component.setValue(prop, data[prop]); } return component; } else { this.log.error(`Component ${id} not found in device ${GREEN}${this.id}${er} (${BLUE}${this.name}${er})`); return undefined; } } get components() { return Array.from(this._components.values()); } *[Symbol.iterator]() { for (const [key, component] of this._components.entries()) { yield [key, component]; } } static normalizeId(hostname) { const parts = hostname.split('-'); if (parts.length < 2) return { type: '', mac: '', id: hostname }; const mac = parts.pop(); if (!mac) return { type: '', mac: '', id: hostname }; const name = parts.join('-'); return { type: name.toLowerCase(), mac: mac.toUpperCase(), id: name.toLowerCase() + '-' + mac.toUpperCase() }; } getBTHomeObjIdText(objId) { const objIdsMap = { 0x01: 'Battery', 0x05: 'Illuminance', 0x21: 'Motion', 0x2d: 'Contact', 0x2e: 'Humidity', 0x3a: 'Button', 0x3f: 'Rotation', 0x45: 'Temperature', }; return objIdsMap[objId] || `Unknown sensor id ${objId}`; } getLocalTimeFromLastUpdated(last_updated_ts) { if (!isValidNumber(last_updated_ts, 1000000000)) return 'Unknown'; const lastUpdatedTime = new Date(last_updated_ts * 1000); return lastUpdatedTime.toLocaleString(); } getBTHomeModelText(model) { const modelsMap = { 'SBBT-002C': 'Shelly BLU Button1', 'SBDW-002C': 'Shelly BLU DoorWindow', 'SBHT-003C': 'Shelly BLU HT', 'SBMO-003Z': 'Shelly BLU Motion', 'SBBT-004CEU': 'Shelly BLU Wall Switch 4', 'SBBT-004CUS': 'Shelly BLU RC Button 4', 'TRV': 'Shelly BLU Trv', }; if (model.startsWith('SBBT-2C')) return modelsMap['SBBT-002C']; if (model.startsWith('SBDW-2C')) return modelsMap['SBDW-002C']; if (model.startsWith('SBHT-3C')) return modelsMap['SBHT-003C']; if (model.startsWith('SBMO-3Z')) return modelsMap['SBMO-003Z']; if (model.startsWith('SBBT-EU')) return modelsMap['SBBT-004CEU']; if (model.startsWith('SBBT-US')) return modelsMap['SBBT-004CUS']; return modelsMap[model] || `Unknown Shelly BLU model ${model}`; } updateBTHomeComponents() { if (this.componentsPayload && this.componentsPayload.components) { this.bthomeTrvs.clear(); this.bthomeDevices.clear(); this.bthomeSensors.clear(); this.scanBTHomeComponents(this.componentsPayload.components); } } scanBTHomeComponents(components) { this.bthomeTrvs.clear(); this.bthomeDevices.clear(); this.bthomeSensors.clear(); if (components.length > 0) this.log.info(`Scanning the device ${hk}${this.id}${nf} host ${zb}${this.host}${nf} for BTHome devices and sensors...`); try { for (const component of components) { if (component.key.startsWith('blutrv:')) { if (!isValidString(component.key, 6) || !isValidObject(component.status, 5) || !isValidObject(component.config, 5)) { this.log.error(`BTHome BLUTrv id ${CYAN}${component.config.id}${er} key ${CYAN}${component.key}${er} address ${CYAN}${component.config.addr}${er} has no valid data!`, component); return; } this.log.debug(`- BLUTrv device id ${CYAN}${component.config.id}${db} key ${CYAN}${component.key}${db} address ${CYAN}${component.config.addr}${db} `); this.bthomeTrvs.set(component.config.addr, { id: component.config.id, key: component.key, addr: component.config.addr, bthomedevice: component.config.trv, }); } } for (const component of components) { if (component.key.startsWith('bthomedevice:')) { if (component.attrs?.model_id === 1) { component.config.meta = { ui: { view: 'regular', local_name: 'SBBT-002C', icon: null } }; } else if (component.attrs?.model_id === 2) { component.config.meta = { ui: { view: 'regular', local_name: 'SBDW-002C', icon: null } }; } else if (component.attrs?.model_id === 3) { component.config.meta = { ui: { view: 'regular', local_name: 'SBHT-003C', icon: null } }; } else if (component.attrs?.model_id === 5) { component.config.meta = { ui: { view: 'regular', local_name: 'SBMO-003Z', icon: null } }; } else if (component.attrs?.model_id === 6) { component.config.meta = { ui: { view: 'regular', local_name: 'SBBT-004CEU', icon: null } }; } else if (component.attrs?.model_id === 7) { component.config.meta = { ui: { view: 'regular', local_name: 'SBBT-004CUS', icon: null } }; } else if (component.attrs?.model_id === 8) { component.config.meta = { ui: { view: 'regular', local_name: 'TRV', icon: null } }; } if (!isValidString(component.key, 12) || !isValidObject(component.status, 5) || !isValidObject(component.config, 5) || !isValidObject(component.config.meta, 1) || !isValidObject(component.config.meta.ui, 2) || !isValidString(component.config.meta.ui.local_name)) { this.log.error(`BTHome device id ${CYAN}${component.config.id}${er} key ${CYAN}${component.key}${er} address ${CYAN}${component.config.addr}${er} ` + `name ${CYAN}${component.config.name}${er} has no valid data!`, component); return; } const blutrv_id = this.bthomeTrvs.get(component.config.addr)?.id ?? 0; this.log.debug(`- BLU device id ${CYAN}${component.config.id}${db} key ${CYAN}${component.key}${db} address ${CYAN}${component.config.addr}${db} ` + `blutrv_id ${CYAN}${blutrv_id}${db} ` + `name ${CYAN}${component.config.name}${db} battery ${CYAN}${component.status.battery}${db} packet_id ${CYAN}${component.status.packet_id}${db} ` + `rssi ${CYAN}${component.status.rssi}${db} last update ${CYAN}${this.getLocalTimeFromLastUpdated(component.status.last_updated_ts)}${db} ` + `model ${CYAN}${component.config.meta.ui.local_name}${db} => ${CYAN}${this.getBTHomeModelText(component.config.meta.ui.local_name)}${db} `); this.bthomeDevices.set(component.config.addr, { id: component.config.id, key: component.key, addr: component.config.addr, blutrv_id: blutrv_id, name: component.config.name ?? `${this.getBTHomeModelText(component.config.meta.ui.local_name)} ` + component.config.addr, model: this.getBTHomeModelText(component.config.meta.ui.local_name), type: component.config.meta.ui.local_name, rssi: component.status.rssi, packet_id: component.status.packet_id, last_updated_ts: component.status.last_updated_ts, }); } } for (const component of components) { if (component.key.startsWith('bthomesensor:')) { if (!isValidString(component.key, 12) || !isValidObject(component.status, 1) || !isValidObject(component.config, 6) || !isValidNumber(component.config.obj_id) || !isValidNumber(component.config.id) || !isValidString(component.config.addr) || !isValidNumber(component.config.idx)) { this.log.error(`BTHome sensor id ${CYAN}${component.config.id}${er} key ${CYAN}${component.key}${er} address ${CYAN}${component.config.addr}${er} ` + `name ${CYAN}${component.config.name}${er} obj_id ${CYAN}${component.config.obj_id}${er} has no valid data!`, component); return; } this.log.debug(`- BLU sensor id ${CYAN}${component.status.id}${db} key ${CYAN}${component.key}${db} address ${CYAN}${component.config.addr}${db} ` + `name ${CYAN}${component.config.name}${db} ` + `obj_id ${CYAN}0x${component.config.obj_id.toString(16).padStart(2, '0')}${db} => ${CYAN}${this.getBTHomeObjIdText(component.config.obj_id)}${db} idx ${CYAN}${component.config.idx}${db} ` + `value ${CYAN}${component.status.value}${db} last update ${CYAN}${this.getLocalTimeFromLastUpdated(component.status.last_updated_ts)}${db} `); this.bthomeSensors.set(component.key, { id: component.config.id, key: component.key, name: component.config.name ?? this.getBTHomeObjIdText(component.config.obj_id), addr: component.config.addr, sensorId: component.config.obj_id, sensorIdx: component.config.idx, value: component.status.value, last_updated_ts: component.status.last_updated_ts, }); } } } catch (error) { this.log.error(`Error scanning the device ${hk}${this.id}${db} host ${zb}${this.host}${db} for BTHome devices and sensors: ${error}`); } } static async create(shelly, log, host) { let shellyPayload = null; let statusPayload = null; let settingsPayload = null; let componentsPayload = null; shellyPayload = await ShellyDevice.fetch(shelly, log, host, 'shelly'); if (!shellyPayload) { log.debug(`Error creating device at host ${zb}${host}${db}. No shelly data found.`); return undefined; } const device = new ShellyDevice(shelly, log, host); device.mac = shellyPayload.mac; device.online = true; device.lastseen = Date.now(); device.sleepMode = shellyPayload.sleep_mode ?? false; if (shellyPayload.mode === 'relay') device.profile = 'switch'; if (shellyPayload.mode === 'roller') device.profile = 'cover'; if (shellyPayload.mode === 'color') device.profile = 'color'; if (shellyPayload.mode === 'white') device.profile = 'white'; if (shellyPayload.profile !== undefined) device.profile = shellyPayload.profile; if (!shellyPayload.gen) { statusPayload = await ShellyDevice.fetch(shelly, log, host, 'status'); settingsPayload = await ShellyDevice.fetch(shelly, log, host, 'settings'); if (!statusPayload || !settingsPayload) { log.debug(`Error creating device gen 1 from host ${zb}${host}${db}. No data found.`); return undefined; } device.model = shellyPayload.type; device.id = ShellyDevice.normalizeId(settingsPayload.device.hostname).id; device.firmware = shellyPayload.fw.split('/')[1]; device.auth = shellyPayload.auth; device.name = settingsPayload.name ? settingsPayload.name : device.id; device.gen = 1; device.hasUpdate = statusPayload.has_update; for (const key in settingsPayload) { if (key === 'wifi_ap') device.addComponent(new ShellyComponent(device, key, 'WiFi', settingsPayload[key])); if (key === 'wifi_sta') device.addComponent(new ShellyComponent(device, key, 'WiFi', settingsPayload[key])); if (key === 'wifi_sta1') device.addComponent(new ShellyComponent(device, key, 'WiFi', settingsPayload[key])); if (key === 'mqtt') device.addComponent(new ShellyComponent(device, key, 'MQTT', settingsPayload[key])); if (key === 'coiot') device.addComponent(new ShellyComponent(device, key, 'CoIoT', settingsPayload[key])); if (key === 'sntp') device.addComponent(new ShellyComponent(device, key, 'Sntp', settingsPayload[key])); if (key === 'cloud') device.addComponent(new ShellyComponent(device, key, 'Cloud', settingsPayload[key])); if (key === 'lights') { let index = 0; for (const light of settingsPayload[key]) { device.addComponent(new ShellyComponent(device, `light:${index++}`, 'Light', light)); } } if (key === 'relays' && device.profile !== 'cover') { let index = 0; for (const relay of settingsPayload[key]) { device.addComponent(new ShellyComponent(device, `relay:${index++}`, 'Relay', relay)); } } if (key === 'rollers' && device.profile !== 'switch') { let index = 0; for (const roller of settingsPayload[key]) { device.addComponent(new ShellyComponent(device, `roller:${index++}`, 'Roller', roller)); } } if (key === 'inputs') { let index = 0; for (const input of settingsPayload[key]) { device.addComponent(new ShellyComponent(device, `input:${index++}`, 'Input', input)); } } if (key === 'thermostats') { let index = 0; for (const thermostat of settingsPayload[key]) { device.addComponent(new ShellyComponent(device, `thermostat:${index++}`, 'Thermostat', thermostat)); } } if (key === 'mode' && device.model === 'SHCB-1') { device.profile = settingsPayload[key]; device.addComponent(new ShellyComponent(device, 'sys', 'Sys')); } } for (const key in statusPayload) { if (key === 'ext_temperature' && isValidObject(statusPayload[key], 1)) device.addComponent(new ShellyComponent(device, 'temperature', 'Temperature')); if (key === 'ext_humidity' && isValidObject(statusPayload[key], 1)) device.addComponent(new ShellyComponent(device, 'humidity', 'Humidity')); if (key === 'temperature') device.addComponent(new ShellyComponent(device, 'sys', 'Sys')); if (key === 'overtemperature') device.addComponent(new ShellyComponent(device, 'sys', 'Sys')); if (key === 'tmp' && statusPayload.temperature === undefined && statusPayload.overtemperature === undefined) { device.addComponent(new ShellyComponent(device, 'temperature', 'Temperature')); } if (key === 'hum') device.addComponent(new ShellyComponent(device, 'humidity', 'Humidity')); if (key === 'voltage') device.addComponent(new ShellyComponent(device, 'sys', 'Sys')); if (key === 'mode') device.addComponent(new ShellyComponent(device, 'sys', 'Sys')); if (key === 'bat') device.addComponent(new ShellyComponent(device, 'battery', 'Battery')); if (key === 'charger') device.addComponent(new ShellyComponent(device, 'battery', 'Battery')); if (key === 'lux') device.addComponent(new ShellyComponent(device, 'lux', 'Lux')); if (key === 'flood') device.addComponent(new ShellyComponent(device, 'flood', 'Flood')); if (key === 'gas_sensor') device.addComponent(new ShellyComponent(device, 'gas', 'Gas')); if (key === 'sensor') { device.addComponent(new ShellyComponent(device, 'sensor', 'Sensor')); const sensor = statusPayload[key]; if (sensor.vibration !== undefined) device.addComponent(new ShellyComponent(device, 'vibration', 'Vibration')); if (sensor.state !== undefined) device.addComponent(new ShellyComponent(device, 'contact', 'Contact')); if (sensor.motion !== undefined) device.addComponent(new ShellyComponent(device, 'motion', 'Motion')); } if (key === 'accel') { const accel = statusPayload[key]; if (accel.vibration !== undefined) device.addComponent(new ShellyComponent(device, 'vibration', 'Vibration')); } if (key === 'inputs') { let index = 0; for (const input of statusPayload[key]) { if (!device.hasComponent(`input:${index}`)) device.addComponent(new ShellyComponent(device, `input:${index++}`, 'Input', input)); } } if (key === 'meters') { let index = 0; for (const meter of statusPayload[key]) { if (device.profile === 'cover' && index > 0) break; device.addComponent(new ShellyComponent(device, `meter:${index++}`, 'PowerMeter', meter)); } } if (key === 'emeters') { let index = 0; for (const emeter of statusPayload[key]) { device.addComponent(new ShellyComponent(device, `emeter:${index++}`, 'PowerMeter', emeter)); } } } device.addComponent(new ShellyComponent(device, 'sys', 'Sys')); } if (shellyPayload.gen === 2 || shellyPayload.gen === 3 || shellyPayload.gen === 4) { statusPayload = await ShellyDevice.fetch(shelly, log, host, 'Shelly.GetStatus'); settingsPayload = await ShellyDevice.fetch(shelly, log, host, 'Shelly.GetConfig'); if (!statusPayload || !settingsPayload) { log.debug(`Error creating device gen 2+ from host ${zb}${host}${db}. No data found.`); return undefined; } if (statusPayload.sys.wakeup_period) device.sleepMode = true; device.model = shellyPayload.model; device.id = ShellyDevice.normalizeId(shellyPayload.id).id; device.firmware = shellyPayload.fw_id.split('/')[1]; device.auth = shellyPayload.auth_en; device.gen = shellyPayload.gen; const available_updates = statusPayload.sys.available_updates; device.hasUpdate = available_updates.stable !== undefined; for (const key in settingsPayload) { if (key === 'wifi') { const wifi = settingsPayload[key]; if (wifi.ap) device.addComponent(new ShellyComponent(device, 'wifi_ap', 'WiFi', wifi.ap)); if (wifi.sta) device.addComponent(new ShellyComponent(device, 'wifi_sta', 'WiFi', wifi.sta)); if (wifi.sta1) device.addComponent(new ShellyComponent(device, 'wifi_sta1', 'WiFi', wifi.sta1)); } if (key === 'sys') { device.addComponent(new ShellyComponent(device, 'sys', 'Sys', settingsPayload[key])); const sys = settingsPayload[key]; if (sys.sntp) { device.addComponent(new ShellyComponent(device, 'sntp', 'Sntp', sys.sntp)); } const dev = sys.device; device.name = dev.name ? dev.name : device.id; } if (key === 'blugw') device.addComponent(new ShellyComponent(device, key, 'Blugw', settingsPayload[key])); if (key === 'mqtt') device.addComponent(new ShellyComponent(device, key, 'MQTT', settingsPayload[key])); if (key === 'ws') device.addComponent(new ShellyComponent(device, key, 'WS', settingsPayload[key])); if (key === 'cloud') device.addComponent(new ShellyComponent(device, key, 'Cloud', settingsPayload[key])); if (key === 'ble') device.addComponent(new ShellyComponent(device, key, 'Ble', settingsPayload[key])); if (key === 'eth') device.addComponent(new ShellyComponent(device, key, 'Eth', settingsPayload[key])); if (key === 'matter') device.addComponent(new ShellyComponent(device, key, 'Matter', settingsPayload[key])); if (key.startsWith('switch:')) device.addComponent(new ShellyComponent(device, key, 'Switch', settingsPayload[key])); if (key.startsWith('cover:')) device.addComponent(new ShellyComponent(device, key, 'Cover', settingsPayload[key])); if (key.startsWith('light:')) device.addComponent(new ShellyComponent(device, key, 'Light', settingsPayload[key])); if (key.startsWith('rgb:')) device.addComponent(new ShellyComponent(device, key, 'Rgb', settingsPayload[key])); if (key.startsWith('rgbw:')) device.addComponent(new ShellyComponent(device, key, 'Rgbw', settingsPayload[key])); if (key.startsWith('input:')) device.addComponent(new ShellyComponent(device, key, 'Input', settingsPayload[key])); if (key.startsWith('pm1:')) device.addComponent(new ShellyComponent(device, key, 'PowerMeter', settingsPayload[key])); if (key.startsWith('em1:')) device.addComponent(new ShellyComponent(device, key, 'PowerMeter', settingsPayload[key])); if (key.startsWith('em:')) device.addComponent(new ShellyComponent(device, key, 'PowerMeter', settingsPayload[key])); if (key.startsWith('temperature:')) device.addComponent(new ShellyComponent(device, key, 'Temperature', settingsPayload[key])); if (key.startsWith('humidity:')) device.addComponent(new ShellyComponent(device, key, 'Humidity', settingsPayload[key])); if (key.startsWith('illuminance:')) device.addComponent(new ShellyComponent(device, key, 'Illuminance', settingsPayload[key])); if (key.startsWith('smoke:')) device.addComponent(new ShellyComponent(device, key, 'Smoke', settingsPayload[key])); if (key.startsWith('thermostat:')) device.addComponent(new ShellyComponent(device, key, 'Thermostat', settingsPayload[key])); if (key.startsWith('devicepower:')) device.addComponent(new ShellyComponent(device, key, 'Devicepower', settingsPayload[key])); } const btHomeComponents = []; let btHomePayload; let offset = 0; do { btHomePayload = (await ShellyDevice.fetch(shelly, log, host, 'Shelly.GetComponents', { dynamic_only: true, offset })); if (btHomePayload && btHomePayload.components) { btHomeComponents.push(...btHomePayload.components); offset += btHomePayload.components.length; } } while (btHomePayload && offset < btHomePayload.total); componentsPayload = { components: btHomeComponents, cfg_rev: btHomePayload?.cfg_rev | 0, offset: 0, total: btHomeComponents.length }; device.scanBTHomeComponents(btHomeComponents); } if (statusPayload) device.onUpdate(statusPayload); if (device.gen === 1) { const CoIoT = device.getComponent('coiot'); if (CoIoT) { if (CoIoT.hasProperty('enabled') && CoIoT.getValue('enabled') === false) log.warn(`The CoIoT service is not enabled for device ${dn}${device.name}${wr} id ${hk}${device.id}${wr}. Enable it in the web ui settings to receive updates from the device.`); if (CoIoT.hasProperty('peer') && CoIoT.getValue('peer') !== '') { const peer = CoIoT.getValue('peer'); const ipv4 = shelly.ipv4Address + ':5683'; if (peer !== ipv4) log.warn(`The CoIoT peer for device ${dn}${device.name}${wr} id ${hk}${device.id}${wr} is not mcast or ${ipv4}. Set it in the web ui settings to receive updates from the device.`); } } else { log.error(`CoIoT service not found for device ${dn}${device.name}${er} id ${hk}${device.id}${er}.`); } } if (device.gen >= 2 && device.sleepMode === true) { const ws = device.getComponent('ws'); if (ws) { if (ws.getValue('enable') === false) { log.warn(`The Outbound websocket settings is not enabled for device ${dn}${device.name}${wr} id ${hk}${device.id}${wr}. Enable it in the web ui settings to receive updates from the device.`); } const ipv4 = shelly.ipv4Address; const server = ws.getValue('server'); if (!server || !server.endsWith(':8485')) { log.warn(`The Outbound websocket settings is not configured correctly for device ${dn}${device.name}${wr} id ${hk}${device.id}${wr}. The port must be 8485 (i.e. ws://${ipv4}:8485). Set it in the web ui settings to receive updates from the device.`); } if (!server || !server.includes(ipv4 ?? '')) { log.warn(`The Outbound websocket settings is not configured correctly for device ${dn}${device.name}${wr} id ${hk}${device.id}${wr}. The ip must be the matterbridge ip (i.e. ws://${ipv4}:8485). Set it in the web ui settings to receive updates from the device.`); } } else { log.error(`WebSocket server component not found for device ${dn}${device.name}${er} id ${hk}${device.id}${er}.`); } } if (device.gen === 1) { if (device.profile === 'cover') { const roller = device.getComponent('roller:0'); const pos = roller?.hasProperty('current_pos') ? roller?.getValue('current_pos') : undefined; if (roller && pos && pos > 100) { device.log.notice(`Roller device ${hk}${device.id}${nt} host ${zb}${device.host}${nt} does not have position control enabled.`); } } } else if (device.gen >= 2) { if (device.profile === 'cover') { const cover = device.getComponent('cover:0'); if (cover && cover.getValue('pos_control') === false) { device.log.notice(`Cover device ${hk}${device.id}${nt} host ${zb}${device.host}${nt} does not have position control enabled.`); } } } if (device.hasUpdate) log.notice(`Device ${hk}${device.id}${nt} host ${zb}${device.host}${nt} has an available firmware update.`); device.lastseenInterval = setInterval(() => { const lastSeenDate = new Date(device.lastseen); log.debug(`Device ${hk}${device.id}${db} host ${zb}${device.host}${db} online ${!device.online ? wr : CYAN}${device.online}${db} ` + `sleep mode ${device.sleepMode ? wr : CYAN}${device.sleepMode}${db} cached ${device.cached ? wr : CYAN}${device.cached}${db} ` + `${device.gen >= 2 && device.sleepMode === false && device.wsClient?.isConnected === false ? 'websocket ' + er + 'false ' + db : ''}` + `last seen ${CYAN}${lastSeenDate.toLocaleString()}${db}.`); if (device.gen >= 2 && !device.sleepMode && device.wsClient && device.wsClient.isConnected === false) { log.info(`WebSocket client for device ${hk}${device.id}${nf} host ${zb}${device.host}${nf} is not connected. Starting connection...`); device.wsClient.start(); } }, 60 * 1000); if (device.gen >= 2 && !device.sleepMode) { device.wsClient = new WsClient(device.id, host, shelly.password); if (!host.endsWith('.json')) device.wsClient.start(); device.wsClient.on('response', (message) => { log.debug(`WebSocket response from device ${hk}${device.id}${db} host ${zb}${device.host}${db}`); device.lastseen = Date.now(); if (!device.online) { device.online = true; device.emit('online'); log.debug(`Device ${hk}${device.id}${db} host ${zb}${device.host}${db} received a WebSocket message: setting online to true`); } if (device.cached) { device.cached = false; log.debug(`Device ${hk}${device.id}${db} host ${zb}${device.host}${db} received a WebSocket message: setting cached to false`); } }); device.wsClient.on('update', (params) => { log.debug(`WebSocket update from device ${hk}${device.id}${db} host ${zb}${device.host}${db}`); device.lastseen = Date.now(); if (!device.online) { device.online = true; device.emit('online'); log.debug(`Device ${hk}${device.id}${db} host ${zb}${device.host}${db} received a WebSocket message: setting online to true`); } if (device.cached) { device.cached = false; log.debug(`Device ${hk}${device.id}${db} host ${zb}${device.host}${db} received a WebSocket message: setting cached to false`); } device.onUpdate(params); }); device.wsClient.on('event', (events) => { log.debug(`WebSocket event from device ${hk}${device.id}${db} host ${zb}${device.host}${db}`); device.lastseen = Date.now(); if (!device.online) { device.online = true; device.emit('online'); log.debug(`Device ${hk}${device.id}${db} host ${zb}${device.host}${db} received a WebSocket message: setting online to true`); } if (device.cached) { device.cached = false; log.debug(`Device ${hk}${device.id}${db} host ${zb}${device.host}${db} received a WebSocket message: setting cached to false`); } device.onEvent(events); }); } device.on('awake', async () => { log.debug(`Device ${hk}${device.id}${db} host ${zb}${device.host}${db} is awake (cached: ${device.cached}).`); if (device.sleepMode && (device.cached || Date.now() - device.lastFetched > device.fetchInterval)) { try { device.lastFetched = Date.now(); const awaken = await ShellyDevice.create(shelly, log, device.host); if (awaken) { if (device.gen === 1) shelly.coapServer.registerDevice(device.host, device.id, false); await awaken.saveDevicePayloads(shelly.dataPath); awaken.destroy(); } log.debug(`Updated cache file for sleepy device ${hk}${device.id}${db} host ${zb}${device.host}${db}`); } catch (error) { log.debug(`Error saving device cache ${hk}${device.id}${db} host ${zb}${device.host}${db}: ${error instanceof Error ? error.message : error}`); } } }); device.shellyPayload = shellyPayload; device.statusPayload = statusPayload; device.settingsPayload = settingsPayload; device.componentsPayload = componentsPayload; return device; } onEvent(events) { for (const event of events) { if (isValidObject(event) && isValidString(event.event) && isValidNumber(event.ts) && isValidString(event.component) && event.component === 'bthome') { this.log.debug(`Device ${hk}${this.id}${db} has event ${YELLOW}${event.event}${db} at ${CYAN}${this.getLocalTimeFromLastUpdated(event.ts)}${db}`); this.emit('bthome_event', event); } else if (isValidObject(event) && isValidString(event.event) && isValidNumber(event.ts) && isValidString(event.component) && event.component.startsWith('bthomedevice:')) { const device = Array.from(this.bthomeDevices).find(([_addr, _device]) => _device.key === event.component)?.[1]; if (device) { this.log.debug(`Device ${hk}${this.id}${db} has event ${YELLOW}${event.event}${db} at ${CYAN}${this.getLocalTimeFromLastUpdated(event.ts)}${db} ` + `from BTHomeDevice addr ${idn}${device.addr}${rs}${db} name ${CYAN}${device.name}${db} `); this.emit('bthomedevice_event', device.addr, event); } else { this.log.debug(`*Unknown bthomedevice ${event.component} with event: ${debugStringify(event)}${rs}`); } } else if (isValidObject(event) && isValidString(event.event) && isValidNumber(event.ts) && isValidString(event.component) && event.component.startsWith('bthomesensor:')) { const sensor = this.bthomeSensors.get(event.component); if (sensor) { this.log.debug(`Device ${hk}${this.id}${db} has event ${YELLOW}${event.event}${db} at ${CYAN}${this.getLocalTimeFromLastUpdated(event.ts)}${db} ` + `from BTHomeSensor addr ${idn}${sensor.addr}${rs}${db} name ${CYAN}${sensor.name}${db} ` + `sensorId ${CYAN}${this.getBTHomeObjIdText(sensor.sensorId)}${db} (${CYAN}${sensor.sensorId}${db}) index ${CYAN}${sensor.sensorIdx}${db}`); this.emit('bthomesensor_event', sensor.addr, this.getBTHomeObjIdText(sensor.sensorId), sensor.sensorIdx, event); } else { this.log.debug(`*Unknown bthomesensor ${event.component} with event: ${debugStringify(event)}${rs}`); } } else if (isValidObject(event) && isValidString(event.event) && isValidString(event.component)) { this.log.debug(`Device ${hk}${this.id}${db} has event ${YELLOW}${event.event}${db} from component ${idn}${event.component}${rs}${db}${rk} ${debugStringify(event)}`); this.getComponent(event.component)?.emit('event', event.component, event.event, event); } else { this.log.debug(`*Unknown event:${rs}\n`, event); } } this.lastseen = Date.now(); } onUpdate(data) { for (const key in data) { if (key.startsWith('bthomedevice:')) { let device = undefined; for (const _device of this.bthomeDevices.values()) { if (_device.key === key) { device = _device; } } if (device) { const bthomeDevice = data[key]; this.log.debug(`Device ${hk}${this.id}${db} has device update from BTHomeDevice id ${CYAN}${device.id}${db} key ${CYAN}${device.key}${db} ` + `addr ${idn}${device.addr}${rs}${db} name ${CYAN}${device.name}${db} model ${CYAN}${device.model}${db} (${CYAN}${device.type}${db}) ` + `rssi ${CYAN}${bthomeDevice.rssi}${db} packet_id ${CYAN}${bthomeDevice.packet_id}${db} last_updated_ts ${CYAN}${this.getLocalTimeFromLastUpdated(bthomeDevice.last_updated_ts)}${db}`); if (isValidNumber(bthomeDevice.rssi, -100, 0) || isValidNumber(bthomeDevice.last_updated_ts, 0)) { if (isValidNumber(bthomeDevice.rssi, -100, 0)) device.rssi = bthomeDevice.rssi; if (isValidNumber(bthomeDevice.last_updated_ts, 0)) device.last_updated_ts = bthomeDevice.last_updated_ts; this.emit('bthomedevice_update', device.addr, bthomeDevice.rssi, bthomeDevice.packet_id, bthomeDevice.last_updated_ts); } } else { this.log.debug(`*Unknown bthomedevice ${key}`); } } else if (key.startsWith('bthomesensor:')) { const sensor = this.bthomeSensors.get(key); if (sensor) { const bthomeSensor = data[key]; this.log.debug(`Device ${hk}${this.id}${db} has sensor update from BTHomeSensor id ${CYAN}${sensor.id}${db} key ${CYAN}${sensor.key}${db} ` + `addr ${idn}${sensor.addr}${rs}${db} name ${CYAN}${sensor.name}${db} ` + `sensorId ${CYAN}${this.getBTHomeObjIdText(sensor.sensorId)}${db} (${CYAN}${sensor.sensorId}${db}) index ${CYAN}${sensor.sensorIdx}${db} ` + `last update ${CYAN}${this.getLocalTimeFromLastUpdated(bthomeSensor.last_updated_ts)}${db}: ${YELLOW}${bthomeSensor.value}${db}`); if (bthomeSensor.value !== undefined && bthomeSensor.value !== null) { sensor.value = bthomeSensor.value; this.emit('bthomesensor_update', sensor.addr, this.getBTHomeObjIdText(sensor.sensorId), sensor.sensorIdx, bthomeSensor.value); } } else { this.log.debug(`*Unknown bthomesensor ${key}`); } } } if (this.gen === 1) { for (const key in data) { if (key === 'lights') { let index = 0; for (const light of data[key]) { this.updateComponent(`light:${index++}`, light); } } if (key === 'relays') { let index = 0; for (const relay of data[key]) { this.updateComponent(`relay:${index++}`, relay); } } if (key === 'rollers') { let index = 0; for (const roller of data[key]) { this.updateComponent(`roller:${index++}`, roller); } } if (key === 'inputs') { let index = 0; for (const input of data[key]) { this.updateComponent(`input:${index++}`, input); } } if (key === 'thermostats') { let index = 0; for (const thermostat of data[key]) { this.updateComponent(`thermostat:${index++}`, thermostat); } } if (key === 'meters') { let index = 0; for (const meter of data[key]) { if (this.profile === 'cover' && index > 0) break; this.updateComponent(`meter:${index++}`, meter); } } if (key === 'emeters') { let index = 0; for (const emeter of data[key]) { this.updateComponent(`emeter:${index++}`, emeter); } } if (key === 'bat') { const battery = this.getComponent('battery'); battery?.setValue('level', data.bat ? data.bat.value : 0); battery?.setValue('voltage', data.bat ? data.bat.voltage : 0); } if (key === 'charger') { const battery = this.getComponent('battery'); battery?.setValue('charging', data[key]); } if (key === 'sensor') { this.updateComponent(key, data[key]); const sensor = data.sensor; if (sensor.is_valid === true && sensor.state !== undefined) this.getComponent('sensor')?.setValue('contact_open', sensor.state !== 'close'); if (sensor.vibration !== undefined) this.getComponent('vibration')?.setValue('vibration', sensor.vibration); } if (key === 'accel') { const accel = data.accel; if (accel.vibration !== undefined) this.getComponent('vibration')?.setValue('vibration', accel.vibration === 1); } if (key === 'lux') { this.updateComponent(key, data[key]); } if (key === 'flood') { if (typeof data[key] === 'boolean') this.getComponent('flood')?.setValue('flood', data[key]); } if (key === 'gas_sensor') { this.updateComponent('gas', data[key]); } if (key === 'concentration') { this.updateComponent('gas', data[key]); } if (key === 'ext_temperature' && isValidObject(data[key], 1)) { this.updateComponent('temperature', data[key]); const sensor = data[key]['0']; if (sensor && isValidNumber(sensor.tC, -55, 125)) this.getComponent('temperature')?.setValue('value', sensor.tC); } if (key === 'ext_humidity' && isValidObject(data[key], 1)) { this.updateComponent('humidity', data[key]); const sensor = data[key]['0']; if (sensor && isValidNumber(sensor.hum, 0, 100)) this.getComponent('humidity')?.setValue('value', sensor.hum); } if (key === 'tmp') { if (data.temperature === undefined && data.overtemperature === undefined) this.updateComponent('temperature', data[key]); const sensor = data.tmp; if (sensor.is_valid === true && sensor.units === 'C' && isValidNumber(sensor.tC, -55, 125)) this.getComponent('temperature')?.setValue('value', sensor.tC); if (sensor.is_valid === true && sensor.units === 'F' && isValidNumber(sensor.tF, -67, 257)) this.getComponent('temperature')?.setValue('value', sensor.tF); } if (key === 'hum') { this.updateComponent('humidity', data[key]); const sensor = data.hum; if (sensor.is_valid === true && isValidNumber(sensor.value, 0, 100)) this.getComponent('humidity')?.setValue('value', sensor.value); } if (key === 'temperature') { if (data[key] !== null && data[key] !== undefined && typeof data[key] === 'number') this.getComponent('sys')?.setValue('temperature', data[key]); } if (key === 'overtemperature') { if (data[key] !== null && data[key] !== undefined && typeof data[key] === 'boolean') this.getComponent('sys')?.set