matterbridge-shelly
Version:
Matterbridge shelly plugin
901 lines • 67.1 kB
JavaScript
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