Skip to content

Commit 7479a60

Browse files
committed
Make Bosch RM230Z HA climate modes configurable
1 parent d0338db commit 7479a60

3 files changed

Lines changed: 71 additions & 10 deletions

File tree

src/devices/bosch.ts

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -757,28 +757,36 @@ export const definitions: DefinitionWithExtend[] = [
757757
model: "BTH-RM230Z",
758758
vendor: "Bosch",
759759
description: "Room thermostat II 230V",
760+
options: [exposes.options.homeassistant_climate_modes()],
760761
meta: {
761-
overrideHaDiscoveryPayload: (payload) => {
762+
overrideHaDiscoveryPayload: (payload, options) => {
762763
if (payload.mode_command_topic?.endsWith("/system_mode")) {
764+
const climateModes = options?.homeassistant_climate_modes;
765+
const activeModes = climateModes === "cool" ? ["cool"] : climateModes === "heat_cool" ? ["heat", "cool"] : ["heat"];
766+
const fallbackMode = activeModes[0];
767+
const activeModesTemplate = `[${activeModes.map((mode) => `'${mode}'`).join(",")}]`;
768+
763769
payload.mode_command_topic = payload.mode_command_topic.substring(0, payload.mode_command_topic.lastIndexOf("/system_mode"));
764770
payload.mode_command_template =
765-
"{% set values = " +
766-
`{ 'auto':'schedule','heat':'manual','cool':'manual','off':'pause'} %}` +
767-
`{% if value == "heat" or value == "cool" %}` +
771+
`{% set active_modes = ${activeModesTemplate} %}` +
772+
`{% set values = {'auto':'schedule','off':'pause'} %}` +
773+
"{% if value in active_modes %}" +
768774
`{"operating_mode": "manual", "system_mode": "{{ value }}"}` +
769775
"{% else %}" +
770776
`{"operating_mode": "{{ values[value] if value in values.keys() else 'pause' }}"}` +
771777
"{% endif %}";
772778
payload.mode_state_template =
773-
"{% set values = " +
774-
`{'schedule':'auto','manual':'heat','pause':'off'} %}` +
779+
`{% set active_modes = ${activeModesTemplate} %}` +
780+
`{% set fallback_mode = '${fallbackMode}' %}` +
781+
"{% set values = {'schedule':'auto','pause':'off'} %}" +
775782
"{% set value = value_json.operating_mode %}" +
776-
`{% if value == "manual" %}` +
777-
"{{ value_json.system_mode }}" +
783+
"{% set mode = value_json.system_mode %}" +
784+
"{% if value == 'manual' %}" +
785+
"{{ mode if mode in active_modes else fallback_mode }}" +
778786
"{% else %}" +
779787
`{{ values[value] if value in values.keys() else 'off' }}` +
780788
"{% endif %}";
781-
payload.modes = ["off", "heat", "cool", "auto"];
789+
payload.modes = ["off", ...activeModes, "auto"];
782790
}
783791
},
784792
},

src/lib/exposes.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -850,6 +850,10 @@ export const options = {
850850
new Enum("thermostat_unit", access.SET, ["celsius", "fahrenheit"]).withDescription(
851851
"Controls the temperature unit of the thermostat (default celsius).",
852852
),
853+
homeassistant_climate_modes: () =>
854+
new Enum("homeassistant_climate_modes", access.SET, ["heat", "cool", "heat_cool"])
855+
.withLabel("Home Assistant climate modes")
856+
.withDescription("Controls which modes are exposed in Home Assistant climate discovery (default heat)."),
853857
expose_pin: () =>
854858
new Binary("expose_pin", access.SET, true, false)
855859
.withLabel("Expose PIN")

test/bosch.test.ts

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import {describe, expect, it, vi} from "vitest";
22
import {findByDevice} from "../src/index";
33
import {boschThermostatExtend} from "../src/lib/bosch";
44
import {repInterval} from "../src/lib/constants";
5-
import type {DefinitionExposesFunction} from "../src/lib/types";
5+
import type {DefinitionExposesFunction, KeyValueAny} from "../src/lib/types";
66
import {mockDevice, reportingItem} from "./utils";
77

88
describe("Bosch thermostats", () => {
@@ -76,3 +76,52 @@ describe("Bosch thermostat relayState extension", () => {
7676
expect(device.getEndpoint(1).configureReporting).not.toHaveBeenCalled();
7777
});
7878
});
79+
80+
describe("Bosch Room thermostat II 230V Home Assistant climate modes", () => {
81+
const getDefinition = () =>
82+
findByDevice(
83+
mockDevice({
84+
modelID: "RBSH-RTH0-ZB-EU",
85+
manufacturerName: "BOSCH",
86+
endpoints: [{ID: 1, inputClusters: ["hvacThermostat"]}],
87+
}),
88+
);
89+
90+
const buildPayload = async (options = {}) => {
91+
const definition = await getDefinition();
92+
const payload: KeyValueAny = {
93+
mode_command_topic: "zigbee2mqtt/bad_thermostat/set/system_mode",
94+
};
95+
96+
definition.meta?.overrideHaDiscoveryPayload?.(payload, options);
97+
98+
return {definition, payload};
99+
};
100+
101+
it("defaults to heat-only Home Assistant climate discovery", async () => {
102+
const {definition, payload} = await buildPayload();
103+
const option = definition.options?.find((option) => option.name === "homeassistant_climate_modes");
104+
105+
expect(option?.label).toBe("Home Assistant climate modes");
106+
expect(payload.mode_command_topic).toBe("zigbee2mqtt/bad_thermostat/set");
107+
expect(payload.mode_command_template).toContain("{% set active_modes = ['heat'] %}");
108+
expect(payload.mode_state_template).toContain("{% set fallback_mode = 'heat' %}");
109+
expect(payload.modes).toStrictEqual(["off", "heat", "auto"]);
110+
});
111+
112+
it("can expose cool-only Home Assistant climate discovery", async () => {
113+
const {payload} = await buildPayload({homeassistant_climate_modes: "cool"});
114+
115+
expect(payload.mode_command_template).toContain("{% set active_modes = ['cool'] %}");
116+
expect(payload.mode_state_template).toContain("{% set fallback_mode = 'cool' %}");
117+
expect(payload.modes).toStrictEqual(["off", "cool", "auto"]);
118+
});
119+
120+
it("can expose heat and cool Home Assistant climate discovery", async () => {
121+
const {payload} = await buildPayload({homeassistant_climate_modes: "heat_cool"});
122+
123+
expect(payload.mode_command_template).toContain("{% set active_modes = ['heat','cool'] %}");
124+
expect(payload.mode_state_template).toContain("{% set fallback_mode = 'heat' %}");
125+
expect(payload.modes).toStrictEqual(["off", "heat", "cool", "auto"]);
126+
});
127+
});

0 commit comments

Comments
 (0)