matterbridge-shelly
Version:
Matterbridge shelly plugin
692 lines (691 loc) • 127 kB
JavaScript
import * as fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { bridgedNode, colorTemperatureLight, contactSensor, coverDevice, dimmableLight, electricalSensor, extendedColorLight, genericSwitch, humiditySensor, lightSensor, MatterbridgeDynamicPlatform, MatterbridgeEndpoint, modeSelect, occupancySensor, onOffLight, onOffOutlet, onOffSwitch, powerSource, smokeCoAlarm, temperatureSensor, thermostatDevice, waterLeakDetector, } from 'matterbridge';
import { AnsiLogger, CYAN, db, debugStringify, dn, er, GREEN, hk, idn, nf, nt, rs, wr, YELLOW, zb } from 'matterbridge/logger';
import { NumberTag } from 'matterbridge/matter';
import { BooleanState, BridgedDeviceBasicInformation, ColorControl, ElectricalEnergyMeasurement, ElectricalPowerMeasurement, IlluminanceMeasurement, LevelControl, ModeSelect, OccupancySensing, OnOff, PowerSource, RelativeHumidityMeasurement, SmokeCoAlarm, Switch, TemperatureMeasurement, Thermostat, WindowCovering, } from 'matterbridge/matter/clusters';
import { VendorId } from 'matterbridge/matter/types';
import { NodeStorageManager } from 'matterbridge/storage';
import { hslColorToRgbColor, isValidArray, isValidBoolean, isValidIpv4Address, isValidNumber, isValidObject, isValidString, kelvinToRGB, miredToKelvin, rgbColorToHslColor, waiter, xyColorToRgbColor, } from 'matterbridge/utils';
import { shellyCoverCommandHandler, shellyIdentifyCommandHandler, shellyLightCommandHandler, shellySwitchCommandHandler } from './platformCommandHandlers.js';
import { shellyUpdateHandler } from './platformUpdateHandler.js';
import { Shelly } from './shelly.js';
import { isCoverComponent, isLightComponent, isSwitchComponent } from './shellyComponent.js';
import { ShellyDevice } from './shellyDevice.js';
export default function initializePlugin(matterbridge, log, config) {
return new ShellyPlatform(matterbridge, log, config);
}
export class ShellyPlatform extends MatterbridgeDynamicPlatform {
config;
discoveredDevices = new Map();
storedDevices = new Map();
changedDevices = new Map();
gatewayDevices = new Map();
bridgedDevices = new Map();
bluBridgedDevices = new Map();
nodeStorageManager;
nodeStorage;
shelly;
username = '';
password = '';
postfix;
failsafeCountSeconds = 360;
firstRun = false;
constructor(matterbridge, log, config) {
super(matterbridge, log, config);
this.config = config;
if (this.verifyMatterbridgeVersion === undefined || typeof this.verifyMatterbridgeVersion !== 'function' || !this.verifyMatterbridgeVersion('3.7.0')) {
throw new Error(`This plugin requires Matterbridge version >= "3.7.0". Please update Matterbridge from ${this.matterbridge.matterbridgeVersion} to the latest version in the frontend."`);
}
if (config.username)
this.username = config.username;
if (config.password)
this.password = config.password;
this.postfix = config.postfix ?? '';
if (!isValidString(this.postfix, 0, 3))
this.postfix = '';
if (!isValidNumber(config.failsafeCount, 0))
config.failsafeCount = 0;
{
const config = this.config;
if (config.exposeSwitch !== undefined)
delete config.exposeSwitch;
if (config.outletList !== undefined)
delete config.outletList;
if (config.exposeInput !== undefined)
delete config.exposeInput;
if (config.exposeInputEvent !== undefined)
delete config.exposeInputEvent;
if (config.inputEventList !== undefined)
delete config.inputEventList;
if (config.exposePowerMeter !== undefined)
delete config.exposePowerMeter;
if (config.enableConfigDiscover !== undefined)
delete config.enableConfigDiscover;
if (config.deviceIp !== undefined)
delete config.deviceIp;
}
if (config.firstRun === undefined) {
config.firstRun = true;
this.firstRun = true;
if (!isValidArray(config.whiteList, 1) && !isValidArray(config.blackList, 1) && !isValidArray(config.entityBlackList, 1) && !isValidObject(config.deviceEntityBlackList, 1))
config.expertMode = false;
else
config.expertMode = true;
if (!isValidArray(config.entityBlackList, 1))
config.entityBlackList = ['Lux', 'Illuminance', 'Vibration', 'Button'];
}
if (config.expertMode === false) {
this.setSchema(baseSchema);
}
log.debug(`Initializing platform: ${idn}${config.name}${rs}${db} v.${CYAN}${config.version}`);
log.debug(`- username: ${CYAN}${config.username ? '********' : 'undefined'}`);
log.debug(`- password: ${CYAN}${config.password ? '********' : 'undefined'}`);
log.debug(`- mdnsDiscover: ${CYAN}${config.enableMdnsDiscover}`);
log.debug(`- storageDiscover: ${CYAN}${config.enableStorageDiscover}`);
log.debug(`- bleDiscover: ${CYAN}${config.enableBleDiscover}`);
log.debug(`- resetStorage: ${CYAN}${config.resetStorageDiscover}`);
log.debug(`- postfix: ${CYAN}${config.postfix}`);
log.debug(`- failsafeCount: ${CYAN}${config.failsafeCount}`);
log.debug(`- expertMode: ${CYAN}${config.expertMode}`);
log.debug(`- debug: ${CYAN}${config.debug}`);
log.debug(`- debugMdns: ${CYAN}${config.debugMdns}`);
log.debug(`- debugCoap: ${CYAN}${config.debugCoap}`);
log.debug(`- debugWs: ${CYAN}${config.debugWs}`);
log.debug(`- unregisterOnShutdown: ${CYAN}${config.unregisterOnShutdown}`);
const entities = [
{ name: 'Relay', description: 'Output component of switches gen 1', icon: 'component' },
{ name: 'Switch', description: 'Output component of switches gen 2+', icon: 'component' },
{ name: 'Light', description: 'Output component of lights', icon: 'component' },
{ name: 'Rgb', description: 'Output component of lights gen 2+', icon: 'component' },
{ name: 'Input', description: 'Input component of WiFi devices', icon: 'component' },
{ name: 'Roller', description: 'Window covering component of switches gen 1', icon: 'component' },
{ name: 'Cover', description: 'Window covering component of switches gen 2+', icon: 'component' },
{ name: 'PowerMeter', description: 'Electrical measurements component', icon: 'component' },
{ name: 'Button', description: 'Button component of BLU devices', icon: 'component' },
{ name: 'Temperature', description: 'Temperature component', icon: 'component' },
{ name: 'Humidity', description: 'Humidity component', icon: 'component' },
{ name: 'Flood', description: 'Flood component of flood sensors', icon: 'component' },
{ name: 'Motion', description: 'Motion component of motion sensors', icon: 'component' },
{ name: 'Lux', description: 'Illuminance component of illuminance sensors gen 1', icon: 'component' },
{ name: 'Illuminance', description: 'Illuminance component of illuminance sensors BLU and gen 2+', icon: 'component' },
{ name: 'Contact', description: 'Contact component', icon: 'component' },
{ name: 'Vibration', description: 'Vibration component of vibration sensors', icon: 'component' },
{ name: 'Battery', description: 'Battery component of battery powered devices gen 1', icon: 'component' },
{ name: 'Devicepower', description: 'Battery component of battery powered devices gen 2+', icon: 'component' },
{ name: 'PowerSource', description: 'Matter component to select wired or battery powered devices', icon: 'matter' },
];
for (const entity of entities) {
this.setSelectEntity(entity.name, entity.description, entity.icon);
}
this.shelly = new Shelly(log, this.username, this.password);
this.shelly.setLogLevel(log.logLevel, this.config.debugMdns, this.config.debugCoap, this.config.debugWs);
this.shelly.dataPath = path.join(matterbridge.matterbridgePluginDirectory, 'matterbridge-shelly');
this.shelly.interfaceName = matterbridge.systemInformation.interfaceName;
this.shelly.ipv4Address = matterbridge.systemInformation.ipv4Address;
this.shelly.ipv6Address = matterbridge.systemInformation.ipv6Address;
const networkInterfaces = os.networkInterfaces();
const availableAddresses = Object.entries(networkInterfaces);
for (const [ifaceName, ifaces] of availableAddresses) {
if (ifaces && ifaces.length > 0) {
this.log.debug(`Network interface: ${CYAN}${ifaceName}${db}:`);
ifaces.forEach((iface) => {
this.log.debug(`- ${CYAN}${iface.family}${db} address ${CYAN}${iface.address}${db} netmask ${CYAN}${iface.netmask}${db} mac ${CYAN}${iface.mac}${db} scopeid ${CYAN}${iface.scopeid}${db} ${iface.internal ? 'internal' : 'external'}`);
});
}
}
this.log.debug(`Shelly platform v.${CYAN}${this.config.version}${db} interface ${CYAN}${this.shelly.interfaceName}${db} ipv4 ${CYAN}${this.shelly.ipv4Address}${db} ipv6 ${CYAN}${this.shelly.ipv6Address}${db}`);
this.shelly.on('discovered', async (discoveredDevice) => {
if (discoveredDevice.port === 9000) {
this.log.warn(`Shelly device ${hk}${discoveredDevice.id}${wr} host ${zb}${discoveredDevice.host}${wr} has been discovered on port ${discoveredDevice.port}. Unofficial Shelly firmware are not supported.`);
return;
}
if (discoveredDevice.id.startsWith('shellyspot2') || discoveredDevice.id.startsWith('shellypresence') || discoveredDevice.id.startsWith('shellysense')) {
this.log.info(`Shelly device ${hk}${discoveredDevice.id}${wr} host ${zb}${discoveredDevice.host}${wr} is not supported.`);
return;
}
if (this.discoveredDevices.has(discoveredDevice.id)) {
const stored = this.storedDevices.get(discoveredDevice.id);
if (stored?.host !== discoveredDevice.host) {
this.log.warn(`Shelly device ${hk}${discoveredDevice.id}${wr} host ${zb}${discoveredDevice.host}${wr} has been discovered with a different host.`);
this.log.warn(`Setting the new address for shelly device ${hk}${discoveredDevice.id}${wr} from ${zb}${stored?.host}${wr} to ${zb}${discoveredDevice.host}${wr}...`);
this.discoveredDevices.set(discoveredDevice.id, discoveredDevice);
this.storedDevices.set(discoveredDevice.id, discoveredDevice);
this.changedDevices.set(discoveredDevice.id, discoveredDevice.id);
await this.saveStoredDevices();
if (this.bridgedDevices.has(discoveredDevice.id)) {
const bridgedDevice = this.bridgedDevices.get(discoveredDevice.id);
bridgedDevice.configUrl = 'http://' + discoveredDevice.host;
}
if (this.shelly.hasDevice(discoveredDevice.id)) {
const device = this.shelly.getDevice(discoveredDevice.id);
device.host = discoveredDevice.host;
if (device.gen === 1) {
this.shelly.coapServer.registerDevice(device.host, device.id, true);
}
else {
device.wsClient?.stop();
device.wsClient?.setHost(device.host);
device.wsClient?.start();
}
device.log.warn(`Shelly device ${hk}${discoveredDevice.id}${wr} host ${zb}${discoveredDevice.host}${wr} updated`);
}
else {
await this.addDevice(discoveredDevice.id, discoveredDevice.host);
}
}
else {
this.log.info(`Shelly device ${hk}${discoveredDevice.id}${nf} host ${zb}${discoveredDevice.host}${nf} already discovered`);
}
}
else {
this.discoveredDevices.set(discoveredDevice.id, discoveredDevice);
this.storedDevices.set(discoveredDevice.id, discoveredDevice);
await this.saveStoredDevices();
if (discoveredDevice.gen === 1)
this.shelly.coapServer.registerDevice(discoveredDevice.host, discoveredDevice.id, false);
await this.addDevice(discoveredDevice.id, discoveredDevice.host);
}
if (this.shelly.hasDevice(discoveredDevice.id)) {
const device = this.shelly.getDevice(discoveredDevice.id);
if (device)
device.lastseen = Date.now();
}
if ((this.firstRun === true || config.expertMode === false) &&
(discoveredDevice.id.includes('shellybutton1') ||
discoveredDevice.id.includes('shellyix3') ||
discoveredDevice.id.includes('shellyplusi4') ||
discoveredDevice.id.includes('shellyi4g3'))) {
if (!config.inputMomentaryList)
config.inputMomentaryList = [];
if (!config.inputMomentaryList.includes(discoveredDevice.id)) {
config.inputMomentaryList.push(discoveredDevice.id);
this.log.info(`Shelly device ${hk}${discoveredDevice.id}${nf} host ${zb}${discoveredDevice.host}${nf} added to inputMomentaryList`);
}
this.saveConfig(this.config);
}
});
this.shelly.on('add', async (device) => {
device.log.info(`Shelly added ${idn}${device.name}${rs} device id ${hk}${device.id}${rs}${nf} host ${zb}${device.host}${nf}`);
device.log.info(`- gen: ${CYAN}${device.gen}${nf}`);
device.log.info(`- mac: ${CYAN}${device.mac}${nf}`);
device.log.info(`- model: ${CYAN}${device.model}${nf}`);
device.log.info(`- firmware: ${CYAN}${device.firmware}${nf}`);
if (device.profile)
device.log.info(`- profile: ${CYAN}${device.profile}${nf}`);
if (device.sleepMode)
device.log.info(`- sleep: ${CYAN}${device.sleepMode}${nf}`);
device.log.info('- components:');
for (const [key, component] of device) {
device.log.info(` - ${CYAN}${key}${nf} (${GREEN}${component.name}${nf})`);
}
if (config.debug)
device.logDevice();
if (!isValidString(device.name, 1) ||
!isValidString(device.id, 1) ||
!isValidString(device.host, 1) ||
!isValidNumber(device.gen, 1, 4) ||
!isValidString(device.mac, 1) ||
!isValidString(device.model, 1) ||
!isValidString(device.firmware, 1) ||
!isValidNumber(device.getComponentNames().length, 1)) {
this.log.error(`Shelly device ${hk}${device.id}${er} host ${zb}${device.host}${er} is not valid. Please put it in the blackList and open an issue.`);
return;
}
if (config.enableBleDiscover === true) {
if (device.bthomeDevices.size && device.bthomeSensors.size) {
this.log.info(`Shelly device ${hk}${device.id}${nf} host ${zb}${device.host}${nf} is a ble gateway. Adding paired BLU devices...`);
this.gatewayDevices.set(device.id, device.id);
for (const [, bthomeDevice] of device.bthomeDevices) {
this.setSelectDevice(bthomeDevice.addr, bthomeDevice.name, 'http://' + device.host, 'ble');
if (!this.validateDevice([bthomeDevice.addr, bthomeDevice.name]))
continue;
await this.addBluDevice(device, bthomeDevice);
}
device.on('bthomedevice_update', (addr, rssi, packet_id, last_updated_ts) => {
if (!isValidString(addr, 11) || !isValidNumber(rssi, -100, 0) || !isValidNumber(packet_id, 0) || !isValidNumber(last_updated_ts))
return;
const blu = this.bluBridgedDevices.get(addr);
const bthomeDevice = device.bthomeDevices.get(addr);
if (bthomeDevice && !this.validateDevice([bthomeDevice.addr, bthomeDevice.name], false))
return;
if (!blu || !bthomeDevice) {
this.log.error(`Shelly device ${hk}${device.id}${er} host ${zb}${device.host}${er} sent an unknown BLU device address ${CYAN}${addr}${er}`);
return;
}
blu.log.info(`${idn}BLU${rs}${db} observer device update message for BLU device ${idn}${blu.deviceName ?? addr}${rs}${db}: rssi ${YELLOW}${rssi}${db} packet_id ${YELLOW}${packet_id}${db} last_updated ${YELLOW}${device.getLocalTimeFromLastUpdated(last_updated_ts)}${db}`);
});
device.on('bthomesensor_update', (addr, sensorName, sensorIndex, value) => {
if (!isValidString(addr, 11) || !isValidString(sensorName, 6) || !isValidNumber(sensorIndex, 0, 3))
return;
const blu = this.bluBridgedDevices.get(addr);
const bthomeDevice = device.bthomeDevices.get(addr);
if (bthomeDevice && !this.validateDevice([bthomeDevice.addr, bthomeDevice.name], false))
return;
if (!blu || !bthomeDevice) {
this.log.error(`Shelly device ${hk}${device.id}${er} host ${zb}${device.host}${er} sent an unknown BLU device address ${CYAN}${addr}${er}`);
return;
}
blu.log.info(`${idn}BLU${rs}${db} observer sensor update message for BLU device ${idn}${blu.deviceName ?? addr}${rs}${db}: sensor ${YELLOW}${sensorName}${db} index ${YELLOW}${sensorIndex}${db} value ${YELLOW}${value}${db}`);
if (blu && sensorName === 'Battery' && isValidNumber(value, 0, 100)) {
blu.setAttribute(PowerSource.Cluster.id, 'batPercentRemaining', value * 2, blu.log);
if (value < 10)
blu.setAttribute(PowerSource.Cluster.id, 'batChargeLevel', PowerSource.BatChargeLevel.Critical, blu.log);
else if (value < 20)
blu.setAttribute(PowerSource.Cluster.id, 'batChargeLevel', PowerSource.BatChargeLevel.Warning, blu.log);
else
blu.setAttribute(PowerSource.Cluster.id, 'batChargeLevel', PowerSource.BatChargeLevel.Ok, blu.log);
}
if (blu && sensorName === 'Temperature' && isValidNumber(value, -100, 100)) {
if (bthomeDevice.model === 'Shelly BLU Trv' && sensorIndex === 0)
blu.setAttribute(Thermostat.Cluster.id, 'occupiedHeatingSetpoint', value * 100, blu.log);
else if (bthomeDevice.model === 'Shelly BLU Trv' && sensorIndex === 1)
blu.setAttribute(Thermostat.Cluster.id, 'localTemperature', value * 100, blu.log);
else {
const child = blu.getChildEndpointByName('Temperature');
child?.setAttribute(TemperatureMeasurement.Cluster.id, 'measuredValue', value * 100, blu.log);
}
}
if (blu && sensorName === 'Humidity' && isValidNumber(value, 0, 100)) {
const child = blu.getChildEndpointByName('Humidity');
child?.setAttribute(RelativeHumidityMeasurement.Cluster.id, 'measuredValue', value * 100, blu.log);
}
if (blu && sensorName === 'Illuminance' && isValidNumber(value, 0, 10000) && this.validateEntity(bthomeDevice.addr, 'Illuminance')) {
const child = blu.getChildEndpointByName('Illuminance');
const matterLux = Math.round(Math.max(Math.min(10000 * Math.log10(value), 0xfffe), 0));
child?.setAttribute(IlluminanceMeasurement.Cluster.id, 'measuredValue', matterLux, blu.log);
}
if (blu && sensorName === 'Motion' && isValidBoolean(value)) {
const child = blu.getChildEndpointByName('Motion');
child?.setAttribute(OccupancySensing.Cluster.id, 'occupancy', { occupied: value }, blu.log);
}
if (blu && sensorName === 'Contact' && isValidBoolean(value)) {
const child = blu.getChildEndpointByName('Contact');
child?.setAttribute(BooleanState.Cluster.id, 'stateValue', !value, blu.log);
}
});
device.on('bthome_event', (event) => {
if (!isValidObject(event))
return;
device.log.info(`${idn}BLU${rs}${db} observer home event message: ${debugStringify(event)}${db}`);
if (event.event === 'device_discovered') {
this.changedDevices.set(device.id, device.id);
device.log.notice(`Shelly device ${idn}${device.name}${rs}${nt} id ${hk}${device.id}${nt} host ${zb}${device.host}${nt} discovered a new BLU device`);
}
if (event.event === 'discovery_done') {
this.changedDevices.set(device.id, device.id);
device.log.notice(`Shelly device ${idn}${device.name}${rs}${nt} id ${hk}${device.id}${nt} host ${zb}${device.host}${nt} discovery done`);
}
if (event.event === 'associations_done') {
this.changedDevices.set(device.id, device.id);
device.log.notice(`Shelly device ${idn}${device.name}${rs}${nt} id ${hk}${device.id}${nt} host ${zb}${device.host}${nt} paired a new BLU device`);
}
});
device.on('bthomedevice_event', (addr, event) => {
if (!isValidString(addr, 11) || !isValidObject(event, 3))
return;
const blu = this.bluBridgedDevices.get(addr);
const bthomeDevice = device.bthomeDevices.get(addr);
if (bthomeDevice && !this.validateDevice([bthomeDevice.addr, bthomeDevice.name], false))
return;
if (!blu || !bthomeDevice) {
this.log.error(`Shelly device ${hk}${device.id}${er} host ${zb}${device.host}${er} sent an unknown BLU device address ${CYAN}${addr}${er}`);
return;
}
blu.log.info(`${idn}BLU${rs}${db} observer device event message for BLU device ${idn}${blu?.deviceName ?? addr}${rs}${db}: event ${debugStringify(event)}${db}`);
if (event.event === 'ota_begin') {
this.changedDevices.set(device.id, device.id);
device.log.notice(`Shelly device ${idn}${device.name}${rs}${nt} id ${hk}${device.id}${nt} host ${zb}${device.host}${nt} is starting OTA`);
}
if (event.event === 'ota_progress') {
device.log.notice(`Shelly device ${idn}${device.name}${rs}${nt} id ${hk}${device.id}${nt} host ${zb}${device.host}${nt} OTA is progressing`);
}
if (event.event === 'ota_success') {
this.changedDevices.set(device.id, device.id);
device.log.notice(`Shelly device ${idn}${device.name}${rs}${nt} id ${hk}${device.id}${nt} host ${zb}${device.host}${nt} finished succesfully OTA`);
}
if (event.event === 'config_changed') {
this.changedDevices.set(device.id, device.id);
device.log.notice(`Shelly device ${idn}${device.name}${rs}${nt} id ${hk}${device.id}${nt} host ${zb}${device.host}${nt} changed BTHome device configuration`);
device
.fetchUpdate()
.then(() => {
device.updateBTHomeComponents();
device.saveDevicePayloads(this.shelly.dataPath);
return;
})
.catch((err) => {
device.log.error(`Shelly device ${idn}${device.name}${rs}${er} id ${hk}${device.id}${er} host ${zb}${device.host}${er} failed to fetch update: ${CYAN}${err}${er}`);
});
}
if (['single_push', 'double_push', 'long_push'].includes(event.event)) {
let buttonEndpoint;
if (bthomeDevice.model === 'Shelly BLU RC Button 4' || bthomeDevice.model === 'Shelly BLU RC Button 4 ZB') {
buttonEndpoint = blu.getChildEndpointByName('Button' + event.idx);
}
else if (bthomeDevice.model === 'Shelly BLU Wall Switch 4' || bthomeDevice.model === 'Shelly BLU Wall Switch 4 ZB') {
buttonEndpoint = blu.getChildEndpointByName('Button' + event.idx);
}
else if (bthomeDevice.model === 'Shelly BLU Button1' || bthomeDevice.model === 'Shelly BLU Button Tough 1 ZB') {
buttonEndpoint = blu;
}
else {
buttonEndpoint = blu.getChildEndpointByName('Button');
}
if (!buttonEndpoint) {
if ([
'Shelly BLU Button1',
'Shelly BLU Button Tough 1 ZB',
'Shelly BLU RC Button 4',
'Shelly BLU RC Button 4 ZB',
'Shelly BLU Wall Switch 4',
'Shelly BLU Wall Switch 4 ZB',
].includes(bthomeDevice.model))
blu.log.warn(`Shelly device ${idn}${blu?.deviceName ?? addr}${rs}${wr} child endpoint for button not found`);
return;
}
if (event.event === 'single_push') {
buttonEndpoint?.triggerSwitchEvent('Single', blu.log);
}
else if (event.event === 'double_push') {
buttonEndpoint?.triggerSwitchEvent('Double', blu.log);
}
else if (event.event === 'long_push') {
buttonEndpoint?.triggerSwitchEvent('Long', blu.log);
}
}
});
device.on('bthomesensor_event', (addr, sensorName, sensorIndex, event) => {
if (!isValidString(addr, 11) || !isValidString(sensorName, 6) || !isValidNumber(sensorIndex, 0, 3) || !isValidObject(event, 3))
return;
const blu = this.bluBridgedDevices.get(addr);
const bthomeDevice = device.bthomeDevices.get(addr);
if (bthomeDevice && !this.validateDevice([bthomeDevice.addr, bthomeDevice.name], false))
return;
if (!blu || !bthomeDevice) {
this.log.error(`Shelly device ${hk}${device.id}${er} host ${zb}${device.host}${er} sent an unknown BLU device address ${CYAN}${addr}${er}`);
return;
}
blu.log.info(`${idn}BLU${rs}${db} observer sensor event message for BLU device ${idn}${blu?.deviceName ?? addr}${rs}${db}: sensor ${YELLOW}${sensorName}${db} index ${YELLOW}${sensorIndex}${db} event ${debugStringify(event)}${db}`);
let buttonEndpoint;
if (bthomeDevice.model === 'Shelly BLU RC Button 4' || bthomeDevice.model === 'Shelly BLU RC Button 4 ZB') {
buttonEndpoint = blu.getChildEndpointByName('Button' + sensorIndex);
}
else if (bthomeDevice.model === 'Shelly BLU Wall Switch 4' || bthomeDevice.model === 'Shelly BLU Wall Switch 4 ZB') {
buttonEndpoint = blu.getChildEndpointByName('Button' + sensorIndex);
}
else if (bthomeDevice.model === 'Shelly BLU Button1' || bthomeDevice.model === 'Shelly BLU Button Tough 1 ZB') {
buttonEndpoint = blu;
}
else {
buttonEndpoint = blu.getChildEndpointByName('Button');
}
if (!buttonEndpoint) {
if ([
'Shelly BLU Button1',
'Shelly BLU Button Tough 1 ZB',
'Shelly BLU RC Button 4',
'Shelly BLU RC Button 4 ZB',
'Shelly BLU Wall Switch 4',
'Shelly BLU Wall Switch 4 ZB',
].includes(bthomeDevice.model))
blu.log.warn(`Shelly device ${idn}${blu?.deviceName ?? addr}${rs}${wr} child endpoint for button not found`);
return;
}
if (sensorName === 'Button') {
if (event.event === 'single_push') {
buttonEndpoint?.triggerSwitchEvent('Single', blu.log);
}
else if (event.event === 'double_push') {
buttonEndpoint?.triggerSwitchEvent('Double', blu.log);
}
else if (event.event === 'long_push') {
buttonEndpoint?.triggerSwitchEvent('Long', blu.log);
}
}
});
}
else {
this.gatewayDevices.delete(device.id);
}
}
if (!this.validateDevice([device.id, device.mac, device.name]))
return;
const deviceTypes = [bridgedNode];
if (this.validateEntity(device.id, 'PowerSource'))
deviceTypes.push(powerSource);
const mbDevice = new MatterbridgeEndpoint(deviceTypes, { id: device.name }, config.debug);
mbDevice.configUrl = `http://${device.host}`;
mbDevice.log.logName = device.name;
mbDevice.createDefaultBridgedDeviceBasicInformationClusterServer(device.name, device.id + (this.postfix ? '-' + this.postfix : ''), 0xfff1, 'Shelly', device.model, 1, device.firmware);
this.setSelectDevice(device.id, device.name, 'http://' + device.host, 'wifi', []);
if (this.validateEntity(device.id, 'PowerSource')) {
const batteryComponent = device.getComponent('battery');
const devicepowerComponent = device.getComponent('devicepower:0');
if (batteryComponent) {
let level = batteryComponent.hasProperty('level') ? batteryComponent.getValue('level') : undefined;
level = isValidNumber(level, 0, 100) ? level : undefined;
let status = PowerSource.BatChargeLevel.Ok;
if (level && level < 10)
status = PowerSource.BatChargeLevel.Critical;
else if (level && level < 20)
status = PowerSource.BatChargeLevel.Warning;
let voltage = batteryComponent.hasProperty('voltage') ? batteryComponent.getValue('voltage') : undefined;
voltage = isValidNumber(voltage, 0, 12) ? Math.round(voltage * 1000) : undefined;
if (batteryComponent.hasProperty('charging')) {
mbDevice.createDefaultPowerSourceRechargeableBatteryClusterServer(level, status, voltage);
}
else {
mbDevice.createDefaultPowerSourceReplaceableBatteryClusterServer(level, status, voltage);
}
batteryComponent.on('update', (component, property, value) => {
shellyUpdateHandler(this, mbDevice, device, component, property, value, 'PowerSource');
});
}
else if (devicepowerComponent) {
if (devicepowerComponent.hasProperty('battery') && isValidObject(devicepowerComponent.getValue('battery'), 2)) {
const battery = devicepowerComponent.getValue('battery');
if (isValidNumber(battery.V, 0, 12) && isValidNumber(battery.percent, 0, 100)) {
mbDevice.createDefaultPowerSourceReplaceableBatteryClusterServer(battery.percent, battery.percent > 20 ? PowerSource.BatChargeLevel.Ok : PowerSource.BatChargeLevel.Critical, battery.V * 1000);
}
}
else if (devicepowerComponent.hasProperty('external') && isValidObject(devicepowerComponent.getValue('external'), 1)) {
const external = devicepowerComponent.getValue('external');
if (isValidBoolean(external.present) && external.present === true) {
mbDevice.createDefaultPowerSourceWiredClusterServer();
}
}
devicepowerComponent.on('update', (component, property, value) => {
shellyUpdateHandler(this, mbDevice, device, component, property, value, 'PowerSource');
});
}
else {
mbDevice.createDefaultPowerSourceWiredClusterServer();
}
}
const names = device.getComponentNames();
if (names.includes('Light') || names.includes('Rgb')) {
mbDevice.addFixedLabel('composed', 'Light');
}
else if (names.includes('Switch') || names.includes('Relay')) {
mbDevice.addFixedLabel('composed', 'Switch');
}
else if (names.includes('Cover') || names.includes('Roller')) {
mbDevice.addFixedLabel('composed', 'Cover');
}
else if (names.includes('PowerMeter')) {
mbDevice.addFixedLabel('composed', 'PowerMeter');
}
else if (names.includes('Input')) {
mbDevice.addFixedLabel('composed', 'Input');
}
else if (names.includes('Blugw')) {
mbDevice.addFixedLabel('composed', 'BLU Gateway');
}
else {
mbDevice.addFixedLabel('composed', 'Sensor');
}
for (const [key, component] of device) {
if (!['ble', 'cloud', 'coiot', 'matter', 'zigbee', 'mqtt', 'sys', 'sntp', 'wifi_ap', 'wifi_sta', 'wifi_sta1', 'ws', 'eth'].includes(component.id)) {
this.setSelectDeviceEntity(device.id, component.name, 'All the device ' + component.name + ' components', 'component');
this.setSelectDeviceEntity(device.id, component.id, 'Device ' + component.id + ' component', 'component');
}
if (!this.validateEntity(device.id, component.name))
continue;
if (!this.validateEntity(device.id, key))
continue;
if (component.name === 'Ble') {
component.on('event', (component, event, data) => {
this.log.debug(`Received event ${CYAN}${event}${db} from component ${CYAN}${component}${db}: ${debugStringify(data)}`);
});
}
else if (component.name === 'Sys') {
component.on('update', (component, property, value) => {
device.log.debug(`Received update component ${CYAN}${component}${db} property ${CYAN}${property}${db}: ${isValidObject(value) || isValidArray(value) ? debugStringify(value) : value}`);
if (property === 'cfg_rev') {
if (!device.sleepMode)
this.changedDevices.set(device.id, device.id);
if (!device.id.startsWith('shellyblugwg3') && !device.id.startsWith('shellycolorbulb')) {
device.log.notice(`Shelly device ${idn}${device.name}${rs}${nt} id ${hk}${device.id}${nt} host ${zb}${device.host}${nt} sent config changed rev: ${CYAN}${value}${nt}`);
device.log.notice(`If the configuration on shelly device ${idn}${device.name}${rs}${nt} has changed, please restart matterbridge for the change to take effect.`);
}
}
});
component.on('event', (component, event, data) => {
device.log.debug(`Received event ${CYAN}${event}${db} from component ${CYAN}${component}${db}: ${debugStringify(data)}`);
if (event === 'component_added') {
if (!device.sleepMode)
this.changedDevices.set(device.id, device.id);
device.log.notice(`Shelly device ${idn}${device.name}${rs}${nt} id ${hk}${device.id}${nt} host ${zb}${device.host}${nt} added a component: ${CYAN}${data.target}${nt}.`);
device.log.notice(`Please restart matterbridge for the change to take effect.`);
this.wssSendRestartRequired();
}
if (event === 'component_removed') {
if (!device.sleepMode)
this.changedDevices.set(device.id, device.id);
device.log.notice(`Shelly device ${idn}${device.name}${rs}${nt} id ${hk}${device.id}${nt} host ${zb}${device.host}${nt} removed a component: ${CYAN}${data.target}${nt}.`);
device.log.notice(`Please restart matterbridge for the change to take effect.`);
this.wssSendRestartRequired();
}
if (event === 'scheduled_restart') {
if (!device.sleepMode)
this.changedDevices.set(device.id, device.id);
device.log.notice(`Shelly device ${idn}${device.name}${rs}${nt} id ${hk}${device.id}${nt} host ${zb}${device.host}${nt} is restarting in ${CYAN}${data.time_ms}${nt} ms`);
device.log.notice(`If the configuration on shelly device ${idn}${device.name}${rs}${nt} has changed, please restart matterbridge for the change to take effect.`);
}
if (event === 'config_changed') {
if (!device.sleepMode)
this.changedDevices.set(device.id, device.id);
device.log.notice(`Shelly device ${idn}${device.name}${rs}${nt} id ${hk}${device.id}${nt} host ${zb}${device.host}${nt} sent config changed rev: ${CYAN}${data.cfg_rev}${nt}`);
device.log.notice(`If the configuration on shelly device ${idn}${device.name}${rs}${nt} has changed, please restart matterbridge for the change to take effect.`);
}
if (event === 'ota_begin') {
if (!device.sleepMode)
this.changedDevices.set(device.id, device.id);
device.log.notice(`Shelly device ${idn}${device.name}${rs}${nt} id ${hk}${device.id}${nt} host ${zb}${device.host}${nt} is starting OTA`);
}
if (event === 'ota_progress') {
device.log.notice(`Shelly device ${idn}${device.name}${rs}${nt} id ${hk}${device.id}${nt} host ${zb}${device.host}${nt} OTA is progressing: ${CYAN}${data.progress_percent}${nt}%`);
}
if (event === 'ota_success') {
if (!device.sleepMode)
this.changedDevices.set(device.id, device.id);
device.log.notice(`Shelly device ${idn}${device.name}${rs}${nt} id ${hk}${device.id}${nt} host ${zb}${device.host}${nt} finished succesfully OTA`);
device.log.notice(`The firmware on shelly device ${idn}${device.name}${rs}${nt} has changed, please restart matterbridge for the change to take effect.`);
}
if (event === 'sleep') {
device.log.info(`Shelly device ${idn}${device.name}${rs}${nf} id ${hk}${device.id}${nf} host ${zb}${device.host}${nf} is sleeping`);
}
});
}
else if (isLightComponent(component)) {
let deviceType = onOffLight;
if (component.hasProperty('brightness')) {
deviceType = dimmableLight;
}
if ((component.hasProperty('temp') && device.profile !== 'color') || component.hasProperty('ct')) {
deviceType = colorTemperatureLight;
}
if ((component.hasProperty('red') && component.hasProperty('green') && component.hasProperty('blue') && device.profile !== 'white') || component.hasProperty('rgb')) {
deviceType = extendedColorLight;
}
const tagList = this.addTagList(component);
if (tagList)
this.log.debug(`***Shelly device ${idn}${device.name}${rs}${nf} id ${hk}${device.id}${nf} host ${zb}${device.host}${nf} added tagList: ${debugStringify(tagList)}`);
const child = mbDevice.addChildDeviceType(key, this.hasElectricalMeasurements(device, component) && this.validateEntity(device.id, 'PowerMeter') ? [deviceType, electricalSensor] : [deviceType], tagList ? { tagList } : undefined, config.debug);
child.log.logName = `${device.name} ${key}`;
child.createDefaultIdentifyClusterServer();
child.createDefaultGroupsClusterServer();
child.createDefaultOnOffClusterServer();
if (deviceType.code === dimmableLight.code || deviceType.code === colorTemperatureLight.code || deviceType.code === extendedColorLight.code) {
child.createDefaultLevelControlClusterServer();
}
if (deviceType.code === colorTemperatureLight.code) {
child.createCtColorControlClusterServer();
}
if (deviceType.code === extendedColorLight.code) {
child.createDefaultColorControlClusterServer();
}
this.addElectricalMeasurements(mbDevice, child, device, component);
child.addCommandHandler('identify', async ({ request }) => {
shellyIdentifyCommandHandler(child, component, request);
});
child.addCommandHandler('on', async () => {
shellyLightCommandHandler(child, component, 'On');
});
child.addCommandHandler('off', async () => {
shellyLightCommandHandler(child, component, 'Off');
});
child.addCommandHandler('toggle', async () => {
shellyLightCommandHandler(child, component, 'Toggle');
});
child.addCommandHandler('moveToLevel', async ({ request }) => {
shellyLightCommandHandler(child, component, 'Level', request.level);
});
child.addCommandHandler('moveToLevelWithOnOff', async ({ request }) => {
shellyLightCommandHandler(child, component, 'Level', request.level);
});
child.addCommandHandler('moveToHue', async ({ request }) => {
child.setAttribute(ColorControl.Cluster.id, 'colorMode', ColorControl.ColorMode.CurrentHueAndCurrentSaturation, child.log);
const saturation = child.getAttribute(ColorControl.Cluster.id, 'currentSaturation', child.log);
const rgb = hslColorToRgbColor((request.hue / 254) * 360, (saturation / 254) * 100, 50);
mbDevice.log.debug(`Sending command moveToHue => ColorRGB(${rgb.r}, ${rgb.g}, ${rgb.b})`);
if (device.colorCommandTimeout)
clearTimeout(device.colorCommandTimeout);
device.colorCommandTimeout = setTimeout(() => {
shellyLightCommandHandler(child, component, 'ColorRGB', undefined, { r: rgb.r, g: rgb.g, b: rgb.b });
}, 500);
});
child.addCommandHandler('moveToSaturation', async ({ request }) => {
child.setAttribute(ColorControl.Cluster.id, 'colorMode', ColorControl.ColorMode.CurrentHueAndCurrentSaturation, child.log);
const hue = child.getAttribute(ColorControl.Cluster.id, 'currentHue', child.log);
const rgb = hslColorToRgbColor((hue / 254) * 360, (request.saturation / 254) * 100, 50);
mbDevice.log.debug(`Sending command moveToSaturation => ColorRGB(${rgb.r}, ${rgb.g}, ${rgb.b})`);
if (device.colorCommandTimeout)
clearTimeout(device.colorCommandTimeout);
device.colorCommandTimeout = setTimeout(() => {
shellyLightCommandHandler(child, component, 'ColorRGB', undefined, { r: rgb.r, g: rgb.g, b: rgb.b });
}, 500);
});
child.addCommandHandler('moveToHueAndSaturation', async ({ request }) => {
child.setAttribute(ColorControl.Cluster.id, 'colorMode', ColorControl.ColorMode.CurrentHueAndCurrentSaturation, child.log);
const rgb = hslColorToRgbColor((request.hue / 254) * 360, (request.saturation / 254) * 100, 50);
shellyLightCommandHandler(child, component, 'ColorRGB', undefined, { r: rgb.r, g: rgb.g, b: rgb.b });
});
child.addCommandHandler('moveToColor', async ({ request }) => {
child.setAttribute(ColorControl.Cluster.id, 'colorMode', ColorControl.ColorMode.CurrentXAndCurrentY, child.log);
const rgb = xyColorToRgbColor(request.colorX / 65536, request.colorY / 65536);
shellyLightCommandHandler(child, component, 'ColorRGB', undefined, { r: rgb.r, g: rgb.g, b: rgb.b });
});
child.addCommandHandler('moveToColorTemperature', async ({ request }) => {
child.setAttribute(ColorControl.Cluster.id, 'colorMode', ColorControl.ColorMode.ColorTemperatureMireds, child.log);
if (component.hasProperty('temp')) {
shellyLightCommandHandler(child, component, 'ColorTemp', undefined, undefined, request.colorTemperatureMireds);
}
else if (component.hasProperty('ct')) {
shellyLightCommandHandler(child, component, 'ColorTemp', undefined, undefined, request.colorTemperatureMireds);
}
else {
const rgb = kelvinToRGB(miredToKelvin(request.colorTemperatureMireds));
shellyLightCommandHandler(child, component, 'ColorRGB', undefined, { r: rgb.r, g: rgb.g, b: rgb.b });