From 63e679dec3bc2ed3bedc213505fcc8d556a7e3f2 Mon Sep 17 00:00:00 2001 From: Nerivec <62446222+Nerivec@users.noreply.github.com> Date: Sun, 2 Mar 2025 18:48:51 +0100 Subject: [PATCH] fix: endpoint device & group members typing --- src/controller/model/device.ts | 2 +- src/controller/model/endpoint.ts | 29 +++++++++++++++++---- src/controller/model/group.ts | 43 +++++++++++++++++++++----------- test/controller.test.ts | 6 ++--- 4 files changed, 56 insertions(+), 24 deletions(-) diff --git a/src/controller/model/device.ts b/src/controller/model/device.ts index 6d46b9af49..db9534966f 100755 --- a/src/controller/model/device.ts +++ b/src/controller/model/device.ts @@ -476,7 +476,7 @@ export class Device extends Entity { const commandHasResponse = frame.command.response != undefined; const disableDefaultResponse = frame.header.frameControl.disableDefaultResponse; /* v8 ignore next */ - const disableTuyaDefaultResponse = endpoint.getDevice().manufacturerName?.startsWith('_TZ') && process.env['DISABLE_TUYA_DEFAULT_RESPONSE']; + const disableTuyaDefaultResponse = endpoint.getDevice()!.manufacturerName?.startsWith('_TZ') && process.env['DISABLE_TUYA_DEFAULT_RESPONSE']; // Sometimes messages are received twice, prevent responding twice const alreadyResponded = this._lastDefaultResponseSequenceNumber === frame.header.transactionSequenceNumber; diff --git a/src/controller/model/endpoint.ts b/src/controller/model/endpoint.ts index 9a6bafd669..46f95c2070 100644 --- a/src/controller/model/endpoint.ts +++ b/src/controller/model/endpoint.ts @@ -118,8 +118,11 @@ export class Endpoint extends Entity { } get configuredReportings(): ConfiguredReporting[] { + const device = this.getDevice(); + assert(device, `Cannot get configured reportings for unknown/deleted device ${this.deviceIeeeAddress}`); + return this._configuredReportings.map((entry, index) => { - const cluster = Zcl.Utils.getCluster(entry.cluster, entry.manufacturerCode, this.getDevice().customClusters); + const cluster = Zcl.Utils.getCluster(entry.cluster, entry.manufacturerCode, device.customClusters); const attribute: ZclTypes.Attribute = cluster.hasAttribute(entry.attrId) ? cluster.getAttribute(entry.attrId) : {ID: entry.attrId, name: `attr${index}`, type: Zcl.DataType.UNKNOWN, manufacturerCode: undefined}; @@ -165,8 +168,8 @@ export class Endpoint extends Entity { /** * Get device of this endpoint */ - public getDevice(): Device { - return Device.byIeeeAddr(this.deviceIeeeAddress)!; // XXX: no way for device to not exist? + public getDevice(): Device | undefined { + return Device.byIeeeAddr(this.deviceIeeeAddress); } /** @@ -309,6 +312,8 @@ export class Endpoint extends Entity { ): Promise { const logPrefix = `Request Queue (${this.deviceIeeeAddress}/${this.ID}): `; const device = this.getDevice(); + assert(device, `Cannot send to unknown/deleted device ${this.deviceIeeeAddress}`); + const request = new Request(func, frame, device.pendingRequestTimeout, options.sendPolicy); if (request.sendPolicy !== 'bulk') { @@ -428,6 +433,8 @@ export class Endpoint extends Entity { public async read(clusterKey: number | string, attributes: (string | number)[], options?: Options): Promise { const device = this.getDevice(); + assert(device, `Cannot read from unknown/deleted device ${this.deviceIeeeAddress}`); + const cluster = this.getCluster(clusterKey, device); const optionsWithDefaults = this.getOptionsWithDefaults(options, true, Zcl.Direction.CLIENT_TO_SERVER, cluster.manufacturerCode); optionsWithDefaults.manufacturerCode = this.ensureManufacturerCodeIsUniqueAndGet( @@ -575,7 +582,7 @@ export class Endpoint extends Entity { } public save(): void { - this.getDevice().save(); + this.getDevice()?.save(); } public async unbind(clusterKey: number | string, target: Endpoint | Group | number): Promise { @@ -734,6 +741,8 @@ export class Endpoint extends Entity { assert(options?.transactionSequenceNumber === undefined, 'Use parameter'); const device = this.getDevice(); + assert(device, `Cannot respond to unknown/deleted device ${this.deviceIeeeAddress}`); + const cluster = this.getCluster(clusterKey, device); const command = cluster.getCommandResponse(commandKey); transactionSequenceNumber = transactionSequenceNumber || ZclTransactionSequenceNumber.next(); @@ -790,6 +799,8 @@ export class Endpoint extends Entity { timeout: number, ): {promise: Promise<{header: Zcl.Header; payload: KeyValue}>; cancel: () => void} { const device = this.getDevice(); + assert(device, `Cannot wait for unknown/deleted device ${this.deviceIeeeAddress}`); + const cluster = this.getCluster(clusterKey, device); const command = cluster.getCommand(commandKey); const waiter = Entity.adapter!.waitFor( @@ -883,7 +894,11 @@ export class Endpoint extends Entity { } private getCluster(clusterKey: number | string, device: Device | undefined = undefined): ZclTypes.Cluster { - device = device ?? this.getDevice(); + if (!device) { + device = this.getDevice(); + assert(device, `Cannot get cluster for unknown/deleted device ${this.deviceIeeeAddress}`); + } + return Zcl.Utils.getCluster(clusterKey, device.manufacturerID, device.customClusters); } @@ -922,6 +937,8 @@ export class Endpoint extends Entity { frameType: Zcl.FrameType = Zcl.FrameType.GLOBAL, ): Promise { const device = this.getDevice(); + assert(device, `Cannot send to unknown/deleted device ${this.deviceIeeeAddress}`); + const cluster = this.getCluster(clusterKey, device); const command = frameType == Zcl.FrameType.GLOBAL ? Zcl.Utils.getGlobalCommand(commandKey) : cluster.getCommand(commandKey); const hasResponse = frameType == Zcl.FrameType.GLOBAL ? true : command.response != undefined; @@ -972,6 +989,8 @@ export class Endpoint extends Entity { options?: Options, ): Promise { const device = this.getDevice(); + assert(device, `Cannot send to unknown/deleted device ${this.deviceIeeeAddress}`); + const cluster = this.getCluster(clusterKey, device); const command = cluster.getCommand(commandKey); const optionsWithDefaults = this.getOptionsWithDefaults(options, true, Zcl.Direction.CLIENT_TO_SERVER, cluster.manufacturerCode); diff --git a/src/controller/model/group.ts b/src/controller/model/group.ts index 28305dc046..827e1c82ac 100644 --- a/src/controller/model/group.ts +++ b/src/controller/model/group.ts @@ -26,10 +26,7 @@ interface OptionsWithDefaults extends Options { export class Group extends Entity { private databaseID: number; public readonly groupID: number; - private readonly _members: Set; - get members(): Endpoint[] { - return Array.from(this._members).filter((e) => e.getDevice()); - } + private readonly _members: Endpoint[]; // Can be used by applications to store data. public readonly meta: KeyValue; @@ -38,7 +35,12 @@ export class Group extends Entity { private static readonly groups: Map = new Map(); private static loadedFromDatabase: boolean = false; - private constructor(databaseID: number, groupID: number, members: Set, meta: KeyValue) { + /** Member endpoints with valid devices (not unknown/deleted) */ + get members(): Endpoint[] { + return this._members.filter((e) => e.getDevice() !== undefined); + } + + private constructor(databaseID: number, groupID: number, members: Endpoint[], meta: KeyValue) { super(); this.databaseID = databaseID; this.groupID = groupID; @@ -59,7 +61,8 @@ export class Group extends Entity { } private static fromDatabaseEntry(entry: DatabaseEntry): Group { - const members = new Set(); + // db is expected to never contain duplicate, so no need for explicit check + const members: Endpoint[] = []; for (const member of entry.members) { const device = Device.byIeeeAddr(member.deviceIeeeAddr); @@ -68,7 +71,7 @@ export class Group extends Entity { const endpoint = device.getEndpoint(member.endpointID); if (endpoint) { - members.add(endpoint); + members.push(endpoint); } } } @@ -79,8 +82,12 @@ export class Group extends Entity { private toDatabaseRecord(): DatabaseEntry { const members: DatabaseEntry['members'] = []; - for (const member of this.members) { - members.push({deviceIeeeAddr: member.getDevice().ieeeAddr, endpointID: member.ID}); + for (const member of this._members) { + const device = member.getDevice(); + + if (device) { + members.push({deviceIeeeAddr: device.ieeeAddr, endpointID: member.ID}); + } } return {id: this.databaseID, type: 'Group', groupID: this.groupID, members, meta: this.meta}; @@ -133,7 +140,7 @@ export class Group extends Entity { } const databaseID = Entity.database!.newID(); - const group = new Group(databaseID, groupID, new Set(), {}); + const group = new Group(databaseID, groupID, [], {}); Entity.database!.insert(group.toDatabaseRecord()); Group.groups.set(group.groupID, group); @@ -163,17 +170,23 @@ export class Group extends Entity { } public addMember(endpoint: Endpoint): void { - this._members.add(endpoint); - this.save(); + if (!this._members.includes(endpoint)) { + this._members.push(endpoint); + this.save(); + } } public removeMember(endpoint: Endpoint): void { - this._members.delete(endpoint); - this.save(); + const i = this._members.indexOf(endpoint); + + if (i > -1) { + this._members.splice(i, 1); + this.save(); + } } public hasMember(endpoint: Endpoint): boolean { - return this._members.has(endpoint); + return this._members.includes(endpoint); } /* diff --git a/test/controller.test.ts b/test/controller.test.ts index 8c1b0c5d35..1c74943c80 100755 --- a/test/controller.test.ts +++ b/test/controller.test.ts @@ -6852,7 +6852,7 @@ describe('Controller', () => { expect((await controller.getGroups()).length).toBe(2); const group1 = controller.getGroupByID(1)!; - expect(deepClone(group1)).toStrictEqual(deepClone({_events: {}, _eventsCount: 0, databaseID: 2, groupID: 1, _members: new Set(), meta: {}})); + expect(deepClone(group1)).toStrictEqual(deepClone({_events: {}, _eventsCount: 0, databaseID: 2, groupID: 1, _members: [], meta: {}})); const group2 = controller.getGroupByID(2)!; expect(deepClone(group2)).toStrictEqual( deepClone({ @@ -6860,7 +6860,7 @@ describe('Controller', () => { _eventsCount: 0, databaseID: 5, groupID: 2, - _members: new Set([ + _members: [ { meta: {}, _binds: [], @@ -6877,7 +6877,7 @@ describe('Controller', () => { pendingRequests: {ID: 1, deviceIeeeAddress: '0x000b57fffec6a5b2', sendInProgress: false}, profileID: 49246, }, - ]), + ], meta: {}, }), );