Skip to content

Commit 805a696

Browse files
authored
fix: Bosch BTH-RM: Expose all system_mode for compatibility (#10709)
1 parent bf986aa commit 805a696

File tree

2 files changed

+35
-203
lines changed

2 files changed

+35
-203
lines changed

src/devices/bosch.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -701,6 +701,24 @@ export const definitions: DefinitionWithExtend[] = [
701701
model: "BTH-RA",
702702
vendor: "Bosch",
703703
description: "Radiator thermostat II",
704+
meta: {
705+
overrideHaDiscoveryPayload: (payload) => {
706+
// Override climate discovery
707+
// https://github.com/Koenkk/zigbee2mqtt/pull/23075#issue-2355829475
708+
if (payload.mode_command_topic?.endsWith("/system_mode")) {
709+
payload.mode_command_topic = payload.mode_command_topic.substring(0, payload.mode_command_topic.lastIndexOf("/system_mode"));
710+
payload.mode_command_template =
711+
"{% set values = " +
712+
`{ 'auto':'schedule','heat':'manual','off':'pause'} %}` +
713+
`{"operating_mode": "{{ values[value] if value in values.keys() else 'pause' }}"}`;
714+
payload.mode_state_template =
715+
"{% set values = " +
716+
`{'schedule':'auto','manual':'heat','pause':'off'} %}` +
717+
`{% set value = value_json.operating_mode %}{{ values[value] if value in values.keys() else 'off' }}`;
718+
payload.modes = ["off", "heat", "auto"];
719+
}
720+
},
721+
},
704722
extend: [
705723
boschThermostatExtend.customThermostatCluster(),
706724
boschThermostatExtend.customUserInterfaceCfgCluster(),
@@ -730,7 +748,6 @@ export const definitions: DefinitionWithExtend[] = [
730748
boschGeneralExtend.handleZclVersionReadRequest(),
731749
boschThermostatExtend.customThermostatCluster(),
732750
boschThermostatExtend.customUserInterfaceCfgCluster(),
733-
boschThermostatExtend.customSystemMode(),
734751
boschThermostatExtend.operatingMode({enableReporting: true}),
735752
boschThermostatExtend.rmThermostat(),
736753
boschThermostatExtend.setpointChangeSource({enableReporting: true}),
@@ -758,7 +775,6 @@ export const definitions: DefinitionWithExtend[] = [
758775
boschThermostatExtend.customThermostatCluster(),
759776
boschThermostatExtend.customUserInterfaceCfgCluster(),
760777
boschThermostatExtend.relayState(),
761-
boschThermostatExtend.customSystemMode(),
762778
boschThermostatExtend.operatingMode({enableReporting: true}),
763779
boschThermostatExtend.rmThermostat(),
764780
boschThermostatExtend.setpointChangeSource({enableReporting: true}),

src/lib/bosch.ts

Lines changed: 17 additions & 201 deletions
Original file line numberDiff line numberDiff line change
@@ -11,20 +11,7 @@ import {logger} from "./logger";
1111
import type {ElectricityMeterArgs} from "./modernExtend";
1212
import {payload} from "./reporting";
1313
import * as globalStore from "./store";
14-
import type {
15-
Configure,
16-
DefinitionExposesFunction,
17-
DefinitionMeta,
18-
DummyDevice,
19-
Expose,
20-
Fz,
21-
KeyValue,
22-
KeyValueAny,
23-
ModernExtend,
24-
OnEvent,
25-
Tz,
26-
Zh,
27-
} from "./types";
14+
import type {Configure, DefinitionExposesFunction, DummyDevice, Expose, Fz, KeyValue, KeyValueAny, ModernExtend, OnEvent, Tz, Zh} from "./types";
2815
import * as utils from "./utils";
2916
import {sleep, toNumber} from "./utils";
3017

@@ -3160,6 +3147,7 @@ const boschThermostatLookup = {
31603147
systemModes: {
31613148
heat: 0x04,
31623149
cool: 0x03,
3150+
off: 0x00,
31633151
},
31643152
raRunningStates: <("idle" | "heat" | "cool" | "fan_only")[]>["idle", "heat"],
31653153
heaterType: {
@@ -3255,6 +3243,16 @@ export const boschThermostatExtend = {
32553243
entityCategory: "config",
32563244
}),
32573245
humidity: () => m.humidity({reporting: false}),
3246+
operatingMode: (args?: {enableReporting: boolean}) =>
3247+
m.enumLookup<"hvacThermostat", BoschThermostatCluster>({
3248+
name: "operating_mode",
3249+
cluster: "hvacThermostat",
3250+
attribute: "operatingMode",
3251+
description: "Bosch-specific operating mode",
3252+
lookup: {schedule: 0x00, manual: 0x01, pause: 0x05},
3253+
reporting: args?.enableReporting ? {min: "MIN", max: "MAX", change: null} : false,
3254+
entityCategory: "config",
3255+
}),
32583256
windowOpenMode: (args?: {enableReporting: boolean}) =>
32593257
m.binary<"hvacThermostat", BoschThermostatCluster>({
32603258
name: "window_open_mode",
@@ -3389,7 +3387,7 @@ export const boschThermostatExtend = {
33893387
rmThermostat: (): ModernExtend => {
33903388
const thermostat = m.thermostat({
33913389
localTemperature: {
3392-
configure: {reporting: false},
3390+
configure: {reporting: {min: 30, max: 900, change: 10}},
33933391
},
33943392
localTemperatureCalibration: {
33953393
values: {min: -5, max: 5, step: 0.1},
@@ -3403,121 +3401,25 @@ export const boschThermostatExtend = {
34033401
configure: {reporting: {min: "10_SECONDS", max: "MAX", change: 50}},
34043402
},
34053403
systemMode: {
3406-
values: ["heat", "cool"],
3407-
toZigbee: {skip: true},
3408-
configure: {skip: true},
3404+
values: ["off", "heat", "cool"],
3405+
configure: {reporting: {min: "MIN", max: "MAX", change: null}},
34093406
},
34103407
runningState: {
34113408
values: ["idle", "heat", "cool"],
34123409
configure: {reporting: {min: "MIN", max: "MAX", change: null}},
34133410
},
34143411
});
34153412

3416-
const expose: DefinitionExposesFunction = (device: Zh.Device | DummyDevice, options: KeyValue) => {
3417-
const returnedThermostat = <Expose[]>thermostat.exposes;
3418-
3419-
if (utils.isDummyDevice(device)) {
3420-
return returnedThermostat;
3421-
}
3422-
3423-
let currentSystemMode: string;
3424-
try {
3425-
currentSystemMode = utils.getFromLookupByValue(
3426-
device.getEndpoint(1).getClusterAttributeValue("hvacThermostat", "systemMode"),
3427-
boschThermostatLookup.systemModes,
3428-
);
3429-
} catch {
3430-
currentSystemMode = "heat";
3431-
}
3432-
3433-
// The thermostat is a singleton, thus the values must be set
3434-
// manually as filtering will lead to an array without
3435-
// heat/cool in them after two systemMode changes.
3436-
returnedThermostat[0].features.forEach((exposedAttribute, index, array) => {
3437-
if (exposedAttribute.type === "enum") {
3438-
if (exposedAttribute.name === "system_mode") {
3439-
exposedAttribute.label = "Active system mode";
3440-
exposedAttribute.description =
3441-
"Currently used system mode by the thermostat. This field is primarily " +
3442-
"used to configure the thermostat in Home Assistant correctly.";
3443-
exposedAttribute.values = [currentSystemMode];
3444-
exposedAttribute.access = ea.STATE;
3445-
}
3446-
3447-
if (exposedAttribute.name === "running_state") {
3448-
exposedAttribute.values = ["idle", currentSystemMode];
3449-
}
3450-
}
3451-
});
3452-
return returnedThermostat;
3453-
};
3413+
const exposes: (Expose | DefinitionExposesFunction)[] = thermostat.exposes;
34543414

34553415
return {
3456-
exposes: [expose],
3416+
exposes: exposes,
34573417
fromZigbee: thermostat.fromZigbee,
34583418
toZigbee: thermostat.toZigbee,
34593419
configure: thermostat.configure,
34603420
isModernExtend: true,
34613421
};
34623422
},
3463-
customSystemMode: (): ModernExtend => {
3464-
const exposes: Expose[] = [
3465-
e
3466-
.enum("custom_system_mode", ea.ALL, Object.keys(boschThermostatLookup.systemModes))
3467-
.withLabel("Available system modes")
3468-
.withDescription("Select if the thermostat is connected to a heating or a cooling device")
3469-
.withCategory("config"),
3470-
];
3471-
3472-
const fromZigbee = [
3473-
{
3474-
cluster: "hvacThermostat",
3475-
type: ["attributeReport", "readResponse"],
3476-
convert: (model, msg, publish, options, meta) => {
3477-
const result: KeyValue = {};
3478-
const data = msg.data;
3479-
3480-
if (data.systemMode !== undefined) {
3481-
result.custom_system_mode = utils.getFromLookupByValue(data.systemMode, boschThermostatLookup.systemModes);
3482-
meta.deviceExposesChanged();
3483-
}
3484-
3485-
return result;
3486-
},
3487-
} satisfies Fz.Converter<"hvacThermostat", undefined, ["attributeReport", "readResponse"]>,
3488-
];
3489-
3490-
const toZigbee: Tz.Converter[] = [
3491-
{
3492-
key: ["custom_system_mode"],
3493-
convertSet: async (entity, key, value, meta) => {
3494-
await entity.write("hvacThermostat", {
3495-
systemMode: utils.toNumber(utils.getFromLookup(value, boschThermostatLookup.systemModes)),
3496-
});
3497-
3498-
return {state: {custom_system_mode: value}};
3499-
},
3500-
convertGet: async (entity, key, meta) => {
3501-
await entity.read("hvacThermostat", ["systemMode"]);
3502-
},
3503-
},
3504-
];
3505-
3506-
const configure: Configure[] = [
3507-
m.setupConfigureForReporting("hvacThermostat", "systemMode", {
3508-
config: false,
3509-
access: ea.ALL,
3510-
}),
3511-
];
3512-
3513-
return {
3514-
exposes,
3515-
fromZigbee,
3516-
toZigbee,
3517-
configure,
3518-
isModernExtend: true,
3519-
};
3520-
},
35213423
raThermostat: (): ModernExtend => {
35223424
// Native thermostat
35233425
const thermostat = m.thermostat({
@@ -3629,92 +3531,6 @@ export const boschThermostatExtend = {
36293531
isModernExtend: true,
36303532
};
36313533
},
3632-
operatingMode: (args?: {enableReporting: boolean}): ModernExtend => {
3633-
const operatingModeLookup = {schedule: 0x00, manual: 0x01, pause: 0x05};
3634-
3635-
const operatingMode = m.enumLookup<"hvacThermostat", BoschThermostatCluster>({
3636-
name: "operating_mode",
3637-
cluster: "hvacThermostat",
3638-
attribute: "operatingMode",
3639-
description: "Bosch-specific operating mode. This is being used as mode on the exposed thermostat when using Home Assistant.",
3640-
lookup: operatingModeLookup,
3641-
reporting: args?.enableReporting ? {min: "MIN", max: "MAX", change: null} : false,
3642-
entityCategory: "config",
3643-
});
3644-
3645-
const exposes: (Expose | DefinitionExposesFunction)[] = operatingMode.exposes;
3646-
const fromZigbee = operatingMode.fromZigbee;
3647-
const toZigbee: Tz.Converter[] = operatingMode.toZigbee;
3648-
const configure: Configure[] = operatingMode.configure;
3649-
3650-
const removeLowAndHighTemperatureFields = (payload: KeyValueAny) => {
3651-
payload.temperature_high_command_topic = undefined;
3652-
payload.temperature_low_command_topic = undefined;
3653-
3654-
payload.temperature_high_state_template = undefined;
3655-
payload.temperature_low_state_template = undefined;
3656-
3657-
payload.temperature_high_state_topic = undefined;
3658-
payload.temperature_low_state_topic = undefined;
3659-
};
3660-
3661-
// Override the payload send to Home Assistant to achieve the following:
3662-
// 1. Use the Bosch operating mode instead of system modes
3663-
// See: https://github.com/Koenkk/zigbee2mqtt/pull/23075#issue-2355829475
3664-
// 2. Remove setpoints not compatible with the currently used system mode
3665-
// See: https://github.com/Koenkk/zigbee2mqtt/issues/28892
3666-
const meta: DefinitionMeta = {
3667-
overrideHaDiscoveryPayload: (payload) => {
3668-
if (payload.modes !== undefined) {
3669-
if (payload.modes.includes("heat")) {
3670-
payload.mode_command_template =
3671-
`{% set values = { 'auto':'schedule', 'heat':'manual', 'off':'pause' } %}` +
3672-
`{"operating_mode": "{{ values[value] if value in values.keys() else 'pause' }}"}`;
3673-
payload.mode_state_template =
3674-
`{% set values = { 'schedule':'auto', 'manual':'heat', 'pause':'off' } %}` +
3675-
"{% set value = value_json.operating_mode %}" +
3676-
`{{ values[value] if value in values.keys() else 'off' }}`;
3677-
3678-
if (payload.temperature_low_command_topic !== undefined) {
3679-
payload.temperature_command_topic = payload.temperature_low_command_topic;
3680-
payload.temperature_state_template = payload.temperature_low_state_template;
3681-
payload.temperature_state_topic = payload.temperature_low_state_topic;
3682-
3683-
removeLowAndHighTemperatureFields(payload);
3684-
}
3685-
} else if (payload.modes.includes("cool")) {
3686-
payload.mode_command_template =
3687-
`{% set values = { 'auto':'schedule', 'cool':'manual', 'off':'pause' } %}` +
3688-
`{"operating_mode": "{{ values[value] if value in values.keys() else 'pause' }}"}`;
3689-
payload.mode_state_template =
3690-
`{% set values = { 'schedule':'auto', 'manual':'cool', 'pause':'off' } %}` +
3691-
"{% set value = value_json.operating_mode %}" +
3692-
`{{ values[value] if value in values.keys() else 'off' }}`;
3693-
3694-
if (payload.temperature_high_command_topic !== undefined) {
3695-
payload.temperature_command_topic = payload.temperature_high_command_topic;
3696-
payload.temperature_state_template = payload.temperature_high_state_template;
3697-
payload.temperature_state_topic = payload.temperature_high_state_topic;
3698-
3699-
removeLowAndHighTemperatureFields(payload);
3700-
}
3701-
}
3702-
3703-
payload.modes = ["off", ...payload.modes, "auto"];
3704-
payload.mode_command_topic = payload.mode_command_topic.replace("/system_mode", "");
3705-
}
3706-
},
3707-
};
3708-
3709-
return {
3710-
exposes,
3711-
fromZigbee,
3712-
toZigbee,
3713-
configure,
3714-
meta,
3715-
isModernExtend: true,
3716-
};
3717-
},
37183534
boostHeating: (args?: {enableReporting: boolean}): ModernExtend => {
37193535
const boostHeatingLookup: KeyValue = {
37203536
OFF: 0x00,

0 commit comments

Comments
 (0)