From 0018ca8f809708d22221c9d557b3208ba2e5c057 Mon Sep 17 00:00:00 2001 From: Ilya Kirov Date: Sat, 27 Jun 2026 22:38:16 +0300 Subject: [PATCH] fix: Home Assistant: support cooling setpoint in climate discovery (#32411) Co-authored-by: Koen Kanters --- lib/extension/homeassistant.ts | 29 ++++++++++++++----------- test/extensions/homeassistant.test.ts | 31 +++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 13 deletions(-) diff --git a/lib/extension/homeassistant.ts b/lib/extension/homeassistant.ts index 193b95adb..4c498bef6 100644 --- a/lib/extension/homeassistant.ts +++ b/lib/extension/homeassistant.ts @@ -643,15 +643,19 @@ export class HomeAssistant extends Extension { break; } case "climate": { - const setpointProperties = ["occupied_heating_setpoint", "current_heating_setpoint"]; - const setpoint = (firstExpose as zhc.Climate).features.filter(isNumericExpose).find((f) => setpointProperties.includes(f.name)); + const heatingSetpoint = (firstExpose as zhc.Climate).features + .filter(isNumericExpose) + .find((f) => ["occupied_heating_setpoint", "current_heating_setpoint"].includes(f.name)); + const coolingSetpoint = (firstExpose as zhc.Climate).features + .filter(isNumericExpose) + .find((f) => f.name === "occupied_cooling_setpoint"); + const primarySetpoint = heatingSetpoint ?? coolingSetpoint; assert( - setpoint && setpoint.value_min !== undefined && setpoint.value_max !== undefined, + primarySetpoint && primarySetpoint.value_min !== undefined && primarySetpoint.value_max !== undefined, "No setpoint found or it is missing value_min/max", ); const temperature = (firstExpose as zhc.Climate).features.find((f) => f.name === "local_temperature"); assert(temperature, "No temperature found"); - const discoveryEntry: DiscoveryEntry = { type: "climate", object_id: endpointName ? `climate_${endpointName}` : "climate", @@ -662,9 +666,9 @@ export class HomeAssistant extends Extension { state_topic: false, temperature_unit: "C", // Setpoint - temp_step: setpoint.value_step, - min_temp: setpoint.value_min.toString(), - max_temp: setpoint.value_max.toString(), + temp_step: primarySetpoint.value_step, + min_temp: primarySetpoint.value_min.toString(), + max_temp: primarySetpoint.value_max.toString(), // Temperature current_temperature_topic: true, current_temperature_template: `{{ value_json["${temperature.property}"] }}`, @@ -693,17 +697,16 @@ export class HomeAssistant extends Extension { discoveryEntry.discovery_payload.action_template = `{% set values = {None:None,'idle':'idle','heat':'heating','cool':'cooling','fan_only':'fan'} %}{{ values[value_json["${state.property}"]] }}`; } - const coolingSetpoint = (firstExpose as zhc.Climate).features.find((f) => f.name === "occupied_cooling_setpoint"); - if (coolingSetpoint) { - discoveryEntry.discovery_payload.temperature_low_command_topic = setpoint.name; - discoveryEntry.discovery_payload.temperature_low_state_template = `{{ value_json["${setpoint.property}"] }}`; + if (heatingSetpoint && coolingSetpoint) { + discoveryEntry.discovery_payload.temperature_low_command_topic = heatingSetpoint.name; + discoveryEntry.discovery_payload.temperature_low_state_template = `{{ value_json["${heatingSetpoint.property}"] }}`; discoveryEntry.discovery_payload.temperature_low_state_topic = true; discoveryEntry.discovery_payload.temperature_high_command_topic = coolingSetpoint.name; discoveryEntry.discovery_payload.temperature_high_state_template = `{{ value_json["${coolingSetpoint.property}"] }}`; discoveryEntry.discovery_payload.temperature_high_state_topic = true; } else { - discoveryEntry.discovery_payload.temperature_command_topic = setpoint.name; - discoveryEntry.discovery_payload.temperature_state_template = `{{ value_json["${setpoint.property}"] }}`; + discoveryEntry.discovery_payload.temperature_command_topic = primarySetpoint.name; + discoveryEntry.discovery_payload.temperature_state_template = `{{ value_json["${primarySetpoint.property}"] }}`; discoveryEntry.discovery_payload.temperature_state_topic = true; } diff --git a/test/extensions/homeassistant.test.ts b/test/extensions/homeassistant.test.ts index 9e0d09bc5..172f466c2 100644 --- a/test/extensions/homeassistant.test.ts +++ b/test/extensions/homeassistant.test.ts @@ -1450,6 +1450,37 @@ describe("Extension: HomeAssistant", () => { }); }); + it("Should discover climate with cooling-only setpoint", () => { + const climateExpose = new zhc.Climate() + .withSetpoint("occupied_cooling_setpoint", 16, 32, 0.5) + .withLocalTemperature() + .withSystemMode(["off", "cool", "auto"]); + const device = { + definition: {}, + isDevice: (): boolean => true, + isGroup: (): boolean => false, + endpoint: () => undefined, + options: {}, + exposes: (): zhc.Expose[] => [climateExpose], + zh: {endpoints: []}, + } as Device; + + // @ts-expect-error private + const configs = extension.getConfigs(device); + const climate = configs.find((c) => c.type === "climate"); + expect(climate).toBeDefined(); + expect(climate!.discovery_payload).toMatchObject({ + temperature_command_topic: "occupied_cooling_setpoint", + temperature_state_template: '{{ value_json["occupied_cooling_setpoint"] }}', + temperature_state_topic: true, + min_temp: "16", + max_temp: "32", + temp_step: 0.5, + }); + expect(climate!.discovery_payload).not.toHaveProperty("temperature_low_command_topic"); + expect(climate!.discovery_payload).not.toHaveProperty("temperature_high_command_topic"); + }); + it("Should discover devices with cover_position", () => { let payload;