Skip to content

Commit

Permalink
fix: Improve bind/unbind logic (#1144)
Browse files Browse the repository at this point in the history
* Improve bind/unbind logic.

* Add `hasBind` shortcut
  • Loading branch information
Nerivec committed Aug 9, 2024
1 parent 5e43d65 commit a3aeb33
Show file tree
Hide file tree
Showing 2 changed files with 87 additions and 56 deletions.
100 changes: 52 additions & 48 deletions src/controller/model/endpoint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,42 +89,26 @@ class Endpoint extends Entity {

// Getters/setters
get binds(): Bind[] {
return this._binds
.map((entry) => {
let target: Group | Endpoint = null;
if (entry.type === 'endpoint') {
const device = Device.byIeeeAddr(entry.deviceIeeeAddress);
if (device) {
target = device.getEndpoint(entry.endpointID);
}
} else {
target = Group.byGroupID(entry.groupID);
}
const binds: Bind[] = [];

if (target) {
return {target, cluster: this.getCluster(entry.cluster)};
} else {
return undefined;
}
})
.filter((b) => b !== undefined);
for (const bind of this._binds) {
const target: Group | Endpoint =
bind.type === 'endpoint' ? Device.byIeeeAddr(bind.deviceIeeeAddress)?.getEndpoint(bind.endpointID) : Group.byGroupID(bind.groupID);

if (target) {
binds.push({target, cluster: this.getCluster(bind.cluster)});
}
}

return binds;
}

get configuredReportings(): ConfiguredReporting[] {
return this._configuredReportings.map((entry) => {
const cluster = Zcl.Utils.getCluster(entry.cluster, entry.manufacturerCode, this.getDevice().customClusters);
let attribute: ZclTypes.Attribute;

if (cluster.hasAttribute(entry.attrId)) {
attribute = cluster.getAttribute(entry.attrId);
} else {
attribute = {
ID: entry.attrId,
name: undefined,
type: undefined,
manufacturerCode: undefined,
};
}
const attribute: ZclTypes.Attribute = cluster.hasAttribute(entry.attrId)
? cluster.getAttribute(entry.attrId)
: {ID: entry.attrId, name: undefined, type: undefined, manufacturerCode: undefined};

return {
cluster,
Expand Down Expand Up @@ -469,13 +453,26 @@ class Endpoint extends Entity {
);
}

public hasBind(clusterId: number, target: Endpoint | Group): boolean {
return this.getBindIndex(clusterId, target) !== -1;
}

public getBindIndex(clusterId: number, target: Endpoint | Group): number {
return this.binds.findIndex((b) => b.cluster.ID === clusterId && b.target === target);
}

public addBinding(clusterKey: number | string, target: Endpoint | Group | number): void {
const cluster = this.getCluster(clusterKey);

if (typeof target === 'number') {
target = Group.byGroupID(target) || Group.create(target);
}

if (!this.binds.find((b) => b.cluster.ID === cluster.ID && b.target === target)) {
this.addBindingInternal(cluster, target);
}

private addBindingInternal(cluster: ZclTypes.Cluster, target: Endpoint | Group): void {
if (!this.hasBind(cluster.ID, target)) {
if (target instanceof Group) {
this._binds.push({cluster: cluster.ID, groupID: target.groupID, type: 'group'});
} else {
Expand All @@ -494,15 +491,14 @@ class Endpoint extends Entity {
public async bind(clusterKey: number | string, target: Endpoint | Group | number): Promise<void> {
const cluster = this.getCluster(clusterKey);
const type = target instanceof Endpoint ? 'endpoint' : 'group';

if (typeof target === 'number') {
target = Group.byGroupID(target) || Group.create(target);
}

const destinationAddress = target instanceof Endpoint ? target.deviceIeeeAddress : target.groupID;

const log =
`Bind ${this.deviceIeeeAddress}/${this.ID} ${cluster.name} from ` +
`'${target instanceof Endpoint ? `${destinationAddress}/${target.ID}` : destinationAddress}'`;
const log = `Bind ${this.deviceIeeeAddress}/${this.ID} ${cluster.name} from '${target instanceof Endpoint ? `${destinationAddress}/${target.ID}` : destinationAddress}'`;
logger.debug(log, NS);

try {
Expand All @@ -516,7 +512,7 @@ class Endpoint extends Entity {
target instanceof Endpoint ? target.ID : null,
);

this.addBinding(clusterKey, target);
this.addBindingInternal(cluster, target);
} catch (error) {
error.message = `${log} failed (${error.message})`;
logger.debug(error, NS);
Expand All @@ -530,13 +526,28 @@ class Endpoint extends Entity {

public async unbind(clusterKey: number | string, target: Endpoint | Group | number): Promise<void> {
const cluster = this.getCluster(clusterKey);
const action = `Unbind ${this.deviceIeeeAddress}/${this.ID} ${cluster.name}`;

if (typeof target === 'number') {
const groupTarget = Group.byGroupID(target);

if (!groupTarget) {
throw new Error(`${action} invalid target '${target}' (no group with this ID exists).`);
}

target = groupTarget;
}

const type = target instanceof Endpoint ? 'endpoint' : 'group';
const destinationAddress = target instanceof Endpoint ? target.deviceIeeeAddress : target.groupID;
const log = `${action} from '${target instanceof Endpoint ? `${destinationAddress}/${target.ID}` : destinationAddress}'`;
const index = this.getBindIndex(cluster.ID, target);

const destinationAddress = target instanceof Endpoint ? target.deviceIeeeAddress : target instanceof Group ? target.groupID : target;
if (index === -1) {
logger.debug(`${log} no bind present, skipping.`, NS);
return;
}

const log =
`Unbind ${this.deviceIeeeAddress}/${this.ID} ${cluster.name} from ` +
`'${target instanceof Endpoint ? `${destinationAddress}/${target.ID}` : destinationAddress}'`;
logger.debug(log, NS);

try {
Expand All @@ -550,15 +561,8 @@ class Endpoint extends Entity {
target instanceof Endpoint ? target.ID : null,
);

if (typeof target === 'number' && Group.byGroupID(target)) {
target = Group.byGroupID(target);
}

const index = this.binds.findIndex((b) => b.cluster.ID === cluster.ID && b.target === target);
if (index !== -1) {
this._binds.splice(index, 1);
this.save();
}
this._binds.splice(index, 1);
this.save();
} catch (error) {
error.message = `${log} failed (${error.message})`;
logger.debug(error, NS);
Expand Down
43 changes: 35 additions & 8 deletions test/controller.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7542,34 +7542,61 @@ describe('Controller', () => {
expect(error.message).toStrictEqual(`Use parameter`);
});

it('Skip unbind if not bound', async () => {
await controller.start();
await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'});
await mockAdapterEvents['deviceJoined']({networkAddress: 170, ieeeAddr: '0x170'});
const endpoint = controller.getDeviceByIeeeAddr('0x129').getEndpoint(1);
const target = controller.getDeviceByIeeeAddr('0x170').getEndpoint(1);
mockAdapterUnbind.mockClear();
await endpoint.unbind('genOnOff', target);
expect(mockAdapterUnbind).toHaveBeenCalledTimes(0);
});

it('Handle unbind with number not matching any group', async () => {
await controller.start();
await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'});
const endpoint = controller.getDeviceByIeeeAddr('0x129').getEndpoint(1);
let error;
try {
await endpoint.unbind('genOnOff', 1);
} catch (e) {
error = e;
}
expect(error).toStrictEqual(new Error(`Unbind 0x129/1 genOnOff invalid target '1' (no group with this ID exists).`));
});

it('Unbind error', async () => {
await controller.start();
await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'});
const device = controller.getDeviceByIeeeAddr('0x129');
const endpoint = device.getEndpoint(1);
await mockAdapterEvents['deviceJoined']({networkAddress: 170, ieeeAddr: '0x170'});
const endpoint = controller.getDeviceByIeeeAddr('0x129').getEndpoint(1);
const target = controller.getDeviceByIeeeAddr('0x170').getEndpoint(1);
await endpoint.bind('genOnOff', target);
mockAdapterUnbind.mockRejectedValueOnce(new Error('timeout occurred'));
let error;
try {
await endpoint.unbind('genOnOff', 1);
await endpoint.unbind('genOnOff', target);
} catch (e) {
error = e;
}
expect(error).toStrictEqual(new Error(`Unbind 0x129/1 genOnOff from '1' failed (timeout occurred)`));
expect(error).toStrictEqual(new Error(`Unbind 0x129/1 genOnOff from '0x170/1' failed (timeout occurred)`));
});

it('Bind error', async () => {
await controller.start();
await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'});
const device = controller.getDeviceByIeeeAddr('0x129');
const endpoint = device.getEndpoint(1);
await mockAdapterEvents['deviceJoined']({networkAddress: 170, ieeeAddr: '0x170'});
const endpoint = controller.getDeviceByIeeeAddr('0x129').getEndpoint(1);
const target = controller.getDeviceByIeeeAddr('0x170').getEndpoint(1);
mockAdapterBind.mockRejectedValueOnce(new Error('timeout occurred'));
let error;
try {
await endpoint.bind('genOnOff', 1);
await endpoint.bind('genOnOff', target);
} catch (e) {
error = e;
}
expect(error).toStrictEqual(new Error(`Bind 0x129/1 genOnOff from '1' failed (timeout occurred)`));
expect(error).toStrictEqual(new Error(`Bind 0x129/1 genOnOff from '0x170/1' failed (timeout occurred)`));
});

it('ReadResponse error', async () => {
Expand Down

0 comments on commit a3aeb33

Please sign in to comment.