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

Improve bind/unbind logic. #1144

Merged
merged 2 commits into from
Aug 9, 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
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 {
Nerivec marked this conversation as resolved.
Show resolved Hide resolved
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