Skip to content

Commit

Permalink
feat: Support custom clusters (#1019)
Browse files Browse the repository at this point in the history
* Support custom clusters

* WIP

* updates

* updates

* updates

* update

* updates

* Process feedback

* fix lint
  • Loading branch information
Koenkk committed Apr 23, 2024
1 parent ebeb21f commit d845f29
Show file tree
Hide file tree
Showing 21 changed files with 367 additions and 219 deletions.
6 changes: 3 additions & 3 deletions src/adapter/ember/adapter/emberAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {BackupUtils, RealpathSync, Wait} from "../../../utils";
import {Adapter, TsType} from "../..";
import {Backup, UnifiedBackupStorage} from "../../../models";
import {FrameType, Direction, ZclFrame, ZclHeader, Foundation, ManufacturerCode} from "../../../zcl";
import Cluster from "../../../zcl/definition/cluster";
import Clusters from "../../../zcl/definition/cluster";
import {
DeviceAnnouncePayload,
DeviceJoinedPayload,
Expand Down Expand Up @@ -628,7 +628,7 @@ export class EmberAdapter extends Adapter {
private async onTouchlinkMessage(sourcePanId: EmberPanId, sourceAddress: EmberEUI64, groupId: number | null, lastHopLqi: number,
messageContents: Buffer): Promise<void> {
const payload: ZclPayload = {
clusterID: Cluster.touchlink.ID,
clusterID: Clusters.touchlink.ID,
data: messageContents,
header: ZclHeader.fromBuffer(messageContents),
address: sourceAddress,
Expand Down Expand Up @@ -673,7 +673,7 @@ export class EmberAdapter extends Adapter {
const payload: ZclPayload = {
header: ZclHeader.fromBuffer(data),
data,
clusterID: Cluster.greenPower.ID,
clusterID: Clusters.greenPower.ID,
address: sourceId,
endpoint: GP_ENDPOINT,
linkquality: gpdLink,
Expand Down
56 changes: 28 additions & 28 deletions src/adapter/ember/adapter/endpoints.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import Cluster from '../../../zcl/definition/cluster';
import Clusters from '../../../zcl/definition/cluster';
import {GP_ENDPOINT, GP_PROFILE_ID, HA_PROFILE_ID} from '../consts';
import {ClusterId, EmberMulticastId, ProfileId} from '../types';

Expand Down Expand Up @@ -35,34 +35,34 @@ export const FIXED_ENDPOINTS: readonly FixedEndpointInfo[] = [
deviceId: 0x65,// ?
deviceVersion: 1,
inClusterList: [
Cluster.genBasic.ID,// 0x0000,// Basic
Cluster.genIdentify.ID,// 0x0003,// Identify
Cluster.genOnOff.ID,// 0x0006,// On/off
Cluster.genLevelCtrl.ID,// 0x0008,// Level Control
Cluster.genTime.ID,// 0x000A,// Time
Cluster.genOta.ID,// 0x0019,// Over the Air Bootloading
Clusters.genBasic.ID,// 0x0000,// Basic
Clusters.genIdentify.ID,// 0x0003,// Identify
Clusters.genOnOff.ID,// 0x0006,// On/off
Clusters.genLevelCtrl.ID,// 0x0008,// Level Control
Clusters.genTime.ID,// 0x000A,// Time
Clusters.genOta.ID,// 0x0019,// Over the Air Bootloading
// Cluster.genPowerProfile.ID,// 0x001A,// Power Profile XXX: missing ZCL cluster def in Z2M?
Cluster.lightingColorCtrl.ID,// 0x0300,// Color Control
Clusters.lightingColorCtrl.ID,// 0x0300,// Color Control
],
outClusterList: [
Cluster.genBasic.ID,// 0x0000,// Basic
Cluster.genIdentify.ID,// 0x0003,// Identify
Cluster.genGroups.ID,// 0x0004,// Groups
Cluster.genScenes.ID,// 0x0005,// Scenes
Cluster.genOnOff.ID,// 0x0006,// On/off
Cluster.genLevelCtrl.ID,// 0x0008,// Level Control
Cluster.genPollCtrl.ID,// 0x0020,// Poll Control
Cluster.lightingColorCtrl.ID,// 0x0300,// Color Control
Cluster.msIlluminanceMeasurement.ID,// 0x0400,// Illuminance Measurement
Cluster.msTemperatureMeasurement.ID,// 0x0402,// Temperature Measurement
Cluster.msRelativeHumidity.ID,// 0x0405,// Relative Humidity Measurement
Cluster.msOccupancySensing.ID,// 0x0406,// Occupancy Sensing
Cluster.ssIasZone.ID,// 0x0500,// IAS Zone
Cluster.seMetering.ID,// 0x0702,// Simple Metering
Cluster.haMeterIdentification.ID,// 0x0B01,// Meter Identification
Cluster.haApplianceStatistics.ID,// 0x0B03,// Appliance Statistics
Cluster.haElectricalMeasurement.ID,// 0x0B04,// Electrical Measurement
Cluster.touchlink.ID,// 0x1000, // touchlink
Clusters.genBasic.ID,// 0x0000,// Basic
Clusters.genIdentify.ID,// 0x0003,// Identify
Clusters.genGroups.ID,// 0x0004,// Groups
Clusters.genScenes.ID,// 0x0005,// Scenes
Clusters.genOnOff.ID,// 0x0006,// On/off
Clusters.genLevelCtrl.ID,// 0x0008,// Level Control
Clusters.genPollCtrl.ID,// 0x0020,// Poll Control
Clusters.lightingColorCtrl.ID,// 0x0300,// Color Control
Clusters.msIlluminanceMeasurement.ID,// 0x0400,// Illuminance Measurement
Clusters.msTemperatureMeasurement.ID,// 0x0402,// Temperature Measurement
Clusters.msRelativeHumidity.ID,// 0x0405,// Relative Humidity Measurement
Clusters.msOccupancySensing.ID,// 0x0406,// Occupancy Sensing
Clusters.ssIasZone.ID,// 0x0500,// IAS Zone
Clusters.seMetering.ID,// 0x0702,// Simple Metering
Clusters.haMeterIdentification.ID,// 0x0B01,// Meter Identification
Clusters.haApplianceStatistics.ID,// 0x0B03,// Appliance Statistics
Clusters.haElectricalMeasurement.ID,// 0x0B04,// Electrical Measurement
Clusters.touchlink.ID,// 0x1000, // touchlink
],
networkIndex: 0x00,
// Cluster spec 3.7.2.4.1: group identifier 0x0000 is reserved for the global scene used by the OnOff cluster.
Expand All @@ -76,10 +76,10 @@ export const FIXED_ENDPOINTS: readonly FixedEndpointInfo[] = [
deviceId: 0x66,
deviceVersion: 1,
inClusterList: [
Cluster.greenPower.ID,// 0x0021,// Green Power
Clusters.greenPower.ID,// 0x0021,// Green Power
],
outClusterList: [
Cluster.greenPower.ID,// 0x0021,// Green Power
Clusters.greenPower.ID,// 0x0021,// Green Power
],
networkIndex: 0x00,
multicastIds: [0x0B84],
Expand Down
8 changes: 4 additions & 4 deletions src/adapter/ember/ezsp/ezsp.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/* istanbul ignore file */
import EventEmitter from "events";
import {SerialPortOptions} from "../../tstype";
import Cluster from "../../../zcl/definition/cluster";
import Clusters from "../../../zcl/definition/cluster";
import {byteToBits, getMacCapFlags, highByte, highLowToInt, lowByte, lowHighBits} from "../utils/math";
import {
EmberOutgoingMessageType,
Expand Down Expand Up @@ -5233,7 +5233,7 @@ export class Ezsp extends EventEmitter {
const profileId = msgBuffalo.readUInt16();
const payload = msgBuffalo.readRest();

if (profileId === TOUCHLINK_PROFILE_ID && clusterId === Cluster.touchlink.ID) {
if (profileId === TOUCHLINK_PROFILE_ID && clusterId === Clusters.touchlink.ID) {
this.emit(EzspEvents.TOUCHLINK_MESSAGE, sourcePanId, sourceAddress, groupId, lastHopLqi, payload);
}
}
Expand Down Expand Up @@ -7587,7 +7587,7 @@ export class Ezsp extends EventEmitter {
return;
}

let commandIdentifier = Cluster.greenPower.commands.notification.ID;
let commandIdentifier = Clusters.greenPower.commands.notification.ID;

if (gpdCommandId === 0xE0) {
if (!gpdCommandPayload.length) {
Expand All @@ -7596,7 +7596,7 @@ export class Ezsp extends EventEmitter {
return;
}

commandIdentifier = Cluster.greenPower.commands.commissioningNotification.ID;
commandIdentifier = Clusters.greenPower.commands.commissioningNotification.ID;
}

this.emit(
Expand Down
6 changes: 3 additions & 3 deletions src/adapter/ezsp/driver/driver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import equals from 'fast-deep-equal/es6';
import {ParamsDesc} from './commands';
import {EZSPAdapterBackup} from '../adapter/backup';
import {logger} from '../../../utils/logger';
import Cluster from '../../../zcl/definition/cluster';
import Clusters from '../../../zcl/definition/cluster';

const NS = 'zh:ezsp:driv';

Expand Down Expand Up @@ -429,7 +429,7 @@ export class Driver extends EventEmitter {
// break;
// }
case (frameName == 'gpepIncomingMessageHandler'): {
let commandIdentifier = Cluster.greenPower.commands.notification.ID;
let commandIdentifier = Clusters.greenPower.commands.notification.ID;

if (frame.gpdCommandId === 0xE0) {
if (!frame.gpdCommandPayload.length) {
Expand All @@ -438,7 +438,7 @@ export class Driver extends EventEmitter {
return;
}

commandIdentifier = Cluster.greenPower.commands.commissioningNotification.ID;
commandIdentifier = Clusters.greenPower.commands.commissioningNotification.ID;
}

const gpdHeader = Buffer.alloc(15);
Expand Down
8 changes: 4 additions & 4 deletions src/adapter/z-stack/adapter/endpoints.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as Constants from '../constants';
import * as Zcl from '../../../zcl';
import {Clusters} from '../../../zcl/index';

const EndpointDefaults: {
appdeviceid: number;
Expand Down Expand Up @@ -35,10 +35,10 @@ export const Endpoints = [
appprofid: 0x0104,
appdeviceid: 0x0400,
appnumoutclusters: 2,
appoutclusterlist: [Zcl.Utils.getCluster('ssIasZone').ID, Zcl.Utils.getCluster('ssIasWd').ID],
appoutclusterlist: [Clusters.ssIasZone.ID, Clusters.ssIasWd.ID],
appnuminclusters: 2,
// genTime required for https://github.com/Koenkk/zigbee2mqtt/issues/10816
appinclusterlist: [Zcl.Utils.getCluster('ssIasAce').ID, Zcl.Utils.getCluster('genTime').ID]
appinclusterlist: [Clusters.ssIasAce.ID, Clusters.genTime.ID]

},
// TERNCY: https://github.com/Koenkk/zigbee-herdsman/issues/82
Expand All @@ -49,7 +49,7 @@ export const Endpoints = [
endpoint: 13,
appprofid: 0x0104,
appnuminclusters: 1,
appinclusterlist: [Zcl.Utils.getCluster('genOta').ID]
appinclusterlist: [Clusters.genOta.ID]
},
// Insta/Jung/Gira: OTA fallback EP (since it's buggy in firmware 10023202 when it tries to find a matching EP for
// OTA - it queries for ZLL profile, but then contacts with HA profile)
Expand Down
87 changes: 45 additions & 42 deletions src/controller/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {ZclFrameConverter} from './helpers';
import * as Events from './events';
import {KeyValue, DeviceType, GreenPowerEvents, GreenPowerDeviceJoinedPayload} from './tstype';
import fs from 'fs';
import {Utils as ZclUtils, FrameControl, ZclFrame} from '../zcl';
import {Utils as ZclUtils, FrameControl, ZclFrame, Clusters} from '../zcl';
import Touchlink from './touchlink';
import GreenPower from './greenPower';
import {BackupUtils} from "../utils";
Expand Down Expand Up @@ -613,53 +613,56 @@ class Controller extends events.EventEmitter {

private async onZclPayload(payload: AdapterEvents.ZclPayload): Promise<void> {
let frame: ZclFrame | undefined = undefined;

try {
frame = ZclFrame.fromBuffer(payload.clusterID, payload.header, payload.data);
} catch (error) {
logger.debug(`Failed to parse frame: ${error}`, NS);
}

logger.debug(`Received payload: clusterID=${payload.clusterID}, address=${payload.address}, groupID=${payload.groupID}, `
+ `endpoint=${payload.endpoint}, destinationEndpoint=${payload.destinationEndpoint}, wasBroadcast=${payload.wasBroadcast}, `
+ `linkQuality=${payload.linkquality}, frame=${frame?.toString()}`, NS);

let gpDevice = null;

if (frame?.cluster.name === 'touchlink') {
let device: Device = undefined;
if (payload.clusterID === Clusters.touchlink.ID) {
// This is handled by touchlink
return;
} else if (frame?.cluster.name === 'greenPower') {
} else if (payload.clusterID === Clusters.greenPower.ID) {
try {
// Custom clusters are not supported for Green Power since we need to parse the frame to get the device.
frame = ZclFrame.fromBuffer(payload.clusterID, payload.header, payload.data, {});
} catch (error) {
logger.debug(`Failed to parse frame green power frame, ignoring it: ${error}`, NS);
return;
}

await this.greenPower.onZclGreenPowerData(payload, frame);
// lookup encapsulated gpDevice for further processing
gpDevice = Device.byNetworkAddress(frame.payload.srcID & 0xFFFF);
}

let device = gpDevice ? gpDevice : (typeof payload.address === 'string' ?
Device.byIeeeAddr(payload.address) : Device.byNetworkAddress(payload.address));

/**
* Handling of re-transmitted Xiaomi messages.
* https://github.com/Koenkk/zigbee2mqtt/issues/1238
* https://github.com/Koenkk/zigbee2mqtt/issues/3592
*
* Some Xiaomi router devices re-transmit messages from Xiaomi end devices.
* The network address of these message is set to the one of the Xiaomi router.
* Therefore it looks like if the message came from the Xiaomi router, while in
* fact it came from the end device.
* Handling these message would result in false state updates.
* The group ID attribute of these message defines the network address of the end device.
*/
if (device?.manufacturerName === 'LUMI' && device?.type == 'Router' && payload.groupID) {
logger.debug(`Handling re-transmitted Xiaomi message ${device.networkAddress} -> ${payload.groupID}`, NS);
device = Device.byNetworkAddress(payload.groupID);
device = Device.byNetworkAddress(frame.payload.srcID & 0xFFFF);
} else {
/**
* Handling of re-transmitted Xiaomi messages.
* https://github.com/Koenkk/zigbee2mqtt/issues/1238
* https://github.com/Koenkk/zigbee2mqtt/issues/3592
*
* Some Xiaomi router devices re-transmit messages from Xiaomi end devices.
* The network address of these message is set to the one of the Xiaomi router.
* Therefore it looks like if the message came from the Xiaomi router, while in
* fact it came from the end device.
* Handling these message would result in false state updates.
* The group ID attribute of these message defines the network address of the end device.
*/
device = Device.find(payload.address);
if (device?.manufacturerName === 'LUMI' && device?.type == 'Router' && payload.groupID) {
logger.debug(`Handling re-transmitted Xiaomi message ${device.networkAddress} -> ${payload.groupID}`, NS);
device = Device.byNetworkAddress(payload.groupID);
}
try {
frame = ZclFrame.fromBuffer(payload.clusterID, payload.header, payload.data, device?.customClusters);
} catch (error) {
logger.debug(`Failed to parse frame: ${error}`, NS);
}
}

if (!device) {
logger.debug(`Data is from unknown device with address '${payload.address}', skipping...`, NS);
return;
}

logger.debug(`Received payload: clusterID=${payload.clusterID}, address=${payload.address}, groupID=${payload.groupID}, `
+ `endpoint=${payload.endpoint}, destinationEndpoint=${payload.destinationEndpoint}, wasBroadcast=${payload.wasBroadcast}, `
+ `linkQuality=${payload.linkquality}, frame=${frame?.toString()}`, NS);

device.updateLastSeen();
//no implicit checkin for genPollCtrl data because it might interfere with the explicit checkin
if (!frame?.isCluster("genPollCtrl")) {
Expand Down Expand Up @@ -697,18 +700,18 @@ class Controller extends events.EventEmitter {
if (frame.header.isGlobal) {
if (frame.isCommand('report')) {
type = 'attributeReport';
data = ZclFrameConverter.attributeKeyValue(frame, device.manufacturerID);
data = ZclFrameConverter.attributeKeyValue(frame, device.manufacturerID, device.customClusters);
} else if (frame.isCommand('read')) {
type = 'read';
data = ZclFrameConverter.attributeList(frame, device.manufacturerID);
data = ZclFrameConverter.attributeList(frame, device.manufacturerID, device.customClusters);
} else if (frame.isCommand('write')) {
type = 'write';
data = ZclFrameConverter.attributeKeyValue(frame, device.manufacturerID);
data = ZclFrameConverter.attributeKeyValue(frame, device.manufacturerID, device.customClusters);
} else {
/* istanbul ignore else */
if (frame.isCommand('readRsp')) {
type = 'readResponse';
data = ZclFrameConverter.attributeKeyValue(frame, device.manufacturerID);
data = ZclFrameConverter.attributeKeyValue(frame, device.manufacturerID, device.customClusters);
}
}
} else {
Expand Down Expand Up @@ -737,7 +740,7 @@ class Controller extends events.EventEmitter {
} else {
type = 'raw';
data = payload.data;
const name = ZclUtils.getCluster(payload.clusterID).name;
const name = ZclUtils.getCluster(payload.clusterID, device.manufacturerID, device.customClusters).name;
clusterName = Number.isNaN(Number(name)) ? name : Number(name);
}

Expand Down
Loading

0 comments on commit d845f29

Please sign in to comment.