Files

820 lines
40 KiB
TypeScript

// biome-ignore assist/source/organizeImports: import mocks first
import {afterAll, beforeAll, beforeEach, describe, expect, it, vi} from "vitest";
import * as data from "../mocks/data";
import {mockLogger} from "../mocks/logger";
import {events as mockMQTTEvents, mockMQTTPublishAsync} from "../mocks/mqtt";
import {flushPromises} from "../mocks/utils";
import {devices, groups, events as mockZHEvents, resetGroupMembers, returnDevices} from "../mocks/zigbeeHerdsman";
import stringify from "json-stable-stringify-without-jsonify";
import * as zhcGlobalStore from "zigbee-herdsman-converters/lib/store";
import {Controller} from "../../lib/controller";
import * as settings from "../../lib/util/settings";
returnDevices.push(
devices.coordinator.ieeeAddr,
devices.bulb_color.ieeeAddr,
devices.bulb.ieeeAddr,
devices.QBKG03LM.ieeeAddr,
devices.bulb_color_2.ieeeAddr,
devices.bulb_2.ieeeAddr,
devices.GLEDOPTO_2ID.ieeeAddr,
devices.InovelliVZM31SN.ieeeAddr,
);
describe("Extension: Groups", () => {
let controller: Controller;
beforeAll(async () => {
vi.useFakeTimers();
controller = new Controller(vi.fn(), vi.fn());
await controller.start();
await flushPromises();
});
afterAll(async () => {
await controller?.stop();
await flushPromises();
vi.useRealTimers();
});
beforeEach(() => {
resetGroupMembers();
data.writeDefaultConfiguration();
settings.reRead();
mockMQTTPublishAsync.mockClear();
groups.gledopto_group.command.mockClear();
zhcGlobalStore.clear();
controller.state.clear();
});
it("Should publish group state change when a device in it changes state", async () => {
const device = devices.bulb_color;
const endpoint = device.getEndpoint(1)!;
const group = groups.group_1;
group.members.push(endpoint);
mockMQTTPublishAsync.mockClear();
const payload = {data: {onOff: 1}, cluster: "genOnOff", device, endpoint, type: "attributeReport", linkquality: 10};
await mockZHEvents.message(payload);
await flushPromises();
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(2);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bulb_color", stringify({state: "ON"}), {retain: false, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/group_1", stringify({state: "ON"}), {retain: false, qos: 0});
});
it("Should not republish identical optimistic group states", async () => {
const device1 = devices.bulb_2;
const device2 = devices.bulb_color_2;
mockMQTTPublishAsync.mockClear();
await mockZHEvents.message({
data: {onOff: 1},
cluster: "genOnOff",
device: device1,
endpoint: device1.getEndpoint(1),
type: "attributeReport",
linkquality: 10,
});
await mockZHEvents.message({
data: {onOff: 1},
cluster: "genOnOff",
device: device2,
endpoint: device2.getEndpoint(1),
type: "attributeReport",
linkquality: 10,
});
await flushPromises();
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(6);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/group_tradfri_remote", stringify({state: "ON"}), {retain: false, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bulb_2", stringify({state: "ON"}), {retain: false, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bulb_color_2", stringify({state: "ON"}), {retain: false, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/group_with_tradfri", stringify({state: "ON"}), {retain: false, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/ha_discovery_group", stringify({state: "ON"}), {retain: false, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/switch_group", stringify({state: "ON"}), {retain: false, qos: 0});
});
it("Should publish state change of all members when a group changes its state", async () => {
const device = devices.bulb_color;
const endpoint = device.getEndpoint(1)!;
const group = groups.group_1;
group.members.push(endpoint);
mockMQTTPublishAsync.mockClear();
await mockMQTTEvents.message("zigbee2mqtt/group_1/set", stringify({state: "ON"}));
await flushPromises();
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(2);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bulb_color", stringify({state: "ON"}), {retain: false, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/group_1", stringify({state: "ON"}), {retain: false, qos: 0});
});
it("Should not publish state change when group changes state and device is disabled", async () => {
const device = devices.bulb_color;
const endpoint = device.getEndpoint(1)!;
const group = groups.group_1;
group.members.push(endpoint);
settings.set(["devices", device.ieeeAddr, "disabled"], true);
mockMQTTPublishAsync.mockClear();
await mockMQTTEvents.message("zigbee2mqtt/group_1/set", stringify({state: "ON"}));
await flushPromises();
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/group_1", stringify({state: "ON"}), {retain: false, qos: 0});
});
it("Should publish state change for group when members state change", async () => {
// Created for https://github.com/Koenkk/zigbee2mqtt/issues/5725
const device = devices.bulb_color;
const endpoint = device.getEndpoint(1)!;
const group = groups.group_1;
group.members.push(endpoint);
mockMQTTPublishAsync.mockClear();
await mockMQTTEvents.message("zigbee2mqtt/bulb_color/set", stringify({state: "ON"}));
await flushPromises();
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(2);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bulb_color", stringify({state: "ON"}), {retain: false, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/group_1", stringify({state: "ON"}), {retain: false, qos: 0});
mockMQTTPublishAsync.mockClear();
await mockMQTTEvents.message("zigbee2mqtt/group_1/set", stringify({state: "OFF"}));
await flushPromises();
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(2);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bulb_color", stringify({state: "OFF"}), {retain: false, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/group_1", stringify({state: "OFF"}), {retain: false, qos: 0});
mockMQTTPublishAsync.mockClear();
await mockMQTTEvents.message("zigbee2mqtt/bulb_color/set", stringify({state: "ON"}));
await flushPromises();
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(2);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bulb_color", stringify({state: "ON"}), {retain: false, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/group_1", stringify({state: "ON"}), {retain: false, qos: 0});
});
it("Should publish state of device with endpoint name", async () => {
const group = groups.gledopto_group;
mockMQTTPublishAsync.mockClear();
await mockMQTTEvents.message("zigbee2mqtt/gledopto_group/set", stringify({state: "ON"}));
await flushPromises();
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(2);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/GLEDOPTO_2ID", stringify({state_cct: "ON"}), {retain: false, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/gledopto_group", stringify({state: "ON"}), {retain: false, qos: 0});
expect(group.command).toHaveBeenCalledTimes(1);
expect(group.command).toHaveBeenCalledWith("genOnOff", "on", {}, {});
});
it("Should publish state of group when specific state of specific endpoint is changed", async () => {
const group = groups.gledopto_group;
mockMQTTPublishAsync.mockClear();
await mockMQTTEvents.message("zigbee2mqtt/GLEDOPTO_2ID/set", stringify({state_cct: "ON"}));
await flushPromises();
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(2);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/GLEDOPTO_2ID", stringify({state_cct: "ON"}), {retain: false, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/gledopto_group", stringify({state: "ON"}), {retain: false, qos: 0});
expect(group.command).toHaveBeenCalledTimes(0);
});
it("Should publish state change of all members when a group changes its state, filtered", async () => {
const device = devices.bulb_color;
const endpoint = device.getEndpoint(1)!;
const group = groups.group_1;
group.members.push(endpoint);
settings.set(["groups"], {1: {friendly_name: "group_1", retain: false, filtered_attributes: ["brightness"]}});
mockMQTTPublishAsync.mockClear();
await mockMQTTEvents.message("zigbee2mqtt/group_1/set", stringify({state: "ON", brightness: 100}));
await flushPromises();
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(2);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bulb_color", stringify({state: "ON", brightness: 100}), {
retain: false,
qos: 0,
});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/group_1", stringify({state: "ON"}), {retain: false, qos: 0});
});
it("Shouldnt publish group state change when a group is not optimistic", async () => {
const device = devices.bulb_color;
const endpoint = device.getEndpoint(1)!;
const group = groups.group_1;
group.members.push(endpoint);
settings.set(["groups"], {1: {friendly_name: "group_1", optimistic: false, retain: false}});
mockMQTTPublishAsync.mockClear();
const payload = {data: {onOff: 1}, cluster: "genOnOff", device, endpoint, type: "attributeReport", linkquality: 10};
await mockZHEvents.message(payload);
await flushPromises();
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bulb_color", stringify({state: "ON"}), {retain: false, qos: 0});
});
it("Should publish state change of another group with shared device when a group changes its state", async () => {
const device = devices.bulb_color;
const endpoint = device.getEndpoint(1)!;
groups.group_1.members.push(endpoint);
groups.group_2.members.push(endpoint);
mockMQTTPublishAsync.mockClear();
await mockMQTTEvents.message("zigbee2mqtt/group_1/set", stringify({state: "ON"}));
await flushPromises();
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(3);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bulb_color", stringify({state: "ON"}), {retain: false, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/group_1", stringify({state: "ON"}), {retain: false, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/group_2", stringify({state: "ON"}), {retain: false, qos: 0});
});
it("Should not publish state change off if any lights within are still on when changed via device", async () => {
const device_1 = devices.bulb_color;
const device_2 = devices.bulb;
const endpoint_1 = device_1.getEndpoint(1)!;
const endpoint_2 = device_2.getEndpoint(1)!;
const group = groups.group_1;
group.members.push(endpoint_1);
group.members.push(endpoint_2);
await mockMQTTEvents.message("zigbee2mqtt/group_1/set", stringify({state: "ON"}));
await flushPromises();
mockMQTTPublishAsync.mockClear();
await mockMQTTEvents.message("zigbee2mqtt/bulb_color/set", stringify({state: "OFF"}));
await flushPromises();
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bulb_color", stringify({state: "OFF"}), {retain: false, qos: 0});
});
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 = devices.bulb_color;
const device_2 = devices.bulb;
const endpoint_1 = device_1.getEndpoint(1)!;
const endpoint_2 = device_2.getEndpoint(1)!;
const group = groups.group_1;
group.members.push(endpoint_1);
group.members.push(endpoint_2);
settings.set(["groups"], {
1: {friendly_name: "group_1", retain: false, off_state: "last_member_state"},
});
await mockMQTTEvents.message("zigbee2mqtt/group_1/set", stringify({state: "ON"}));
await flushPromises();
mockMQTTPublishAsync.mockClear();
await mockMQTTEvents.message("zigbee2mqtt/bulb_color/set", stringify({state: "OFF"}));
await flushPromises();
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(2);
expect(mockMQTTPublishAsync).toHaveBeenNthCalledWith(1, "zigbee2mqtt/group_1", stringify({state: "OFF"}), {retain: false, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenNthCalledWith(2, "zigbee2mqtt/bulb_color", stringify({state: "OFF"}), {retain: false, qos: 0});
});
it("Should not publish state change off if any lights within with non default-ep are still on when changed via device", async () => {
const device_1 = devices.bulb_color;
const device_2 = devices.QBKG03LM;
const endpoint_1 = device_1.getEndpoint(1)!;
const endpoint_2 = device_2.getEndpoint(2)!;
const group = groups.group_1;
group.members.push(endpoint_1);
group.members.push(endpoint_2);
await mockMQTTEvents.message("zigbee2mqtt/group_1/set", stringify({state: "ON"}));
await flushPromises();
mockMQTTPublishAsync.mockClear();
await mockMQTTEvents.message("zigbee2mqtt/bulb_color/set", stringify({state: "OFF"}));
await flushPromises();
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bulb_color", stringify({state: "OFF"}), {retain: false, qos: 0});
});
it("Should not publish state change off if any lights within are still on when changed via device with non default-ep", async () => {
const device_1 = devices.bulb_color;
const device_2 = devices.QBKG03LM;
const endpoint_1 = device_1.getEndpoint(1)!;
const endpoint_2 = device_2.getEndpoint(2)!;
const endpoint_3 = device_2.getEndpoint(3)!;
endpoint_3.removeFromGroup(groups.ha_discovery_group);
const group = groups.group_1;
group.members.push(endpoint_1);
group.members.push(endpoint_2);
group.members.push(endpoint_3);
await mockMQTTEvents.message("zigbee2mqtt/group_1/set", stringify({state: "ON"}));
await flushPromises();
mockMQTTPublishAsync.mockClear();
await mockMQTTEvents.message("zigbee2mqtt/wall_switch_double/set", stringify({state_left: "OFF"}));
await flushPromises();
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/wall_switch_double", stringify({state_left: "OFF", state_right: "ON"}), {
retain: false,
qos: 0,
});
});
it("Should publish state change off if all lights within turn off with non default-ep", async () => {
const device_1 = devices.bulb_color;
const device_2 = devices.QBKG03LM;
const endpoint_1 = device_1.getEndpoint(1)!;
const endpoint_2 = device_2.getEndpoint(2)!;
const group = groups.group_1;
group.members.push(endpoint_1);
group.members.push(endpoint_2);
settings.set(["groups"], {
1: {friendly_name: "group_1", retain: false},
});
await mockMQTTEvents.message("zigbee2mqtt/group_1/set", stringify({state: "ON"}));
await flushPromises();
mockMQTTPublishAsync.mockClear();
await mockMQTTEvents.message("zigbee2mqtt/bulb_color/set", stringify({state: "OFF"}));
await mockMQTTEvents.message("zigbee2mqtt/wall_switch_double/set", stringify({state_left: "OFF"}));
await flushPromises();
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(3);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bulb_color", stringify({state: "OFF"}), {retain: false, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/wall_switch_double", stringify({state_left: "OFF"}), {retain: false, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/group_1", stringify({state: "OFF"}), {retain: false, qos: 0});
});
it("Should publish state change off if all lights within turn off with non default-ep, but device state does not use them", async () => {
const device_1 = devices.bulb_color;
const device_2 = devices.InovelliVZM31SN;
const endpoint_1 = device_1.getEndpoint(1)!;
const endpoint_2 = device_2.getEndpoint(2)!;
const group = groups.group_1;
group.members.push(endpoint_1);
group.members.push(endpoint_2);
settings.set(["groups"], {
1: {friendly_name: "group_1", retain: false},
});
await mockMQTTEvents.message("zigbee2mqtt/group_1/set", stringify({state: "ON"}));
await flushPromises();
mockMQTTPublishAsync.mockClear();
await mockMQTTEvents.message("zigbee2mqtt/bulb_color/set", stringify({state: "OFF"}));
await mockMQTTEvents.message("zigbee2mqtt/wall_switch_double/set", stringify({state_left: "OFF"}));
await flushPromises();
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(3);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bulb_color", stringify({state: "OFF"}), {retain: false, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/wall_switch_double", stringify({state_left: "OFF"}), {retain: false, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/group_1", stringify({state: "OFF"}), {retain: false, qos: 0});
});
it("Should not publish state change off if any lights within are still on when changed via shared group", async () => {
const device_1 = devices.bulb_color;
const device_2 = devices.bulb;
const endpoint_1 = device_1.getEndpoint(1)!;
const endpoint_2 = device_2.getEndpoint(1)!;
groups.group_1.members.push(endpoint_1);
groups.group_1.members.push(endpoint_2);
groups.group_2.members.push(endpoint_1);
await mockMQTTEvents.message("zigbee2mqtt/group_1/set", stringify({state: "ON"}));
await flushPromises();
mockMQTTPublishAsync.mockClear();
await mockMQTTEvents.message("zigbee2mqtt/group_2/set", stringify({state: "OFF"}));
await flushPromises();
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(2);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/group_2", stringify({state: "OFF"}), {retain: false, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bulb_color", stringify({state: "OFF"}), {retain: false, qos: 0});
});
it("Should publish state change off if all lights within turn off", async () => {
const device_1 = devices.bulb_color;
const device_2 = devices.bulb;
const endpoint_1 = device_1.getEndpoint(1)!;
const endpoint_2 = device_2.getEndpoint(1)!;
const group = groups.group_1;
group.members.push(endpoint_1);
group.members.push(endpoint_2);
settings.set(["groups"], {
1: {friendly_name: "group_1", retain: false},
});
await mockMQTTEvents.message("zigbee2mqtt/group_1/set", stringify({state: "ON"}));
await flushPromises();
mockMQTTPublishAsync.mockClear();
await mockMQTTEvents.message("zigbee2mqtt/bulb_color/set", stringify({state: "OFF"}));
await mockMQTTEvents.message("zigbee2mqtt/bulb/set", stringify({state: "OFF"}));
await flushPromises();
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(3);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bulb_color", stringify({state: "OFF"}), {retain: false, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bulb", stringify({state: "OFF"}), {retain: true, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/group_1", stringify({state: "OFF"}), {retain: false, qos: 0});
});
it("Should only update group state with changed properties", async () => {
const device_1 = devices.bulb_color;
const device_2 = devices.bulb;
const endpoint_1 = device_1.getEndpoint(1)!;
const endpoint_2 = device_2.getEndpoint(1)!;
const group = groups.group_1;
group.members.push(endpoint_1);
group.members.push(endpoint_2);
settings.set(["groups"], {
1: {friendly_name: "group_1", retain: false},
});
mockMQTTPublishAsync.mockClear();
await mockMQTTEvents.message("zigbee2mqtt/bulb_color/set", stringify({state: "OFF", color_temp: 200}));
await mockMQTTEvents.message("zigbee2mqtt/bulb/set", stringify({state: "ON", color_temp: 250}));
await flushPromises();
mockMQTTPublishAsync.mockClear();
await mockMQTTEvents.message("zigbee2mqtt/group_1/set", stringify({color_temp: 300}));
await flushPromises();
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(3);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bulb_color",
stringify({color_mode: "color_temp", color_temp: 300, state: "OFF"}),
{retain: false, qos: 0},
);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bulb", stringify({color_mode: "color_temp", color_temp: 300, state: "ON"}), {
retain: true,
qos: 0,
});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/group_1",
stringify({color_mode: "color_temp", color_temp: 300, state: "ON"}),
{retain: false, qos: 0},
);
});
it("Should publish state change off even when missing current state", async () => {
const device_1 = devices.bulb_color;
const device_2 = devices.bulb;
const endpoint_1 = device_1.getEndpoint(1)!;
const endpoint_2 = device_2.getEndpoint(1)!;
const group = groups.group_1;
group.members.push(endpoint_1);
group.members.push(endpoint_2);
settings.set(["groups"], {
1: {friendly_name: "group_1", retain: false},
});
await mockMQTTEvents.message("zigbee2mqtt/group_1/set", stringify({state: "ON"}));
await flushPromises();
mockMQTTPublishAsync.mockClear();
controller.state.clear();
await mockMQTTEvents.message("zigbee2mqtt/bulb_color/set", stringify({state: "OFF"}));
await flushPromises();
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(2);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bulb_color", stringify({state: "OFF"}), {retain: false, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/group_1", stringify({state: "OFF"}), {retain: false, qos: 0});
});
it("Add to group via MQTT", async () => {
const device = devices.bulb_color;
const endpoint = device.getEndpoint(1);
const group = groups.group_1;
expect(group.members.length).toBe(0);
mockMQTTPublishAsync.mockClear();
mockMQTTEvents.message(
"zigbee2mqtt/bridge/request/group/members/add",
stringify({transaction: "123", group: "group_1", device: "bulb_color"}),
);
await flushPromises();
expect(group.members).toStrictEqual([endpoint]);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/groups", expect.any(String), expect.any(Object));
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/group/members/add",
stringify({data: {device: "bulb_color", endpoint: "default", group: "group_1"}, transaction: "123", status: "ok"}),
{},
);
});
it("Add to group via MQTT fails", async () => {
const device = devices.bulb_color;
const endpoint = device.getEndpoint(1)!;
const group = groups.group_1;
expect(group.members.length).toBe(0);
endpoint.addToGroup.mockImplementationOnce(() => {
throw new Error("timeout");
});
await flushPromises();
mockMQTTPublishAsync.mockClear();
mockMQTTEvents.message("zigbee2mqtt/bridge/request/group/members/add", stringify({group: "group_1", device: "bulb_color"}));
await flushPromises();
expect(group.members).toStrictEqual([]);
expect(mockMQTTPublishAsync).not.toHaveBeenCalledWith("zigbee2mqtt/bridge/groups", expect.any(String), expect.any(Object));
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/group/members/add",
stringify({data: {}, status: "error", error: "Failed to add to group (timeout)"}),
{},
);
});
it("Add to group with slashes via MQTT", async () => {
const device = devices.bulb_color;
const endpoint = device.getEndpoint(1);
const group = groups["group/with/slashes"];
settings.set(["groups"], {99: {friendly_name: "group/with/slashes", retain: false}});
expect(group.members.length).toBe(0);
mockMQTTPublishAsync.mockClear();
mockMQTTEvents.message("zigbee2mqtt/bridge/request/group/members/add", stringify({group: "group/with/slashes", device: "bulb_color"}));
await flushPromises();
expect(group.members).toStrictEqual([endpoint]);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/groups", expect.any(String), expect.any(Object));
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/group/members/add",
stringify({data: {device: "bulb_color", endpoint: "default", group: "group/with/slashes"}, status: "ok"}),
{},
);
});
it("Add to group via MQTT with postfix", async () => {
const device = devices.QBKG03LM;
const endpoint = device.getEndpoint(3)!;
const group = groups.group_1;
expect(group.members.length).toBe(0);
mockMQTTPublishAsync.mockClear();
mockMQTTEvents.message(
"zigbee2mqtt/bridge/request/group/members/add",
stringify({group: "group_1", device: "wall_switch_double", endpoint: "right"}),
);
await flushPromises();
expect(group.members).toStrictEqual([endpoint]);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/groups", expect.any(String), expect.any(Object));
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/group/members/add",
stringify({data: {device: "wall_switch_double", endpoint: "right", group: "group_1"}, status: "ok"}),
{},
);
});
it("Add to group via MQTT with postfix shouldnt add it twice", async () => {
const device = devices.QBKG03LM;
const endpoint = device.getEndpoint(3)!;
const group = groups.group_1;
expect(group.members.length).toBe(0);
mockMQTTPublishAsync.mockClear();
mockMQTTEvents.message(
"zigbee2mqtt/bridge/request/group/members/add",
stringify({group: "group_1", device: "wall_switch_double", endpoint: "right"}),
);
await flushPromises();
mockMQTTEvents.message(
"zigbee2mqtt/bridge/request/group/members/add",
stringify({group: "group_1", device: "0x0017880104e45542", endpoint: "3"}),
);
await flushPromises();
expect(group.members).toStrictEqual([endpoint]);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/groups", expect.any(String), expect.any(Object));
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/group/members/add",
stringify({data: {device: "wall_switch_double", endpoint: "right", group: "group_1"}, status: "ok"}),
{},
);
});
it("Remove from group via MQTT", async () => {
const device = devices.bulb_color;
const endpoint = device.getEndpoint(1)!;
const group = groups.group_1;
group.members.push(endpoint);
mockMQTTPublishAsync.mockClear();
mockMQTTEvents.message("zigbee2mqtt/bridge/request/group/members/remove", stringify({group: "group_1", device: "bulb_color"}));
await flushPromises();
expect(group.members).toStrictEqual([]);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/groups", expect.any(String), expect.any(Object));
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/group/members/remove",
stringify({data: {device: "bulb_color", endpoint: "default", group: "group_1"}, status: "ok"}),
{},
);
});
it("Remove from group via MQTT fails", async () => {
const device = devices.bulb_color;
const endpoint = device.getEndpoint(1)!;
const group = groups.group_1;
group.members.push(endpoint);
endpoint.removeFromGroup.mockImplementationOnce(() => {
throw new Error("timeout");
});
mockMQTTPublishAsync.mockClear();
mockMQTTEvents.message("zigbee2mqtt/bridge/request/group/members/remove", stringify({group: "group_1", device: "bulb_color"}));
await flushPromises();
expect(group.members.length).toStrictEqual(1);
expect(mockMQTTPublishAsync).not.toHaveBeenCalledWith("zigbee2mqtt/bridge/groups", expect.any(String), expect.any(Object));
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/group/members/remove",
stringify({data: {}, status: "error", error: "Failed to remove from group (timeout)"}),
{},
);
});
it("Remove from group via MQTT keeping device reporting", async () => {
const device = devices.bulb_color;
const endpoint = device.getEndpoint(1)!;
const group = groups.group_1;
group.members.push(endpoint);
mockMQTTPublishAsync.mockClear();
mockMQTTEvents.message(
"zigbee2mqtt/bridge/request/group/members/remove",
stringify({group: "group_1", device: "bulb_color", skip_disable_reporting: true}),
);
await flushPromises();
expect(group.members).toStrictEqual([]);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/groups", expect.any(String), expect.any(Object));
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/group/members/remove",
stringify({data: {device: "bulb_color", endpoint: "default", group: "group_1"}, status: "ok"}),
{},
);
});
it("Remove from group via MQTT with postfix variant 1", async () => {
const device = devices.QBKG03LM;
const endpoint = device.getEndpoint(3)!;
const group = groups.group_1;
group.members.push(endpoint);
mockMQTTPublishAsync.mockClear();
mockMQTTEvents.message(
"zigbee2mqtt/bridge/request/group/members/remove",
stringify({group: "group_1", device: "0x0017880104e45542", endpoint: "3"}),
);
await flushPromises();
expect(group.members).toStrictEqual([]);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/groups", expect.any(String), expect.any(Object));
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/group/members/remove",
stringify({data: {device: "0x0017880104e45542", endpoint: "3", group: "group_1"}, status: "ok"}),
{},
);
});
it("Remove from group via MQTT with postfix variant 2", async () => {
const device = devices.QBKG03LM;
const endpoint = device.getEndpoint(3)!;
const group = groups.group_1;
group.members.push(endpoint);
mockMQTTPublishAsync.mockClear();
mockMQTTEvents.message(
"zigbee2mqtt/bridge/request/group/members/remove",
stringify({group: "group_1", device: "wall_switch_double", endpoint: "3"}),
);
await flushPromises();
expect(group.members).toStrictEqual([]);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/groups", expect.any(String), expect.any(Object));
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/group/members/remove",
stringify({data: {device: "wall_switch_double", endpoint: "3", group: "group_1"}, status: "ok"}),
{},
);
});
it("Remove from group via MQTT with postfix variant 3", async () => {
const device = devices.QBKG03LM;
const endpoint = device.getEndpoint(3)!;
const group = groups.group_1;
group.members.push(endpoint);
mockMQTTPublishAsync.mockClear();
mockMQTTEvents.message(
"zigbee2mqtt/bridge/request/group/members/remove",
stringify({group: "group_1", device: "0x0017880104e45542", endpoint: "right"}),
);
await flushPromises();
expect(group.members).toStrictEqual([]);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/groups", expect.any(String), expect.any(Object));
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/group/members/remove",
stringify({data: {device: "0x0017880104e45542", endpoint: "right", group: "group_1"}, status: "ok"}),
{},
);
});
it("Remove from group all", async () => {
const group = groups.group_1;
groups.group_1.members.push(devices.QBKG03LM.endpoints[2]);
mockMQTTPublishAsync.mockClear();
mockMQTTEvents.message("zigbee2mqtt/bridge/request/group/members/remove_all", stringify({device: "0x0017880104e45542", endpoint: "right"}));
await flushPromises();
expect(group.members).toStrictEqual([]);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/groups", expect.any(String), expect.any(Object));
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/group/members/remove_all",
stringify({data: {device: "0x0017880104e45542", endpoint: "right"}, status: "ok"}),
{},
);
});
it("Error when adding to non-existing group", async () => {
mockLogger.error.mockClear();
mockMQTTPublishAsync.mockClear();
mockMQTTEvents.message("zigbee2mqtt/bridge/request/group/members/remove", stringify({group: "group_1_not_existing", device: "bulb_color"}));
await flushPromises();
expect(mockMQTTPublishAsync).not.toHaveBeenCalledWith("zigbee2mqtt/bridge/groups", expect.any(String), expect.any(Object));
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/group/members/remove",
stringify({data: {}, status: "error", error: "Group 'group_1_not_existing' does not exist"}),
{},
);
});
it("Error when adding a non-existing device", async () => {
mockLogger.error.mockClear();
mockMQTTPublishAsync.mockClear();
mockMQTTEvents.message("zigbee2mqtt/bridge/request/group/members/add", stringify({group: "group_1", device: "bulb_color_not_existing"}));
await flushPromises();
expect(mockMQTTPublishAsync).not.toHaveBeenCalledWith("zigbee2mqtt/bridge/groups", expect.any(String), expect.any(Object));
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/group/members/add",
stringify({data: {}, status: "error", error: "Device 'bulb_color_not_existing' does not exist"}),
{},
);
});
it("Error when adding a non-existing endpoint", async () => {
mockLogger.error.mockClear();
mockMQTTPublishAsync.mockClear();
mockMQTTEvents.message(
"zigbee2mqtt/bridge/request/group/members/add",
stringify({group: "group_1", device: "bulb_color", endpoint: "not_existing_endpoint"}),
);
await flushPromises();
expect(mockMQTTPublishAsync).not.toHaveBeenCalledWith("zigbee2mqtt/bridge/groups", expect.any(String), expect.any(Object));
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/group/members/add",
stringify({data: {}, status: "error", error: "Device 'bulb_color' does not have endpoint 'not_existing_endpoint'"}),
{},
);
});
it("Error when invalid payload", async () => {
mockLogger.error.mockClear();
mockMQTTPublishAsync.mockClear();
mockMQTTEvents.message("zigbee2mqtt/bridge/request/group/members/add", stringify({group: "group_1", devicez: "bulb_color"}));
await flushPromises();
expect(mockMQTTPublishAsync).not.toHaveBeenCalledWith("zigbee2mqtt/bridge/groups", expect.any(String), expect.any(Object));
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/group/members/add",
stringify({data: {}, status: "error", error: "Invalid payload"}),
{},
);
});
it("Error when add/remove with invalid payload", async () => {
mockLogger.error.mockClear();
mockMQTTPublishAsync.mockClear();
mockMQTTEvents.message("zigbee2mqtt/bridge/request/group/members/add", stringify({groupz: "group_1", device: "bulb_color"}));
await flushPromises();
expect(mockMQTTPublishAsync).not.toHaveBeenCalledWith("zigbee2mqtt/bridge/groups", expect.any(String), expect.any(Object));
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/group/members/add",
stringify({data: {}, status: "error", error: "Invalid payload"}),
{},
);
});
it("Should only include relevant properties when publishing member states", async () => {
const bulbColor = devices.bulb_color;
const bulbColorTemp = devices.bulb;
const group = groups.group_1;
group.members.push(bulbColor.getEndpoint(1)!);
group.members.push(bulbColorTemp.getEndpoint(1)!);
mockMQTTPublishAsync.mockClear();
await mockMQTTEvents.message("zigbee2mqtt/group_1/set", stringify({color_temp: 50}));
await flushPromises();
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(3);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bulb_color", stringify({color_mode: "color_temp", color_temp: 50}), {
retain: false,
qos: 0,
});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/group_1", stringify({color_mode: "color_temp", color_temp: 50}), {
retain: false,
qos: 0,
});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bulb", stringify({color_mode: "color_temp", color_temp: 50}), {
retain: true,
qos: 0,
});
mockMQTTPublishAsync.mockClear();
await mockMQTTEvents.message("zigbee2mqtt/group_1/set", stringify({color: {x: 0.5, y: 0.3}}));
await flushPromises();
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(3);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bulb_color",
stringify({color: {x: 0.5, y: 0.3}, color_mode: "xy", color_temp: 548}),
{retain: false, qos: 0},
);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/group_1",
stringify({color: {x: 0.5, y: 0.3}, color_mode: "xy", color_temp: 548}),
{retain: false, qos: 0},
);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bulb", stringify({color_mode: "color_temp", color_temp: 548}), {
retain: true,
qos: 0,
});
});
});