Skip to content

Commit ee68796

Browse files
authored
Merge pull request #211 from athombv/feature/export-endpoint
feat: export Endpoint, so a device serving bound clusters need not fake a peer
2 parents 94449de + 5d13c90 commit ee68796

4 files changed

Lines changed: 94 additions & 8 deletions

File tree

‎index.d.ts‎

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2778,12 +2778,21 @@ declare module "zigbee-clusters" {
27782778
getLogId(endpointId: number, clusterId: number): string;
27792779
}
27802780

2781-
class ZCLNodeEndpoint extends EventEmitter {
2782-
constructor(node: ZCLNode, descriptor: {
2781+
/**
2782+
* Minimum shape an Endpoint needs from its owner. Endpoint only ever calls `sendFrame`, so a
2783+
* plain object is enough - a device serving bound clusters does not need a full ZCLNode, which
2784+
* models one remote peer and bakes that peer's address into its own `sendFrame`.
2785+
*/
2786+
type EndpointInput = {
2787+
sendFrame(endpointId: number, clusterId: number, frame: Buffer): Promise<unknown>;
2788+
};
2789+
2790+
class Endpoint extends EventEmitter {
2791+
constructor(node: EndpointInput, descriptor: {
27832792
endpointId: number;
27842793
inputClusters: number[];
2785-
outputClusters: number[];
2786-
}[]);
2794+
outputClusters?: number[];
2795+
});
27872796
// Typed lookup: known cluster names (per the generated `ClustersByName`)
27882797
// resolve to their specific class (e.g. `clusters.onOff` is `OnOffCluster`),
27892798
// while unknown keys fall back to the generic Cluster via the index half.
@@ -2795,8 +2804,21 @@ declare module "zigbee-clusters" {
27952804
unbind(clusterName: string): void;
27962805

27972806
makeDefaultResponseFrame(receivedFrame: unknown, success: boolean, status: types.ZCLEnum8Status): typeof zclFrames.ZCLStandardHeader | typeof zclFrames.ZCLMfgSpecificHeader;
2807+
2808+
/**
2809+
* Dispatch an incoming ZCL frame to the bound cluster and write the response. Handles the
2810+
* command-response rules: no reply to a Default Response or a group-addressed frame
2811+
* (ZCL 2.5.12.2), a Default Response carrying a thrown ZCLError's status, and
2812+
* `disableDefaultResponse`.
2813+
*
2814+
* Pass `meta.groupId` for a group-addressed frame, or the suppression cannot be applied.
2815+
*/
2816+
handleFrame(clusterId: number, frame: Buffer, meta?: { groupId?: number }): Promise<void>;
27982817
}
27992818

2819+
/** @deprecated Use {@link Endpoint}; kept so existing references keep compiling. */
2820+
type ZCLNodeEndpoint = Endpoint;
2821+
28002822
type ZCLDataType<Value> = import('@athombv/data-types').DataType<Value>;
28012823
const ZCLDataTypes: ZCLDataTypes;
28022824
const ZCLStruct: typeof import('@athombv/data-types').Struct;

‎index.js‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
const _debug = require('debug');
44

55
const ZCLNode = require('./lib/Node');
6+
const Endpoint = require('./lib/Endpoint');
67
const Cluster = require('./lib/Cluster');
78
const BoundCluster = require('./lib/BoundCluster');
89
const zclTypes = require('./lib/zclTypes');
@@ -105,6 +106,7 @@ module.exports = {
105106

106107
// ZCL
107108
ZCLNode,
109+
Endpoint,
108110
zclTypes,
109111
zclFrames,
110112
ZCLDataTypes,

‎scripts/template.d.ts.txt‎

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,12 +53,21 @@ declare module 'zigbee-clusters' {
5353
getLogId(endpointId: number, clusterId: number): string;
5454
}
5555

56-
class ZCLNodeEndpoint extends EventEmitter {
57-
constructor(node: ZCLNode, descriptor: {
56+
/**
57+
* Minimum shape an Endpoint needs from its owner. Endpoint only ever calls `sendFrame`, so a
58+
* plain object is enough - a device serving bound clusters does not need a full ZCLNode, which
59+
* models one remote peer and bakes that peer's address into its own `sendFrame`.
60+
*/
61+
type EndpointInput = {
62+
sendFrame(endpointId: number, clusterId: number, frame: Buffer): Promise<unknown>;
63+
};
64+
65+
class Endpoint extends EventEmitter {
66+
constructor(node: EndpointInput, descriptor: {
5867
endpointId: number;
5968
inputClusters: number[];
60-
outputClusters: number[];
61-
}[]);
69+
outputClusters?: number[];
70+
});
6271
// Typed lookup: known cluster names (per the generated `ClustersByName`)
6372
// resolve to their specific class (e.g. `clusters.onOff` is `OnOffCluster`),
6473
// while unknown keys fall back to the generic Cluster via the index half.
@@ -70,8 +79,21 @@ declare module 'zigbee-clusters' {
7079
unbind(clusterName: string): void;
7180

7281
makeDefaultResponseFrame(receivedFrame: unknown, success: boolean, status: types.ZCLEnum8Status): typeof zclFrames.ZCLStandardHeader | typeof zclFrames.ZCLMfgSpecificHeader;
82+
83+
/**
84+
* Dispatch an incoming ZCL frame to the bound cluster and write the response. Handles the
85+
* command-response rules: no reply to a Default Response or a group-addressed frame
86+
* (ZCL 2.5.12.2), a Default Response carrying a thrown ZCLError's status, and
87+
* `disableDefaultResponse`.
88+
*
89+
* Pass `meta.groupId` for a group-addressed frame, or the suppression cannot be applied.
90+
*/
91+
handleFrame(clusterId: number, frame: Buffer, meta?: { groupId?: number }): Promise<void>;
7392
}
7493

94+
/** @deprecated Use {@link Endpoint}; kept so existing references keep compiling. */
95+
type ZCLNodeEndpoint = Endpoint;
96+
7597
type ZCLDataType<Value> = import('@athombv/data-types').DataType<Value>;
7698
const ZCLDataTypes: ZCLDataTypes;
7799
const ZCLStruct: typeof import('@athombv/data-types').Struct;

‎test/endpointStandalone.js‎

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
'use strict';
2+
3+
const assert = require('assert');
4+
5+
const { Endpoint, BoundCluster } = require('..');
6+
7+
const BASIC_CLUSTER_ID = 0x0000;
8+
const MODEL_ID = 0x0005;
9+
10+
class Basic extends BoundCluster {
11+
12+
get modelId() {
13+
return 'Homey Bridge';
14+
}
15+
16+
}
17+
18+
describe('Endpoint standalone', function() {
19+
it('serves bound clusters with nothing but a sendFrame owner', async function() {
20+
// The whole point of the export: a device answering incoming commands is not a peer, so it
21+
// should not have to construct a ZCLNode - which models one remote node and bakes that node's
22+
// address into its own sendFrame. Endpoint touches its owner in exactly one place (sendFrame),
23+
// so a plain object is enough. If that ever stops being true, this fails.
24+
let sent;
25+
const endpoint = new Endpoint(
26+
{
27+
sendFrame: async (_endpointId, _clusterId, frame) => {
28+
sent = frame;
29+
},
30+
},
31+
{ endpointId: 1, inputClusters: [BASIC_CLUSTER_ID], outputClusters: [] },
32+
);
33+
endpoint.bind('basic', new Basic());
34+
35+
const request = Buffer.from([0x00, 0x07, 0x00, MODEL_ID, 0x00]);
36+
await endpoint.handleFrame(BASIC_CLUSTER_ID, request);
37+
38+
assert.equal(sent.toString('hex'), '180701050000420c486f6d657920427269646765');
39+
});
40+
});

0 commit comments

Comments
 (0)