diff --git a/lib/controller.ts b/lib/controller.ts index deb216015..bd0066f61 100644 --- a/lib/controller.ts +++ b/lib/controller.ts @@ -465,7 +465,10 @@ export class Controller { let message: string | undefined; // Special cases - if (key === "color" && utils.objectHasProperties(subPayload, ["r", "g", "b"])) { + // `objectHasProperties` indexes its argument, so it has to be given an object. + // The null check three lines below is too late: `color` is nullable like any + // other attribute, and a null one reaches here before that branch runs. + if (key === "color" && subPayload != null && utils.objectHasProperties(subPayload, ["r", "g", "b"])) { subPayload = [subPayload.r, subPayload.g, subPayload.b]; } diff --git a/test/controller.test.ts b/test/controller.test.ts index de43d0e24..318784ebe 100644 --- a/test/controller.test.ts +++ b/test/controller.test.ts @@ -1061,6 +1061,17 @@ describe("Controller", () => { ); }); + it("Publish entity state attribute output with a null color", async () => { + await controller.start(); + settings.set(["advanced", "output"], "attribute_and_json"); + mockMQTTPublishAsync.mockClear(); + const device = getZ2MDevice("bulb"); + await controller.publishEntityState(device, {state: "ON", color: null}); + await flushPromises(); + expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bulb/state", "ON", {qos: 0, retain: true}); + expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bulb/color", "", {qos: 0, retain: true}); + }); + it("Publish entity state attribute_json output filtered", async () => { await controller.start(); settings.set(["advanced", "output"], "attribute_and_json");