Skip to content

Commit 13ac8a0

Browse files
authored
fix: Improve looping performance (#23541)
* Use herdsman improved looping. * Fix lint. * Fix tests. * Feedback
1 parent fe6cacd commit 13ac8a0

21 files changed

+385
-162
lines changed

lib/controller.ts

+8-4
Original file line numberDiff line numberDiff line change
@@ -157,15 +157,19 @@ export class Controller {
157157
}
158158

159159
// Log zigbee clients on startup
160-
const devices = this.zigbee.devices(false);
161-
logger.info(`Currently ${devices.length} devices are joined:`);
162-
for (const device of devices) {
160+
let deviceCount = 0;
161+
162+
for (const device of this.zigbee.devicesIterator(utils.deviceNotCoordinator)) {
163163
const model = device.isSupported
164164
? `${device.definition.model} - ${device.definition.vendor} ${device.definition.description}`
165165
: 'Not supported';
166166
logger.info(`${device.name} (${device.ieeeAddr}): ${model} (${device.zh.type})`);
167+
168+
deviceCount++;
167169
}
168170

171+
logger.info(`Currently ${deviceCount} devices are joined.`);
172+
169173
// Enable zigbee join
170174
try {
171175
if (settings.get().permit_join) {
@@ -193,7 +197,7 @@ export class Controller {
193197

194198
// Send all cached states.
195199
if (settings.get().advanced.cache_state_send_on_startup && settings.get().advanced.cache_state) {
196-
for (const entity of [...devices, ...this.zigbee.groups()]) {
200+
for (const entity of this.zigbee.devicesAndGroupsIterator()) {
197201
if (this.state.exists(entity)) {
198202
await this.publishEntityState(entity, this.state.get(entity), 'publishCached');
199203
}

lib/extension/availability.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ export default class Availability extends Extension {
140140
await this.publishAvailabilityForAllEntities();
141141

142142
// Start availability for the devices
143-
for (const device of this.zigbee.devices(false)) {
143+
for (const device of this.zigbee.devicesIterator(utils.deviceNotCoordinator)) {
144144
if (utils.isAvailabilityEnabledForEntity(device, settings.get())) {
145145
this.resetTimer(device);
146146

@@ -153,7 +153,7 @@ export default class Availability extends Extension {
153153
}
154154

155155
@bind private async publishAvailabilityForAllEntities(): Promise<void> {
156-
for (const entity of [...this.zigbee.devices(false), ...this.zigbee.groups()]) {
156+
for (const entity of this.zigbee.devicesAndGroupsIterator(utils.deviceNotCoordinator)) {
157157
if (utils.isAvailabilityEnabledForEntity(entity, settings.get())) {
158158
await this.publishAvailability(entity, true, false, true);
159159
}
@@ -187,7 +187,7 @@ export default class Availability extends Extension {
187187
await this.mqtt.publish(topic, payload, {retain: true, qos: 1});
188188

189189
if (!skipGroups && entity.isDevice()) {
190-
for (const group of this.zigbee.groups()) {
190+
for (const group of this.zigbee.groupsIterator()) {
191191
if (group.hasMember(entity) && utils.isAvailabilityEnabledForEntity(group, settings.get())) {
192192
await this.publishAvailability(group, false, forcePublish);
193193
}

lib/extension/bind.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -363,7 +363,7 @@ export default class Bind extends Extension {
363363
if (data.action === 'add') {
364364
const bindsToGroup: zh.Bind[] = [];
365365

366-
for (const device of this.zigbee.devices(false)) {
366+
for (const device of this.zigbee.devicesIterator(utils.deviceNotCoordinator)) {
367367
for (const endpoint of device.zh.endpoints) {
368368
for (const bind of endpoint.binds) {
369369
if (bind.target === data.group.zh) {
@@ -443,7 +443,7 @@ export default class Bind extends Extension {
443443
const endpoints = utils.isEndpoint(target) ? [target] : target.members;
444444
const allBinds: zh.Bind[] = [];
445445

446-
for (const device of this.zigbee.devices(false)) {
446+
for (const device of this.zigbee.devicesIterator(utils.deviceNotCoordinator)) {
447447
for (const endpoint of device.zh.endpoints) {
448448
for (const bind of endpoint.binds) {
449449
allBinds.push(bind);

lib/extension/bridge.ts

+36-21
Original file line numberDiff line numberDiff line change
@@ -725,8 +725,12 @@ export default class Bridge extends Extension {
725725
scenes: Scene[];
726726
}
727727

728-
const devices = this.zigbee.devices().map((device) => {
728+
// XXX: definition<>DefinitionPayload don't match to use `Device[]` type here
729+
const devices: KeyValue[] = [];
730+
731+
for (const device of this.zigbee.devicesIterator()) {
729732
const endpoints: {[s: number]: Data} = {};
733+
730734
for (const endpoint of device.zh.endpoints) {
731735
const data: Data = {
732736
scenes: utils.getScenes(endpoint),
@@ -758,7 +762,7 @@ export default class Bridge extends Extension {
758762
endpoints[endpoint.ID] = data;
759763
}
760764

761-
return {
765+
devices.push({
762766
ieee_address: device.ieeeAddr,
763767
type: device.zh.type,
764768
network_address: device.zh.networkAddress,
@@ -775,24 +779,32 @@ export default class Bridge extends Extension {
775779
interview_completed: device.zh.interviewCompleted,
776780
manufacturer: device.zh.manufacturerName,
777781
endpoints,
778-
};
779-
});
782+
});
783+
}
780784

781785
await this.mqtt.publish('bridge/devices', stringify(devices), {retain: true, qos: 0}, settings.get().mqtt.base_topic, true);
782786
}
783787

784788
async publishGroups(): Promise<void> {
785-
const groups = this.zigbee.groups().map((g) => {
786-
return {
787-
id: g.ID,
788-
friendly_name: g.ID === 901 ? 'default_bind_group' : g.name,
789-
description: g.options.description,
790-
scenes: utils.getScenes(g.zh),
791-
members: g.zh.members.map((e) => {
792-
return {ieee_address: e.getDevice().ieeeAddr, endpoint: e.ID};
793-
}),
794-
};
795-
});
789+
// XXX: id<>ID can't use `Group[]` type
790+
const groups: KeyValue[] = [];
791+
792+
for (const group of this.zigbee.groupsIterator()) {
793+
const members = [];
794+
795+
for (const member of group.zh.members) {
796+
members.push({ieee_address: member.getDevice().ieeeAddr, endpoint: member.ID});
797+
}
798+
799+
groups.push({
800+
id: group.ID,
801+
friendly_name: group.ID === 901 ? 'default_bind_group' : group.name,
802+
description: group.options.description,
803+
scenes: utils.getScenes(group.zh),
804+
members,
805+
});
806+
}
807+
796808
await this.mqtt.publish('bridge/groups', stringify(groups), {retain: true, qos: 0}, settings.get().mqtt.base_topic, true);
797809
}
798810

@@ -807,20 +819,23 @@ export default class Bridge extends Extension {
807819
custom_clusters: {},
808820
};
809821

810-
for (const device of this.zigbee.devices()) {
811-
if (Object.keys(device.customClusters).length !== 0) {
812-
data.custom_clusters[device.ieeeAddr] = device.customClusters;
813-
}
822+
for (const device of this.zigbee.devicesIterator((d) => !utils.objectIsEmpty(d.customClusters))) {
823+
data.custom_clusters[device.ieeeAddr] = device.customClusters;
814824
}
815825

816826
await this.mqtt.publish('bridge/definitions', stringify(data), {retain: true, qos: 0}, settings.get().mqtt.base_topic, true);
817827
}
818828

819-
getDefinitionPayload(device: Device): DefinitionPayload {
820-
if (!device.definition) return null;
829+
getDefinitionPayload(device: Device): DefinitionPayload | null {
830+
if (!device.definition) {
831+
return null;
832+
}
833+
834+
// TODO: better typing to avoid @ts-expect-error
821835
// @ts-expect-error icon is valid for external definitions
822836
const definitionIcon = device.definition.icon;
823837
let icon = device.options.icon ?? definitionIcon;
838+
824839
if (icon) {
825840
icon = icon.replace('${zigbeeModel}', utils.sanitizeImageParameter(device.zh.modelID));
826841
icon = icon.replace('${model}', utils.sanitizeImageParameter(device.definition.model));

lib/extension/configure.ts

+2-3
Original file line numberDiff line numberDiff line change
@@ -68,9 +68,8 @@ export default class Configure extends Extension {
6868
setImmediate(async () => {
6969
// Only configure routers on startup, end devices are likely sleeping and
7070
// will reconfigure once they send a message
71-
for (const device of this.zigbee.devices(false).filter((d) => d.zh.type === 'Router')) {
72-
// Sleep 10 seconds between configuring on startup to not DDoS the coordinator
73-
// when many devices have to be configured.
71+
for (const device of this.zigbee.devicesIterator((d) => d.type === 'Router')) {
72+
// Sleep 10 seconds between configuring on startup to not DDoS the coordinator when many devices have to be configured.
7473
await utils.sleep(10);
7574
await this.configure(device, 'started');
7675
}

lib/extension/frontend.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ export default class Frontend extends Extension {
139139
}
140140
}
141141

142-
for (const device of this.zigbee.devices(false)) {
142+
for (const device of this.zigbee.devicesIterator(utils.deviceNotCoordinator)) {
143143
const payload = this.state.get(device);
144144
const lastSeen = settings.get().advanced.last_seen;
145145
/* istanbul ignore if */

lib/extension/groups.ts

+18-11
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,6 @@ export default class Groups extends Extension {
5151

5252
private async syncGroupsWithSettings(): Promise<void> {
5353
const settingsGroups = settings.getGroups();
54-
const zigbeeGroups = this.zigbee.groups();
5554

5655
const addRemoveFromGroup = async (
5756
action: 'add' | 'remove',
@@ -76,7 +75,7 @@ export default class Groups extends Extension {
7675

7776
for (const settingGroup of settingsGroups) {
7877
const groupID = settingGroup.ID;
79-
const zigbeeGroup = zigbeeGroups.find((g) => g.ID === groupID) || this.zigbee.createGroup(groupID);
78+
const zigbeeGroup = this.zigbee.groupsIterator((g) => g.groupID === groupID).next().value || this.zigbee.createGroup(groupID);
8079
const settingsEndpoints: zh.Endpoint[] = [];
8180

8281
for (const d of settingGroup.devices) {
@@ -113,13 +112,11 @@ export default class Groups extends Extension {
113112
}
114113
}
115114

116-
for (const zigbeeGroup of zigbeeGroups) {
117-
if (!settingsGroups.some((g) => g.ID === zigbeeGroup.ID)) {
118-
for (const endpoint of zigbeeGroup.zh.members) {
119-
const deviceName = settings.getDevice(endpoint.getDevice().ieeeAddr).friendly_name;
115+
for (const zigbeeGroup of this.zigbee.groupsIterator((zg) => !settingsGroups.some((sg) => sg.ID === zg.groupID))) {
116+
for (const endpoint of zigbeeGroup.zh.members) {
117+
const deviceName = settings.getDevice(endpoint.getDevice().ieeeAddr).friendly_name;
120118

121-
await addRemoveFromGroup('remove', deviceName, zigbeeGroup.ID, endpoint, zigbeeGroup);
122-
}
119+
await addRemoveFromGroup('remove', deviceName, zigbeeGroup.ID, endpoint, zigbeeGroup);
123120
}
124121
}
125122
}
@@ -153,7 +150,13 @@ export default class Groups extends Extension {
153150

154151
if (payloadKeys.length) {
155152
const entity = data.entity;
156-
const groups = this.zigbee.groups().filter((g) => g.options && (g.options.optimistic == undefined || g.options.optimistic));
153+
const groups = [];
154+
155+
for (const group of this.zigbee.groupsIterator()) {
156+
if (group.options && (group.options.optimistic == undefined || group.options.optimistic)) {
157+
groups.push(group);
158+
}
159+
}
157160

158161
if (entity instanceof Device) {
159162
for (const group of groups) {
@@ -359,7 +362,7 @@ export default class Groups extends Extension {
359362
resolvedEntityEndpoint,
360363
} = parsed;
361364
let error = parsed.error;
362-
let changedGroups: Group[] = [];
365+
const changedGroups: Group[] = [];
363366

364367
if (!error) {
365368
try {
@@ -406,7 +409,11 @@ export default class Groups extends Extension {
406409
} else {
407410
// remove_all
408411
logger.info(`Removing '${resolvedEntityDevice.name}' from all groups`);
409-
changedGroups = this.zigbee.groups().filter((g) => g.zh.members.includes(resolvedEntityEndpoint));
412+
413+
for (const group of this.zigbee.groupsIterator((g) => g.members.includes(resolvedEntityEndpoint))) {
414+
changedGroups.push(group);
415+
}
416+
410417
await resolvedEntityEndpoint.removeFromAllGroups();
411418

412419
for (const settingsGroup of settings.getGroups()) {

lib/extension/homeassistant.ts

+7-3
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,9 @@ export default class HomeAssistant extends Extension {
225225
const discoverWait = 5;
226226
// Discover with `published = false`, this will populate `this.discovered` without publishing the discoveries.
227227
// This is needed for clearing outdated entries in `this.onMQTTMessage()`
228-
for (const e of [this.bridge, ...this.zigbee.devices(false), ...this.zigbee.groups()]) {
228+
await this.discover(this.bridge, false);
229+
230+
for (const e of this.zigbee.devicesAndGroupsIterator(utils.deviceNotCoordinator)) {
229231
await this.discover(e, false);
230232
}
231233

@@ -235,7 +237,9 @@ export default class HomeAssistant extends Extension {
235237
this.mqtt.unsubscribe(`${this.discoveryTopic}/#`);
236238
logger.debug(`Discovering entities to Home Assistant`);
237239

238-
for (const e of [this.bridge, ...this.zigbee.devices(false), ...this.zigbee.groups()]) {
240+
await this.discover(this.bridge);
241+
242+
for (const e of this.zigbee.devicesAndGroupsIterator(utils.deviceNotCoordinator)) {
239243
await this.discover(e);
240244
}
241245
}, utils.seconds(discoverWait));
@@ -1780,7 +1784,7 @@ export default class HomeAssistant extends Extension {
17801784
} else if ((data.topic === this.statusTopic || data.topic === defaultStatusTopic) && data.message.toLowerCase() === 'online') {
17811785
const timer = setTimeout(async () => {
17821786
// Publish all device states.
1783-
for (const entity of [...this.zigbee.devices(false), ...this.zigbee.groups()]) {
1787+
for (const entity of this.zigbee.devicesAndGroupsIterator(utils.deviceNotCoordinator)) {
17841788
if (this.state.exists(entity)) {
17851789
await this.publishEntityState(entity, this.state.get(entity), 'publishCached');
17861790
}

lib/extension/legacy/bridgeLegacy.ts

+5-3
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,9 @@ export default class BridgeLegacy extends Extension {
127127

128128
@bind async devices(topic: string): Promise<void> {
129129
const coordinator = await this.zigbee.getCoordinatorVersion();
130-
const devices = this.zigbee.devices().map((device) => {
130+
const devices: KeyValue[] = [];
131+
132+
for (const device of this.zigbee.devicesIterator()) {
131133
const payload: KeyValue = {
132134
ieeeAddr: device.ieeeAddr,
133135
type: device.zh.type,
@@ -155,8 +157,8 @@ export default class BridgeLegacy extends Extension {
155157
payload.lastSeen = Date.now();
156158
}
157159

158-
return payload;
159-
});
160+
devices.push(payload);
161+
}
160162

161163
if (topic.split('/').pop() == 'get') {
162164
await this.mqtt.publish(`bridge/config/devices`, stringify(devices), {}, settings.get().mqtt.base_topic, false, false);

lib/extension/legacy/report.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import * as zhc from 'zigbee-herdsman-converters';
22

33
import logger from '../../util/logger';
44
import * as settings from '../../util/settings';
5+
import utils from '../../util/utils';
56
import Extension from '../extension';
67

78
const defaultConfiguration = {
@@ -178,7 +179,7 @@ export default class Report extends Extension {
178179
}
179180

180181
override async start(): Promise<void> {
181-
for (const device of this.zigbee.devices(false)) {
182+
for (const device of this.zigbee.devicesIterator(utils.deviceNotCoordinator)) {
182183
if (this.shouldSetupReporting(device, null)) {
183184
await this.setupReporting(device);
184185
}

0 commit comments

Comments
 (0)