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 8 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/z-stack/adapter/endpoints.ts
Original file line number Diff line number Diff line change
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: [Zcl.Cluster.ssIasZone.ID, Zcl.Cluster.ssIasWd.ID],
Koenkk marked this conversation as resolved.
Show resolved Hide resolved
appnuminclusters: 2,
// genTime required for https://github.com/Koenkk/zigbee2mqtt/issues/10816
appinclusterlist: [Zcl.Utils.getCluster('ssIasAce').ID, Zcl.Utils.getCluster('genTime').ID]
appinclusterlist: [Zcl.Cluster.ssIasAce.ID, Zcl.Cluster.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: [Zcl.Cluster.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
51 changes: 26 additions & 25 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, Cluster} from '../zcl';
import Touchlink from './touchlink';
import GreenPower from './greenPower';
import {BackupUtils} from "../utils";
Expand Down Expand Up @@ -612,32 +612,27 @@ 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;
const parseFrame = (device: Device): ZclFrame | undefined => {
try {
return ZclFrame.fromBuffer(payload.clusterID, payload.header, payload.data, device?.customClusters);
} catch (error) {
logger.debug(`Failed to parse frame: ${error}`, NS);
return undefined;
}
};

if (frame?.cluster.name === 'touchlink') {
let frame: ZclFrame | undefined = undefined;
let device = typeof payload.address === 'string' ? Device.byIeeeAddr(payload.address) : Device.byNetworkAddress(payload.address);
if (payload.clusterID === Cluster.touchlink.ID) {
// This is handled by touchlink
return;
} else if (frame?.cluster.name === 'greenPower') {
} else if (payload.clusterID === Cluster.greenPower.ID) {
frame = parseFrame(device);
await this.greenPower.onZclGreenPowerData(payload, frame);
// lookup encapsulated gpDevice for further processing
gpDevice = Device.byNetworkAddress(frame.payload.srcID & 0xFFFF);
device = 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
Expand All @@ -660,6 +655,12 @@ class Controller extends events.EventEmitter {
return;
}

frame = parseFrame(device);

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 +698,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 +738,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
13 changes: 9 additions & 4 deletions src/controller/greenPower.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import ZclTransactionSequenceNumber from './helpers/zclTransactionSequenceNumber
import events from 'events';
import {GreenPowerEvents, GreenPowerDeviceJoinedPayload} from './tstype';
import {logger} from '../utils/logger';
import {Cluster} from '../zcl';

const NS = 'zh:controller:greenpower';

Expand Down Expand Up @@ -69,7 +70,8 @@ class GreenPower extends events.EventEmitter {

const replyFrame = Zcl.ZclFrame.create(
Zcl.FrameType.SPECIFIC, Zcl.Direction.SERVER_TO_CLIENT, true,
null, ZclTransactionSequenceNumber.next(), 'pairing', 33, payload
null, ZclTransactionSequenceNumber.next(), 'pairing', Cluster.greenPower.ID, payload,
{},
);


Expand Down Expand Up @@ -131,7 +133,8 @@ class GreenPower extends events.EventEmitter {

const replyFrame = Zcl.ZclFrame.create(
Zcl.FrameType.SPECIFIC, Zcl.Direction.SERVER_TO_CLIENT, true,
null, ZclTransactionSequenceNumber.next(), 'response', 33, payload
null, ZclTransactionSequenceNumber.next(), 'response', Cluster.greenPower.ID, payload,
{},
);
await this.adapter.sendZclFrameToAll(242, replyFrame, 242);

Expand Down Expand Up @@ -194,7 +197,8 @@ class GreenPower extends events.EventEmitter {

const replyFrame = Zcl.ZclFrame.create(
Zcl.FrameType.SPECIFIC, Zcl.Direction.SERVER_TO_CLIENT, true,
null, ZclTransactionSequenceNumber.next(), 'response', 33, payload
null, ZclTransactionSequenceNumber.next(), 'response', Cluster.greenPower.ID, payload,
{},
);

await this.adapter.sendZclFrameToAll(242, replyFrame, 242);
Expand All @@ -220,7 +224,8 @@ class GreenPower extends events.EventEmitter {

const frame = Zcl.ZclFrame.create(
Zcl.FrameType.SPECIFIC, Zcl.Direction.SERVER_TO_CLIENT, true,
null, ZclTransactionSequenceNumber.next(), 'commisioningMode', 33, payload
null, ZclTransactionSequenceNumber.next(), 'commisioningMode', Cluster.greenPower.ID, payload,
{},
);

if (networkAddress === null) {
Expand Down
13 changes: 7 additions & 6 deletions src/controller/helpers/zclFrameConverter.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {ZclFrame, Utils as ZclUtils} from '../../zcl';
import {Cluster} from '../../zcl/tstype';
import ManufacturerCode from '../../zcl/definition/manufacturerCode';
import {ClusterDefinition} from '../../zcl/definition/tstype';

interface KeyValue {[s: string]: number | string}

Expand All @@ -9,17 +10,17 @@ interface KeyValue {[s: string]: number | string}
// This leads to incorrect reported attribute names.
// Remap the attributes using the target device's manufacturer ID
// if the header is lacking the information.
function getCluster(frame: ZclFrame, deviceManufacturerID: number): Cluster {
function getCluster(frame: ZclFrame, deviceManufacturerID: number, customClusters: {[s: string]: ClusterDefinition}): Cluster {
let cluster = frame.cluster;
if (!frame?.header?.manufacturerCode && frame?.cluster && deviceManufacturerID == ManufacturerCode.LEGRAND_GROUP) {
cluster = ZclUtils.getCluster(frame.cluster.ID, deviceManufacturerID);
cluster = ZclUtils.getCluster(frame.cluster.ID, deviceManufacturerID, customClusters);
}
return cluster;
}

function attributeKeyValue(frame: ZclFrame, deviceManufacturerID: number): KeyValue {
function attributeKeyValue(frame: ZclFrame, deviceManufacturerID: number, customClusters: {[s: string]: ClusterDefinition}): KeyValue {
const payload: KeyValue = {};
const cluster = getCluster(frame, deviceManufacturerID);
const cluster = getCluster(frame, deviceManufacturerID, customClusters);

for (const item of frame.payload) {
try {
Expand All @@ -32,9 +33,9 @@ function attributeKeyValue(frame: ZclFrame, deviceManufacturerID: number): KeyVa
return payload;
}

function attributeList(frame: ZclFrame, deviceManufacturerID: number): Array<string | number> {
function attributeList(frame: ZclFrame, deviceManufacturerID: number, customClusters: {[s: string]: ClusterDefinition}): Array<string | number> {
const payload: Array<string | number> = [];
const cluster = getCluster(frame, deviceManufacturerID);
const cluster = getCluster(frame, deviceManufacturerID, customClusters);

for (const item of frame.payload) {
try {
Expand Down
32 changes: 28 additions & 4 deletions src/controller/model/device.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ import * as Zcl from '../../zcl';
import assert from 'assert';
import {ZclFrameConverter} from '../helpers';
import {logger} from '../../utils/logger';
import {ClusterDefinition} from '../../zcl/definition/tstype';
import {Cluster} from '../../zcl';
import {isClusterName} from '../../zcl/utils';

/**
* @ignore
Expand Down Expand Up @@ -55,6 +58,7 @@ class Device extends Entity {
private _lastDefaultResponseSequenceNumber: number;
private _checkinInterval: number;
private _pendingRequestTimeout: number;
private _customClusters: {[s: string]: ClusterDefinition} = {};

// Getters/setters
get ieeeAddr(): string {return this._ieeeAddr;}
Expand Down Expand Up @@ -107,6 +111,7 @@ class Device extends Entity {
};
get pendingRequestTimeout(): number {return this._pendingRequestTimeout;}
set pendingRequestTimeout(pendingRequestTimeout: number) {this._pendingRequestTimeout = pendingRequestTimeout;}
get customClusters(): {[s: string]: ClusterDefinition} {return this._customClusters;}

public meta: KeyValue;

Expand Down Expand Up @@ -213,7 +218,7 @@ class Device extends Entity {
public async onZclData(dataPayload: AdapterEvents.ZclPayload, frame: Zcl.ZclFrame, endpoint: Endpoint): Promise<void> {
// Update reportable properties
if (frame.isCluster('genBasic') && (frame.isCommand('readRsp') || frame.isCommand('report'))) {
for (const [key, val] of Object.entries(ZclFrameConverter.attributeKeyValue(frame, this.manufacturerID))) {
for (const [key, val] of Object.entries(ZclFrameConverter.attributeKeyValue(frame, this.manufacturerID, this.customClusters))) {
Device.ReportablePropertiesMapping[key]?.set(val, this);
}
}
Expand Down Expand Up @@ -349,7 +354,7 @@ class Device extends Entity {

// default: no timeout (messages expire immediately after first send attempt)
let pendingRequestTimeout = 0;
if((endpoints.filter((e): boolean => e.supportsInputCluster('genPollCtrl'))).length > 0) {
if((endpoints.filter((e): boolean => e.inputClusters.includes(Cluster.genPollCtrl.ID))).length > 0) {
// default for devices that support genPollCtrl cluster (RX off when idle): 1 day
pendingRequestTimeout = 86400000;
/* istanbul ignore else */
Expand Down Expand Up @@ -749,8 +754,8 @@ class Device extends Entity {
};

const frame = Zcl.ZclFrame.create(
Zcl.FrameType.SPECIFIC, Zcl.Direction.SERVER_TO_CLIENT, true,
null, ZclTransactionSequenceNumber.next(), 'pairing', 33, payload
Zcl.FrameType.SPECIFIC, Zcl.Direction.SERVER_TO_CLIENT, true, null,
ZclTransactionSequenceNumber.next(), 'pairing', 33, payload, this.customClusters,
);

await Entity.adapter.sendZclFrameToAll(242, frame, 242);
Expand Down Expand Up @@ -798,6 +803,25 @@ class Device extends Entity {
const endpoint = this.endpoints.find((ep) => ep.inputClusters.includes(0)) ?? this.endpoints[0];
await endpoint.read('genBasic', ['zclVersion'], {disableRecovery});
}

public addCustomCluster(name: string, cluster: ClusterDefinition): void {
assert(!([Cluster.touchlink.ID, Cluster.touchlink.ID].includes(cluster.ID)),
Koenkk marked this conversation as resolved.
Show resolved Hide resolved
'Overriding of greenPower or touchlink cluster is not supported');
if (isClusterName(name)) {
const existingCluster = Cluster[name];

// Extend existing cluster
assert(existingCluster.ID === cluster.ID, `Custom cluster ID (${cluster.ID}) should match existing cluster ID (${existingCluster.ID})`);
cluster = {
ID: cluster.ID,
manufacturerCode: cluster.manufacturerCode,
attributes: {...existingCluster.attributes, ...cluster.attributes},
commands: {...existingCluster.commands, ...cluster.commands},
commandsResponse: {...existingCluster.commandsResponse, ...cluster.commandsResponse},
};
}
this._customClusters[name] = cluster;
}
}

export default Device;
Loading