From f2e953cf18f81016a62ba77608dcc42b7f3bc787 Mon Sep 17 00:00:00 2001 From: Koen Kanters Date: Sat, 18 Jun 2022 15:47:06 +0200 Subject: [PATCH] Add group `off_state` option (#12868) --- lib/extension/groups.ts | 23 ++++++++++++++--------- lib/types/types.d.ts | 1 + lib/util/settings.schema.json | 8 ++++++++ test/group.test.js | 24 ++++++++++++++++++++++++ 4 files changed, 47 insertions(+), 9 deletions(-) diff --git a/lib/extension/groups.ts b/lib/extension/groups.ts index c1c140e3e2..72da5d5f71 100644 --- a/lib/extension/groups.ts +++ b/lib/extension/groups.ts @@ -129,11 +129,10 @@ export default class Groups extends Extension { if (entity instanceof Device) { for (const group of groups) { if (group.zh.hasMember(entity.endpoint(endpointName)) && - !equals(this.lastOptimisticState[group.ID], payload)) { - if (!payload || payload.state !== 'OFF' || this.areAllMembersOff(group)) { - this.lastOptimisticState[group.ID] = payload; - await this.publishEntityState(group, payload, reason); - } + !equals(this.lastOptimisticState[group.ID], payload) && + this.shouldPublishPayloadForGroup(group, payload)) { + this.lastOptimisticState[group.ID] = payload; + await this.publishEntityState(group, payload, reason); } } } else { @@ -161,10 +160,9 @@ export default class Groups extends Extension { await this.publishEntityState(device, memberPayload, reason); for (const zigbeeGroup of groups) { - if (zigbeeGroup.zh.hasMember(member)) { - if (!payload || payload.state !== 'OFF' || this.areAllMembersOff(zigbeeGroup)) { - groupsToPublish.add(zigbeeGroup); - } + if (zigbeeGroup.zh.hasMember(member) && + this.shouldPublishPayloadForGroup(zigbeeGroup, payload)) { + groupsToPublish.add(zigbeeGroup); } } } @@ -176,6 +174,13 @@ 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; + } + private areAllMembersOff(group: Group): boolean { for (const member of group.zh.members) { const device = this.zigbee.resolveEntity(member.getDevice()); diff --git a/lib/types/types.d.ts b/lib/types/types.d.ts index 4e300bb39e..efec9e46d7 100644 --- a/lib/types/types.d.ts +++ b/lib/types/types.d.ts @@ -294,6 +294,7 @@ declare global { devices?: string[], ID?: number, optimistic?: boolean, + off_state?: 'all_members_off' | 'last_member_state' filtered_optimistic?: string[], retrieve_state?: boolean, homeassistant?: KeyValue, diff --git a/lib/util/settings.schema.json b/lib/util/settings.schema.json index 1e9887fb17..c13370722a 100644 --- a/lib/util/settings.schema.json +++ b/lib/util/settings.schema.json @@ -870,6 +870,14 @@ "qos": { "type": "number" }, + "off_state": { + "type": ["string"], + "enum": ["all_members_off", "last_member_state"], + "title": "Group off state", + "default": "auto", + "requiresRestart": true, + "description": "Control when to publish state OFF for a group. 'all_members_off': only publish state OFF when all group memebers are in state OFF, 'last_member_state': publish state OFF whenever one of its members changes to OFF" + }, "filtered_attributes": { "type": "array", "items": { diff --git a/test/group.test.js b/test/group.test.js index bc0b38a41f..45390e6ff0 100644 --- a/test/group.test.js +++ b/test/group.test.js @@ -469,6 +469,30 @@ describe('Groups', () => { expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb_color", stringify({state:"OFF"}), {"retain": false, qos: 0}, expect.any(Function)); }); + it('Should publish state change off if any lights within are still on when changed via device when off_state: last_member_state is used', async () => { + const device_1 = zigbeeHerdsman.devices.bulb_color; + const device_2 = zigbeeHerdsman.devices.bulb; + const endpoint_1 = device_1.getEndpoint(1); + const endpoint_2 = device_2.getEndpoint(1); + const group = zigbeeHerdsman.groups.group_1; + group.members.push(endpoint_1); + group.members.push(endpoint_2); + settings.set(['groups'], { + '1': {friendly_name: 'group_1', devices: [device_1.ieeeAddr, device_2.ieeeAddr], retain: false, off_state: 'last_member_state'} + }); + await resetExtension(); + + await MQTT.events.message('zigbee2mqtt/group_1/set', stringify({state: 'ON'})); + await flushPromises(); + MQTT.publish.mockClear(); + + await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({state: 'OFF'})); + await flushPromises(); + expect(MQTT.publish).toHaveBeenCalledTimes(2); + expect(MQTT.publish).toHaveBeenNthCalledWith(1, "zigbee2mqtt/group_1", stringify({state:"OFF"}), {"retain": false, qos: 0}, expect.any(Function)); + expect(MQTT.publish).toHaveBeenNthCalledWith(2, "zigbee2mqtt/bulb_color", stringify({state:"OFF"}), {"retain": false, qos: 0}, expect.any(Function)); + }); + it('Should not publish state change off if any lights within are still on when changed via shared group', async () => { const device_1 = zigbeeHerdsman.devices.bulb_color; const device_2 = zigbeeHerdsman.devices.bulb;