Skip to content

Commit

Permalink
Release 2.0.11
Browse files Browse the repository at this point in the history
  • Loading branch information
Luligu committed Apr 26, 2024
1 parent 8d89261 commit 06e852c
Show file tree
Hide file tree
Showing 5 changed files with 202 additions and 32 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ All notable changes to this project will be documented in this file.
- [mqtt]: Added handles for device_joined, device_announce, device_leave, device_remove, device_interview, device_rename.
- [exposes]: Added deviceFeatureBlackList to the config to exclude a feature on a device level.
- [mqtt]: Incoming messages are filtered by featureBlackList and deviceFeatureBlackList (if only blacklisted features change, the message is not processed). If present, the features included in featureBlackList and deviceFeatureBlackList are also removed from the payload.
- [routers] Added the SMLIGHT routers (with router firmware) to the router list. They are exposed like DoorLock so it is possible, like for the Coordinator and the Texas instruments router, to ask Siri to turn on/off permit join.

## [2.0.10] - 2024-04-22

Expand Down
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@ The latest release also supports all clusters in the multi endpoints devices (e.
Since the Matter support in the available ecosystems (controllers) is very limited and, when available, only covers Matter 1.1 specifications, some z2m devices cannot be exposed properly or cannot be exposed at all.

We discoverd that Matter support in Home Assistant is instead advanced and includes some clusters not supported by other ecosystems. These clusters like EveHistory have been added so with HA you can see Voltage, Current, Consumption and TotalConsumption (screenshot https://github.com/Luligu/matterbridge/blob/main/screenshot/Screenshot%20HA%20sm-dc-power-m.png).

## Unsupported devices

If one of your devices is not supported out of the box, open an issue and we will try to support it if possible.
Expand All @@ -185,10 +186,15 @@ If one of your devices is not supported out of the box, open an issue and we wil

## Conversion issues between zigbee2MQTT and Matter ecosystems

- Scene buttons are exposed correctly only when they send "single", "double" and "hold" actions (this is due to the Home app supporting only these 3 events.) In the next releases the scene buttons with more actions will be mapped to different endpoints like in my plugin homebridge-mqtt-accessories.

## Apple Home issues

The HomePods, being a WiFi devices, create some message trasmission errors sometimes. The Apple TV with network cable is more reliable (but also more expensive).

### DoorLock
The DoorLock cluster in the Home app takes a while to get online. The Home app shows no response for 1 or 2 seconds but then the accessory goes online.

The DoorLock cluster in the Home app takes a while to get online. The Home app shows no response for 1 or 2 seconds but then the accessory goes online. With the Eve app or the Controller app this issue is not present.

## Home Assistant issues (Matter Server for HA is still in Beta)

49 changes: 39 additions & 10 deletions src/entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ import {
DoorLockCluster,
} from 'matterbridge';

import { AnsiLogger, TimestampFormat, gn, dn, ign, idn, rs, db, wr, debugStringify, hk, zb, or, nf } from 'node-ansi-logger';
import { AnsiLogger, TimestampFormat, gn, dn, ign, idn, rs, db, wr, debugStringify, hk, zb, or } from 'node-ansi-logger';
import { ZigbeePlatform } from './platform.js';
import { BridgeDevice, BridgeGroup } from './zigbee2mqttTypes.js';
import { Payload, PayloadValue } from './payloadTypes.js';
Expand All @@ -87,6 +87,7 @@ export class ZigbeeEntity extends EventEmitter {
public bridgedDevice: BridgedBaseDevice | undefined;
public eidn = `${or}`;
private lastPayload: Payload = {};
private lastSeen: number = 0;
protected ignoreFeatures: string[] = []; // ['last_seen', 'linkquality'];

constructor(platform: ZigbeePlatform, entity: BridgeDevice | BridgeGroup) {
Expand All @@ -110,15 +111,24 @@ export class ZigbeeEntity extends EventEmitter {
this.log.debug(`Created MatterEntity: ${this.entityName}`);

this.platform.z2m.on('MESSAGE-' + this.entityName, (payload: Payload) => {
// this.log.warn(`Message for device ${this.ien}${this.accessoryName}${rs}${wr} ignoreFeatures: ${debugStringify(this.ignoreFeatures)}`);
// this.log.debug(`Message for device ${this.ien}${this.accessoryName}${rs}${db} ignoreFeatures: ${debugStringify(this.ignoreFeatures)}`);

// Check if the message is a duplicate that can be ingored cause only linkquality and last_seen have changed (action is always passed)
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 linkquality MQTT message for device ${this.ien}${this.entityName}${rs}${db} payload: ${debugStringify(payload)}`);
return;
}
this.lastSeen = Date.now();

// Check and deep copy the payload
if (deepEqual(this.lastPayload, payload, this.ignoreFeatures)) return;
this.lastPayload = deepCopy(payload);
if (Object.prototype.hasOwnProperty.call(this.lastPayload, 'action')) delete this.lastPayload['action'];
// Remove each key in ignoreFeatures from the payload copy
for (const key of this.ignoreFeatures) {
if (Object.prototype.hasOwnProperty.call(payload, key)) {
this.log.warn(`***Removing key ${nf}${key}${wr} from payload`);
// this.log.debug(`Removing key ${nf}${key}${db} from payload`);
delete payload[key];
}
}
Expand Down Expand Up @@ -247,14 +257,14 @@ export class ZigbeeEntity extends EventEmitter {
cluster?.triggerShortReleaseEvent({ previousPosition: 1 });
cluster?.setCurrentPositionAttribute(0);
cluster?.triggerMultiPressCompleteEvent({ previousPosition: 1, totalNumberOfPressesCounted: 1 });
this.log.debug(`*Set accessory ${hk}Switch.currentPosition: ${position}`);
this.log.info(`Trigger 'single' event for ${this.ien}${this.entityName}${rs}`);
}
if (value === 'double') {
position = 2;
this.bridgedDevice.getClusterServerById(Switch.Cluster.id)?.setCurrentPositionAttribute(position);
this.bridgedDevice.getClusterServerById(Switch.Cluster.id)?.triggerMultiPressCompleteEvent({ previousPosition: 1, totalNumberOfPressesCounted: 2 });
this.bridgedDevice.getClusterServerById(Switch.Cluster.id)?.setCurrentPositionAttribute(0);
this.log.debug(`*Set accessory ${hk}Switch.currentPosition: ${position}`);
this.log.info(`Trigger 'double' event for ${this.ien}${this.entityName}${rs}`);
}
if (value === 'hold') {
position = 1;
Expand All @@ -263,7 +273,7 @@ export class ZigbeeEntity extends EventEmitter {
this.bridgedDevice.getClusterServerById(Switch.Cluster.id)?.triggerLongPressEvent({ newPosition: 1 });
this.bridgedDevice.getClusterServerById(Switch.Cluster.id)?.triggerLongReleaseEvent({ previousPosition: 1 });
this.bridgedDevice.getClusterServerById(Switch.Cluster.id)?.setCurrentPositionAttribute(0);
this.log.debug(`*Set accessory ${hk}Switch.currentPosition: ${position}`);
this.log.info(`Trigger 'hold' event for ${this.ien}${this.entityName}${rs}`);
}
if (value === 'release') {
// this.bridgedDevice?.getClusterServerById(BridgedDeviceBasicInformation.Cluster.id)?.triggerReachableChangedEvent({ reachableNewValue: true });
Expand Down Expand Up @@ -349,6 +359,27 @@ export class ZigbeeGroup extends ZigbeeEntity {

// TODO Add the group scanning for real groups. This cover only automations
this.bridgedDevice = new BridgedBaseDevice(this, [onOffSwitch], [Identify.Cluster.id, Groups.Cluster.id, Scenes.Cluster.id, OnOff.Cluster.id]);
if (group.members.length > 0) {
let useState = false;
let useBrightness = false;
let useColor = false;
let useColorTemperature = false;
let minColorTemperature = 140;
let maxColorTemperature = 500;
group.members.forEach((member) => {
const device = this.platform.z2m.getDevice(member.ieee_address)!;
useState = useState === true || device.exposes.find((feature) => feature.name === 'state') !== undefined ? true : false;
useBrightness = useBrightness === true || device.exposes.find((feature) => feature.name === 'brightness') !== undefined ? true : false;
useColor = useColor === true || device.exposes.find((feature) => feature.property === 'color') !== undefined ? true : false;
useColorTemperature = useColorTemperature === true || device.exposes.find((feature) => feature.name === 'color_temp') !== undefined ? true : false;
const feature = device.exposes.find((feature) => feature.name === 'color_temp');
if (feature) {
minColorTemperature = Math.min(minColorTemperature, feature.value_min);
maxColorTemperature = Math.max(maxColorTemperature, feature.value_max);
}
});
this.log.info(`Group: ${gn}${group.friendly_name}${rs} state: ${useState} brightness: ${useBrightness} color: ${useColor} color_temp: ${useColorTemperature}-${minColorTemperature}-${maxColorTemperature}`);
}

// Command handlers
this.bridgedDevice.addCommandHandler('identify', async ({ request: { identifyTime } }) => {
Expand Down Expand Up @@ -508,13 +539,11 @@ export class ZigbeeDevice extends ZigbeeEntity {
// this.log.debug(`*Device ${this.ien}${device.friendly_name}${rs}${db} - properties[${properties.length}]: ${debugStringify(properties)}`);
names.forEach((name, index) => {
if (platform.featureBlackList.includes(name)) {
this.log.info(`Device ${this.en}${device.friendly_name}${nf} feature ${name} is globally blacklisted`);
// this.ignoreFeatures.push(name);
this.log.debug(`Device ${this.en}${device.friendly_name}${db} feature ${name} is globally blacklisted`);
return;
}
if (platform.deviceFeatureBlackList[device.friendly_name]?.includes(name)) {
this.log.info(`Device ${this.en}${device.friendly_name}${nf} feature ${name} is blacklisted`);
// this.ignoreFeatures.push(name);
this.log.debug(`Device ${this.en}${device.friendly_name}${db} feature ${name} is blacklisted`);
return;
}
const type = types[index];
Expand Down
39 changes: 22 additions & 17 deletions src/platform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
*/

import { BridgedDeviceBasicInformationCluster, DoorLock, DoorLockCluster, Level, Logger, Matterbridge, MatterbridgeDevice, MatterbridgeDynamicPlatform, PlatformConfig } from 'matterbridge';
import { AnsiLogger, dn, gn, db, wr, zb, payloadStringify, rs } from 'node-ansi-logger';
import { AnsiLogger, dn, gn, db, wr, zb, payloadStringify, rs, debugStringify } from 'node-ansi-logger';

import { ZigbeeDevice, ZigbeeEntity, ZigbeeGroup, BridgedBaseDevice } from './entity.js';
import { Zigbee2MQTT } from './zigbee2mqtt.js';
Expand Down Expand Up @@ -117,19 +117,19 @@ export class ZigbeePlatform extends MatterbridgeDynamicPlatform {
});

this.z2m.on('online', () => {
this.log.info('*zigbee2MQTT is online');
this.log.info('zigbee2MQTT is online');
// TODO check single availability
this.updateAvailability(true);
});

this.z2m.on('offline', () => {
this.log.warn('*zigbee2MQTT is offline');
this.log.warn('zigbee2MQTT is offline');
// TODO check single availability
this.updateAvailability(false);
});

this.z2m.on('permit_join', async (device: string, time: number, status: boolean) => {
this.log.info(`*zigbee2MQTT sent permit_join device: ${device} time: ${time} status: ${status}`);
this.log.info(`zigbee2MQTT sent permit_join device: ${device} time: ${time} status: ${status}`);
for (const zigbeeEntity of this.zigbeeEntities) {
if (zigbeeEntity.bridgedDevice?.isRouter && (device === undefined || device === zigbeeEntity.bridgedDevice.deviceName)) {
this.log.info(`*- ${zigbeeEntity.bridgedDevice.deviceName} ${zigbeeEntity.bridgedDevice.number} (${zigbeeEntity.bridgedDevice.name})`);
Expand All @@ -148,45 +148,49 @@ export class ZigbeePlatform extends MatterbridgeDynamicPlatform {
});

this.z2m.on('device_joined', async (friendly_name: string, ieee_address: string) => {
this.log.info(`*zigbee2MQTT sent device_joined device: ${friendly_name} ieee_address: ${ieee_address}`);
this.log.info(`zigbee2MQTT sent device_joined device: ${friendly_name} ieee_address: ${ieee_address}`);
// Here nothing to do, we wait eventually device_interview
});

this.z2m.on('device_announce', async (friendly_name: string, ieee_address: string) => {
this.log.info(`*zigbee2MQTT sent device_announce device: ${friendly_name} ieee_address: ${ieee_address}`);
this.log.info(`zigbee2MQTT sent device_announce device: ${friendly_name} ieee_address: ${ieee_address}`);
// Here nothing to do, we wait device_interview
});

this.z2m.on('device_leave', async (friendly_name: string, ieee_address: string) => {
this.log.info(`*zigbee2MQTT sent device_leave device: ${friendly_name} ieee_address: ${ieee_address}`);
this.log.info(`zigbee2MQTT sent device_leave device: ${friendly_name} ieee_address: ${ieee_address}`);
await this.unregisterZigbeeDevice(friendly_name);
});

this.z2m.on('device_remove', async (friendly_name: string, status: string, block: boolean, force: boolean) => {
this.log.info(`*zigbee2MQTT sent device_remove device: ${friendly_name} status: ${status} block: ${block} force: ${force}`);
this.log.info(`zigbee2MQTT sent device_remove device: ${friendly_name} status: ${status} block: ${block} force: ${force}`);
if (status === 'ok') await this.unregisterZigbeeDevice(friendly_name);
});

this.z2m.on('device_interview', async (friendly_name: string, ieee_address: string, status: string, supported: boolean) => {
this.log.info(`*zigbee2MQTT sent device_interview device: ${friendly_name} ieee_address: ${ieee_address} status: ${status} supported: ${supported}`);
this.log.info(`zigbee2MQTT sent device_interview device: ${friendly_name} ieee_address: ${ieee_address} status: ${status} supported: ${supported}`);
if (status === 'successful' && supported) {
if (!this.validateWhiteBlackList(friendly_name)) return;
this.log.info(`*Registering device: ${friendly_name}`);
this.log.info(`Registering device: ${friendly_name}`);
const bridgedDevice = this.z2mBridgeDevices?.find((device) => device.friendly_name === friendly_name);
if (bridgedDevice) await this.registerZigbeeDevice(bridgedDevice);
}
});

this.z2m.on('device_rename', async (ieee_address: string, from: string, to: string) => {
this.log.info(`*zigbee2MQTT sent device_rename ieee_address: ${ieee_address} from: ${from} to: ${to}`);
this.log.info(`zigbee2MQTT sent device_rename ieee_address: ${ieee_address} from: ${from} to: ${to}`);
await this.unregisterZigbeeDevice(from);
const bridgedDevice = this.z2mBridgeDevices?.find((device) => device.ieee_address === ieee_address);
if (bridgedDevice) await this.registerZigbeeDevice(bridgedDevice);
});

this.z2m.on('device_options', async (ieee_address: string, status: string, from: object, to: object) => {
this.log.info(`zigbee2MQTT sent device_options ieee_address: ${ieee_address} status ${status} from: ${debugStringify(from)} to: ${debugStringify(to)}`);
});

this.z2m.on('bridge-info', async (bridgeInfo: BridgeInfo) => {
this.z2mBridgeInfo = bridgeInfo;
this.log.debug(`*zigbee2MQTT sent bridge-info version: ${this.z2mBridgeInfo.version}`);
this.log.debug(`zigbee2MQTT sent bridge-info version: ${this.z2mBridgeInfo.version}`);
});

this.z2m.on('bridge-devices', async (devices: BridgeDevice[]) => {
Expand All @@ -213,7 +217,7 @@ export class ZigbeePlatform extends MatterbridgeDynamicPlatform {
device.configure();
}
}
this.log.info(`*zigbee2MQTT sent ${devices.length} devices ${this.z2mDevicesRegistered ? 'already registered' : ''}`);
this.log.info(`zigbee2MQTT sent ${devices.length} devices ${this.z2mDevicesRegistered ? 'already registered' : ''}`);
});

this.z2m.on('bridge-groups', async (groups: BridgeGroup[]) => {
Expand All @@ -235,7 +239,7 @@ export class ZigbeePlatform extends MatterbridgeDynamicPlatform {
device.configure();
}
}
this.log.info(`*zigbee2MQTT sent ${groups.length} groups ${this.z2mGroupsRegistered ? 'already registered' : ''}`);
this.log.info(`zigbee2MQTT sent ${groups.length} groups ${this.z2mGroupsRegistered ? 'already registered' : ''}`);
});

this.log.debug('Created zigbee2mqtt dynamic platform');
Expand All @@ -246,7 +250,7 @@ export class ZigbeePlatform extends MatterbridgeDynamicPlatform {

if (!this.z2mDevicesRegistered || !this.z2mGroupsRegistered) {
this.shouldStart = true;
this.log.debug('Setting flag to start when zigbee2mqtt sent devices: ', reason);
this.log.debug('Setting flag to start when zigbee2mqtt sends devices: ', reason);
}

if (this.debugEnabled) Logger.defaultLogLevel = Level.INFO;
Expand All @@ -271,7 +275,7 @@ export class ZigbeePlatform extends MatterbridgeDynamicPlatform {
override async onConfigure() {
if (!this.z2mDevicesRegistered || !this.z2mGroupsRegistered) {
this.shouldConfigure = true;
this.log.debug('Setting flag to configure when zigbee2mqtt sent devices');
this.log.debug('Setting flag to configure when zigbee2mqtt sends devices');
}

this.log.info(`Configuring ${this.zigbeeEntities.length} zigbee entities.`);
Expand Down Expand Up @@ -438,7 +442,7 @@ export class ZigbeePlatform extends MatterbridgeDynamicPlatform {
*/
const entity = this.zigbeeEntities.find((entity) => entity.entityName === friendly_name);
if (entity) {
this.log.info(`*Removing device: ${friendly_name}`);
this.log.info(`Removing device: ${friendly_name}`);
await this.unregisterDevice(entity.bridgedDevice as unknown as MatterbridgeDevice);
this.zigbeeEntities = this.zigbeeEntities.filter((entity) => entity.entityName !== friendly_name);
this.bridgedDevices = this.bridgedDevices.filter((device) => device.deviceName !== friendly_name);
Expand All @@ -452,6 +456,7 @@ export class ZigbeePlatform extends MatterbridgeDynamicPlatform {
}

private updateAvailability(available: boolean) {
if (this.bridgedDevices.length === 0) return;
this.log.info(`Setting availability for ${this.bridgedDevices.length} devices to ${available}`);
for (const bridgedDevice of this.bridgedDevices) {
bridgedDevice.getClusterServer(BridgedDeviceBasicInformationCluster)?.setReachableAttribute(available);
Expand Down
Loading

0 comments on commit 06e852c

Please sign in to comment.