Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support custom clusters #1019

Merged
merged 11 commits into from
Apr 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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