From dcc1107204c376f0d856fed6f76cf11217a23975 Mon Sep 17 00:00:00 2001 From: Nerivec <62446222+Nerivec@users.noreply.github.com> Date: Tue, 11 Jun 2024 17:52:11 +0200 Subject: [PATCH] Groups code optimization/formatting. --- lib/extension/groups.ts | 183 ++++++++++++++++++++++------------------ 1 file changed, 99 insertions(+), 84 deletions(-) diff --git a/lib/extension/groups.ts b/lib/extension/groups.ts index 14139592e2..fd566dbc27 100644 --- a/lib/extension/groups.ts +++ b/lib/extension/groups.ts @@ -16,17 +16,11 @@ const legacyTopicRegexRemoveAll = new RegExp(`^${settings.get().mqtt.base_topic} const stateProperties: {[s: string]: (value: string, exposes: zhc.Expose[]) => boolean} = { 'state': () => true, - 'brightness': (value, exposes) => - !!exposes.find((e) => e.type === 'light' && e.features.find((f) => f.name === 'brightness')), - 'color_temp': (value, exposes) => - !!exposes.find((e) => e.type === 'light' && e.features.find((f) => f.name === 'color_temp')), - 'color': (value, exposes) => - !!exposes.find((e) => e.type === 'light' && - e.features.find((f) => f.name === 'color_xy' || f.name === 'color_hs')), - 'color_mode': (value, exposes) => - !!exposes.find((e) => e.type === 'light' && ( - (e.features.find((f) => f.name === `color_${value}`)) || - (value === 'color_temp' && e.features.find((f) => f.name === 'color_temp')) )), + 'brightness': (value, exposes) => exposes.some((e) => e.type === 'light' && e.features.some((f) => f.name === 'brightness')), + 'color_temp': (value, exposes) => exposes.some((e) => e.type === 'light' && e.features.some((f) => f.name === 'color_temp')), + 'color': (value, exposes) => exposes.some((e) => e.type === 'light' && e.features.some((f) => f.name === 'color_xy' || f.name === 'color_hs')), + 'color_mode': (value, exposes) => exposes.some((e) => e.type === 'light' && + ((e.features.some((f) => f.name === `color_${value}`)) || (value === 'color_temp' && e.features.some((f) => f.name === 'color_temp')))), }; interface ParsedMQTTMessage { @@ -49,10 +43,11 @@ export default class Groups extends Extension { const settingsGroups = settings.getGroups(); const zigbeeGroups = this.zigbee.groups(); - const addRemoveFromGroup = async (action: 'add' | 'remove', deviceName: string, - groupName: string | number, endpoint: zh.Endpoint, group: Group): Promise => { + const addRemoveFromGroup = async (action: 'add' | 'remove', deviceName: string, groupName: string | number, + endpoint: zh.Endpoint, group: Group): Promise => { try { logger.info(`${action === 'add' ? 'Adding' : 'Removing'} '${deviceName}' to group '${groupName}'`); + if (action === 'remove') { await endpoint.removeFromGroup(group.zh); } else { @@ -67,36 +62,47 @@ 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 settingsEndpoint = settingGroup.devices.map((d) => { + const settingsEndpoints: zh.Endpoint[] = []; + + for (const d of settingGroup.devices) { const parsed = this.zigbee.resolveEntityAndEndpoint(d); - const entity = parsed.entity as Device; - if (!entity) logger.error(`Cannot find '${d}' of group '${settingGroup.friendly_name}'`); - if (parsed.endpointID && !parsed.endpoint) { - logger.error(`Cannot find endpoint '${parsed.endpointID}' of device '${parsed.ID}'`); + const device = parsed.entity as Device; + + if (!device) { + logger.error(`Cannot find '${d}' of group '${settingGroup.friendly_name}'`); + } + + if (!parsed.endpoint) { + if (parsed.endpointID) { + logger.error(`Cannot find endpoint '${parsed.endpointID}' of device '${parsed.ID}'`); + } + + continue; } - return {'endpoint': parsed.endpoint, 'name': entity?.name}; - }).filter((e) => e.endpoint != null); - // In settings but not in zigbee - for (const entity of settingsEndpoint) { - if (!zigbeeGroup.zh.hasMember(entity.endpoint)) { - await addRemoveFromGroup('add', entity.name, settingGroup.friendly_name, entity.endpoint, zigbeeGroup); + // In settings but not in zigbee + if (!zigbeeGroup.zh.hasMember(parsed.endpoint)) { + await addRemoveFromGroup('add', device?.name, settingGroup.friendly_name, parsed.endpoint, zigbeeGroup); } + + settingsEndpoints.push(parsed.endpoint); } // In zigbee but not in settings for (const endpoint of zigbeeGroup.zh.members) { - if (!settingsEndpoint.find((e) => e.endpoint === endpoint)) { + if (!settingsEndpoints.includes(endpoint)) { const deviceName = settings.getDevice(endpoint.getDevice().ieeeAddr).friendly_name; + await addRemoveFromGroup('remove', deviceName, settingGroup.friendly_name, endpoint, zigbeeGroup); } } } for (const zigbeeGroup of zigbeeGroups) { - if (!settingsGroups.find((g) => g.ID === zigbeeGroup.ID)) { + if (!settingsGroups.some((g) => g.ID === zigbeeGroup.ID)) { for (const endpoint of zigbeeGroup.zh.members) { const deviceName = settings.getDevice(endpoint.getDevice().ieeeAddr).friendly_name; + await addRemoveFromGroup('remove', deviceName, zigbeeGroup.ID, endpoint, zigbeeGroup); } } @@ -105,16 +111,18 @@ export default class Groups extends Extension { @bind async onStateChange(data: eventdata.StateChange): Promise { const reason = 'groupOptimistic'; + if (data.reason === reason || data.reason === 'publishCached') { return; } const payload: KeyValue = {}; - let endpointName: string = null; const endpointNames: string[] = data.entity instanceof Device ? data.entity.getEndpointNames() : []; + for (let [prop, value] of Object.entries(data.update)) { const endpointNameMatch = endpointNames.find((n) => prop.endsWith(`_${n}`)); + if (endpointNameMatch) { prop = prop.substring(0, prop.length - endpointNameMatch.length - 1); endpointName = endpointNameMatch; @@ -125,18 +133,18 @@ export default class Groups extends Extension { } } - if (Object.keys(payload).length) { + const payloadKeys = Object.keys(payload); + + if (payloadKeys.length) { const entity = data.entity; - const groups = this.zigbee.groups().filter((g) => { - return g.options && (!g.options.hasOwnProperty('optimistic') || g.options.optimistic); - }); + const groups = this.zigbee.groups().filter((g) => g.options && (g.options.optimistic == undefined || g.options.optimistic)); if (entity instanceof Device) { for (const group of groups) { - if (group.zh.hasMember(entity.endpoint(endpointName)) && - !equals(this.lastOptimisticState[group.ID], payload) && + if (group.zh.hasMember(entity.endpoint(endpointName)) && !equals(this.lastOptimisticState[group.ID], payload) && this.shouldPublishPayloadForGroup(group, payload)) { this.lastOptimisticState[group.ID] = payload; + await this.publishEntityState(group, payload, reason); } } @@ -145,34 +153,43 @@ export default class Groups extends Extension { delete this.lastOptimisticState[entity.ID]; const groupsToPublish: Set = new Set(); + for (const member of entity.zh.members) { const device = this.zigbee.resolveEntity(member.getDevice()) as Device; - if (device.options.disabled) continue; + + if (device.options.disabled) { + continue; + } + const exposes = device.exposes(); const memberPayload: KeyValue = {}; - Object.keys(payload).forEach((key) => { + + for (const key of payloadKeys) { if (stateProperties[key](payload[key], exposes)) { memberPayload[key] = payload[key]; } - }); + } const endpointName = device.endpointName(member); + if (endpointName) { - Object.keys(memberPayload).forEach((key) => { + for (const key of Object.keys(memberPayload)) { memberPayload[`${key}_${endpointName}`] = memberPayload[key]; delete memberPayload[key]; - }); + } } await this.publishEntityState(device, memberPayload, reason); + for (const zigbeeGroup of groups) { - if (zigbeeGroup.zh.hasMember(member) && - this.shouldPublishPayloadForGroup(zigbeeGroup, payload)) { + if (zigbeeGroup.zh.hasMember(member) && this.shouldPublishPayloadForGroup(zigbeeGroup, payload)) { groupsToPublish.add(zigbeeGroup); } } } + groupsToPublish.delete(entity); + for (const group of groupsToPublish) { await this.publishEntityState(group, payload, reason); } @@ -181,22 +198,22 @@ export default class Groups extends Extension { } private shouldPublishPayloadForGroup(group: Group, payload: KeyValue): boolean { - if (group.options.off_state === 'last_member_state') return true; - if (!payload || payload.state !== 'OFF') return true; - if (this.areAllMembersOff(group)) return true; - return false; + return ((group.options.off_state === 'last_member_state') || (!payload || payload.state !== 'OFF') || this.areAllMembersOff(group)); } private areAllMembersOff(group: Group): boolean { for (const member of group.zh.members) { const device = this.zigbee.resolveEntity(member.getDevice()); + if (this.state.exists(device)) { const state = this.state.get(device); + if (state.state === 'ON') { return false; } } } + return true; } @@ -218,6 +235,7 @@ export default class Groups extends Extension { if (this.legacyApi && (legacyTopicRegexMatch || legacyTopicRegexRemoveAllMatch)) { triggeredViaLegacyApi = true; + if (legacyTopicRegexMatch) { resolvedEntityGroup = this.zigbee.resolveEntity(legacyTopicRegexMatch[1]) as Group; type = legacyTopicRegexMatch[2] as 'remove' | 'remove_all' | 'add'; @@ -227,12 +245,9 @@ export default class Groups extends Extension { /* istanbul ignore else */ if (settings.get().advanced.legacy_api) { - const payload = {friendly_name: data.message, - group: legacyTopicRegexMatch[1], error: 'group doesn\'t exists'}; - await this.mqtt.publish( - 'bridge/log', - stringify({type: `device_group_${type}_failed`, message: payload}), - ); + const message = {friendly_name: data.message, group: legacyTopicRegexMatch[1], error: `group doesn't exists`}; + + await this.mqtt.publish('bridge/log', stringify({type: `device_group_${type}_failed`, message})); } return null; @@ -243,24 +258,22 @@ export default class Groups extends Extension { const parsedEntity = this.zigbee.resolveEntityAndEndpoint(data.message); resolvedEntityDevice = parsedEntity.entity as Device; + if (!resolvedEntityDevice || !(resolvedEntityDevice instanceof Device)) { logger.error(`Device '${data.message}' does not exist`); /* istanbul ignore else */ if (settings.get().advanced.legacy_api) { - const payload = { - friendly_name: data.message, group: legacyTopicRegexMatch[1], error: 'entity doesn\'t exists', - }; - await this.mqtt.publish( - 'bridge/log', - stringify({type: `device_group_${type}_failed`, message: payload}), - ); + const message = {friendly_name: data.message, group: legacyTopicRegexMatch[1], error: 'entity doesn\'t exists'}; + + await this.mqtt.publish('bridge/log', stringify({type: `device_group_${type}_failed`, message})); } return null; } resolvedEntityEndpoint = parsedEntity.endpoint; + if (parsedEntity.endpointID && !resolvedEntityEndpoint) { logger.error(`Device '${parsedEntity.ID}' does not have endpoint '${parsedEntity.endpointID}'`); return null; @@ -274,6 +287,7 @@ export default class Groups extends Extension { if (type !== 'remove_all') { groupKey = message.group; resolvedEntityGroup = this.zigbee.resolveEntity(message.group) as Group; + if (!resolvedEntityGroup || !(resolvedEntityGroup instanceof Group)) { error = `Group '${message.group}' does not exist`; } @@ -281,11 +295,14 @@ export default class Groups extends Extension { const parsed = this.zigbee.resolveEntityAndEndpoint(message.device); resolvedEntityDevice = parsed?.entity as Device; + if (!error && (!resolvedEntityDevice || !(resolvedEntityDevice instanceof Device))) { error = `Device '${message.device}' does not exist`; } + if (!error) { resolvedEntityEndpoint = parsed.endpoint; + if (parsed.endpointID && !resolvedEntityEndpoint) { error = `Device '${parsed.ID}' does not have endpoint '${parsed.endpointID}'`; } @@ -300,27 +317,25 @@ export default class Groups extends Extension { @bind private async onMQTTMessage(data: eventdata.MQTTMessage): Promise { const parsed = await this.parseMQTTMessage(data); - if (!parsed || !parsed.type) return; + + if (!parsed || !parsed.type) { + return; + } + let { resolvedEntityGroup, resolvedEntityDevice, type, error, triggeredViaLegacyApi, groupKey, deviceKey, skipDisableReporting, resolvedEntityEndpoint, } = parsed; - const message = utils.parseJSON(data.message, data.message); let changedGroups: Group[] = []; - const responseData: KeyValue = {device: deviceKey}; - if (groupKey) { - responseData.group = groupKey; - } - if (!error) { try { const keys = [ `${resolvedEntityDevice.ieeeAddr}/${resolvedEntityEndpoint.ID}`, `${resolvedEntityDevice.name}/${resolvedEntityEndpoint.ID}`, ]; - const endpointNameLocal = resolvedEntityDevice.endpointName(resolvedEntityEndpoint); + if (endpointNameLocal) { keys.push(`${resolvedEntityDevice.ieeeAddr}/${endpointNameLocal}`); keys.push(`${resolvedEntityDevice.name}/${endpointNameLocal}`); @@ -339,11 +354,9 @@ export default class Groups extends Extension { /* istanbul ignore else */ if (settings.get().advanced.legacy_api) { - const payload = {friendly_name: resolvedEntityDevice.name, group: resolvedEntityGroup.name}; - await this.mqtt.publish( - 'bridge/log', - stringify({type: `device_group_add`, message: payload}), - ); + const message = {friendly_name: resolvedEntityDevice.name, group: resolvedEntityGroup.name}; + + await this.mqtt.publish('bridge/log', stringify({type: `device_group_add`, message})); } } else if (type === 'remove') { logger.info(`Removing '${resolvedEntityDevice.name}' from '${resolvedEntityGroup.name}'`); @@ -353,26 +366,23 @@ export default class Groups extends Extension { /* istanbul ignore else */ if (settings.get().advanced.legacy_api) { - const payload = {friendly_name: resolvedEntityDevice.name, group: resolvedEntityGroup.name}; - await this.mqtt.publish( - 'bridge/log', - stringify({type: `device_group_remove`, message: payload}), - ); + const message = {friendly_name: resolvedEntityDevice.name, group: resolvedEntityGroup.name}; + + await this.mqtt.publish('bridge/log', stringify({type: `device_group_remove`, message})); } } else { // remove_all logger.info(`Removing '${resolvedEntityDevice.name}' from all groups`); changedGroups = this.zigbee.groups().filter((g) => g.zh.members.includes(resolvedEntityEndpoint)); await resolvedEntityEndpoint.removeFromAllGroups(); + for (const settingsGroup of settings.getGroups()) { settings.removeDeviceFromGroup(settingsGroup.ID.toString(), keys); /* istanbul ignore else */ if (settings.get().advanced.legacy_api) { - const payload = {friendly_name: resolvedEntityDevice.name}; - await this.mqtt.publish( - 'bridge/log', - stringify({type: `device_group_remove_all`, message: payload}), - ); + const message = {friendly_name: resolvedEntityDevice.name}; + + await this.mqtt.publish('bridge/log', stringify({type: `device_group_remove_all`, message})); } } } @@ -383,16 +393,21 @@ export default class Groups extends Extension { } if (!triggeredViaLegacyApi) { - const response = utils.getResponse(message, responseData, error); - await this.mqtt.publish(`bridge/response/group/members/${type}`, stringify(response)); + const message = utils.parseJSON(data.message, data.message); + const responseData: KeyValue = {device: deviceKey}; + + if (groupKey) { + responseData.group = groupKey; + } + + await this.mqtt.publish(`bridge/response/group/members/${type}`, stringify(utils.getResponse(message, responseData, error))); } if (error) { logger.error(error); } else { for (const group of changedGroups) { - this.eventBus.emitGroupMembersChanged({ - group, action: type, endpoint: resolvedEntityEndpoint, skipDisableReporting}); + this.eventBus.emitGroupMembersChanged({group, action: type, endpoint: resolvedEntityEndpoint, skipDisableReporting}); } } }