matterbridge-shelly
Version:
Matterbridge shelly plugin
304 lines (303 loc) • 22 kB
JavaScript
import { db, debugStringify, dn, er, hk, idn, or, rs, YELLOW, zb } from 'matterbridge/logger';
import { isValidArray, isValidBoolean, isValidNumber, isValidObject, isValidString, rgbColorToHslColor } from 'matterbridge/utils';
import { OnOff, PowerSource, WindowCovering, ColorControl, LevelControl, BooleanState, OccupancySensing, IlluminanceMeasurement, TemperatureMeasurement, RelativeHumidityMeasurement, Thermostat, ElectricalPowerMeasurement, ElectricalEnergyMeasurement, } from 'matterbridge/matter/clusters';
import { isLightComponent, isSwitchComponent } from './shellyComponent.js';
export function shellyUpdateHandler(platform, matterbridgeDevice, shellyDevice, component, property, value, endpointName) {
let endpoint;
if (endpointName === 'PowerSource')
endpoint = matterbridgeDevice;
if (!endpoint)
endpoint = matterbridgeDevice.getChildEndpointByName(endpointName ?? component);
if (!endpoint)
endpoint = matterbridgeDevice.getChildEndpointByName(component.replace(':', ''));
if (!endpoint) {
shellyDevice.log.debug(`shellyUpdateHandler error: endpoint ${component} not found for shelly device ${dn}${shellyDevice?.id}${db}`);
return;
}
const shellyComponent = shellyDevice.getComponent(component);
if (!shellyComponent) {
shellyDevice.log.debug(`shellyUpdateHandler error: component ${component} not found for shelly device ${dn}${shellyDevice?.id}${db}`);
return;
}
shellyDevice.log.info(`${db}Shelly message for device ${idn}${shellyDevice.id}${rs}${db} ` +
`${hk}${shellyComponent.name}${db}:${hk}${component}${db}:${zb}${property}${db}:${YELLOW}${value !== null && typeof value === 'object' ? debugStringify(value) : value}${rs}`);
if ((isLightComponent(shellyComponent) || isSwitchComponent(shellyComponent)) && property === 'state' && isValidBoolean(value)) {
endpoint.setAttribute(OnOff.Cluster.id, 'onOff', value, shellyDevice.log);
}
if (isLightComponent(shellyComponent) && (property === 'gain' || property === 'brightness') && isValidNumber(value, 0, 100)) {
endpoint.setAttribute(LevelControl.Cluster.id, 'currentLevel', Math.max(Math.min(Math.round((value / 100) * 254), 254), 1), shellyDevice.log);
}
if (isLightComponent(shellyComponent) && ['red', 'green', 'blue'].includes(property) && isValidNumber(value, 0, 255)) {
const red = property === 'red' ? value : shellyComponent.getValue('red');
const green = property === 'green' ? value : shellyComponent.getValue('green');
const blue = property === 'blue' ? value : shellyComponent.getValue('blue');
const hsl = rgbColorToHslColor({ r: red, g: green, b: blue });
matterbridgeDevice.log.debug(`ColorRgbToHsl: R:${red} G:${green} B:${blue} => H:${hsl.h} S:${hsl.s} L:${hsl.l}`);
if (shellyDevice.colorUpdateTimeout)
clearTimeout(shellyDevice.colorUpdateTimeout);
shellyDevice.colorUpdateTimeout = setTimeout(() => {
const hue = Math.max(Math.min(Math.round((hsl.h / 360) * 254), 254), 0);
const saturation = Math.max(Math.min(Math.round((hsl.s / 100) * 254), 254), 0);
if (isValidNumber(hue, 0, 254))
endpoint.setAttribute(ColorControl.Cluster.id, 'currentHue', hue, shellyDevice.log);
if (isValidNumber(saturation, 0, 254))
endpoint.setAttribute(ColorControl.Cluster.id, 'currentSaturation', saturation, shellyDevice.log);
endpoint.setAttribute(ColorControl.Cluster.id, 'colorMode', ColorControl.ColorMode.CurrentHueAndCurrentSaturation, shellyDevice.log);
}, 200);
}
if (isLightComponent(shellyComponent) && property === 'temp' && isValidNumber(value, 2700, 6500)) {
const minValue = shellyDevice.model === 'SHBDUO-1' ? 2700 : 3000;
const maxValue = 6500;
const minMatterTemp = 147;
const maxMatterTemp = 500;
const matterTemp = Math.max(Math.min(Math.round(((value - minValue) / (maxValue - minValue)) * (minMatterTemp - maxMatterTemp) + maxMatterTemp), maxMatterTemp), minMatterTemp);
matterbridgeDevice.log.debug(`ColorTemp for ${shellyDevice.model}: colorTemperature:${value} => colorTemperatureMireds:${matterTemp}`);
endpoint.setAttribute(ColorControl.Cluster.id, 'colorMode', ColorControl.ColorMode.ColorTemperatureMireds, shellyDevice.log);
endpoint.setAttribute(ColorControl.Cluster.id, 'enhancedColorMode', ColorControl.EnhancedColorMode.ColorTemperatureMireds, shellyDevice.log);
endpoint.setAttribute(ColorControl.Cluster.id, 'colorTemperatureMireds', matterTemp, shellyDevice.log);
}
if (isLightComponent(shellyComponent) &&
property === 'rgb' &&
isValidArray(value, 3, 3) &&
isValidNumber(value[0], 0, 255) &&
isValidNumber(value[1], 0, 255) &&
isValidNumber(value[2], 0, 255)) {
const hsl = rgbColorToHslColor({ r: value[0], g: value[1], b: value[2] });
matterbridgeDevice.log.debug(`ColorRgbToHsl: R:${value[0]} G:${value[1]} B:${value[2]} => H:${hsl.h} S:${hsl.s} L:${hsl.l}`);
const hue = Math.max(Math.min(Math.round((hsl.h / 360) * 254), 254), 0);
const saturation = Math.max(Math.min(Math.round((hsl.s / 100) * 254), 254), 0);
if (isValidNumber(hue, 0, 254))
endpoint.setAttribute(ColorControl.Cluster.id, 'currentHue', hue, shellyDevice.log);
if (isValidNumber(hue, 0, 254))
endpoint.setAttribute(ColorControl.Cluster.id, 'currentSaturation', saturation, shellyDevice.log);
endpoint.setAttribute(ColorControl.Cluster.id, 'colorMode', ColorControl.ColorMode.CurrentHueAndCurrentSaturation, shellyDevice.log);
}
if (shellyComponent.name === 'Input' && property === 'state' && isValidBoolean(value)) {
if (platform.config.inputContactList && platform.config.inputContactList.includes(shellyDevice.id)) {
endpoint.setAttribute(BooleanState.Cluster.id, 'stateValue', value, shellyDevice.log);
}
if (platform.config.inputMomentaryList && platform.config.inputMomentaryList.includes(shellyDevice.id) && value === true) {
endpoint.triggerSwitchEvent('Single', shellyDevice.log);
}
if (platform.config.inputLatchingList && platform.config.inputLatchingList.includes(shellyDevice.id)) {
endpoint.triggerSwitchEvent(value ? 'Press' : 'Release', shellyDevice.log);
}
}
if (shellyComponent.name === 'Input' && property === 'event_cnt' && isValidNumber(value) && shellyComponent.hasProperty('event')) {
if (platform.config.inputMomentaryList && platform.config.inputMomentaryList.includes(shellyDevice.id)) {
const event = shellyComponent.getValue('event');
if (!isValidString(event, 1))
return;
if (event === 'S') {
endpoint.triggerSwitchEvent('Single', shellyDevice.log);
}
if (event === 'SS') {
endpoint.triggerSwitchEvent('Double', shellyDevice.log);
}
if (event === 'L') {
endpoint.triggerSwitchEvent('Long', shellyDevice.log);
}
}
}
if (shellyComponent.name === 'Battery' && property === 'level' && isValidNumber(value, 0, 100)) {
endpoint.setAttribute(PowerSource.Cluster.id, 'batPercentRemaining', Math.min(Math.max(value * 2, 0), 200), shellyDevice.log);
if (value < 10)
endpoint.setAttribute(PowerSource.Cluster.id, 'batChargeLevel', PowerSource.BatChargeLevel.Critical, shellyDevice.log);
else if (value < 20)
endpoint.setAttribute(PowerSource.Cluster.id, 'batChargeLevel', PowerSource.BatChargeLevel.Warning, shellyDevice.log);
else
endpoint.setAttribute(PowerSource.Cluster.id, 'batChargeLevel', PowerSource.BatChargeLevel.Ok, shellyDevice.log);
}
if (shellyComponent.name === 'Battery' && property === 'voltage' && isValidNumber(value, 0)) {
endpoint.setAttribute(PowerSource.Cluster.id, 'batVoltage', value * 1000, shellyDevice.log);
}
if (shellyComponent.name === 'Battery' && property === 'charging' && isValidNumber(value)) {
endpoint.setAttribute(PowerSource.Cluster.id, 'batChargeState', value ? PowerSource.BatChargeState.IsCharging : PowerSource.BatChargeState.IsNotCharging, matterbridgeDevice.log);
}
if (shellyComponent.name === 'Devicepower' && property === 'battery' && isValidObject(value, 2)) {
const battery = value;
if (isValidNumber(battery.V, 0, 12) && isValidNumber(battery.percent, 0, 100)) {
endpoint.setAttribute(PowerSource.Cluster.id, 'batPercentRemaining', battery.percent * 2, shellyDevice.log);
if (battery.percent < 10)
endpoint.setAttribute(PowerSource.Cluster.id, 'batChargeLevel', PowerSource.BatChargeLevel.Critical, shellyDevice.log);
else if (battery.percent < 20)
endpoint.setAttribute(PowerSource.Cluster.id, 'batChargeLevel', PowerSource.BatChargeLevel.Warning, shellyDevice.log);
else
endpoint.setAttribute(PowerSource.Cluster.id, 'batChargeLevel', PowerSource.BatChargeLevel.Ok, shellyDevice.log);
endpoint.setAttribute(PowerSource.Cluster.id, 'batVoltage', battery.V * 1000, shellyDevice.log);
}
}
if (shellyComponent.name === 'Sensor' && property === 'motion' && isValidBoolean(value)) {
endpoint.setAttribute(OccupancySensing.Cluster.id, 'occupancy', { occupied: value }, shellyDevice.log);
}
if (shellyComponent.name === 'Sensor' && property === 'contact_open' && isValidBoolean(value)) {
endpoint.setAttribute(BooleanState.Cluster.id, 'stateValue', !value, shellyDevice.log);
}
if (shellyComponent.name === 'Flood' && property === 'flood' && isValidBoolean(value)) {
endpoint.setAttribute(BooleanState.Cluster.id, 'stateValue', !value, shellyDevice.log);
}
if (shellyComponent.name === 'Gas' && property === 'alarm_state' && isValidString(value)) {
endpoint.setAttribute(BooleanState.Cluster.id, 'stateValue', value === 'none', shellyDevice.log);
}
if (shellyComponent.name === 'Smoke' && property === 'alarm' && isValidBoolean(value)) {
endpoint.setAttribute(BooleanState.Cluster.id, 'stateValue', !value, shellyDevice.log);
}
if (shellyComponent.name === 'Lux' && property === 'value' && isValidNumber(value, 0)) {
const matterLux = Math.round(Math.max(Math.min(10000 * Math.log10(value), 0xfffe), 0));
endpoint.setAttribute(IlluminanceMeasurement.Cluster.id, 'measuredValue', matterLux, shellyDevice.log);
}
if (shellyComponent.name === 'Temperature' && (property === 'value' || property === 'tC') && isValidNumber(value, -100, +100)) {
endpoint.setAttribute(TemperatureMeasurement.Cluster.id, 'measuredValue', value * 100, shellyDevice.log);
}
if (shellyComponent.name === 'Humidity' && (property === 'value' || property === 'rh') && isValidNumber(value, 0, 100)) {
endpoint.setAttribute(RelativeHumidityMeasurement.Cluster.id, 'measuredValue', value * 100, shellyDevice.log);
}
if (shellyComponent.name === 'Illuminance' && property === 'lux' && isValidNumber(value, 0)) {
const matterLux = Math.round(Math.max(Math.min(10000 * Math.log10(value), 0xfffe), 0));
endpoint.setAttribute(IlluminanceMeasurement.Cluster.id, 'measuredValue', matterLux, shellyDevice.log);
}
if (shellyDevice.gen === 1) {
if (shellyComponent.name === 'Thermostat' && property === 'target_t' && isValidObject(value)) {
const target = value;
if (target.enabled === false)
endpoint.setAttribute(Thermostat.Cluster.id, 'systemMode', Thermostat.SystemMode.Off, shellyDevice.log);
else if (target.enabled === true)
endpoint.setAttribute(Thermostat.Cluster.id, 'systemMode', Thermostat.SystemMode.Heat, shellyDevice.log);
if (isValidNumber(target.value, 4, 31))
endpoint.setAttribute(Thermostat.Cluster.id, 'occupiedHeatingSetpoint', target.value * 100, shellyDevice.log);
}
if (shellyComponent.name === 'Thermostat' && property === 'tmp' && isValidObject(value)) {
const current = value;
if (isValidNumber(current.value, -55, +125) && current.is_valid === true)
endpoint.setAttribute(Thermostat.Cluster.id, 'localTemperature', current.value * 100, shellyDevice.log);
}
}
else if (shellyDevice.gen > 1) {
if (shellyComponent.name === 'Thermostat' && property === 'enable' && isValidBoolean(value)) {
if (value === false)
endpoint.setAttribute(Thermostat.Cluster.id, 'systemMode', Thermostat.SystemMode.Off, shellyDevice.log);
else if (value === true && shellyComponent.hasProperty('type') && shellyComponent.getValue('type') === 'heating')
endpoint.setAttribute(Thermostat.Cluster.id, 'systemMode', Thermostat.SystemMode.Heat, shellyDevice.log);
else if (value === true && shellyComponent.hasProperty('type') && shellyComponent.getValue('type') === 'cooling')
endpoint.setAttribute(Thermostat.Cluster.id, 'systemMode', Thermostat.SystemMode.Cool, shellyDevice.log);
}
if (shellyComponent.name === 'Thermostat' && property === 'current_C' && isValidNumber(value, -100, +100)) {
endpoint.setAttribute(Thermostat.Cluster.id, 'localTemperature', value * 100, shellyDevice.log);
}
if (shellyComponent.name === 'Thermostat' && property === 'target_C' && isValidNumber(value, -100, +100)) {
if (shellyComponent.hasProperty('type') && shellyComponent.getValue('type') === 'heating')
endpoint.setAttribute(Thermostat.Cluster.id, 'occupiedHeatingSetpoint', value * 100, shellyDevice.log);
if (shellyComponent.hasProperty('type') && shellyComponent.getValue('type') === 'cooling')
endpoint.setAttribute(Thermostat.Cluster.id, 'occupiedCoolingSetpoint', value * 100, shellyDevice.log);
}
}
if (shellyComponent.name === 'Vibration' && property === 'vibration' && isValidBoolean(value)) {
if (value)
endpoint.triggerSwitchEvent('Single', shellyDevice.log);
}
if (shellyComponent.name === 'Cover' || shellyComponent.name === 'Roller') {
if (property === 'state' && isValidString(value, 4)) {
if ((shellyDevice.gen === 1 && value === 'stop') || (shellyDevice.gen > 1 && value === 'stopped')) {
const status = WindowCovering.MovementStatus.Stopped;
endpoint.setAttribute(WindowCovering.Cluster.id, 'operationalStatus', { global: status, lift: status, tilt: status }, shellyDevice.log);
setTimeout(() => {
shellyDevice.log.debug(`Setting target position to current position on endpoint ${or}${endpoint.name}:${endpoint.number}${db}`);
const current = endpoint.getAttribute(WindowCovering.Cluster.id, 'currentPositionLiftPercent100ths', shellyDevice.log);
if (!isValidNumber(current, 0, 10000)) {
matterbridgeDevice.log.error(`Error: current position not found on endpoint ${or}${endpoint.name}:${endpoint.number}${er}`);
return;
}
endpoint.setAttribute(WindowCovering.Cluster.id, 'targetPositionLiftPercent100ths', current, shellyDevice.log);
}, 1000);
}
if (shellyDevice.gen > 1 && value === 'open') {
endpoint.setAttribute(WindowCovering.Cluster.id, 'targetPositionLiftPercent100ths', 0, shellyDevice.log);
endpoint.setAttribute(WindowCovering.Cluster.id, 'currentPositionLiftPercent100ths', 0, shellyDevice.log);
const status = WindowCovering.MovementStatus.Stopped;
endpoint.setAttribute(WindowCovering.Cluster.id, 'operationalStatus', { global: status, lift: status, tilt: status }, shellyDevice.log);
}
if (shellyDevice.gen > 1 && value === 'closed') {
endpoint.setAttribute(WindowCovering.Cluster.id, 'targetPositionLiftPercent100ths', 10000, shellyDevice.log);
endpoint.setAttribute(WindowCovering.Cluster.id, 'currentPositionLiftPercent100ths', 10000, shellyDevice.log);
const status = WindowCovering.MovementStatus.Stopped;
endpoint.setAttribute(WindowCovering.Cluster.id, 'operationalStatus', { global: status, lift: status, tilt: status }, shellyDevice.log);
}
if ((shellyDevice.gen === 1 && value === 'open') || (shellyDevice.gen > 1 && value === 'opening')) {
const status = WindowCovering.MovementStatus.Opening;
endpoint.setAttribute(WindowCovering.Cluster.id, 'operationalStatus', { global: status, lift: status, tilt: status }, shellyDevice.log);
}
if ((shellyDevice.gen === 1 && value === 'close') || (shellyDevice.gen > 1 && value === 'closing')) {
const status = WindowCovering.MovementStatus.Closing;
endpoint.setAttribute(WindowCovering.Cluster.id, 'operationalStatus', { global: status, lift: status, tilt: status }, shellyDevice.log);
}
}
else if (property === 'current_pos' && isValidNumber(value, 0, 100)) {
const matterPos = 10000 - Math.min(Math.max(Math.round(value * 100), 0), 10000);
endpoint.setAttribute(WindowCovering.Cluster.id, 'currentPositionLiftPercent100ths', matterPos, shellyDevice.log);
}
else if (property === 'target_pos' && isValidNumber(value, 0, 100)) {
const matterPos = 10000 - Math.min(Math.max(Math.round(value * 100), 0), 10000);
endpoint.setAttribute(WindowCovering.Cluster.id, 'targetPositionLiftPercent100ths', matterPos, shellyDevice.log);
}
}
if (['Light', 'Rgb', 'Relay', 'Switch', 'Cover', 'Roller', 'PowerMeter'].includes(shellyComponent.name)) {
if (isValidArray(platform.config.entityBlackList, 1) && platform.config.entityBlackList.includes('PowerMeter'))
return;
if ((property === 'power' || property === 'apower' || property === 'act_power') && isValidNumber(value, 0)) {
if (property === 'power' && shellyComponent.id.startsWith('light') && shellyDevice.id.startsWith('shellyrgbw2'))
return;
const power = Math.round(value * 1000) / 1000;
endpoint.setAttribute(ElectricalPowerMeasurement.Cluster.id, 'activePower', power * 1000, shellyDevice.log);
if (property === 'act_power')
return;
if (shellyComponent.id.startsWith('emeter'))
return;
if (shellyComponent.hasProperty('current'))
return;
if (property === 'power' && shellyDevice.hasComponent('sys') && shellyDevice.getComponent('sys')?.hasProperty('voltage')) {
const voltage = shellyDevice.getComponent('sys')?.getValue('voltage');
if (isValidNumber(voltage, 10)) {
endpoint.setAttribute(ElectricalPowerMeasurement.Cluster.id, 'voltage', voltage * 1000, shellyDevice.log);
const current = Math.round((value / voltage) * 1000) / 1000;
endpoint.setAttribute(ElectricalPowerMeasurement.Cluster.id, 'activeCurrent', current * 1000, shellyDevice.log);
}
}
const voltage = shellyComponent.hasProperty('voltage') ? shellyComponent.getValue('voltage') : undefined;
if (isValidNumber(voltage, 10)) {
const current = Math.round((value / voltage) * 1000) / 1000;
endpoint.setAttribute(ElectricalPowerMeasurement.Cluster.id, 'activeCurrent', current * 1000, shellyDevice.log);
}
}
if (property === 'total' && isValidNumber(value, 0)) {
const energy = Math.round(value * 1000) / 1000;
endpoint.setAttribute(ElectricalEnergyMeasurement.Cluster.id, 'cumulativeEnergyImported', { energy: energy * 1000 }, shellyDevice.log);
}
if (property === 'aenergy' && isValidObject(value) && isValidNumber(value.total, 0)) {
const energy = Math.round(value.total * 1000) / 1000;
endpoint.setAttribute(ElectricalEnergyMeasurement.Cluster.id, 'cumulativeEnergyImported', { energy: energy * 1000 }, shellyDevice.log);
}
if (property === 'total_act_energy' && isValidNumber(value, 0)) {
const energy = Math.round(value * 1000) / 1000;
endpoint.setAttribute(ElectricalEnergyMeasurement.Cluster.id, 'cumulativeEnergyImported', { energy: energy * 1000 }, shellyDevice.log);
}
if (property === 'voltage' && isValidNumber(value, 0)) {
endpoint.setAttribute(ElectricalPowerMeasurement.Cluster.id, 'voltage', value * 1000, shellyDevice.log);
}
if (property === 'current' && isValidNumber(value, 0)) {
endpoint.setAttribute(ElectricalPowerMeasurement.Cluster.id, 'activeCurrent', value * 1000, shellyDevice.log);
if (shellyComponent.hasProperty('act_power'))
return;
if (shellyComponent.hasProperty('apower'))
return;
if (shellyComponent.hasProperty('power'))
return;
if (shellyComponent.id.startsWith('emeter'))
return;
const voltage = shellyComponent.hasProperty('voltage') ? shellyComponent.getValue('voltage') : undefined;
if (isValidNumber(voltage, 0)) {
const power = Math.round(value * voltage * 1000) / 1000;
endpoint.setAttribute(ElectricalPowerMeasurement.Cluster.id, 'activePower', power * 1000, shellyDevice.log);
}
}
}
}