matterbridge-zigbee2mqtt
Version:
Matterbridge zigbee2mqtt plugin
632 lines • 101 kB
JavaScript
import { airQualitySensor, colorTemperatureSwitch, dimmableSwitch, onOffSwitch, powerSource, bridgedNode, electricalSensor, onOffLight, dimmableLight, colorTemperatureLight, onOffOutlet, coverDevice, thermostatDevice, MatterbridgeEndpoint, dimmableOutlet, doorLockDevice, occupancySensor, lightSensor, contactSensor, temperatureSensor, humiditySensor, pressureSensor, genericSwitch, } from 'matterbridge';
import { AnsiLogger, gn, dn, ign, idn, rs, db, debugStringify, hk, zb, or, nf, CYAN, er, YELLOW } from 'matterbridge/logger';
import { deepCopy, deepEqual, isValidNumber } from 'matterbridge/utils';
import * as color from 'matterbridge/utils';
import { SwitchesTag, NumberTag } from 'matterbridge/matter';
import { getClusterNameById, ClusterId } from 'matterbridge/matter/types';
import { ElectricalEnergyMeasurement, ElectricalPowerMeasurement, OnOffCluster, LevelControlCluster, WindowCoveringCluster, DoorLockCluster, BridgedDeviceBasicInformation, OnOff, LevelControl, ColorControl, ColorControlCluster, TemperatureMeasurement, BooleanState, RelativeHumidityMeasurement, PressureMeasurement, OccupancySensing, IlluminanceMeasurement, PowerSource, WindowCovering, DoorLock, ThermostatCluster, Thermostat, AirQuality, TotalVolatileOrganicCompoundsConcentrationMeasurement, CarbonDioxideConcentrationMeasurement, CarbonMonoxideConcentrationMeasurement, FormaldehydeConcentrationMeasurement, Pm1ConcentrationMeasurement, Pm25ConcentrationMeasurement, Pm10ConcentrationMeasurement, } from 'matterbridge/matter/clusters';
import EventEmitter from 'node:events';
import { hostname } from 'node:os';
export class ZigbeeEntity extends EventEmitter {
log;
serial = '';
platform;
device;
group;
entityName = '';
isDevice = false;
isGroup = false;
actions = [];
en = '';
ien = '';
bridgedDevice;
eidn = `${or}`;
lastPayload = {};
lastSeen = 0;
ignoreFeatures = [];
transition = false;
propertyMap = new Map();
mutableDevice = new Map();
colorTimeout = undefined;
thermostatTimeout = undefined;
thermostatSystemModeLookup = ['off', 'auto', '', 'cool', 'heat', '', '', 'fan_only'];
composedType = '';
hasEndpoints = false;
isRouter = false;
noUpdate = false;
constructor(platform, entity) {
super();
this.platform = platform;
if (entity.ieee_address !== undefined) {
this.device = entity;
this.entityName = entity.friendly_name;
this.isDevice = true;
this.en = dn;
this.ien = idn;
}
if (entity.id !== undefined) {
this.group = entity;
this.entityName = entity.friendly_name;
this.isGroup = true;
this.en = gn;
this.ien = ign;
}
this.log = new AnsiLogger({ logName: this.entityName, logTimestampFormat: 4, logLevel: platform.debugEnabled ? "debug" : "info" });
this.log.debug(`Created MatterEntity: ${this.entityName}`);
this.platform.z2m.on('MESSAGE-' + this.entityName, (payload) => {
const now = Date.now();
if (now - this.lastSeen < 1000 * 60 && deepEqual(this.lastPayload, payload, ['linkquality', 'last_seen', ...this.ignoreFeatures]) && !Object.prototype.hasOwnProperty.call(this.lastPayload, 'action')) {
this.log.debug(`Skipping not changed ${platform.z2mDevicesRegistered ? 'MQTT message' : 'State update'} for accessory ${this.entityName}`);
return;
}
this.lastSeen = Date.now();
if (deepEqual(this.lastPayload, payload, this.ignoreFeatures))
return;
this.lastPayload = deepCopy(payload);
if (Object.prototype.hasOwnProperty.call(this.lastPayload, 'action'))
delete this.lastPayload.action;
for (const key of this.ignoreFeatures) {
if (Object.prototype.hasOwnProperty.call(payload, key)) {
delete payload[key];
this.log.debug(`Removed key ${CYAN}${key}${db} from payload`);
}
}
if (this.bridgedDevice === undefined) {
this.log.debug(`Skipping (no device) ${platform.z2mDevicesRegistered ? 'MQTT message' : 'State update'} for accessory ${this.entityName}`);
return;
}
if (this.noUpdate) {
this.log.debug(`Skipping (no update) ${platform.z2mDevicesRegistered ? 'MQTT message' : 'State update'} for accessory ${this.entityName}`);
return;
}
this.log.info(`${db}${platform.z2mDevicesRegistered ? 'MQTT message' : 'State update'} for device ${this.ien}${this.entityName}${rs}${db} payload: ${debugStringify(payload)}`);
Object.entries(payload).forEach(([key, value]) => {
if (value === undefined || value === null)
return;
if (this.bridgedDevice === undefined || this.noUpdate)
return;
if (key === 'voltage' && this.isDevice && this.device?.power_source === 'Battery')
key = 'battery_voltage';
const propertyMap = this.propertyMap.get(key);
if (propertyMap) {
this.log.debug(`Payload entry ${CYAN}${key}${db} => name: ${CYAN}${propertyMap.name}${db} type: ${CYAN}${propertyMap.type === '' ? 'generic' : propertyMap.type}${db} ` +
`endpoint: ${CYAN}${propertyMap.endpoint === '' ? 'main' : propertyMap.endpoint}${db}`);
let z2m;
z2m = z2ms.find((z2m) => z2m.type === propertyMap?.type && z2m.property === propertyMap?.name);
if (!z2m)
z2m = z2ms.find((z2m) => z2m.property === propertyMap?.name);
if (z2m) {
if (z2m.converter || z2m.valueLookup) {
this.updateAttributeIfChanged(this.bridgedDevice, propertyMap === undefined || propertyMap.endpoint === '' ? undefined : propertyMap.endpoint, z2m.cluster, z2m.attribute, z2m.converter ? z2m.converter(value) : value, z2m.valueLookup);
return;
}
}
else
this.log.debug(`*Payload entry ${CYAN}${key}${db} not found in zigbeeToMatter converter`);
}
else
this.log.debug(`*Payload entry ${CYAN}${key}${db} not found in propertyMap`);
if (key === 'action' && value !== '') {
const propertyMap = this.propertyMap.get(('action_' + value));
if (propertyMap) {
const child = this.bridgedDevice.getChildEndpointByName(propertyMap.endpoint);
if (child && child.number)
child.triggerSwitchEvent(propertyMap.action, this.log);
}
else
this.log.debug(`*Payload entry ${CYAN}${('action_' + value)}${db} not found in propertyMap`);
}
if (key === 'position' && this.isDevice && isValidNumber(value, 0, 100)) {
this.updateAttributeIfChanged(this.bridgedDevice, undefined, WindowCovering.Cluster.id, 'currentPositionLiftPercent100ths', value * 100);
}
if (key === 'moving' && this.isDevice) {
if (value === 'UP') {
const status = WindowCovering.MovementStatus.Opening;
this.updateAttributeIfChanged(this.bridgedDevice, undefined, WindowCovering.Cluster.id, 'operationalStatus', { global: status, lift: status, tilt: status });
}
else if (value === 'DOWN') {
const status = WindowCovering.MovementStatus.Closing;
this.updateAttributeIfChanged(this.bridgedDevice, undefined, WindowCovering.Cluster.id, 'operationalStatus', { global: status, lift: status, tilt: status });
}
else if (value === 'STOP') {
const status = WindowCovering.MovementStatus.Stopped;
this.updateAttributeIfChanged(this.bridgedDevice, undefined, WindowCovering.Cluster.id, 'operationalStatus', { global: status, lift: status, tilt: status });
const position = this.bridgedDevice.getAttribute(WindowCovering.Cluster.id, 'currentPositionLiftPercent100ths', this.log);
this.updateAttributeIfChanged(this.bridgedDevice, undefined, WindowCovering.Cluster.id, 'currentPositionLiftPercent100ths', position);
this.updateAttributeIfChanged(this.bridgedDevice, undefined, WindowCovering.Cluster.id, 'targetPositionLiftPercent100ths', position);
}
}
if (key === 'current_heating_setpoint' && 'system_mode' in payload && payload['system_mode'] === 'heat' && isValidNumber(value)) {
this.updateAttributeIfChanged(this.bridgedDevice, undefined, Thermostat.Cluster.id, 'occupiedHeatingSetpoint', value * 100);
}
if (key === 'current_heating_setpoint' && 'system_mode' in payload && payload['system_mode'] === 'cool' && isValidNumber(value)) {
this.updateAttributeIfChanged(this.bridgedDevice, undefined, Thermostat.Cluster.id, 'occupiedCoolingSetpoint', value * 100);
}
if (key === 'color_temp' && 'color_mode' in payload && payload['color_mode'] === 'color_temp') {
this.updateAttributeIfChanged(this.bridgedDevice, undefined, ColorControl.Cluster.id, 'colorMode', ColorControl.ColorMode.ColorTemperatureMireds);
this.updateAttributeIfChanged(this.bridgedDevice, undefined, ColorControl.Cluster.id, 'colorTemperatureMireds', Math.max(147, Math.min(500, typeof value === 'number' ? value : 0)));
}
if (key === 'color' && 'color_mode' in payload && payload['color_mode'] === 'hs') {
const { hue, saturation } = value;
this.updateAttributeIfChanged(this.bridgedDevice, undefined, ColorControl.Cluster.id, 'colorMode', ColorControl.ColorMode.CurrentHueAndCurrentSaturation);
this.updateAttributeIfChanged(this.bridgedDevice, undefined, ColorControl.Cluster.id, 'currentHue', Math.round((hue / 360) * 254));
this.updateAttributeIfChanged(this.bridgedDevice, undefined, ColorControl.Cluster.id, 'currentSaturation', Math.round((saturation / 100) * 254));
}
if (key === 'color' && 'color_mode' in payload && payload['color_mode'] === 'xy') {
const { x, y } = value;
const hsl = color.xyToHsl(x, y);
const rgb = color.xyColorToRgbColor(x, y);
this.log.debug(`ColorControl xyToHsl ${CYAN}${x}${db} ${CYAN}${y}${db} => h ${CYAN}${hsl.h}${db} s ${CYAN}${hsl.s}${db} l ${CYAN}${hsl.l}${db}`);
this.log.debug(`ColorControl xyToRgb ${CYAN}${x}${db} ${CYAN}${y}${db} => r ${CYAN}${rgb.r}${db} g ${CYAN}${rgb.g}${db} b ${CYAN}${rgb.b}${db}`);
this.updateAttributeIfChanged(this.bridgedDevice, undefined, ColorControl.Cluster.id, 'colorMode', ColorControl.ColorMode.CurrentHueAndCurrentSaturation);
this.updateAttributeIfChanged(this.bridgedDevice, undefined, ColorControl.Cluster.id, 'currentHue', Math.round((hsl.h / 360) * 254));
this.updateAttributeIfChanged(this.bridgedDevice, undefined, ColorControl.Cluster.id, 'currentSaturation', Math.round((hsl.s / 100) * 254));
}
});
});
this.platform.z2m.on('ONLINE-' + this.entityName, () => {
this.log.info(`ONLINE message for device ${this.ien}${this.entityName}${rs}`);
if (this.bridgedDevice?.maybeNumber !== undefined) {
this.bridgedDevice?.setAttribute(BridgedDeviceBasicInformation.Cluster.id, 'reachable', true, this.log);
this.bridgedDevice?.triggerEvent(BridgedDeviceBasicInformation.Cluster.id, 'reachableChanged', { reachableNewValue: true }, this.log);
}
});
this.platform.z2m.on('OFFLINE-' + this.entityName, () => {
this.log.warn(`OFFLINE message for device ${this.ien}${this.entityName}${rs}`);
if (this.bridgedDevice?.maybeNumber !== undefined) {
this.bridgedDevice?.setAttribute(BridgedDeviceBasicInformation.Cluster.id, 'reachable', false, this.log);
this.bridgedDevice?.triggerEvent(BridgedDeviceBasicInformation.Cluster.id, 'reachableChanged', { reachableNewValue: false }, this.log);
}
});
}
destroy() {
this.removeAllListeners();
if (this.colorTimeout)
clearTimeout(this.colorTimeout);
this.colorTimeout = undefined;
if (this.thermostatTimeout)
clearTimeout(this.thermostatTimeout);
this.thermostatTimeout = undefined;
this.device = undefined;
this.group = undefined;
this.bridgedDevice = undefined;
this.mutableDevice.clear();
this.propertyMap.clear();
}
addBridgedDeviceBasicInformation() {
if (!this.bridgedDevice)
throw new Error('No bridged device');
const softwareVersion = parseInt(this.platform.z2mBridgeInfo?.version || '1');
const softwareVersionString = `${this.platform.z2mBridgeInfo?.version} (commit ${this.platform.z2mBridgeInfo?.commit})`;
const hardwareVersion = parseInt(this.platform.matterbridge.matterbridgeVersion || '1');
const hardwareVersionString = this.platform.matterbridge.matterbridgeVersion || 'unknown';
if (this.isDevice && this.device && this.device.friendly_name === 'Coordinator') {
this.bridgedDevice.createDefaultBridgedDeviceBasicInformationClusterServer(this.device.friendly_name, this.serial, 0xfff1, 'zigbee2MQTT', 'Coordinator', softwareVersion, softwareVersionString, hardwareVersion, hardwareVersionString);
return this.bridgedDevice;
}
else if (this.isDevice && this.device) {
this.bridgedDevice.createDefaultBridgedDeviceBasicInformationClusterServer(this.device.friendly_name, this.serial, 0xfff1, this.device.definition ? this.device.definition.vendor : this.device.manufacturer, this.device.definition ? this.device.definition.model : this.device.model_id, softwareVersion, softwareVersionString, hardwareVersion, hardwareVersionString);
return this.bridgedDevice;
}
if (!this.group)
throw new Error('No group found');
this.bridgedDevice.createDefaultBridgedDeviceBasicInformationClusterServer(this.group.friendly_name, this.serial, 0xfff1, 'zigbee2MQTT', 'Group', softwareVersion, softwareVersionString, hardwareVersion, hardwareVersionString);
return this.bridgedDevice;
}
addPowerSource() {
if (!this.bridgedDevice)
throw new Error('No bridged device');
if (this.isDevice) {
if (this.device?.power_source === 'Battery') {
this.bridgedDevice.createDefaultPowerSourceReplaceableBatteryClusterServer(100, PowerSource.BatChargeLevel.Ok);
}
else {
this.bridgedDevice.createDefaultPowerSourceWiredClusterServer();
}
}
if (this.isGroup) {
this.bridgedDevice.createDefaultPowerSourceWiredClusterServer();
}
return this.bridgedDevice;
}
verifyMutableDevice(endpoint) {
if (!endpoint)
return false;
for (const deviceType of endpoint.getDeviceTypes()) {
for (const clusterId of deviceType.requiredServerClusters) {
if (!endpoint.hasClusterServer(clusterId)) {
endpoint.addClusterServers([clusterId]);
this.log.warn(`Endpoint with device type ${deviceType.name} (0x${deviceType.code.toString(16)}) requires cluster server ${getClusterNameById(clusterId)} (0x${clusterId.toString(16)}) but it is not present on endpoint`);
}
}
}
for (const childEndpoint of endpoint.getChildEndpoints()) {
for (const deviceType of childEndpoint.getDeviceTypes()) {
for (const clusterId of deviceType.requiredServerClusters) {
if (!childEndpoint.hasClusterServer(clusterId)) {
childEndpoint.addClusterServers([clusterId]);
this.log.warn(`Child endpoint with device type ${deviceType.name} (0x${deviceType.code.toString(16)}) requires cluster server ${getClusterNameById(clusterId)} (0x${clusterId.toString(16)}) but it is not present on child endpoint`);
}
}
}
}
return true;
}
async configure() {
if (this.bridgedDevice?.hasClusterServer(WindowCovering.Cluster.id)) {
this.log.info(`Configuring ${this.bridgedDevice?.deviceName} WindowCovering cluster`);
await this.bridgedDevice?.setWindowCoveringTargetAsCurrentAndStopped();
}
if (this.bridgedDevice?.hasClusterServer(DoorLock.Cluster.id)) {
this.log.info(`Configuring ${this.bridgedDevice?.deviceName} DoorLock cluster`);
const state = this.bridgedDevice?.getAttribute(DoorLock.Cluster.id, 'lockState', this.log);
if (this.bridgedDevice.maybeNumber) {
if (state === DoorLock.LockState.Locked)
this.bridgedDevice?.triggerEvent(DoorLock.Cluster.id, 'lockOperation', { lockOperationType: DoorLock.LockOperationType.Lock, operationSource: DoorLock.OperationSource.Manual, userIndex: null, fabricIndex: null, sourceNode: null }, this.log);
if (state === DoorLock.LockState.Unlocked)
this.bridgedDevice?.triggerEvent(DoorLock.Cluster.id, 'lockOperation', { lockOperationType: DoorLock.LockOperationType.Unlock, operationSource: DoorLock.OperationSource.Manual, userIndex: null, fabricIndex: null, sourceNode: null }, this.log);
}
}
}
updateAttributeIfChanged(deviceEndpoint, childEndpointName, clusterId, attributeName, value, lookup) {
if (value === undefined)
return;
if (childEndpointName && childEndpointName !== '') {
deviceEndpoint = this.bridgedDevice?.getChildEndpointByName(childEndpointName) ?? deviceEndpoint;
}
if (!deviceEndpoint.hasClusterServer(ClusterId(clusterId))) {
this.log.debug(`Update endpoint ${this.eidn}${deviceEndpoint.name}:${deviceEndpoint.number}${db}${childEndpointName ? ' (' + zb + childEndpointName + db + ')' : ''} cluster ${hk}${clusterId}${db}-${hk}${getClusterNameById(ClusterId(clusterId))}${db} not found: is z2m converter exposing all features?`);
return;
}
if (!deviceEndpoint.hasAttributeServer(ClusterId(clusterId), attributeName)) {
this.log.debug(`Update endpoint ${this.eidn}${deviceEndpoint.name}:${deviceEndpoint.number}${db}${childEndpointName ? ' (' + zb + childEndpointName + db + ')' : ''} error attribute ${hk}${clusterId}${db}-${hk}${getClusterNameById(ClusterId(clusterId))}${db}.${hk}${attributeName}${db} not found`);
return;
}
if (lookup !== undefined) {
if (typeof value === 'string' && lookup.indexOf(value) !== -1) {
value = lookup.indexOf(value);
}
else {
this.log.debug(`Update endpoint ${this.eidn}${deviceEndpoint.name}:${deviceEndpoint.name}:${deviceEndpoint.number}${db}${childEndpointName ? ' (' + zb + childEndpointName + db + ')' : ''} ` +
`attribute ${hk}${getClusterNameById(ClusterId(clusterId))}${db}.${hk}${attributeName}${db} value ${zb}${typeof value === 'object' ? debugStringify(value) : value}${db} not found in lookup ${debugStringify(lookup)}`);
return;
}
}
const localValue = deviceEndpoint.getAttribute(ClusterId(clusterId), attributeName, undefined);
if (typeof value === 'object' ? deepEqual(value, localValue) : value === localValue) {
this.log.debug(`Skip update endpoint ${deviceEndpoint.name}:${deviceEndpoint.number}${childEndpointName ? ' (' + childEndpointName + ')' : ''} ` +
`attribute ${getClusterNameById(ClusterId(clusterId))}.${attributeName} already ${typeof value === 'object' ? debugStringify(value) : value}`);
return;
}
this.log.info(`${db}Update endpoint ${this.eidn}${deviceEndpoint.name}:${deviceEndpoint.number}${db}${childEndpointName ? ' (' + zb + childEndpointName + db + ')' : ''} ` +
`attribute ${hk}${getClusterNameById(ClusterId(clusterId))}${db}.${hk}${attributeName}${db} from ${YELLOW}${typeof localValue === 'object' ? debugStringify(localValue) : localValue}${db} to ${YELLOW}${typeof value === 'object' ? debugStringify(value) : value}${db}`);
try {
deviceEndpoint.setAttribute(ClusterId(clusterId), attributeName, value);
}
catch (error) {
this.log.error(`Error setting attribute ${hk}${getClusterNameById(ClusterId(clusterId))}${er}.${hk}${attributeName}${er} to ${value}: ${error}`);
}
}
publishCommand(command, entityName, payload) {
this.log.debug(`executeCommand ${command} called for ${this.ien}${entityName}${rs}${db} payload: ${debugStringify(payload)}`);
if (entityName.startsWith('bridge/request')) {
this.platform.publish(entityName, '', JSON.stringify(payload));
}
else {
this.platform.publish(entityName, 'set', JSON.stringify(payload));
}
}
logPropertyMap() {
this.propertyMap.forEach((value, key) => {
this.log.debug(`Property ${CYAN}${key}${db} name ${CYAN}${value.name}${db} type ${CYAN}${value.type === '' ? 'generic' : value.type}${db} endpoint ${CYAN}${value.endpoint === '' ? 'main' : value.endpoint}${db} ` +
`category ${CYAN}${value.category}${db} description ${CYAN}${value.description}${db} label ${CYAN}${value.label}${db} unit ${CYAN}${value.unit}${db} ` +
`values ${CYAN}${value.values}${db} value_min ${CYAN}${value.value_min}${db} value_max ${CYAN}${value.value_max}${db}`);
});
}
}
export class ZigbeeGroup extends ZigbeeEntity {
constructor(platform, group) {
super(platform, group);
}
static async create(platform, group) {
const zigbeeGroup = new ZigbeeGroup(platform, group);
if (zigbeeGroup.platform.postfixHostname) {
zigbeeGroup.serial = `group-${group.id}_${hostname}`.slice(0, 32);
}
else if (zigbeeGroup.platform.postfix !== '') {
zigbeeGroup.serial = `group-${group.id}-${zigbeeGroup.platform.postfix}`.slice(0, 32);
}
else {
zigbeeGroup.serial = `group-${group.id}`.slice(0, 32);
}
platform.setSelectDevice(`group-${group.id}`, group.friendly_name, 'wifi');
let useState = false;
let useBrightness = false;
let useColor = false;
let useColorTemperature = false;
let minColorTemperature = 140;
let maxColorTemperature = 500;
let isSwitch = false;
let isLight = false;
let isCover = false;
let isThermostat = false;
if (group.members.length === 0) {
zigbeeGroup.log.debug(`Group: ${gn}${group.friendly_name}${rs}${db} is a ${CYAN}virtual${db} group`);
zigbeeGroup.bridgedDevice = new MatterbridgeEndpoint([onOffSwitch, bridgedNode, powerSource], { uniqueStorageKey: group.friendly_name }, zigbeeGroup.log.logLevel === "debug");
isSwitch = true;
zigbeeGroup.propertyMap.set('state', { name: 'state', type: 'switch', endpoint: '' });
}
else {
group.members.forEach((member) => {
const device = zigbeeGroup.platform.z2mBridgeDevices?.find((device) => device.ieee_address === member.ieee_address);
if (!device)
return;
zigbeeGroup.log.debug(`Group ${gn}${group.friendly_name}${db}: member device ${dn}${device.friendly_name}${db}`);
device.definition?.exposes.forEach((expose) => {
if (expose.features) {
expose.features?.forEach((feature) => {
if (expose.type === 'lock' && feature.name === 'state' && feature.property === 'child_lock') {
expose.type = 'child_lock';
feature.name = 'child_lock';
}
zigbeeGroup.log.debug(`- specific type ${CYAN}${expose.type}${db}${feature.endpoint ? ' endpoint ' + CYAN + feature.endpoint + db : ''}${db} feature name ${CYAN}${feature.name}${db} property ${CYAN}${feature.property}${db} min ${CYAN}${feature.value_min}${db} max ${CYAN}${feature.value_max}${db}`);
if (expose.type === 'switch' || expose.type === 'light') {
if (expose.type === 'switch')
isSwitch = true;
if (expose.type === 'light')
isLight = true;
useState = useState === true || feature.name === 'state' ? true : false;
useBrightness = useBrightness === true || feature.name === 'brightness' ? true : false;
useColor = useColor === true || feature.property === 'color' ? true : false;
useColorTemperature = useColorTemperature === true || feature.name === 'color_temp' ? true : false;
if (feature.value_min)
minColorTemperature = Math.min(minColorTemperature, feature.value_min);
if (feature.value_max)
maxColorTemperature = Math.max(maxColorTemperature, feature.value_max);
}
else if (expose.type === 'cover') {
isCover = true;
}
else if (expose.type === 'climate') {
isThermostat = true;
}
});
}
else {
zigbeeGroup.log.debug(`- generic type ${CYAN}${expose.type}${db} expose name ${CYAN}${expose.name}${db} property ${CYAN}${expose.property}${db}`);
}
});
});
zigbeeGroup.log.debug(`Group ${gn}${group.friendly_name}${rs}${db} switch: ${CYAN}${isSwitch}${db} light: ${CYAN}${isLight}${db} cover: ${CYAN}${isCover}${db} thermostat: ${CYAN}${isThermostat}${db}`);
zigbeeGroup.log.debug(`Group ${gn}${group.friendly_name}${rs}${db} state: ${CYAN}${useState}${db} brightness: ${CYAN}${useBrightness}${db} color: ${CYAN}${useColor}${db} color_temp: ${CYAN}${useColorTemperature}${db} min: ${CYAN}${minColorTemperature}${db} max: ${CYAN}${maxColorTemperature}${db}`);
let deviceType;
if (useState) {
deviceType = onOffLight;
if (platform.switchList.includes(group.friendly_name))
deviceType = onOffSwitch;
else if (platform.lightList.includes(group.friendly_name))
deviceType = onOffLight;
else if (platform.outletList.includes(group.friendly_name))
deviceType = onOffOutlet;
zigbeeGroup.propertyMap.set('state', { name: 'state', type: isLight ? 'light' : 'switch', endpoint: '' });
}
if (useBrightness) {
deviceType = dimmableLight;
zigbeeGroup.propertyMap.set('brightness', { name: 'brightness', type: 'light', endpoint: '' });
}
if (useColorTemperature) {
deviceType = colorTemperatureLight;
zigbeeGroup.propertyMap.set('color_temp', { name: 'color_temp', type: 'light', endpoint: '' });
}
if (useColor) {
deviceType = colorTemperatureLight;
zigbeeGroup.propertyMap.set('color', { name: 'color', type: 'light', endpoint: '' });
}
if (isCover) {
deviceType = coverDevice;
zigbeeGroup.propertyMap.set('state', { name: 'state', type: 'cover', endpoint: '' });
zigbeeGroup.propertyMap.set('position', { name: 'position', type: 'cover', endpoint: '' });
zigbeeGroup.propertyMap.set('moving', { name: 'moving', type: 'cover', endpoint: '' });
}
if (isThermostat) {
deviceType = thermostatDevice;
zigbeeGroup.propertyMap.set('local_temperature', { name: 'local_temperature', type: 'climate', endpoint: '' });
zigbeeGroup.propertyMap.set('current_heating_setpoint', { name: 'current_heating_setpoint', type: 'climate', endpoint: '' });
zigbeeGroup.propertyMap.set('current_cooling_setpoint', { name: 'current_cooling_setpoint', type: 'climate', endpoint: '' });
zigbeeGroup.propertyMap.set('running_state', { name: 'running_state', type: 'climate', endpoint: '' });
zigbeeGroup.propertyMap.set('system_mode', { name: 'system_mode', type: 'climate', endpoint: '' });
}
if (!deviceType)
return zigbeeGroup;
zigbeeGroup.bridgedDevice = new MatterbridgeEndpoint([deviceType, bridgedNode, powerSource], { uniqueStorageKey: group.friendly_name }, zigbeeGroup.log.logLevel === "debug");
}
if (platform.config.groupScenes === true) {
group.scenes.forEach((scene) => {
zigbeeGroup.log.debug(`***Group ${gn}${group.friendly_name}${rs}${db} scene ${CYAN}${scene.name}${db} id ${CYAN}${scene.id}${db}`);
});
}
zigbeeGroup.addBridgedDeviceBasicInformation();
zigbeeGroup.addPowerSource();
zigbeeGroup.bridgedDevice.addRequiredClusterServers();
if (!zigbeeGroup.bridgedDevice || !zigbeeGroup.verifyMutableDevice(zigbeeGroup.bridgedDevice))
return zigbeeGroup;
zigbeeGroup.mutableDevice.clear();
zigbeeGroup.logPropertyMap();
if (isSwitch || isLight) {
if (isSwitch && !isLight)
await zigbeeGroup.bridgedDevice.addFixedLabel('type', 'switch');
if (isLight)
await zigbeeGroup.bridgedDevice.addFixedLabel('type', 'light');
zigbeeGroup.bridgedDevice.addCommandHandler('identify', async ({ request: { identifyTime } }) => {
zigbeeGroup.log.warn(`Command identify called for ${zigbeeGroup.ien}${group.friendly_name}${rs}${db} identifyTime:${identifyTime}`);
});
zigbeeGroup.bridgedDevice.addCommandHandler('on', async () => {
zigbeeGroup.log.debug(`Command on called for ${zigbeeGroup.ien}${group.friendly_name}${rs}${db}`);
zigbeeGroup.publishCommand('on', group.friendly_name, { state: 'ON' });
});
zigbeeGroup.bridgedDevice.addCommandHandler('off', async () => {
zigbeeGroup.log.debug(`Command off called for ${zigbeeGroup.ien}${group.friendly_name}${rs}${db}`);
zigbeeGroup.publishCommand('off', group.friendly_name, { state: 'OFF' });
});
zigbeeGroup.bridgedDevice.addCommandHandler('toggle', async () => {
zigbeeGroup.log.debug(`Command toggle called for ${zigbeeGroup.ien}${group.friendly_name}${rs}${db}`);
zigbeeGroup.publishCommand('toggle', group.friendly_name, { state: 'TOGGLE' });
});
}
if (isLight) {
if (useBrightness) {
zigbeeGroup.bridgedDevice.addCommandHandler('moveToLevel', async ({ request: { level } }) => {
zigbeeGroup.log.debug(`Command moveToLevel called for ${zigbeeGroup.ien}${group.friendly_name}${rs}${db} request: ${level}`);
zigbeeGroup.publishCommand('moveToLevel', group.friendly_name, { brightness: level });
});
zigbeeGroup.bridgedDevice.addCommandHandler('moveToLevelWithOnOff', async ({ request: { level } }) => {
zigbeeGroup.log.debug(`Command moveToLevelWithOnOff called for ${zigbeeGroup.ien}${group.friendly_name}${rs}${db} request: ${level}`);
zigbeeGroup.publishCommand('moveToLevelWithOnOff', group.friendly_name, { brightness: level });
});
}
if (useColorTemperature) {
zigbeeGroup.bridgedDevice.addCommandHandler('moveToColorTemperature', async ({ request: request }) => {
zigbeeGroup.log.debug(`Command moveToColorTemperature called for ${zigbeeGroup.ien}${group.friendly_name}${rs}${db} request: ${request.colorTemperatureMireds}`);
await zigbeeGroup.bridgedDevice?.setAttribute(ColorControl.Cluster.id, 'colorMode', ColorControl.ColorMode.ColorTemperatureMireds);
zigbeeGroup.publishCommand('moveToColorTemperature', group.friendly_name, { color_temp: request.colorTemperatureMireds });
});
}
if (useColor) {
let lastRequestedHue = 0;
let lastRequestedSaturation = 0;
zigbeeGroup.bridgedDevice.addCommandHandler('moveToHue', async ({ request: request }) => {
zigbeeGroup.log.debug(`Command moveToHue called for ${zigbeeGroup.ien}${group.friendly_name}${rs}${db} request: ${request.hue}`);
await zigbeeGroup.bridgedDevice?.setAttribute(ColorControl.Cluster.id, 'colorMode', ColorControl.ColorMode.CurrentHueAndCurrentSaturation);
lastRequestedHue = request.hue;
zigbeeGroup.colorTimeout = setTimeout(() => {
clearTimeout(zigbeeGroup.colorTimeout);
const rgb = color.hslColorToRgbColor((request.hue / 254) * 360, (lastRequestedSaturation / 254) * 100, 50);
zigbeeGroup.publishCommand('moveToHue', group.friendly_name, { color: { r: rgb.r, g: rgb.g, b: rgb.b } });
}, 500);
});
zigbeeGroup.bridgedDevice.addCommandHandler('moveToSaturation', async ({ request: request }) => {
zigbeeGroup.log.debug(`Command moveToSaturation called for ${zigbeeGroup.ien}${group.friendly_name}${rs}${db} request: ${request.saturation}`);
await zigbeeGroup.bridgedDevice?.setAttribute(ColorControl.Cluster.id, 'colorMode', ColorControl.ColorMode.CurrentHueAndCurrentSaturation);
lastRequestedSaturation = request.saturation;
zigbeeGroup.colorTimeout = setTimeout(() => {
clearTimeout(zigbeeGroup.colorTimeout);
const rgb = color.hslColorToRgbColor((lastRequestedHue / 254) * 360, (request.saturation / 254) * 100, 50);
zigbeeGroup.publishCommand('moveToSaturation', group.friendly_name, { color: { r: rgb.r, g: rgb.g, b: rgb.b } });
}, 500);
});
zigbeeGroup.bridgedDevice.addCommandHandler('moveToHueAndSaturation', async ({ request: request }) => {
zigbeeGroup.log.debug(`Command moveToHueAndSaturation called for ${zigbeeGroup.ien}${group.friendly_name}${rs}${db} request: ${request.hue}-${request.saturation}`);
await zigbeeGroup.bridgedDevice?.setAttribute(ColorControl.Cluster.id, 'colorMode', ColorControl.ColorMode.CurrentHueAndCurrentSaturation);
const rgb = color.hslColorToRgbColor((request.hue / 254) * 360, (request.saturation / 254) * 100, 50);
zigbeeGroup.publishCommand('moveToHueAndSaturation', group.friendly_name, { color: { r: rgb.r, g: rgb.g, b: rgb.b } });
});
}
}
if (isCover) {
await zigbeeGroup.bridgedDevice.addFixedLabel('type', 'cover');
zigbeeGroup.bridgedDevice.addCommandHandler('upOrOpen', async () => {
zigbeeGroup.log.debug(`Command upOrOpen called for ${zigbeeGroup.ien}${group.friendly_name}${rs}${db}`);
await zigbeeGroup.bridgedDevice?.setWindowCoveringCurrentTargetStatus(0, 0, WindowCovering.MovementStatus.Stopped);
zigbeeGroup.publishCommand('upOrOpen', group.friendly_name, { state: 'OPEN' });
});
zigbeeGroup.bridgedDevice.addCommandHandler('downOrClose', async () => {
zigbeeGroup.log.debug(`Command downOrClose called for ${zigbeeGroup.ien}${group.friendly_name}${rs}${db}`);
await zigbeeGroup.bridgedDevice?.setWindowCoveringCurrentTargetStatus(10000, 10000, WindowCovering.MovementStatus.Stopped);
zigbeeGroup.publishCommand('downOrClose', group.friendly_name, { state: 'CLOSE' });
});
zigbeeGroup.bridgedDevice.addCommandHandler('stopMotion', async () => {
zigbeeGroup.log.debug(`Command stopMotion called for ${zigbeeGroup.ien}${group.friendly_name}${rs}${db}`);
await zigbeeGroup.bridgedDevice?.setWindowCoveringTargetAsCurrentAndStopped();
zigbeeGroup.publishCommand('stopMotion', group.friendly_name, { state: 'STOP' });
});
zigbeeGroup.bridgedDevice.addCommandHandler('goToLiftPercentage', async ({ request: { liftPercent100thsValue } }) => {
zigbeeGroup.log.debug(`Command goToLiftPercentage called for ${zigbeeGroup.ien}${group.friendly_name}${rs}${db} liftPercent100thsValue: ${liftPercent100thsValue}`);
await zigbeeGroup.bridgedDevice?.setWindowCoveringCurrentTargetStatus(liftPercent100thsValue, liftPercent100thsValue, WindowCovering.MovementStatus.Stopped);
zigbeeGroup.publishCommand('goToLiftPercentage', group.friendly_name, { position: 100 - liftPercent100thsValue / 100 });
});
}
if (isThermostat) {
await zigbeeGroup.bridgedDevice.addFixedLabel('type', 'climate');
zigbeeGroup.bridgedDevice.subscribeAttribute(ThermostatCluster.id, 'systemMode', (newValue, oldValue) => {
zigbeeGroup.bridgedDevice?.log.info(`Thermostat systemMode changed from ${oldValue} to ${newValue}`);
if (oldValue !== newValue) {
zigbeeGroup.bridgedDevice?.log.info(`Setting thermostat systemMode to ${newValue}`);
if (newValue === Thermostat.SystemMode.Off) {
zigbeeGroup.publishCommand('SystemMode', group.friendly_name, { system_mode: 'off' });
}
else if (newValue === Thermostat.SystemMode.Heat) {
zigbeeGroup.publishCommand('SystemMode', group.friendly_name, { system_mode: 'heat' });
}
else if (newValue === Thermostat.SystemMode.Cool) {
zigbeeGroup.publishCommand('SystemMode', group.friendly_name, { system_mode: 'cool' });
}
zigbeeGroup.noUpdate = true;
zigbeeGroup.thermostatTimeout = setTimeout(() => {
zigbeeGroup.noUpdate = false;
}, 2 * 1000);
}
}, zigbeeGroup.log);
zigbeeGroup.bridgedDevice.subscribeAttribute(ThermostatCluster.id, 'occupiedHeatingSetpoint', (newValue, oldValue) => {
zigbeeGroup.bridgedDevice?.log.info(`Thermostat occupiedHeatingSetpoint changed from ${oldValue / 100} to ${newValue / 100}`);
zigbeeGroup.bridgedDevice?.log.info(`Setting thermostat occupiedHeatingSetpoint to ${newValue / 100}`);
zigbeeGroup.publishCommand('CurrentHeatingSetpoint', group.friendly_name, { current_heating_setpoint: Math.round(newValue / 100) });
zigbeeGroup.publishCommand('OccupiedHeatingSetpoint', group.friendly_name, { occupied_heating_setpoint: Math.round(newValue / 100) });
zigbeeGroup.noUpdate = true;
zigbeeGroup.thermostatTimeout = setTimeout(() => {
zigbeeGroup.noUpdate = false;
}, 2 * 1000);
}, zigbeeGroup.log);
zigbeeGroup.bridgedDevice.subscribeAttribute(ThermostatCluster.id, 'occupiedCoolingSetpoint', (newValue, oldValue) => {
zigbeeGroup.bridgedDevice?.log.info(`Thermostat occupiedCoolingSetpoint changed from ${oldValue / 100} to ${newValue / 100}`);
zigbeeGroup.bridgedDevice?.log.info(`Setting thermostat occupiedCoolingSetpoint to ${newValue / 100}`);
zigbeeGroup.publishCommand('CurrentCoolingSetpoint', group.friendly_name, { current_heating_setpoint: Math.round(newValue / 100) });
zigbeeGroup.publishCommand('OccupiedCoolingSetpoint', group.friendly_name, { occupied_cooling_setpoint: Math.round(newValue / 100) });
zigbeeGroup.noUpdate = true;
zigbeeGroup.thermostatTimeout = setTimeout(() => {
zigbeeGroup.noUpdate = false;
}, 2 * 1000);
}, zigbeeGroup.log);
}
return zigbeeGroup;
}
}
export const z2ms = [
{ type: 'switch', name: 'state', property: 'state', deviceType: onOffSwitch, cluster: OnOff.Cluster.id, attribute: 'onOff', converter: (value) => { return value === 'ON' ? true : false; } },
{ type: 'switch', name: 'brightness', property: 'brightness', deviceType: dimmableSwitch, cluster: LevelControl.Cluster.id, attribute: 'currentLevel', converter: (value) => { return Math.max(1, Math.min(254, value)); } },
{ type: 'switch', name: 'color_hs', property: 'color_hs', deviceType: colorTemperatureSwitch, cluster: ColorControl.Cluster.id, attribute: 'colorMode' },
{ type: 'switch', name: 'color_xy', property: 'color_xy', deviceType: colorTemperatureSwitch, cluster: ColorControl.Cluster.id, attribute: 'colorMode' },
{ type: 'switch', name: 'color_temp', property: 'color_temp', deviceType: colorTemperatureSwitch, cluster: ColorControl.Cluster.id, attribute: 'colorMode' },
{ type: 'outlet', name: 'state', property: 'state', deviceType: onOffOutlet, cluster: OnOff.Cluster.id, attribute: 'onOff', converter: (value) => { return value === 'ON' ? true : false; } },
{ type: 'outlet', name: 'brightness', property: 'brightness', deviceType: dimmableOutlet, cluster: LevelControl.Cluster.id, attribute: 'currentLevel', converter: (value) => { return Math.max(1, Math.min(254, value)); } },
{ type: 'light', name: 'state', property: 'state', deviceType: onOffLight, cluster: OnOff.Cluster.id, attribute: 'onOff', converter: (value) => { return value === 'ON' ? true : false; } },
{ type: 'light', name: 'brightness', property: 'brightness', deviceType: dimmableLight, cluster: LevelControl.Cluster.id, attribute: 'currentLevel', converter: (value) => { return Math.max(1, Math.min(254, value)); } },
{ type: 'light', name: 'color_hs', property: 'color_hs', deviceType: colorTemperatureLight, cluster: ColorControl.Cluster.id, attribute: 'colorMode' },
{ type: 'light', name: 'color_xy', property: 'color_xy', deviceType: colorTemperatureLight, cluster: ColorControl.Cluster.id, attribute: 'colorMode' },
{ type: 'light', name: 'color_temp', property: 'color_temp', deviceType: colorTemperatureLight, cluster: ColorControl.Cluster.id, attribute: 'colorMode' },
{ type: 'cover', name: 'state', property: 'state', deviceType: coverDevice, cluster: WindowCovering.Cluster.id, attribute: 'targetPositionLiftPercent100ths' },
{ type: 'cover', name: 'moving', property: 'moving', deviceType: coverDevice, cluster: WindowCovering.Cluster.id, attribute: 'operationalStatus' },
{ type: 'cover', name: 'position', property: 'position', deviceType: coverDevice, cluster: WindowCovering.Cluster.id, attribute: 'currentPositionLiftPercent100ths' },
{ type: 'lock', name: 'state', property: 'state', deviceType: doorLockDevice, cluster: DoorLock.Cluster.id, attribute: 'lockState', converter: (value) => { return value === 'LOCK' ? DoorLock.LockState.Locked : DoorLock.LockState.Unlocked; } },
{ type: 'climate', name: 'local_temperature', property: 'local_temperature', deviceType: thermostatDevice, cluster: Thermostat.Cluster.id, attribute: 'localTemperature', converter: (value) => { return Math.max(-5000, Math.min(5000, value * 100)); } },
{ type: 'climate', name: 'current_heating_setpoint', property: 'current_heating_setpoint', deviceType: thermostatDevice, cluster: Thermostat.Cluster.id, attribute: 'occupiedHeatingSetpoint' },
{ type: 'climate', name: 'occupied_heating_setpoint', property: 'occupied_heating_setpoint', deviceType: thermostatDevice, cluster: Thermostat.Cluster.id, attribute: 'occupiedHeatingSetpoint', converter: (value) => { return Math.max(-5000, Math.min(5000, value * 100)); } },
{ type: 'climate', name: 'occupied_cooling_setpoint', property: 'occupied_cooling_setpoint', deviceType: thermostatDevice, cluster: Thermostat.Cluster.id, attribute: 'occupiedCoolingSetpoint', converter: (value) => { return Math.max(-5000, Math.min(5000, value * 100)); } },
{ type: 'climate', name: 'unoccupied_heating_setpoint', property: 'unoccupied_heating_setpoint', deviceType: thermostatDevice, cluster: Thermostat.Cluster.id, attribute: 'occupiedHeatingSetpoint', converter: (value) => { return Math.max(-5000, Math.min(5000, value * 100)); } },
{ type: 'climate', name: 'unoccupied_cooling_setpoint', property: 'unoccupied_cooling_setpoint', deviceType: thermostatDevice, cluster: Thermostat.Cluster.id, attribute: 'occupiedCoolingSetpoint', converter: (value) => { return Math.max(-5000, Math.min(5000, value * 100)); } },
{ type: 'climate', name: 'running_state', property: 'running_state', deviceType: thermostatDevice, cluster: Thermostat.Cluster.id, attribute: 'thermostatRunningMode', valueLookup: ['idle', '', '', 'cool', 'heat'] },
{ type: 'climate', name: 'system_mode', property: 'system_mode', deviceType: thermostatDevice, cluster: Thermostat.Cluster.id, attribute: 'systemMode', valueLookup: ['off', 'auto', '', 'cool', 'heat', '', '', 'fan_only'] },
{ type: '', name: 'min_temperature_limit', property: 'min_temperature_limit', deviceType: thermostatDevice, cluster: Thermostat.Cluster.id, attribute: 'minHeatSetpointLimit', converter: (value) => { return Math.max(-5000, Math.min(5000, value * 100)); } },
{ type: '', name: 'max_temperature_limit', property: 'max_temperature_limit', deviceType: thermostatDevice, cluster: Thermostat.Cluster.id, attribute: 'maxHeatSetpointLimit', converter: (value) => { return Math.max(-5000, Math.min(5000, value * 100)); } },
{ type: '', name: 'min_heat_setpoint_limit', property: 'min_heat_setpoint_limit', deviceType: thermostatDevice, cluster: Thermostat.Cluster.id, attribute: 'minHeatSetpointLimit', converter: (value) => { return Math.max(-5000, Math.min(5000, value * 100)); } },
{ type: '', name: 'max_heat_setpoint_limit', property: 'max_heat_setpoint_limit', deviceType: thermostatDevice, cluster: Thermostat.Cluster.id, attribute: 'maxHeatSetpointLimit', converter: (value) => { return Math.max(-5000, Math.min(5000, value * 100)); } },
{ type: '', name: 'presence', property: 'presence', deviceType: occupancySensor, cluster: OccupancySensing.Cluster.id, attribute: 'occupancy', converter: (value) => { return { occupied: value }; } },
{ type: '', name: 'occupancy', property: 'occupancy', deviceType: occupancySensor, cluster: OccupancySensing.Cluster.id, attribute: 'occupancy', converter: (value) => { return { occupied: value }; } },
{ type: '', name: 'illuminance', property: 'illuminance', deviceType: lightSensor, cluster: IlluminanceMeasurement.Cluster.id, attribute: 'measuredValue', converter: (value) => { return Math.round(Math.max(Math.min(10000 * Math.log10(value), 0xfffe), 0)); } },
{ type: '', name: 'contact', property: 'contact', deviceType: contactSensor, cluster: BooleanState.Cluster.id, attribute: 'stateValue', converter: (value) => { return value; } },
{ type: '', name: 'water_leak', property: 'water_leak', deviceType: contactSensor, cluster: BooleanState.Cluster.id, attribute: 'stateValue', converter: (value) => { return !value; } },
{ type: '', name: 'vibration', property: 'vibration', deviceType: contactSensor, cluster: BooleanState.Cluster.id, attribute: 'stateValue', converter: (value) => { return !value; } },
{ type: '', name: 'smoke', property: 'smoke', deviceType: contactSensor, cluster: BooleanState.Cluster.id, attribute: 'stateValue', converter: (value) => { return !value; } },
{ type: '', name: 'carbon_monoxide', property: 'carbon_monoxide', deviceType: contactSensor, cluster: BooleanState.Cluster.id, attribute: 'stateValue', converter: (value) => { return !value; } },
{ type: '', name: 'temperature', property: 'temperature', deviceType: temperatureSensor, cluster: TemperatureMeasurement.Cluster.id, attribute: 'measuredValue', converter: (value) => { return Math.round(value * 100); } },
{ type: '', name: 'humidity', property: 'humidity', deviceType: humiditySensor, cluster: RelativeHumidityMeasurement.Cluster.id, attribute: 'measuredValue', converter: (value) => { return Math.round(value * 100); } },
{ t