diff --git a/lib/extension/groups.js b/lib/extension/groups.js index 3489ed38..185c6acd 100644 --- a/lib/extension/groups.js +++ b/lib/extension/groups.js @@ -90,21 +90,7 @@ class Groups extends BaseExtension { if (entity.type === 'device') { for (const zigbeeGroup of zigbeeGroups) { if (zigbeeGroup.hasMember(entity.endpoint)) { - let shouldGroupChange = true; - - if (payload.state && payload.state === 'OFF') { - for (const member of zigbeeGroup.members) { - const device = member.getDevice(); - if (this.state.exists(device.ieeeAddr)) { - const state = this.state.get(device.ieeeAddr); - if (state.state && state.state === 'ON') { - shouldGroupChange = false; - } - } - } - } - - if (shouldGroupChange) { + if (!payload || payload.state !== 'OFF' || this.allMembersOff(zigbeeGroup)) { await this.publishEntityState(zigbeeGroup.groupID, payload, reason); } } @@ -115,7 +101,9 @@ class Groups extends BaseExtension { await this.publishEntityState(member.getDevice().ieeeAddr, payload, reason); for (const zigbeeGroup of zigbeeGroups) { if (zigbeeGroup.hasMember(member)) { - groupIDsToPublish.add(zigbeeGroup.groupID); + if (!payload || payload.state !== 'OFF' || this.allMembersOff(zigbeeGroup)) { + groupIDsToPublish.add(zigbeeGroup.groupID); + } } } } @@ -127,6 +115,19 @@ class Groups extends BaseExtension { } } + allMembersOff(zigbeeGroup) { + for (const member of zigbeeGroup.members) { + const device = member.getDevice(); + if (this.state.exists(device.ieeeAddr)) { + const state = this.state.get(device.ieeeAddr); + if (state && state.state === 'ON') { + return false; + } + } + } + return true; + } + async onMQTTMessage(topic, message) { let type; let group; diff --git a/test/group.test.js b/test/group.test.js index 83a79145..64240e65 100644 --- a/test/group.test.js +++ b/test/group.test.js @@ -316,7 +316,7 @@ describe('Groups', () => { expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/group_2", '{"state":"ON"}', {"retain": false, qos: 0}, expect.any(Function)); }); - it('Should not publish state change off if any lights within are still on', async () => { + it('Should not publish state change off if any lights within are still on when changed via device', async () => { const device_1 = zigbeeHerdsman.devices.bulb_color; const device_2 = zigbeeHerdsman.devices.bulb; const endpoint_1 = device_1.getEndpoint(1); @@ -340,6 +340,32 @@ describe('Groups', () => { expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb_color", '{"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; + 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}, + '2': {friendly_name: 'group_2', retain: false, devices: [device_1.ieeeAddr]}, + }); + await controller.start(); + await flushPromises(); + + await MQTT.events.message('zigbee2mqtt/group_1/set', JSON.stringify({state: 'ON'})); + await flushPromises(); + MQTT.publish.mockClear(); + + await MQTT.events.message('zigbee2mqtt/group_2/set', JSON.stringify({state: 'OFF'})); + await flushPromises(); + expect(MQTT.publish).toHaveBeenCalledTimes(2); + expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/group_2", '{"state":"OFF"}', {"retain": false, qos: 0}, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb_color", '{"state":"OFF"}', {"retain": false, qos: 0}, expect.any(Function)); + }); + it('Should publish state change off if all lights within turn off', async () => { const device_1 = zigbeeHerdsman.devices.bulb_color; const device_2 = zigbeeHerdsman.devices.bulb;