mirror of
https://github.com/Koenkk/zigbee2mqtt.git
synced 2026-08-06 10:49:39 +00:00
fix: Improve looping performance (#23541)
* Use herdsman improved looping. * Fix lint. * Fix tests. * Feedback
This commit is contained in:
+8
-4
@@ -157,15 +157,19 @@ export class Controller {
|
||||
}
|
||||
|
||||
// Log zigbee clients on startup
|
||||
const devices = this.zigbee.devices(false);
|
||||
logger.info(`Currently ${devices.length} devices are joined:`);
|
||||
for (const device of devices) {
|
||||
let deviceCount = 0;
|
||||
|
||||
for (const device of this.zigbee.devicesIterator(utils.deviceNotCoordinator)) {
|
||||
const model = device.isSupported
|
||||
? `${device.definition.model} - ${device.definition.vendor} ${device.definition.description}`
|
||||
: 'Not supported';
|
||||
logger.info(`${device.name} (${device.ieeeAddr}): ${model} (${device.zh.type})`);
|
||||
|
||||
deviceCount++;
|
||||
}
|
||||
|
||||
logger.info(`Currently ${deviceCount} devices are joined.`);
|
||||
|
||||
// Enable zigbee join
|
||||
try {
|
||||
if (settings.get().permit_join) {
|
||||
@@ -193,7 +197,7 @@ export class Controller {
|
||||
|
||||
// Send all cached states.
|
||||
if (settings.get().advanced.cache_state_send_on_startup && settings.get().advanced.cache_state) {
|
||||
for (const entity of [...devices, ...this.zigbee.groups()]) {
|
||||
for (const entity of this.zigbee.devicesAndGroupsIterator()) {
|
||||
if (this.state.exists(entity)) {
|
||||
await this.publishEntityState(entity, this.state.get(entity), 'publishCached');
|
||||
}
|
||||
|
||||
@@ -140,7 +140,7 @@ export default class Availability extends Extension {
|
||||
await this.publishAvailabilityForAllEntities();
|
||||
|
||||
// Start availability for the devices
|
||||
for (const device of this.zigbee.devices(false)) {
|
||||
for (const device of this.zigbee.devicesIterator(utils.deviceNotCoordinator)) {
|
||||
if (utils.isAvailabilityEnabledForEntity(device, settings.get())) {
|
||||
this.resetTimer(device);
|
||||
|
||||
@@ -153,7 +153,7 @@ export default class Availability extends Extension {
|
||||
}
|
||||
|
||||
@bind private async publishAvailabilityForAllEntities(): Promise<void> {
|
||||
for (const entity of [...this.zigbee.devices(false), ...this.zigbee.groups()]) {
|
||||
for (const entity of this.zigbee.devicesAndGroupsIterator(utils.deviceNotCoordinator)) {
|
||||
if (utils.isAvailabilityEnabledForEntity(entity, settings.get())) {
|
||||
await this.publishAvailability(entity, true, false, true);
|
||||
}
|
||||
@@ -187,7 +187,7 @@ export default class Availability extends Extension {
|
||||
await this.mqtt.publish(topic, payload, {retain: true, qos: 1});
|
||||
|
||||
if (!skipGroups && entity.isDevice()) {
|
||||
for (const group of this.zigbee.groups()) {
|
||||
for (const group of this.zigbee.groupsIterator()) {
|
||||
if (group.hasMember(entity) && utils.isAvailabilityEnabledForEntity(group, settings.get())) {
|
||||
await this.publishAvailability(group, false, forcePublish);
|
||||
}
|
||||
|
||||
@@ -363,7 +363,7 @@ export default class Bind extends Extension {
|
||||
if (data.action === 'add') {
|
||||
const bindsToGroup: zh.Bind[] = [];
|
||||
|
||||
for (const device of this.zigbee.devices(false)) {
|
||||
for (const device of this.zigbee.devicesIterator(utils.deviceNotCoordinator)) {
|
||||
for (const endpoint of device.zh.endpoints) {
|
||||
for (const bind of endpoint.binds) {
|
||||
if (bind.target === data.group.zh) {
|
||||
@@ -443,7 +443,7 @@ export default class Bind extends Extension {
|
||||
const endpoints = utils.isEndpoint(target) ? [target] : target.members;
|
||||
const allBinds: zh.Bind[] = [];
|
||||
|
||||
for (const device of this.zigbee.devices(false)) {
|
||||
for (const device of this.zigbee.devicesIterator(utils.deviceNotCoordinator)) {
|
||||
for (const endpoint of device.zh.endpoints) {
|
||||
for (const bind of endpoint.binds) {
|
||||
allBinds.push(bind);
|
||||
|
||||
+36
-21
@@ -725,8 +725,12 @@ export default class Bridge extends Extension {
|
||||
scenes: Scene[];
|
||||
}
|
||||
|
||||
const devices = this.zigbee.devices().map((device) => {
|
||||
// XXX: definition<>DefinitionPayload don't match to use `Device[]` type here
|
||||
const devices: KeyValue[] = [];
|
||||
|
||||
for (const device of this.zigbee.devicesIterator()) {
|
||||
const endpoints: {[s: number]: Data} = {};
|
||||
|
||||
for (const endpoint of device.zh.endpoints) {
|
||||
const data: Data = {
|
||||
scenes: utils.getScenes(endpoint),
|
||||
@@ -758,7 +762,7 @@ export default class Bridge extends Extension {
|
||||
endpoints[endpoint.ID] = data;
|
||||
}
|
||||
|
||||
return {
|
||||
devices.push({
|
||||
ieee_address: device.ieeeAddr,
|
||||
type: device.zh.type,
|
||||
network_address: device.zh.networkAddress,
|
||||
@@ -775,24 +779,32 @@ export default class Bridge extends Extension {
|
||||
interview_completed: device.zh.interviewCompleted,
|
||||
manufacturer: device.zh.manufacturerName,
|
||||
endpoints,
|
||||
};
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
await this.mqtt.publish('bridge/devices', stringify(devices), {retain: true, qos: 0}, settings.get().mqtt.base_topic, true);
|
||||
}
|
||||
|
||||
async publishGroups(): Promise<void> {
|
||||
const groups = this.zigbee.groups().map((g) => {
|
||||
return {
|
||||
id: g.ID,
|
||||
friendly_name: g.ID === 901 ? 'default_bind_group' : g.name,
|
||||
description: g.options.description,
|
||||
scenes: utils.getScenes(g.zh),
|
||||
members: g.zh.members.map((e) => {
|
||||
return {ieee_address: e.getDevice().ieeeAddr, endpoint: e.ID};
|
||||
}),
|
||||
};
|
||||
});
|
||||
// XXX: id<>ID can't use `Group[]` type
|
||||
const groups: KeyValue[] = [];
|
||||
|
||||
for (const group of this.zigbee.groupsIterator()) {
|
||||
const members = [];
|
||||
|
||||
for (const member of group.zh.members) {
|
||||
members.push({ieee_address: member.getDevice().ieeeAddr, endpoint: member.ID});
|
||||
}
|
||||
|
||||
groups.push({
|
||||
id: group.ID,
|
||||
friendly_name: group.ID === 901 ? 'default_bind_group' : group.name,
|
||||
description: group.options.description,
|
||||
scenes: utils.getScenes(group.zh),
|
||||
members,
|
||||
});
|
||||
}
|
||||
|
||||
await this.mqtt.publish('bridge/groups', stringify(groups), {retain: true, qos: 0}, settings.get().mqtt.base_topic, true);
|
||||
}
|
||||
|
||||
@@ -807,20 +819,23 @@ export default class Bridge extends Extension {
|
||||
custom_clusters: {},
|
||||
};
|
||||
|
||||
for (const device of this.zigbee.devices()) {
|
||||
if (Object.keys(device.customClusters).length !== 0) {
|
||||
data.custom_clusters[device.ieeeAddr] = device.customClusters;
|
||||
}
|
||||
for (const device of this.zigbee.devicesIterator((d) => !utils.objectIsEmpty(d.customClusters))) {
|
||||
data.custom_clusters[device.ieeeAddr] = device.customClusters;
|
||||
}
|
||||
|
||||
await this.mqtt.publish('bridge/definitions', stringify(data), {retain: true, qos: 0}, settings.get().mqtt.base_topic, true);
|
||||
}
|
||||
|
||||
getDefinitionPayload(device: Device): DefinitionPayload {
|
||||
if (!device.definition) return null;
|
||||
getDefinitionPayload(device: Device): DefinitionPayload | null {
|
||||
if (!device.definition) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// TODO: better typing to avoid @ts-expect-error
|
||||
// @ts-expect-error icon is valid for external definitions
|
||||
const definitionIcon = device.definition.icon;
|
||||
let icon = device.options.icon ?? definitionIcon;
|
||||
|
||||
if (icon) {
|
||||
icon = icon.replace('${zigbeeModel}', utils.sanitizeImageParameter(device.zh.modelID));
|
||||
icon = icon.replace('${model}', utils.sanitizeImageParameter(device.definition.model));
|
||||
|
||||
@@ -68,9 +68,8 @@ export default class Configure extends Extension {
|
||||
setImmediate(async () => {
|
||||
// Only configure routers on startup, end devices are likely sleeping and
|
||||
// will reconfigure once they send a message
|
||||
for (const device of this.zigbee.devices(false).filter((d) => d.zh.type === 'Router')) {
|
||||
// Sleep 10 seconds between configuring on startup to not DDoS the coordinator
|
||||
// when many devices have to be configured.
|
||||
for (const device of this.zigbee.devicesIterator((d) => d.type === 'Router')) {
|
||||
// Sleep 10 seconds between configuring on startup to not DDoS the coordinator when many devices have to be configured.
|
||||
await utils.sleep(10);
|
||||
await this.configure(device, 'started');
|
||||
}
|
||||
|
||||
@@ -139,7 +139,7 @@ export default class Frontend extends Extension {
|
||||
}
|
||||
}
|
||||
|
||||
for (const device of this.zigbee.devices(false)) {
|
||||
for (const device of this.zigbee.devicesIterator(utils.deviceNotCoordinator)) {
|
||||
const payload = this.state.get(device);
|
||||
const lastSeen = settings.get().advanced.last_seen;
|
||||
/* istanbul ignore if */
|
||||
|
||||
+18
-11
@@ -51,7 +51,6 @@ export default class Groups extends Extension {
|
||||
|
||||
private async syncGroupsWithSettings(): Promise<void> {
|
||||
const settingsGroups = settings.getGroups();
|
||||
const zigbeeGroups = this.zigbee.groups();
|
||||
|
||||
const addRemoveFromGroup = async (
|
||||
action: 'add' | 'remove',
|
||||
@@ -76,7 +75,7 @@ export default class Groups extends Extension {
|
||||
|
||||
for (const settingGroup of settingsGroups) {
|
||||
const groupID = settingGroup.ID;
|
||||
const zigbeeGroup = zigbeeGroups.find((g) => g.ID === groupID) || this.zigbee.createGroup(groupID);
|
||||
const zigbeeGroup = this.zigbee.groupsIterator((g) => g.groupID === groupID).next().value || this.zigbee.createGroup(groupID);
|
||||
const settingsEndpoints: zh.Endpoint[] = [];
|
||||
|
||||
for (const d of settingGroup.devices) {
|
||||
@@ -113,13 +112,11 @@ export default class Groups extends Extension {
|
||||
}
|
||||
}
|
||||
|
||||
for (const zigbeeGroup of zigbeeGroups) {
|
||||
if (!settingsGroups.some((g) => g.ID === zigbeeGroup.ID)) {
|
||||
for (const endpoint of zigbeeGroup.zh.members) {
|
||||
const deviceName = settings.getDevice(endpoint.getDevice().ieeeAddr).friendly_name;
|
||||
for (const zigbeeGroup of this.zigbee.groupsIterator((zg) => !settingsGroups.some((sg) => sg.ID === zg.groupID))) {
|
||||
for (const endpoint of zigbeeGroup.zh.members) {
|
||||
const deviceName = settings.getDevice(endpoint.getDevice().ieeeAddr).friendly_name;
|
||||
|
||||
await addRemoveFromGroup('remove', deviceName, zigbeeGroup.ID, endpoint, zigbeeGroup);
|
||||
}
|
||||
await addRemoveFromGroup('remove', deviceName, zigbeeGroup.ID, endpoint, zigbeeGroup);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -153,7 +150,13 @@ export default class Groups extends Extension {
|
||||
|
||||
if (payloadKeys.length) {
|
||||
const entity = data.entity;
|
||||
const groups = this.zigbee.groups().filter((g) => g.options && (g.options.optimistic == undefined || g.options.optimistic));
|
||||
const groups = [];
|
||||
|
||||
for (const group of this.zigbee.groupsIterator()) {
|
||||
if (group.options && (group.options.optimistic == undefined || group.options.optimistic)) {
|
||||
groups.push(group);
|
||||
}
|
||||
}
|
||||
|
||||
if (entity instanceof Device) {
|
||||
for (const group of groups) {
|
||||
@@ -359,7 +362,7 @@ export default class Groups extends Extension {
|
||||
resolvedEntityEndpoint,
|
||||
} = parsed;
|
||||
let error = parsed.error;
|
||||
let changedGroups: Group[] = [];
|
||||
const changedGroups: Group[] = [];
|
||||
|
||||
if (!error) {
|
||||
try {
|
||||
@@ -406,7 +409,11 @@ export default class Groups extends Extension {
|
||||
} else {
|
||||
// remove_all
|
||||
logger.info(`Removing '${resolvedEntityDevice.name}' from all groups`);
|
||||
changedGroups = this.zigbee.groups().filter((g) => g.zh.members.includes(resolvedEntityEndpoint));
|
||||
|
||||
for (const group of this.zigbee.groupsIterator((g) => g.members.includes(resolvedEntityEndpoint))) {
|
||||
changedGroups.push(group);
|
||||
}
|
||||
|
||||
await resolvedEntityEndpoint.removeFromAllGroups();
|
||||
|
||||
for (const settingsGroup of settings.getGroups()) {
|
||||
|
||||
@@ -225,7 +225,9 @@ export default class HomeAssistant extends Extension {
|
||||
const discoverWait = 5;
|
||||
// Discover with `published = false`, this will populate `this.discovered` without publishing the discoveries.
|
||||
// This is needed for clearing outdated entries in `this.onMQTTMessage()`
|
||||
for (const e of [this.bridge, ...this.zigbee.devices(false), ...this.zigbee.groups()]) {
|
||||
await this.discover(this.bridge, false);
|
||||
|
||||
for (const e of this.zigbee.devicesAndGroupsIterator(utils.deviceNotCoordinator)) {
|
||||
await this.discover(e, false);
|
||||
}
|
||||
|
||||
@@ -235,7 +237,9 @@ export default class HomeAssistant extends Extension {
|
||||
this.mqtt.unsubscribe(`${this.discoveryTopic}/#`);
|
||||
logger.debug(`Discovering entities to Home Assistant`);
|
||||
|
||||
for (const e of [this.bridge, ...this.zigbee.devices(false), ...this.zigbee.groups()]) {
|
||||
await this.discover(this.bridge);
|
||||
|
||||
for (const e of this.zigbee.devicesAndGroupsIterator(utils.deviceNotCoordinator)) {
|
||||
await this.discover(e);
|
||||
}
|
||||
}, utils.seconds(discoverWait));
|
||||
@@ -1780,7 +1784,7 @@ export default class HomeAssistant extends Extension {
|
||||
} else if ((data.topic === this.statusTopic || data.topic === defaultStatusTopic) && data.message.toLowerCase() === 'online') {
|
||||
const timer = setTimeout(async () => {
|
||||
// Publish all device states.
|
||||
for (const entity of [...this.zigbee.devices(false), ...this.zigbee.groups()]) {
|
||||
for (const entity of this.zigbee.devicesAndGroupsIterator(utils.deviceNotCoordinator)) {
|
||||
if (this.state.exists(entity)) {
|
||||
await this.publishEntityState(entity, this.state.get(entity), 'publishCached');
|
||||
}
|
||||
|
||||
@@ -127,7 +127,9 @@ export default class BridgeLegacy extends Extension {
|
||||
|
||||
@bind async devices(topic: string): Promise<void> {
|
||||
const coordinator = await this.zigbee.getCoordinatorVersion();
|
||||
const devices = this.zigbee.devices().map((device) => {
|
||||
const devices: KeyValue[] = [];
|
||||
|
||||
for (const device of this.zigbee.devicesIterator()) {
|
||||
const payload: KeyValue = {
|
||||
ieeeAddr: device.ieeeAddr,
|
||||
type: device.zh.type,
|
||||
@@ -155,8 +157,8 @@ export default class BridgeLegacy extends Extension {
|
||||
payload.lastSeen = Date.now();
|
||||
}
|
||||
|
||||
return payload;
|
||||
});
|
||||
devices.push(payload);
|
||||
}
|
||||
|
||||
if (topic.split('/').pop() == 'get') {
|
||||
await this.mqtt.publish(`bridge/config/devices`, stringify(devices), {}, settings.get().mqtt.base_topic, false, false);
|
||||
|
||||
@@ -2,6 +2,7 @@ import * as zhc from 'zigbee-herdsman-converters';
|
||||
|
||||
import logger from '../../util/logger';
|
||||
import * as settings from '../../util/settings';
|
||||
import utils from '../../util/utils';
|
||||
import Extension from '../extension';
|
||||
|
||||
const defaultConfiguration = {
|
||||
@@ -178,7 +179,7 @@ export default class Report extends Extension {
|
||||
}
|
||||
|
||||
override async start(): Promise<void> {
|
||||
for (const device of this.zigbee.devices(false)) {
|
||||
for (const device of this.zigbee.devicesIterator(utils.deviceNotCoordinator)) {
|
||||
if (this.shouldSetupReporting(device, null)) {
|
||||
await this.setupReporting(device);
|
||||
}
|
||||
|
||||
+43
-27
@@ -210,31 +210,31 @@ export default class NetworkMap extends Extension {
|
||||
|
||||
async networkScan(includeRoutes: boolean): Promise<Topology> {
|
||||
logger.info(`Starting network scan (includeRoutes '${includeRoutes}')`);
|
||||
const devices = this.zigbee.devices().filter((d) => d.zh.type !== 'GreenPower' && !d.options.disabled);
|
||||
const lqis: Map<Device, zh.LQI> = new Map();
|
||||
const routingTables: Map<Device, zh.RoutingTable> = new Map();
|
||||
const failed: Map<Device, string[]> = new Map();
|
||||
const requestWithRetry = async <T>(request: () => Promise<T>): Promise<T> => {
|
||||
try {
|
||||
const result = await request();
|
||||
|
||||
return result;
|
||||
} catch {
|
||||
// Network is possibly congested, sleep 5 seconds to let the network settle.
|
||||
await utils.sleep(5);
|
||||
return request();
|
||||
}
|
||||
};
|
||||
|
||||
for (const device of this.zigbee.devicesIterator((d) => d.type !== 'GreenPower' && d.type !== 'EndDevice')) {
|
||||
if (device.options.disabled) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const device of devices.filter((d) => d.zh.type != 'EndDevice')) {
|
||||
failed.set(device, []);
|
||||
await utils.sleep(1); // sleep 1 second between each scan to reduce stress on network.
|
||||
|
||||
const doRequest = async <T>(request: () => Promise<T>, firstAttempt = true): Promise<T> => {
|
||||
try {
|
||||
return await request();
|
||||
} catch (error) {
|
||||
if (!firstAttempt) {
|
||||
throw error;
|
||||
} else {
|
||||
// Network is possibly congested, sleep 5 seconds to let the network settle.
|
||||
await utils.sleep(5);
|
||||
return doRequest(request, false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
const result = await doRequest<zh.LQI>(async () => device.zh.lqi());
|
||||
const result = await requestWithRetry<zh.LQI>(async () => device.zh.lqi());
|
||||
lqis.set(device, result);
|
||||
logger.debug(`LQI succeeded for '${device.name}'`);
|
||||
} catch (error) {
|
||||
@@ -245,12 +245,13 @@ export default class NetworkMap extends Extension {
|
||||
|
||||
if (includeRoutes) {
|
||||
try {
|
||||
const result = await doRequest(async () => device.zh.routingTable());
|
||||
const result = await requestWithRetry<zh.RoutingTable>(async () => device.zh.routingTable());
|
||||
routingTables.set(device, result);
|
||||
logger.debug(`Routing table succeeded for '${device.name}'`);
|
||||
} catch (error) {
|
||||
failed.get(device).push('routingTable');
|
||||
logger.error(`Failed to execute routing table for '${device.name}' (${error.message})`);
|
||||
logger.error(`Failed to execute routing table for '${device.name}'`);
|
||||
logger.debug(error.stack);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -258,8 +259,14 @@ export default class NetworkMap extends Extension {
|
||||
logger.info(`Network scan finished`);
|
||||
|
||||
const topology: Topology = {nodes: [], links: []};
|
||||
// Add nodes
|
||||
for (const device of devices) {
|
||||
|
||||
// XXX: display GP/disabled devices in the map, better feedback than just hiding them?
|
||||
for (const device of this.zigbee.devicesIterator((d) => d.type !== 'GreenPower')) {
|
||||
if (device.options.disabled) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Add nodes
|
||||
const definition = device.definition
|
||||
? {
|
||||
model: device.definition.model,
|
||||
@@ -289,7 +296,7 @@ export default class NetworkMap extends Extension {
|
||||
}
|
||||
|
||||
// Add links
|
||||
lqis.forEach((lqi, device) => {
|
||||
for (const [device, lqi] of lqis) {
|
||||
for (const neighbor of lqi.neighbors) {
|
||||
if (neighbor.relationship > 3) {
|
||||
// Relationship is not active, skip it
|
||||
@@ -298,9 +305,13 @@ export default class NetworkMap extends Extension {
|
||||
|
||||
// Some Xiaomi devices return 0x00 as the neighbor ieeeAddr (obviously not correct).
|
||||
// Determine the correct ieeeAddr based on the networkAddress.
|
||||
const neighborDevice = this.zigbee.deviceByNetworkAddress(neighbor.networkAddress);
|
||||
if (neighbor.ieeeAddr === '0x0000000000000000' && neighborDevice) {
|
||||
neighbor.ieeeAddr = neighborDevice.ieeeAddr;
|
||||
if (neighbor.ieeeAddr === '0x0000000000000000') {
|
||||
const neighborDevice = this.zigbee.deviceByNetworkAddress(neighbor.networkAddress);
|
||||
|
||||
/* istanbul ignore else */
|
||||
if (neighborDevice) {
|
||||
neighbor.ieeeAddr = neighborDevice.ieeeAddr;
|
||||
}
|
||||
}
|
||||
|
||||
const link: Link = {
|
||||
@@ -318,13 +329,18 @@ export default class NetworkMap extends Extension {
|
||||
};
|
||||
|
||||
const routingTable = routingTables.get(device);
|
||||
|
||||
if (routingTable) {
|
||||
link.routes = routingTable.table.filter((t) => t.status === 'ACTIVE' && t.nextHop === neighbor.networkAddress);
|
||||
for (const entry of routingTable.table) {
|
||||
if (entry.status === 'ACTIVE' && entry.nextHop === neighbor.networkAddress) {
|
||||
link.routes.push(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
topology.links.push(link);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return topology;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import * as zhc from 'zigbee-herdsman-converters';
|
||||
|
||||
import utils from '../util/utils';
|
||||
import Extension from './extension';
|
||||
|
||||
/**
|
||||
@@ -7,7 +8,7 @@ import Extension from './extension';
|
||||
*/
|
||||
export default class OnEvent extends Extension {
|
||||
override async start(): Promise<void> {
|
||||
for (const device of this.zigbee.devices(false)) {
|
||||
for (const device of this.zigbee.devicesIterator(utils.deviceNotCoordinator)) {
|
||||
await this.callOnEvent(device, 'start', {});
|
||||
}
|
||||
|
||||
@@ -31,7 +32,8 @@ export default class OnEvent extends Extension {
|
||||
|
||||
override async stop(): Promise<void> {
|
||||
await super.stop();
|
||||
for (const device of this.zigbee.devices(false)) {
|
||||
|
||||
for (const device of this.zigbee.devicesIterator(utils.deviceNotCoordinator)) {
|
||||
await this.callOnEvent(device, 'stop', {});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,10 +65,10 @@ export default class OTAUpdate extends Extension {
|
||||
// In order to support local firmware files we need to let zigbeeOTA know where the data directory is
|
||||
zhc.ota.setDataDir(dataDir.getPath());
|
||||
|
||||
// In case Zigbee2MQTT is restared during an update, progress and remaining values are still in state.
|
||||
// remove them.
|
||||
for (const device of this.zigbee.devices(false)) {
|
||||
// In case Zigbee2MQTT is restared during an update, progress and remaining values are still in state, remove them.
|
||||
for (const device of this.zigbee.devicesIterator(utils.deviceNotCoordinator)) {
|
||||
this.removeProgressAndRemainingFromState(device);
|
||||
|
||||
// Reset update state, e.g. when Z2M restarted during update.
|
||||
if (this.state.get(device).update?.state === 'updating') {
|
||||
this.state.get(device).update.state = 'available';
|
||||
|
||||
@@ -317,7 +317,7 @@ export default class Publish extends Extension {
|
||||
}
|
||||
|
||||
for (const [ID, payload] of Object.entries(toPublish)) {
|
||||
if (Object.keys(payload).length != 0) {
|
||||
if (!utils.objectIsEmpty(payload)) {
|
||||
await this.publishEntityState(toPublishEntity[ID], payload);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,7 +163,7 @@ export default class Receive extends Extension {
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(payload).length) {
|
||||
if (!utils.objectIsEmpty(payload)) {
|
||||
await publish(payload);
|
||||
} else {
|
||||
await utils.publishLastSeen({device: data.device, reason: 'messageEmitted'}, settings.get(), true, this.publishEntityState);
|
||||
|
||||
@@ -92,6 +92,12 @@ function formatDate(time: number, type: 'ISO_8601' | 'ISO_8601_local' | 'epoch'
|
||||
}
|
||||
}
|
||||
|
||||
function objectIsEmpty(object: object): boolean {
|
||||
// much faster than checking `Object.keys(object).length`
|
||||
for (const k in object) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
function objectHasProperties(object: {[s: string]: unknown}, properties: string[]): boolean {
|
||||
for (const property of properties) {
|
||||
if (!object.hasOwnProperty(property)) {
|
||||
@@ -397,6 +403,10 @@ function getScenes(entity: zh.Endpoint | zh.Group): Scene[] {
|
||||
return Object.values(scenes);
|
||||
}
|
||||
|
||||
function deviceNotCoordinator(device: zh.Device): boolean {
|
||||
return device.type !== 'Coordinator';
|
||||
}
|
||||
|
||||
/* istanbul ignore next */
|
||||
const noop = (): void => {};
|
||||
|
||||
@@ -405,6 +415,7 @@ export default {
|
||||
getZigbee2MQTTVersion,
|
||||
getDependencyVersion,
|
||||
formatDate,
|
||||
objectIsEmpty,
|
||||
objectHasProperties,
|
||||
equalsPartial,
|
||||
getObjectProperty,
|
||||
@@ -431,5 +442,6 @@ export default {
|
||||
flatten,
|
||||
arrayUnique,
|
||||
getScenes,
|
||||
deviceNotCoordinator,
|
||||
noop,
|
||||
};
|
||||
|
||||
+27
-17
@@ -66,7 +66,7 @@ export default class Zigbee {
|
||||
throw error;
|
||||
}
|
||||
|
||||
for (const device of this.devices(false)) {
|
||||
for (const device of this.devicesIterator(utils.deviceNotCoordinator)) {
|
||||
await device.resolveDefinition();
|
||||
}
|
||||
|
||||
@@ -124,7 +124,7 @@ export default class Zigbee {
|
||||
logger.info(`Coordinator firmware version: '${stringify(await this.getCoordinatorVersion())}'`);
|
||||
logger.debug(`Zigbee network parameters: ${stringify(await this.herdsman.getNetworkParameters())}`);
|
||||
|
||||
for (const device of this.devices(false)) {
|
||||
for (const device of this.devicesIterator(utils.deviceNotCoordinator)) {
|
||||
// If a passlist is used, all other device will be removed from the network.
|
||||
const passlist = settings.get().passlist;
|
||||
const blocklist = settings.get().blocklist;
|
||||
@@ -135,19 +135,20 @@ export default class Zigbee {
|
||||
logger.error(`Failed to remove '${device.ieeeAddr}' (${error.message})`);
|
||||
}
|
||||
};
|
||||
|
||||
if (passlist.length > 0) {
|
||||
if (!passlist.includes(device.ieeeAddr)) {
|
||||
logger.warning(`Device which is not on passlist connected (${device.ieeeAddr}), removing...`);
|
||||
logger.warning(`Device not on passlist currently connected (${device.ieeeAddr}), removing...`);
|
||||
await remove(device);
|
||||
}
|
||||
} else if (blocklist.includes(device.ieeeAddr)) {
|
||||
logger.warning(`Device on blocklist is connected (${device.ieeeAddr}), removing...`);
|
||||
logger.warning(`Device on blocklist currently connected (${device.ieeeAddr}), removing...`);
|
||||
await remove(device);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if we have to set a transmit power
|
||||
if (settings.get().advanced.hasOwnProperty('transmit_power')) {
|
||||
if (settings.get().advanced.transmit_power != null) {
|
||||
const transmitPower = settings.get().advanced.transmit_power;
|
||||
await this.herdsman.setTransmitPower(transmitPower);
|
||||
logger.info(`Set transmit power to '${transmitPower}'`);
|
||||
@@ -327,20 +328,29 @@ export default class Zigbee {
|
||||
return this.herdsman.getDevicesByType('Coordinator')[0].endpoints[0];
|
||||
}
|
||||
|
||||
groups(): Group[] {
|
||||
return this.herdsman.getGroups().map((g) => this.resolveGroup(g.groupID));
|
||||
}
|
||||
|
||||
devices(includeCoordinator = true): Device[] {
|
||||
const devices: Device[] = [];
|
||||
|
||||
for (const device of this.herdsman.getDevices()) {
|
||||
if (includeCoordinator || device.type !== 'Coordinator') {
|
||||
devices.push(this.resolveDevice(device.ieeeAddr));
|
||||
}
|
||||
*devicesAndGroupsIterator(
|
||||
devicePredicate?: (value: zh.Device) => boolean,
|
||||
groupPredicate?: (value: zh.Group) => boolean,
|
||||
): Generator<Device | Group> {
|
||||
for (const device of this.herdsman.getDevicesIterator(devicePredicate)) {
|
||||
yield this.resolveDevice(device.ieeeAddr);
|
||||
}
|
||||
|
||||
return devices;
|
||||
for (const group of this.herdsman.getGroupsIterator(groupPredicate)) {
|
||||
yield this.resolveGroup(group.groupID);
|
||||
}
|
||||
}
|
||||
|
||||
*groupsIterator(predicate?: (value: zh.Group) => boolean): Generator<Group> {
|
||||
for (const group of this.herdsman.getGroupsIterator(predicate)) {
|
||||
yield this.resolveGroup(group.groupID);
|
||||
}
|
||||
}
|
||||
|
||||
*devicesIterator(predicate?: (value: zh.Device) => boolean): Generator<Device> {
|
||||
for (const device of this.herdsman.getDevicesIterator(predicate)) {
|
||||
yield this.resolveDevice(device.ieeeAddr);
|
||||
}
|
||||
}
|
||||
|
||||
@bind private async acceptJoiningDeviceHandler(ieeeAddr: string): Promise<boolean> {
|
||||
|
||||
@@ -85,7 +85,7 @@ describe('Controller', () => {
|
||||
expect(zigbeeHerdsman.setTransmitPower).toHaveBeenCalledTimes(0);
|
||||
expect(zigbeeHerdsman.permitJoin).toHaveBeenCalledTimes(1);
|
||||
expect(zigbeeHerdsman.permitJoin).toHaveBeenCalledWith(true, undefined, undefined);
|
||||
expect(logger.info).toHaveBeenCalledWith(`Currently ${Object.values(zigbeeHerdsman.devices).length - 1} devices are joined:`);
|
||||
expect(logger.info).toHaveBeenCalledWith(`Currently ${Object.values(zigbeeHerdsman.devices).length - 1} devices are joined.`);
|
||||
expect(logger.info).toHaveBeenCalledWith(
|
||||
'bulb (0x000b57fffec6a5b2): LED1545G12 - IKEA TRADFRI bulb E26/E27, white spectrum, globe, opal, 980 lm (Router)',
|
||||
);
|
||||
|
||||
+190
-56
@@ -66,62 +66,40 @@ describe('Networkmap', () => {
|
||||
* | -> CC2530_ROUTER -> WXKG02LM_rev1
|
||||
*
|
||||
*/
|
||||
coordinator.lqi = () => {
|
||||
return {
|
||||
neighbors: [
|
||||
{ieeeAddr: bulb_color.ieeeAddr, networkAddress: bulb_color.networkAddress, relationship: 2, depth: 1, linkquality: 120},
|
||||
{ieeeAddr: bulb.ieeeAddr, networkAddress: bulb.networkAddress, relationship: 2, depth: 1, linkquality: 92},
|
||||
{
|
||||
ieeeAddr: external_converter_device.ieeeAddr,
|
||||
networkAddress: external_converter_device.networkAddress,
|
||||
relationship: 2,
|
||||
depth: 1,
|
||||
linkquality: 92,
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
||||
coordinator.routingTable = () => {
|
||||
return {table: [{destinationAddress: CC2530_ROUTER.networkAddress, status: 'ACTIVE', nextHop: bulb.networkAddress}]};
|
||||
};
|
||||
|
||||
bulb.lqi = () => {
|
||||
return {
|
||||
neighbors: [
|
||||
{ieeeAddr: bulb_color.ieeeAddr, networkAddress: bulb_color.networkAddress, relationship: 1, depth: 2, linkquality: 110},
|
||||
{ieeeAddr: CC2530_ROUTER.ieeeAddr, networkAddress: CC2530_ROUTER.networkAddress, relationship: 1, depth: 2, linkquality: 100},
|
||||
],
|
||||
};
|
||||
};
|
||||
bulb.routingTable = () => {
|
||||
return {table: []};
|
||||
};
|
||||
|
||||
bulb_color.lqi = () => {
|
||||
return {neighbors: []};
|
||||
};
|
||||
bulb_color.routingTable = () => {
|
||||
return {table: []};
|
||||
};
|
||||
|
||||
CC2530_ROUTER.lqi = () => {
|
||||
return {
|
||||
neighbors: [
|
||||
{ieeeAddr: '0x0000000000000000', networkAddress: WXKG02LM_rev1.networkAddress, relationship: 1, depth: 2, linkquality: 130},
|
||||
{ieeeAddr: bulb_color.ieeeAddr, networkAddress: bulb_color.networkAddress, relationship: 4, depth: 2, linkquality: 130},
|
||||
],
|
||||
};
|
||||
};
|
||||
CC2530_ROUTER.routingTable = () => {
|
||||
return {table: []};
|
||||
};
|
||||
|
||||
unsupported_router.lqi = () => {
|
||||
throw new Error('failed');
|
||||
};
|
||||
unsupported_router.routingTable = () => {
|
||||
throw new Error('failed');
|
||||
};
|
||||
coordinator.lqi = jest.fn().mockResolvedValue({
|
||||
neighbors: [
|
||||
{ieeeAddr: bulb_color.ieeeAddr, networkAddress: bulb_color.networkAddress, relationship: 2, depth: 1, linkquality: 120},
|
||||
{ieeeAddr: bulb.ieeeAddr, networkAddress: bulb.networkAddress, relationship: 2, depth: 1, linkquality: 92},
|
||||
{
|
||||
ieeeAddr: external_converter_device.ieeeAddr,
|
||||
networkAddress: external_converter_device.networkAddress,
|
||||
relationship: 2,
|
||||
depth: 1,
|
||||
linkquality: 92,
|
||||
},
|
||||
],
|
||||
});
|
||||
coordinator.routingTable = jest.fn().mockResolvedValue({
|
||||
table: [{destinationAddress: CC2530_ROUTER.networkAddress, status: 'ACTIVE', nextHop: bulb.networkAddress}],
|
||||
});
|
||||
bulb.lqi = jest.fn().mockResolvedValue({
|
||||
neighbors: [
|
||||
{ieeeAddr: bulb_color.ieeeAddr, networkAddress: bulb_color.networkAddress, relationship: 1, depth: 2, linkquality: 110},
|
||||
{ieeeAddr: CC2530_ROUTER.ieeeAddr, networkAddress: CC2530_ROUTER.networkAddress, relationship: 1, depth: 2, linkquality: 100},
|
||||
],
|
||||
});
|
||||
bulb.routingTable = jest.fn().mockResolvedValue({table: []});
|
||||
bulb_color.lqi = jest.fn().mockResolvedValue({neighbors: []});
|
||||
bulb_color.routingTable = jest.fn().mockResolvedValue({table: []});
|
||||
CC2530_ROUTER.lqi = jest.fn().mockResolvedValue({
|
||||
neighbors: [
|
||||
{ieeeAddr: '0x0000000000000000', networkAddress: WXKG02LM_rev1.networkAddress, relationship: 1, depth: 2, linkquality: 130},
|
||||
{ieeeAddr: bulb_color.ieeeAddr, networkAddress: bulb_color.networkAddress, relationship: 4, depth: 2, linkquality: 130},
|
||||
],
|
||||
});
|
||||
CC2530_ROUTER.routingTable = jest.fn().mockResolvedValue({table: []});
|
||||
unsupported_router.lqi = jest.fn().mockRejectedValue(new Error('failed'));
|
||||
unsupported_router.routingTable = jest.fn().mockRejectedValue(new Error('failed'));
|
||||
}
|
||||
|
||||
it('Output raw networkmap legacy api', async () => {
|
||||
@@ -826,4 +804,160 @@ describe('Networkmap', () => {
|
||||
const actual = JSON.parse(call[1]);
|
||||
expect(actual).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('Handles retrying request when first attempt fails', async () => {
|
||||
settings.set(['devices', '0x000b57fffec6a5b2', 'disabled'], true);
|
||||
mock();
|
||||
bulb.lqi.mockRejectedValueOnce(new Error('failed'));
|
||||
MQTT.publish.mockClear();
|
||||
MQTT.events.message('zigbee2mqtt/bridge/request/networkmap', stringify({type: 'raw', routes: true}));
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(1);
|
||||
let call = MQTT.publish.mock.calls[0];
|
||||
expect(call[0]).toStrictEqual('zigbee2mqtt/bridge/response/networkmap');
|
||||
|
||||
const expected = {
|
||||
data: {
|
||||
routes: true,
|
||||
type: 'raw',
|
||||
value: {
|
||||
links: [
|
||||
{
|
||||
depth: 1,
|
||||
linkquality: 120,
|
||||
lqi: 120,
|
||||
relationship: 2,
|
||||
routes: [],
|
||||
source: {ieeeAddr: '0x000b57fffec6a5b3', networkAddress: 40399},
|
||||
sourceIeeeAddr: '0x000b57fffec6a5b3',
|
||||
sourceNwkAddr: 40399,
|
||||
target: {ieeeAddr: '0x00124b00120144ae', networkAddress: 0},
|
||||
targetIeeeAddr: '0x00124b00120144ae',
|
||||
},
|
||||
{
|
||||
depth: 1,
|
||||
linkquality: 92,
|
||||
lqi: 92,
|
||||
relationship: 2,
|
||||
routes: [{destinationAddress: 6540, nextHop: 40369, status: 'ACTIVE'}],
|
||||
source: {ieeeAddr: '0x000b57fffec6a5b2', networkAddress: 40369},
|
||||
sourceIeeeAddr: '0x000b57fffec6a5b2',
|
||||
sourceNwkAddr: 40369,
|
||||
target: {ieeeAddr: '0x00124b00120144ae', networkAddress: 0},
|
||||
targetIeeeAddr: '0x00124b00120144ae',
|
||||
},
|
||||
{
|
||||
depth: 1,
|
||||
linkquality: 92,
|
||||
lqi: 92,
|
||||
relationship: 2,
|
||||
routes: [],
|
||||
source: {ieeeAddr: '0x0017880104e45511', networkAddress: 1114},
|
||||
sourceIeeeAddr: '0x0017880104e45511',
|
||||
sourceNwkAddr: 1114,
|
||||
target: {ieeeAddr: '0x00124b00120144ae', networkAddress: 0},
|
||||
targetIeeeAddr: '0x00124b00120144ae',
|
||||
},
|
||||
{
|
||||
depth: 2,
|
||||
linkquality: 130,
|
||||
lqi: 130,
|
||||
relationship: 1,
|
||||
routes: [],
|
||||
source: {ieeeAddr: '0x0017880104e45521', networkAddress: 6538},
|
||||
sourceIeeeAddr: '0x0017880104e45521',
|
||||
sourceNwkAddr: 6538,
|
||||
target: {ieeeAddr: '0x0017880104e45559', networkAddress: 6540},
|
||||
targetIeeeAddr: '0x0017880104e45559',
|
||||
},
|
||||
],
|
||||
nodes: [
|
||||
{
|
||||
definition: null,
|
||||
failed: [],
|
||||
friendlyName: 'Coordinator',
|
||||
ieeeAddr: '0x00124b00120144ae',
|
||||
lastSeen: 1000,
|
||||
modelID: null,
|
||||
networkAddress: 0,
|
||||
type: 'Coordinator',
|
||||
},
|
||||
{
|
||||
definition: {
|
||||
description: 'Hue Go',
|
||||
model: '7146060PH',
|
||||
supports:
|
||||
'light (state, brightness, color_temp, color_temp_startup, color_xy, color_hs), power_on_behavior, effect, linkquality',
|
||||
vendor: 'Philips',
|
||||
},
|
||||
failed: [],
|
||||
friendlyName: 'bulb_color',
|
||||
ieeeAddr: '0x000b57fffec6a5b3',
|
||||
lastSeen: 1000,
|
||||
modelID: 'LLC020',
|
||||
networkAddress: 40399,
|
||||
type: 'Router',
|
||||
},
|
||||
{
|
||||
definition: {
|
||||
description: 'Wireless remote switch (double rocker), 2016 model',
|
||||
model: 'WXKG02LM_rev1',
|
||||
supports: 'battery, voltage, power_outage_count, action, linkquality',
|
||||
vendor: 'Aqara',
|
||||
},
|
||||
friendlyName: 'button_double_key',
|
||||
ieeeAddr: '0x0017880104e45521',
|
||||
lastSeen: 1000,
|
||||
modelID: 'lumi.sensor_86sw2.es1',
|
||||
networkAddress: 6538,
|
||||
type: 'EndDevice',
|
||||
},
|
||||
{
|
||||
definition: {
|
||||
description: 'Automatically generated definition',
|
||||
model: 'notSupportedModelID',
|
||||
supports: 'action, linkquality',
|
||||
vendor: 'Boef',
|
||||
},
|
||||
failed: ['lqi', 'routingTable'],
|
||||
friendlyName: '0x0017880104e45525',
|
||||
ieeeAddr: '0x0017880104e45525',
|
||||
lastSeen: 1000,
|
||||
manufacturerName: 'Boef',
|
||||
modelID: 'notSupportedModelID',
|
||||
networkAddress: 6536,
|
||||
type: 'Router',
|
||||
},
|
||||
{
|
||||
definition: {
|
||||
description: 'CC2530 router',
|
||||
model: 'CC2530.ROUTER',
|
||||
supports: 'led, linkquality',
|
||||
vendor: 'Custom devices (DiY)',
|
||||
},
|
||||
failed: [],
|
||||
friendlyName: 'cc2530_router',
|
||||
ieeeAddr: '0x0017880104e45559',
|
||||
lastSeen: 1000,
|
||||
modelID: 'lumi.router',
|
||||
networkAddress: 6540,
|
||||
type: 'Router',
|
||||
},
|
||||
{
|
||||
definition: {description: 'external', model: 'external_converter_device', supports: 'linkquality', vendor: 'external'},
|
||||
friendlyName: '0x0017880104e45511',
|
||||
ieeeAddr: '0x0017880104e45511',
|
||||
lastSeen: 1000,
|
||||
modelID: 'external_converter_device',
|
||||
networkAddress: 1114,
|
||||
type: 'EndDevice',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
status: 'ok',
|
||||
};
|
||||
const actual = JSON.parse(call[1]);
|
||||
expect(actual).toStrictEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -857,8 +857,14 @@ const mock = {
|
||||
events[type] = handler;
|
||||
},
|
||||
stop: jest.fn(),
|
||||
getDevices: jest.fn().mockImplementation(() => {
|
||||
return Object.values(devices).filter((d) => returnDevices.length === 0 || returnDevices.includes(d.ieeeAddr));
|
||||
getDevicesIterator: jest.fn().mockImplementation(function* (predicate) {
|
||||
for (const key in devices) {
|
||||
const device = devices[key];
|
||||
|
||||
if ((returnDevices.length === 0 || returnDevices.includes(device.ieeeAddr)) && (!predicate || predicate(device))) {
|
||||
yield device;
|
||||
}
|
||||
}
|
||||
}),
|
||||
getDevicesByType: jest.fn().mockImplementation((type) => {
|
||||
return Object.values(devices)
|
||||
@@ -875,8 +881,14 @@ const mock = {
|
||||
.filter((d) => returnDevices.length === 0 || returnDevices.includes(d.ieeeAddr))
|
||||
.find((d) => d.networkAddress === networkAddress);
|
||||
}),
|
||||
getGroups: jest.fn().mockImplementation((query) => {
|
||||
return Object.values(groups);
|
||||
getGroupsIterator: jest.fn().mockImplementation(function* (predicate) {
|
||||
for (const key in groups) {
|
||||
const group = groups[key];
|
||||
|
||||
if (!predicate || predicate(group)) {
|
||||
yield group;
|
||||
}
|
||||
}
|
||||
}),
|
||||
getGroupByID: jest.fn().mockImplementation((groupID) => {
|
||||
return Object.values(groups).find((d) => d.groupID === groupID);
|
||||
|
||||
@@ -4,6 +4,11 @@ const versionHerdsman = require('../node_modules/zigbee-herdsman/package.json').
|
||||
const versionHerdsmanConverters = require('../node_modules/zigbee-herdsman-converters/package.json').version;
|
||||
|
||||
describe('Utils', () => {
|
||||
it('Object is empty', () => {
|
||||
expect(utils.objectIsEmpty({})).toBeTruthy();
|
||||
expect(utils.objectIsEmpty({a: 1})).toBeFalsy();
|
||||
});
|
||||
|
||||
it('Object has properties', () => {
|
||||
expect(utils.objectHasProperties({a: 1, b: 2, c: 3}, ['a', 'b'])).toBeTruthy();
|
||||
expect(utils.objectHasProperties({a: 1, b: 2, c: 3}, ['a', 'b', 'd'])).toBeFalsy();
|
||||
|
||||
Reference in New Issue
Block a user