UNPKG

matterbridge-shelly

Version:
701 lines 120 kB
import { MatterbridgeDynamicPlatform, onOffSwitch, powerSource, bridgedNode, electricalSensor, onOffLight, onOffOutlet, thermostatDevice, modeSelect, coverDevice, genericSwitch, contactSensor, lightSensor, occupancySensor, temperatureSensor, humiditySensor, dimmableLight, colorTemperatureLight, MatterbridgeEndpoint, } from 'matterbridge'; import { hslColorToRgbColor, rgbColorToHslColor, isValidIpv4Address, isValidString, isValidNumber, isValidBoolean, isValidArray, isValidObject, waiter, xyColorToRgbColor, miredToKelvin, kelvinToRGB, } from 'matterbridge/utils'; import { AnsiLogger, CYAN, GREEN, YELLOW, db, debugStringify, dn, er, hk, idn, nf, nt, rs, wr, zb } from 'matterbridge/logger'; import { NodeStorageManager } from 'matterbridge/storage'; import { NumberTag } from 'matterbridge/matter'; import { VendorId } from 'matterbridge/matter/types'; import { BridgedDeviceBasicInformation, OnOff, PowerSource, WindowCovering, ColorControl, LevelControl, BooleanState, OccupancySensing, IlluminanceMeasurement, TemperatureMeasurement, RelativeHumidityMeasurement, ElectricalPowerMeasurement, ElectricalEnergyMeasurement, Thermostat, Switch, ModeSelect, } from 'matterbridge/matter/clusters'; import path from 'node:path'; import * as fs from 'node:fs'; import os from 'node:os'; import { Shelly } from './shelly.js'; import { ShellyDevice } from './shellyDevice.js'; import { isCoverComponent, isLightComponent, isSwitchComponent } from './shellyComponent.js'; import { shellyCoverCommandHandler, shellyIdentifyCommandHandler, shellyLightCommandHandler, shellySwitchCommandHandler } from './platformCommandHadlers.js'; import { shellyUpdateHandler } from './platformUpdateHandler.js'; export class ShellyPlatform extends MatterbridgeDynamicPlatform { discoveredDevices = new Map(); storedDevices = new Map(); changedDevices = new Map(); gatewayDevices = new Map(); bridgedDevices = new Map(); bluBridgedDevices = new Map(); nodeStorageManager; nodeStorage; shelly; username = ''; password = ''; postfix; failsafeCount; firstRun = false; constructor(matterbridge, log, config) { super(matterbridge, log, config); if (this.verifyMatterbridgeVersion === undefined || typeof this.verifyMatterbridgeVersion !== 'function' || !this.verifyMatterbridgeVersion('2.2.5')) { throw new Error(`This plugin requires Matterbridge version >= "2.2.5". 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 = ''; this.failsafeCount = config.failsafeCount ?? 0; if (!isValidNumber(this.failsafeCount, 0)) this.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) { if (isValidArray(config.inputEventList, 1)) config.inputMomentaryList = config.inputEventList; 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) { const shelly = matterbridge.plugins.get('matterbridge-shelly'); if (shelly && shelly.schemaJson && isValidObject(shelly.schemaJson.properties, 1)) { const properties = shelly.schemaJson.properties; delete properties.switchList; delete properties.lightList; delete properties.inputContactList; delete properties.inputLatchingList; delete properties.inputMomentaryList; delete properties.inputLatchingList; delete properties.addDevice; delete properties.removeDevice; delete properties.scanNetwork; delete properties.entityBlackList; delete properties.deviceEntityBlackList; delete properties.nocacheList; delete properties.deviceIp; delete properties.enableMdnsDiscover; delete properties.enableStorageDiscover; delete properties.resetStorageDiscover; delete properties.enableConfigDiscover; delete properties.enableBleDiscover; delete properties.failsafeCount; delete properties.postfix; delete properties.debug; delete properties.debugMdns; delete properties.debugCoap; delete properties.debugWs; delete properties.unregisterOnShutdown; } } 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.mdnsInterface; 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`); } const shelly = this.matterbridge.plugins.get('matterbridge-shelly'); if (shelly) this.matterbridge.plugins.saveConfigFromJson(shelly, 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); }); } }); 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') { buttonEndpoint = blu.getChildEndpointByName('Button' + sensorIndex); } else if (bthomeDevice.model === 'Shelly BLU Wall Switch 4') { buttonEndpoint = blu.getChildEndpointByName('Button' + sensorIndex); } else if (bthomeDevice.model === 'Shelly BLU Button1') { buttonEndpoint = blu; } else { buttonEndpoint = blu.getChildEndpointByName('Button'); } if (!buttonEndpoint) { if (['Shelly BLU Button1', 'Shelly BLU RC Button 4', 'Shelly BLU Wall Switch 4'].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, { uniqueStorageKey: 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); } } 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', '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.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.matterbridge.frontend.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.matterbridge.frontend.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('red') && component.hasProperty('green') && component.hasProperty('blue') && device.profile !== 'white') || (component.hasProperty('temp') && device.profile !== 'color') || component.hasProperty('rgb')) { deviceType = colorTemperatureLight; } const tagList = this.addTagList(component); const child = mbDevice.addChildDeviceType(key, this.hasElectricalMeasurements(device, component) ? [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) child.createDefaultLevelControlClusterServer(); if (deviceType.code === colorTemperatureLight.code) { if (component.hasProperty('temp') && component.hasProperty('mode')) child.createHsColorControlClusterServer(); else if (component.hasProperty('temp') && !component.hasProperty('mode')) child.createCtColorControlClusterServer(); else child.createHsColorControlClusterServer(); } 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 { const rgb = kelvinToRGB(miredToKelvin(request.colorTemperatureMireds)); shellyLightCommandHandler(child, component, 'ColorRGB', undefined, { r: rgb.r, g: rgb.g, b: rgb.b }); } }); component.on('update', (component, property, value) => { shellyUpdateHandler(this, mbDevice, device, component, property, value); }); } else if (isSwitchComponent(component)) { let deviceType = onOffOutlet; if (config.switchList && config.switchList.includes(device.id)) deviceType = onOffSwitch; if (config.lightList && config.lightList.includes(device.id)) deviceType = onOffLight; const tagList = this.addTagList(component); const child = mbDevice.addChildDeviceType(key, this.hasElectricalMeasurements(device, component) ? [deviceType, electricalSensor] : [deviceType], tagList ? { tagList } : undefined, config.debug); child.log.logName = `${device.name} ${key}`; child.createDefaultIdentifyClusterServer(); child.createDefaultGroupsClusterServer(); child.createDefaultOnOffClusterServer(); this.addElectricalMeasurements(mbDevice, child, device, component); child.addCommandHandler('identify', async ({ request }) => { shellyIdentifyCommandHandler(child, component, request); }); child.addCommandHandler('on', async () => { shellySwitchCommandHandler(child, component, 'On'); }); child.addCommandHandler('off', async () => { shellySwitchCommandHandler(child, component, 'Off'); }); child.addCommandHandler('toggle', async () => { shellySwitchCommandHandler(child, component, 'Toggle'); }); component.on('update', (component, property, value) => { shellyUpdateHandler(this, mbDevice, device, component, property, value); }); } else if (isCoverComponent(component)) { const tagList = this.addTagList(component); const child = mbDevice.addChildDeviceType(key, this.hasElectricalMeasurements(device, component) ? [coverDevice, electricalSensor] : [coverDevice], tagList ? { tagList } : undefined, config.debug); child.log.logName = `${device.name} ${key}`; child.createDefaultIdentifyClusterServer(); child.create