Skip to content

Commit 80502da

Browse files
authored
fix: OnEvent fixes (#27063)
1 parent 8e547d0 commit 80502da

File tree

5 files changed

+228
-126
lines changed

5 files changed

+228
-126
lines changed

lib/extension/bind.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -575,12 +575,14 @@ export default class Bind extends Extension {
575575
}
576576
}
577577

578-
// If message is published to a group, add members of the group
579-
const group = data.groupID && data.groupID !== 0 && this.zigbee.groupByID(data.groupID);
578+
if (data.groupID && data.groupID !== 0) {
579+
// If message is published to a group, add members of the group
580+
const group = this.zigbee.groupByID(data.groupID);
580581

581-
if (group) {
582-
for (const member of group.zh.members) {
583-
toPoll.add(member);
582+
if (group) {
583+
for (const member of group.zh.members) {
584+
toPoll.add(member);
585+
}
584586
}
585587
}
586588

lib/extension/onEvent.ts

Lines changed: 37 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import * as zhc from 'zigbee-herdsman-converters';
1+
import {onEvent} from 'zigbee-herdsman-converters';
22

33
import utils from '../util/utils';
44
import Extension from './extension';
@@ -9,27 +9,44 @@ import Extension from './extension';
99
export default class OnEvent extends Extension {
1010
override async start(): Promise<void> {
1111
for (const device of this.zigbee.devicesIterator(utils.deviceNotCoordinator)) {
12+
// don't await, in case of repeated failures this would hold startup
1213
this.callOnEvent(device, 'start', {}).catch(utils.noop);
1314
}
1415

15-
this.eventBus.onDeviceMessage(this, (data) => this.callOnEvent(data.device, 'message', this.convertData(data)));
16-
this.eventBus.onDeviceJoined(this, (data) => this.callOnEvent(data.device, 'deviceJoined', this.convertData(data)));
17-
this.eventBus.onDeviceInterview(this, (data) => this.callOnEvent(data.device, 'deviceInterview', this.convertData(data)));
18-
this.eventBus.onDeviceAnnounce(this, (data) => this.callOnEvent(data.device, 'deviceAnnounce', this.convertData(data)));
19-
this.eventBus.onDeviceNetworkAddressChanged(this, (data) =>
20-
this.callOnEvent(data.device, 'deviceNetworkAddressChanged', this.convertData(data)),
21-
);
16+
this.eventBus.onDeviceMessage(this, async (data) => {
17+
await this.callOnEvent(data.device, 'message', {
18+
endpoint: data.endpoint,
19+
meta: data.meta,
20+
cluster: typeof data.cluster === 'string' ? data.cluster : /* v8 ignore next */ undefined, // XXX: ZH typing is wrong?
21+
type: data.type,
22+
data: data.data, // XXX: typing is a bit convoluted: ZHC has `KeyValueAny` here while Z2M has `KeyValue | Array<string | number>`
23+
});
24+
});
25+
this.eventBus.onDeviceJoined(this, async (data) => {
26+
await this.callOnEvent(data.device, 'deviceJoined', {});
27+
});
28+
this.eventBus.onDeviceLeave(this, async (data) => {
29+
if (data.device) {
30+
await this.callOnEvent(data.device, 'stop', {});
31+
}
32+
});
33+
this.eventBus.onDeviceInterview(this, async (data) => {
34+
await this.callOnEvent(data.device, 'deviceInterview', {});
35+
});
36+
this.eventBus.onDeviceAnnounce(this, async (data) => {
37+
await this.callOnEvent(data.device, 'deviceAnnounce', {});
38+
});
39+
this.eventBus.onDeviceNetworkAddressChanged(this, async (data) => {
40+
await this.callOnEvent(data.device, 'deviceNetworkAddressChanged', {});
41+
});
2242
this.eventBus.onEntityOptionsChanged(this, async (data) => {
2343
if (data.entity.isDevice()) {
24-
await this.callOnEvent(data.entity, 'deviceOptionsChanged', data).then(() => this.eventBus.emitDevicesChanged());
44+
await this.callOnEvent(data.entity, 'deviceOptionsChanged', {});
45+
this.eventBus.emitDevicesChanged();
2546
}
2647
});
2748
}
2849

29-
private convertData(data: KeyValue): KeyValue {
30-
return {...data, device: data.device.zh};
31-
}
32-
3350
override async stop(): Promise<void> {
3451
await super.stop();
3552

@@ -38,12 +55,15 @@ export default class OnEvent extends Extension {
3855
}
3956
}
4057

41-
private async callOnEvent(device: Device, type: zhc.OnEventType, data: KeyValue): Promise<void> {
42-
if (device.options.disabled) return;
58+
private async callOnEvent(device: Device, type: Parameters<typeof onEvent>[0], data: Parameters<typeof onEvent>[1]): Promise<void> {
59+
if (device.options.disabled) {
60+
return;
61+
}
62+
4363
const state = this.state.get(device);
44-
const deviceExposesChanged = (): void => this.eventBus.emitExposesAndDevicesChanged(data.device);
64+
const deviceExposesChanged = (): void => this.eventBus.emitExposesAndDevicesChanged(device);
4565

46-
await zhc.onEvent(type, data, device.zh, {deviceExposesChanged});
66+
await onEvent(type, data, device.zh, {deviceExposesChanged});
4767

4868
if (device.definition?.onEvent) {
4969
const options: KeyValue = device.options;

lib/types/types.d.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,15 +71,15 @@ declare global {
7171
type EntityOptionsChanged = {entity: Device | Group; from: KeyValue; to: KeyValue};
7272
type ExposesChanged = {device: Device};
7373
type Reconfigure = {device: Device};
74-
type DeviceLeave = {ieeeAddr: string; name: string};
74+
type DeviceLeave = {ieeeAddr: string; name: string; device?: Device};
7575
type GroupMembersChanged = {group: Group; action: 'remove' | 'add' | 'remove_all'; endpoint: zh.Endpoint; skipDisableReporting: boolean};
7676
type PublishEntityState = {entity: Group | Device; message: KeyValue; stateChangeReason?: StateChangeReason; payload: KeyValue};
7777
type DeviceMessage = {
7878
type: ZHEvents.MessagePayloadType;
7979
device: Device;
8080
endpoint: zh.Endpoint;
8181
linkquality: number;
82-
groupID: number;
82+
groupID: number; // XXX: should this be `?`
8383
cluster: string | number;
8484
data: KeyValue | Array<string | number>;
8585
meta: {zclTransactionSequenceNumber?: number; manufacturerCode?: number; frameControl?: ZHFrameControl};

lib/zigbee.ts

Lines changed: 51 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type {Events as ZHEvents} from 'zigbee-herdsman';
2+
import type {StartResult} from 'zigbee-herdsman/dist/adapter/tstype';
23

34
import {randomInt} from 'node:crypto';
45

@@ -14,20 +15,20 @@ import logger from './util/logger';
1415
import * as settings from './util/settings';
1516
import utils from './util/utils';
1617

17-
const entityIDRegex = new RegExp(`^(.+?)(?:/([^/]+))?$`);
18+
const entityIDRegex = /^(.+?)(?:\/([^/]+))?$/;
1819

1920
export default class Zigbee {
2021
// @ts-expect-error initialized in start
2122
private herdsman: Controller;
2223
private eventBus: EventBus;
23-
private groupLookup: {[s: number]: Group} = {};
24-
private deviceLookup: {[s: string]: Device} = {};
24+
private groupLookup: Map<number /* group ID */, Group> = new Map();
25+
private deviceLookup: Map<string /* IEEE address */, Device> = new Map();
2526

2627
constructor(eventBus: EventBus) {
2728
this.eventBus = eventBus;
2829
}
2930

30-
async start(): Promise<'reset' | 'resumed' | 'restored'> {
31+
async start(): Promise<StartResult> {
3132
const infoHerdsman = await utils.getDependencyVersion('zigbee-herdsman');
3233
logger.info(`Starting zigbee-herdsman (${infoHerdsman.version})`);
3334
const panId = settings.get().advanced.pan_id;
@@ -63,12 +64,12 @@ export default class Zigbee {
6364
`Using zigbee-herdsman with settings: '${stringify(JSON.stringify(herdsmanSettings).replaceAll(JSON.stringify(herdsmanSettings.network.networkKey), '"HIDDEN"'))}'`,
6465
);
6566

66-
let startResult;
67+
let startResult: StartResult;
6768
try {
6869
this.herdsman = new Controller(herdsmanSettings);
6970
startResult = await this.herdsman.start();
7071
} catch (error) {
71-
logger.error(`Error while starting zigbee-herdsman`);
72+
logger.error('Error while starting zigbee-herdsman');
7273
throw error;
7374
}
7475

@@ -109,18 +110,17 @@ export default class Zigbee {
109110
this.herdsman.on('deviceLeave', (data: ZHEvents.DeviceLeavePayload) => {
110111
const name = settings.getDevice(data.ieeeAddr)?.friendly_name || data.ieeeAddr;
111112
logger.warning(`Device '${name}' left the network`);
112-
this.eventBus.emitDeviceLeave({ieeeAddr: data.ieeeAddr, name});
113+
this.eventBus.emitDeviceLeave({ieeeAddr: data.ieeeAddr, name, device: this.deviceLookup.get(data.ieeeAddr)});
113114
});
114115
this.herdsman.on('message', async (data: ZHEvents.MessagePayload) => {
115116
const device = this.resolveDevice(data.device.ieeeAddr)!;
116117
await device.resolveDefinition();
117-
logger.debug(
118-
() =>
119-
`Received Zigbee message from '${device.name}', type '${data.type}', ` +
120-
`cluster '${data.cluster}', data '${stringify(data.data)}' from endpoint ${data.endpoint.ID}` +
121-
(data['groupID'] !== undefined ? ` with groupID ${data.groupID}` : ``) +
122-
(device.zh.type === 'Coordinator' ? `, ignoring since it is from coordinator` : ``),
123-
);
118+
logger.debug(() => {
119+
const groupId = data.groupID !== undefined ? ` with groupID ${data.groupID}` : '';
120+
const fromCoord = device.zh.type === 'Coordinator' ? ', ignoring since it is from coordinator' : '';
121+
122+
return `Received Zigbee message from '${device.name}', type '${data.type}', cluster '${data.cluster}', data '${stringify(data.data)}' from endpoint ${data.endpoint.ID}${groupId}${fromCoord}`;
123+
});
124124
if (device.zh.type === 'Coordinator') return;
125125
this.eventBus.emitDeviceMessage({...data, device});
126126
});
@@ -165,9 +165,7 @@ export default class Zigbee {
165165
logger.info(`Device '${name}' is supported, identified as: ${vendor} ${description} (${model})`);
166166
} else {
167167
logger.warning(
168-
`Device '${name}' with Zigbee model '${data.device.zh.modelID}' and manufacturer name ` +
169-
`'${data.device.zh.manufacturerName}' is NOT supported, ` +
170-
`please follow https://www.zigbee2mqtt.io/advanced/support-new-devices/01_support_new_devices.html`,
168+
`Device '${name}' with Zigbee model '${data.device.zh.modelID}' and manufacturer name '${data.device.zh.manufacturerName}' is NOT supported, please follow https://www.zigbee2mqtt.io/advanced/support-new-devices/01_support_new_devices.html`,
171169
);
172170
}
173171
} else if (data.status === 'failed') {
@@ -241,55 +239,57 @@ export default class Zigbee {
241239
await this.herdsman.permitJoin(time, device?.zh);
242240
}
243241

244-
async resolveDevicesDefinitions(ignoreCache: boolean = false): Promise<void> {
242+
async resolveDevicesDefinitions(ignoreCache = false): Promise<void> {
245243
for (const device of this.devicesIterator(utils.deviceNotCoordinator)) {
246244
await device.resolveDefinition(ignoreCache);
247245
}
248246
}
249247

250248
@bind private resolveDevice(ieeeAddr: string): Device | undefined {
251-
if (!this.deviceLookup[ieeeAddr]) {
249+
if (!this.deviceLookup.has(ieeeAddr)) {
252250
const device = this.herdsman.getDeviceByIeeeAddr(ieeeAddr);
253251
if (device) {
254-
this.deviceLookup[ieeeAddr] = new Device(device);
252+
this.deviceLookup.set(ieeeAddr, new Device(device));
255253
}
256254
}
257255

258-
const device = this.deviceLookup[ieeeAddr];
256+
const device = this.deviceLookup.get(ieeeAddr);
259257
if (device && !device.zh.isDeleted) {
260258
device.ensureInSettings();
261259
return device;
262260
}
263261
}
264262

265-
private resolveGroup(groupID: number): Group {
263+
private resolveGroup(groupID: number): Group | undefined {
266264
const group = this.herdsman.getGroupByID(Number(groupID));
267-
if (group && !this.groupLookup[groupID]) {
268-
this.groupLookup[groupID] = new Group(group, this.resolveDevice);
265+
if (group && !this.groupLookup.has(groupID)) {
266+
this.groupLookup.set(groupID, new Group(group, this.resolveDevice));
269267
}
270268

271-
return this.groupLookup[groupID];
269+
return this.groupLookup.get(groupID);
272270
}
273271

274272
resolveEntity(key: string | number | zh.Device): Device | Group | undefined {
275273
if (typeof key === 'object') {
276274
return this.resolveDevice(key.ieeeAddr);
277-
} else if (typeof key === 'string' && key.toLowerCase() === 'coordinator') {
275+
}
276+
277+
if (typeof key === 'string' && key.toLowerCase() === 'coordinator') {
278278
return this.resolveDevice(this.herdsman.getDevicesByType('Coordinator')[0].ieeeAddr);
279-
} else {
280-
const settingsDevice = settings.getDevice(key.toString());
279+
}
281280

282-
if (settingsDevice) {
283-
return this.resolveDevice(settingsDevice.ID);
284-
}
281+
const settingsDevice = settings.getDevice(key.toString());
285282

286-
const groupSettings = settings.getGroup(key);
283+
if (settingsDevice) {
284+
return this.resolveDevice(settingsDevice.ID);
285+
}
287286

288-
if (groupSettings) {
289-
const group = this.resolveGroup(groupSettings.ID);
290-
// If group does not exist, create it (since it's already in configuration.yaml)
291-
return group ? group : this.createGroup(groupSettings.ID);
292-
}
287+
const groupSettings = settings.getGroup(key);
288+
289+
if (groupSettings) {
290+
const group = this.resolveGroup(groupSettings.ID);
291+
// If group does not exist, create it (since it's already in configuration.yaml)
292+
return group ? group : this.createGroup(groupSettings.ID);
293293
}
294294
}
295295

@@ -339,13 +339,13 @@ export default class Zigbee {
339339
}
340340

341341
for (const group of this.herdsman.getGroupsIterator(groupPredicate)) {
342-
yield this.resolveGroup(group.groupID);
342+
yield this.resolveGroup(group.groupID)!;
343343
}
344344
}
345345

346346
*groupsIterator(predicate?: (value: zh.Group) => boolean): Generator<Group> {
347347
for (const group of this.herdsman.getGroupsIterator(predicate)) {
348-
yield this.resolveGroup(group.groupID);
348+
yield this.resolveGroup(group.groupID)!;
349349
}
350350
}
351351

@@ -363,21 +363,22 @@ export default class Zigbee {
363363
if (passlist.includes(ieeeAddr)) {
364364
logger.info(`Accepting joining device which is on passlist '${ieeeAddr}'`);
365365
return true;
366-
} else {
367-
logger.info(`Rejecting joining not in passlist device '${ieeeAddr}'`);
368-
return false;
369366
}
370-
} else if (blocklist.length > 0) {
367+
368+
logger.info(`Rejecting joining not in passlist device '${ieeeAddr}'`);
369+
return false;
370+
}
371+
372+
if (blocklist.length > 0) {
371373
if (blocklist.includes(ieeeAddr)) {
372374
logger.info(`Rejecting joining device which is on blocklist '${ieeeAddr}'`);
373375
return false;
374-
} else {
375-
logger.info(`Accepting joining not in blocklist device '${ieeeAddr}'`);
376-
return true;
377376
}
378-
} else {
379-
return true;
377+
378+
logger.info(`Accepting joining not in blocklist device '${ieeeAddr}'`);
380379
}
380+
381+
return true;
381382
}
382383

383384
async touchlinkFactoryResetFirst(): Promise<boolean> {
@@ -402,15 +403,15 @@ export default class Zigbee {
402403

403404
createGroup(ID: number): Group {
404405
this.herdsman.createGroup(ID);
405-
return this.resolveGroup(ID);
406+
return this.resolveGroup(ID)!;
406407
}
407408

408409
deviceByNetworkAddress(networkAddress: number): Device | undefined {
409410
const device = this.herdsman.getDeviceByNetworkAddress(networkAddress);
410411
return device && this.resolveDevice(device.ieeeAddr);
411412
}
412413

413-
groupByID(ID: number): Group {
414+
groupByID(ID: number): Group | undefined {
414415
return this.resolveGroup(ID);
415416
}
416417
}

0 commit comments

Comments
 (0)