zigbee-herdsman-converters 25.45.0 → 25.47.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/dist/devices/bosch.d.ts.map +1 -1
  3. package/dist/devices/bosch.js +82 -563
  4. package/dist/devices/bosch.js.map +1 -1
  5. package/dist/devices/box.d.ts.map +1 -1
  6. package/dist/devices/box.js +5 -4
  7. package/dist/devices/box.js.map +1 -1
  8. package/dist/devices/develco.js +2 -2
  9. package/dist/devices/develco.js.map +1 -1
  10. package/dist/devices/elko.js +4 -4
  11. package/dist/devices/elko.js.map +1 -1
  12. package/dist/devices/evn.d.ts.map +1 -1
  13. package/dist/devices/evn.js +7 -0
  14. package/dist/devices/evn.js.map +1 -1
  15. package/dist/devices/gewiss.js +4 -4
  16. package/dist/devices/gewiss.js.map +1 -1
  17. package/dist/devices/ikea.d.ts.map +1 -1
  18. package/dist/devices/ikea.js +14 -0
  19. package/dist/devices/ikea.js.map +1 -1
  20. package/dist/devices/innr.d.ts.map +1 -1
  21. package/dist/devices/innr.js +15 -1
  22. package/dist/devices/innr.js.map +1 -1
  23. package/dist/devices/lumi.d.ts.map +1 -1
  24. package/dist/devices/lumi.js +11 -6
  25. package/dist/devices/lumi.js.map +1 -1
  26. package/dist/devices/moes.js +4 -4
  27. package/dist/devices/moes.js.map +1 -1
  28. package/dist/devices/nordtronic.d.ts.map +1 -1
  29. package/dist/devices/nordtronic.js +7 -1
  30. package/dist/devices/nordtronic.js.map +1 -1
  31. package/dist/devices/paulmann.d.ts.map +1 -1
  32. package/dist/devices/paulmann.js +10 -3
  33. package/dist/devices/paulmann.js.map +1 -1
  34. package/dist/devices/philips.d.ts.map +1 -1
  35. package/dist/devices/philips.js +6 -9
  36. package/dist/devices/philips.js.map +1 -1
  37. package/dist/devices/schneider_electric.js +1 -1
  38. package/dist/devices/schneider_electric.js.map +1 -1
  39. package/dist/devices/sengled.js +1 -1
  40. package/dist/devices/sengled.js.map +1 -1
  41. package/dist/devices/sonoff.d.ts.map +1 -1
  42. package/dist/devices/sonoff.js +9 -0
  43. package/dist/devices/sonoff.js.map +1 -1
  44. package/dist/devices/third_reality.d.ts.map +1 -1
  45. package/dist/devices/third_reality.js +22 -0
  46. package/dist/devices/third_reality.js.map +1 -1
  47. package/dist/devices/tuya.d.ts.map +1 -1
  48. package/dist/devices/tuya.js +82 -18
  49. package/dist/devices/tuya.js.map +1 -1
  50. package/dist/devices/wirenboard.js +1 -1
  51. package/dist/devices/wirenboard.js.map +1 -1
  52. package/dist/lib/bosch.d.ts +134 -12
  53. package/dist/lib/bosch.d.ts.map +1 -1
  54. package/dist/lib/bosch.js +983 -69
  55. package/dist/lib/bosch.js.map +1 -1
  56. package/dist/lib/exposes.d.ts +0 -1
  57. package/dist/lib/exposes.d.ts.map +1 -1
  58. package/dist/lib/exposes.js +4 -8
  59. package/dist/lib/exposes.js.map +1 -1
  60. package/dist/lib/generateDefinition.js +2 -2
  61. package/dist/lib/generateDefinition.js.map +1 -1
  62. package/dist/lib/modernExtend.d.ts +28 -10
  63. package/dist/lib/modernExtend.d.ts.map +1 -1
  64. package/dist/lib/modernExtend.js +71 -24
  65. package/dist/lib/modernExtend.js.map +1 -1
  66. package/dist/lib/utils.js +3 -3
  67. package/dist/lib/utils.js.map +1 -1
  68. package/dist/models-index.json +1 -1
  69. package/package.json +1 -1
package/dist/lib/bosch.js CHANGED
@@ -33,7 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.boschSmartPlugExtend = exports.boschBsenExtend = exports.boschDoorWindowContactExtend = exports.boschBsirExtend = exports.boschBmctExtend = exports.boschGeneralExtend = exports.manufacturerOptions = void 0;
36
+ exports.boschThermostatExtend = exports.boschSmartPlugExtend = exports.boschWaterAlarmExtend = exports.boschBsenExtend = exports.boschDoorWindowContactExtend = exports.boschBsirExtend = exports.boschBmctExtend = exports.boschGeneralSensorDeviceExtend = exports.boschGeneralEnergyDeviceExtend = exports.boschGeneralExtend = exports.manufacturerOptions = void 0;
37
37
  const zigbee_herdsman_1 = require("zigbee-herdsman");
38
38
  const fz = __importStar(require("../converters/fromZigbee"));
39
39
  const tz = __importStar(require("../converters/toZigbee"));
@@ -47,7 +47,11 @@ const utils_1 = require("./utils");
47
47
  const e = exposes.presets;
48
48
  const ea = exposes.access;
49
49
  const NS = "zhc:bosch";
50
- exports.manufacturerOptions = { manufacturerCode: zigbee_herdsman_1.Zcl.ManufacturerCode.ROBERT_BOSCH_GMBH };
50
+ exports.manufacturerOptions = {
51
+ manufacturerCode: zigbee_herdsman_1.Zcl.ManufacturerCode.ROBERT_BOSCH_GMBH,
52
+ sendPolicy: "immediate",
53
+ };
54
+ //region Generally used Bosch functionality
51
55
  exports.boschGeneralExtend = {
52
56
  /** Some devices now use a different name for some custom clusters than
53
57
  * originally used. This can lead to issues like those described in
@@ -98,10 +102,20 @@ exports.boschGeneralExtend = {
98
102
  isModernExtend: true,
99
103
  };
100
104
  },
105
+ /** Some Bosch devices ask the coordinator for their ZCL version
106
+ * during deviceAnnouncement. Without answer, these devices regularly
107
+ * re-join the network. To avoid that, we have to make sure that a readRequest
108
+ * for the zclVersion is always being answered. The answered zclVersion is
109
+ * taken from the Bosch Smart Home Controller II.
110
+ *
111
+ * Exception: BTH-RM and BTH-RM230Z ask the coordinator at regular
112
+ * intervals for their zclVersion (maybe availability check like Z2M does?)
113
+ * and *not* during interview! To avoid code-duplication, we handle that
114
+ * case here as well. */
101
115
  handleZclVersionReadRequest: () => {
102
116
  const onEvent = [
103
117
  (event) => {
104
- if (event.type !== "deviceAnnounce") {
118
+ if (event.type !== "start") {
105
119
  return;
106
120
  }
107
121
  event.data.device.customReadResponse = (frame, endpoint) => {
@@ -124,7 +138,16 @@ exports.boschGeneralExtend = {
124
138
  isModernExtend: true,
125
139
  };
126
140
  },
127
- customSeMeteringCluster: () => m.deviceAddCustomCluster("seMetering", {
141
+ batteryWithPercentageAndLowStatus: (args) => m.battery({
142
+ percentage: true,
143
+ percentageReportingConfig: false,
144
+ lowStatus: true,
145
+ lowStatusReportingConfig: { min: "MIN", max: "MAX", change: null },
146
+ ...args,
147
+ }),
148
+ };
149
+ exports.boschGeneralEnergyDeviceExtend = {
150
+ customMeteringCluster: () => m.deviceAddCustomCluster("seMetering", {
128
151
  ID: zigbee_herdsman_1.Zcl.Clusters.seMetering.ID,
129
152
  attributes: {},
130
153
  commands: {
@@ -252,6 +275,118 @@ exports.boschGeneralExtend = {
252
275
  };
253
276
  },
254
277
  };
278
+ exports.boschGeneralSensorDeviceExtend = {
279
+ customIasZoneCluster: () => m.deviceAddCustomCluster("ssIasZone", {
280
+ ID: zigbee_herdsman_1.Zcl.Clusters.ssIasZone.ID,
281
+ attributes: {},
282
+ commands: {
283
+ initCustomTestMode: {
284
+ ID: 0x02,
285
+ parameters: [
286
+ { name: "timeoutInSeconds", type: zigbee_herdsman_1.Zcl.DataType.UINT8 },
287
+ { name: "reportTestInZoneStatus", type: zigbee_herdsman_1.Zcl.DataType.ENUM8 },
288
+ ],
289
+ },
290
+ },
291
+ commandsResponse: {},
292
+ }),
293
+ testMode: (args) => {
294
+ const { testModeDescription, supportTimeout = false } = args;
295
+ const testModeLookup = {
296
+ ON: true,
297
+ OFF: false,
298
+ };
299
+ const defaultTimeout = 3;
300
+ const enableTestMode = async (endpoint, timeout) => {
301
+ await endpoint.command("ssIasZone", "initCustomTestMode", {
302
+ timeoutInSeconds: timeout,
303
+ reportTestInZoneStatus: 0x80,
304
+ });
305
+ };
306
+ const disableTestMode = async (endpoint) => {
307
+ await endpoint.command("ssIasZone", "initNormalOpMode", {});
308
+ };
309
+ const exposes = [
310
+ e
311
+ .binary("test_mode", ea.ALL, utils.getFromLookupByValue(true, testModeLookup), utils.getFromLookupByValue(false, testModeLookup))
312
+ .withDescription(testModeDescription)
313
+ .withCategory("config"),
314
+ ];
315
+ if (supportTimeout) {
316
+ exposes.push(e
317
+ .numeric("test_mode_timeout", ea.ALL)
318
+ .withDescription(`Determines how long the test mode should be activated. The default length is ${defaultTimeout} seconds.`)
319
+ .withValueMin(1)
320
+ .withValueMax(255)
321
+ .withUnit("seconds")
322
+ .withCategory("config"));
323
+ }
324
+ const fromZigbee = [
325
+ {
326
+ cluster: "ssIasZone",
327
+ type: ["commandStatusChangeNotification", "attributeReport", "readResponse"],
328
+ convert: (model, msg, publish, options, meta) => {
329
+ const zoneStatus = "zonestatus" in msg.data ? msg.data.zonestatus : msg.data.zoneStatus;
330
+ if (zoneStatus === undefined) {
331
+ return;
332
+ }
333
+ const result = {};
334
+ const testModeEnabled = (zoneStatus & (1 << 8)) > 0;
335
+ result.test_mode = utils.getFromLookupByValue(testModeEnabled, testModeLookup);
336
+ return result;
337
+ },
338
+ },
339
+ ];
340
+ const toZigbee = [
341
+ {
342
+ key: ["test_mode", "test_mode_timeout"],
343
+ convertSet: async (entity, key, value, meta) => {
344
+ if (key === "test_mode") {
345
+ if (value === utils.getFromLookupByValue(true, testModeLookup)) {
346
+ let timeout;
347
+ if (supportTimeout) {
348
+ const currentTimeout = meta.state.test_mode_timeout;
349
+ if (currentTimeout == null) {
350
+ timeout = defaultTimeout;
351
+ meta.publish({ test_mode_timeout: timeout });
352
+ }
353
+ else {
354
+ timeout = utils.toNumber(currentTimeout);
355
+ }
356
+ }
357
+ else {
358
+ timeout = 0;
359
+ }
360
+ await enableTestMode(entity, timeout);
361
+ }
362
+ else {
363
+ await disableTestMode(entity);
364
+ }
365
+ }
366
+ if (key === "test_mode_timeout") {
367
+ return { state: { test_mode_timeout: value } };
368
+ }
369
+ },
370
+ convertGet: async (entity, key, meta) => {
371
+ if (key === "test_mode") {
372
+ await entity.read("ssIasZone", ["zoneStatus"]);
373
+ }
374
+ if (key === "test_mode_timeout" && meta.state.test_mode_timeout == null) {
375
+ meta.publish({ test_mode_timeout: supportTimeout ? defaultTimeout : 0 });
376
+ }
377
+ },
378
+ },
379
+ ];
380
+ const configure = [m.setupConfigureForBinding("ssIasZone", "input"), m.setupConfigureForReading("ssIasZone", ["zoneStatus"])];
381
+ return {
382
+ exposes,
383
+ fromZigbee,
384
+ toZigbee,
385
+ configure,
386
+ isModernExtend: true,
387
+ };
388
+ },
389
+ };
255
390
  exports.boschBmctExtend = {
256
391
  switchMode: (args) => {
257
392
  const { endpoint, deviceModeLookup, switchModeLookup, switchTypeLookup } = args;
@@ -1183,20 +1318,6 @@ exports.boschBsirExtend = {
1183
1318
  },
1184
1319
  access: "STATE_GET",
1185
1320
  }),
1186
- battery: () => m.battery({
1187
- percentage: true,
1188
- percentageReportingConfig: {
1189
- min: "MIN",
1190
- max: "MAX",
1191
- change: 1,
1192
- },
1193
- lowStatus: true,
1194
- lowStatusReportingConfig: {
1195
- min: "MIN",
1196
- max: "MAX",
1197
- change: 0,
1198
- },
1199
- }),
1200
1321
  lightDelay: () => m.numeric({
1201
1322
  name: "light_delay",
1202
1323
  cluster: "ssIasWd",
@@ -1415,11 +1536,6 @@ exports.boschDoorWindowContactExtend = {
1415
1536
  commands: {},
1416
1537
  commandsResponse: {},
1417
1538
  }),
1418
- battery: () => m.battery({
1419
- percentage: true,
1420
- lowStatus: true,
1421
- lowStatusReportingConfig: { min: "MIN", max: "MAX", change: 0 },
1422
- }),
1423
1539
  reportContactState: () => m.iasZoneAlarm({
1424
1540
  zoneType: "contact",
1425
1541
  zoneAttributes: ["alarm_1"],
@@ -1693,18 +1809,9 @@ exports.boschDoorWindowContactExtend = {
1693
1809
  };
1694
1810
  },
1695
1811
  };
1812
+ //endregion
1813
+ //region Bosch BSEN-M device (Motion detector)
1696
1814
  exports.boschBsenExtend = {
1697
- customIasZoneCluster: () => m.deviceAddCustomCluster("ssIasZone", {
1698
- ID: zigbee_herdsman_1.Zcl.Clusters.ssIasZone.ID,
1699
- attributes: {},
1700
- commands: {
1701
- initCustomTestMode: {
1702
- ID: 0x02,
1703
- parameters: [{ name: "data", type: zigbee_herdsman_1.Zcl.BuffaloZclDataType.LIST_UINT8 }],
1704
- },
1705
- },
1706
- commandsResponse: {},
1707
- }),
1708
1815
  battery: () => m.battery({
1709
1816
  percentage: false,
1710
1817
  percentageReporting: false,
@@ -1712,7 +1819,12 @@ exports.boschBsenExtend = {
1712
1819
  voltageReporting: true,
1713
1820
  voltageToPercentage: { min: 2500, max: 3000 },
1714
1821
  lowStatus: true,
1715
- lowStatusReportingConfig: { min: "MIN", max: "MAX", change: 0 },
1822
+ lowStatusReportingConfig: { min: "MIN", max: "MAX", change: null },
1823
+ }),
1824
+ testMode: () => exports.boschGeneralSensorDeviceExtend.testMode({
1825
+ testModeDescription: "Activates the test mode. In this mode, the device blinks on every detected motion " +
1826
+ "without any wait time in between to verify the installation. Please keep in mind " +
1827
+ "that it can take up to 45 seconds for the test mode to be activated.",
1716
1828
  }),
1717
1829
  illuminance: () => m.illuminance({ reporting: { min: "1_SECOND", max: 600, change: 3522 } }),
1718
1830
  // The temperature sensor isn't used at all by Bosch on the BSEN-M.
@@ -1867,26 +1979,101 @@ exports.boschBsenExtend = {
1867
1979
  isModernExtend: true,
1868
1980
  };
1869
1981
  },
1870
- testMode: () => {
1871
- const testModeLookup = {
1872
- ON: true,
1873
- OFF: false,
1982
+ };
1983
+ exports.boschWaterAlarmExtend = {
1984
+ waterAlarmCluster: () => m.deviceAddCustomCluster("boschWaterAlarm", {
1985
+ ID: 0xfcac,
1986
+ manufacturerCode: exports.manufacturerOptions.manufacturerCode,
1987
+ attributes: {
1988
+ alarmOnMotion: { ID: 0x0003, type: zigbee_herdsman_1.Zcl.DataType.BOOLEAN },
1989
+ },
1990
+ commands: {
1991
+ muteAlarmControl: { ID: 0x00, parameters: [{ name: "data", type: zigbee_herdsman_1.Zcl.DataType.UINT8 }] },
1992
+ muteAlarmControlResponse: { ID: 0x01, parameters: [{ name: "data", type: zigbee_herdsman_1.Zcl.DataType.ENUM8 }] },
1993
+ },
1994
+ commandsResponse: {},
1995
+ }),
1996
+ changedSensitivityLevel: () => {
1997
+ const configure = [
1998
+ m.setupConfigureForBinding("ssIasZone", "input"),
1999
+ m.setupConfigureForReading("ssIasZone", ["numZoneSensitivityLevelsSupported", "currentZoneSensitivityLevel"]),
2000
+ async (device, coordinatorEndpoint, definition) => {
2001
+ const endpoint = device.getEndpoint(1);
2002
+ // The write request is made when using the proprietary
2003
+ // Bosch Smart Home Controller II as of 16-10-2025. Looks like
2004
+ // the default value was too high, and they didn't want to
2005
+ // push a firmware update. We mimic it here to avoid complaints.
2006
+ await endpoint.write("ssIasZone", { currentZoneSensitivityLevel: 5 });
2007
+ },
2008
+ ];
2009
+ return {
2010
+ configure,
2011
+ isModernExtend: true,
1874
2012
  };
1875
- const enableTestMode = async (endpoint) => {
1876
- await endpoint.command("ssIasZone", "initCustomTestMode", {
1877
- data: [0x00, 0x80],
1878
- });
2013
+ },
2014
+ waterAndTamperAlarm: () => m.iasZoneAlarm({
2015
+ zoneType: "water_leak",
2016
+ zoneAttributes: ["alarm_1", "tamper"],
2017
+ }),
2018
+ muteAlarmControl: () => {
2019
+ const muteAlarmControlLookup = {
2020
+ UNMUTED: false,
2021
+ MUTED: true,
1879
2022
  };
1880
- const disableTestMode = async (endpoint) => {
1881
- await endpoint.command("ssIasZone", "initNormalOpMode", {});
2023
+ const muteAlarmControlResponseLookup = {
2024
+ muted: 0x00,
2025
+ error: 0x01,
2026
+ no_change: 0x02,
2027
+ unmuted: 0x03,
1882
2028
  };
1883
2029
  const exposes = [
1884
2030
  e
1885
- .binary("test_mode", ea.ALL, utils.getFromLookupByValue(true, testModeLookup), utils.getFromLookupByValue(false, testModeLookup))
1886
- .withDescription("Activate the test mode. In this mode, the device blinks on every detected motion without any wait time in between to verify the installation. Please keep in mind that it can take up to 45 seconds for the test mode to be activated.")
1887
- .withCategory("config"),
2031
+ .binary("water_leak_alarm_control", ea.ALL, utils.getFromLookupByValue(true, muteAlarmControlLookup), utils.getFromLookupByValue(false, muteAlarmControlLookup))
2032
+ .withLabel("Mute water leak alarm")
2033
+ .withDescription("In case of an water leak, you can mute and unmute the audible alarm here"),
2034
+ ];
2035
+ const toZigbee = [
2036
+ {
2037
+ key: ["water_leak_alarm_control"],
2038
+ convertSet: async (entity, key, value, meta) => {
2039
+ if (value === utils.getFromLookupByValue(false, muteAlarmControlLookup)) {
2040
+ await entity.command("boschWaterAlarm", "muteAlarmControl", { data: 0x00 }, exports.manufacturerOptions);
2041
+ }
2042
+ else {
2043
+ await entity.command("boschWaterAlarm", "muteAlarmControl", { data: 0x01 }, exports.manufacturerOptions);
2044
+ }
2045
+ },
2046
+ convertGet: async (entity, key, meta) => {
2047
+ await entity.read("ssIasZone", ["zoneStatus"]);
2048
+ },
2049
+ },
1888
2050
  ];
1889
2051
  const fromZigbee = [
2052
+ {
2053
+ cluster: "boschWaterAlarm",
2054
+ type: ["raw"],
2055
+ convert: (model, msg, publish, options, meta) => {
2056
+ const command = msg.data[4];
2057
+ if (command !== 0x01) {
2058
+ return;
2059
+ }
2060
+ const muteAlarmControlResponse = msg.data[5];
2061
+ switch (muteAlarmControlResponse) {
2062
+ case muteAlarmControlResponseLookup.muted:
2063
+ logger_1.logger.debug(`Alarm on device '${meta.device.ieeeAddr}' was muted`, NS);
2064
+ break;
2065
+ case muteAlarmControlResponseLookup.error:
2066
+ logger_1.logger.error(`Alarm on device '${meta.device.ieeeAddr}' could not be muted right now (e.g., no active alarm)!`, NS);
2067
+ break;
2068
+ case muteAlarmControlResponseLookup.no_change:
2069
+ logger_1.logger.debug(`Alarm on device '${meta.device.ieeeAddr}' is already in requested state`, NS);
2070
+ break;
2071
+ case muteAlarmControlResponseLookup.unmuted:
2072
+ logger_1.logger.debug(`Alarm on device '${meta.device.ieeeAddr}' was unmuted`, NS);
2073
+ break;
2074
+ }
2075
+ },
2076
+ },
1890
2077
  {
1891
2078
  cluster: "ssIasZone",
1892
2079
  type: ["commandStatusChangeNotification", "attributeReport", "readResponse"],
@@ -1896,39 +2083,36 @@ exports.boschBsenExtend = {
1896
2083
  return;
1897
2084
  }
1898
2085
  const result = {};
1899
- const testModeEnabled = (zoneStatus & (1 << 8)) > 0;
1900
- result.test_mode = utils.getFromLookupByValue(testModeEnabled, testModeLookup);
2086
+ const alarmMuted = (zoneStatus & (1 << 1)) > 0;
2087
+ result.water_leak_alarm_control = utils.getFromLookupByValue(alarmMuted, muteAlarmControlLookup);
1901
2088
  return result;
1902
2089
  },
1903
2090
  },
1904
2091
  ];
1905
- const toZigbee = [
1906
- {
1907
- key: ["test_mode"],
1908
- convertSet: async (entity, key, value, meta) => {
1909
- if (key === "test_mode") {
1910
- if (value === utils.getFromLookupByValue(true, testModeLookup)) {
1911
- await enableTestMode(entity);
1912
- }
1913
- else {
1914
- await disableTestMode(entity);
1915
- }
1916
- }
1917
- },
1918
- convertGet: async (entity, key, meta) => {
1919
- await entity.read("ssIasZone", ["zoneStatus"]);
1920
- },
1921
- },
1922
- ];
1923
2092
  const configure = [m.setupConfigureForBinding("ssIasZone", "input"), m.setupConfigureForReading("ssIasZone", ["zoneStatus"])];
1924
2093
  return {
1925
2094
  exposes,
1926
- fromZigbee,
1927
2095
  toZigbee,
2096
+ fromZigbee,
1928
2097
  configure,
1929
2098
  isModernExtend: true,
1930
2099
  };
1931
2100
  },
2101
+ alarmOnMotion: () => m.binary({
2102
+ name: "alarm_on_motion",
2103
+ cluster: "boschWaterAlarm",
2104
+ attribute: "alarmOnMotion",
2105
+ description: "If your water alarm is moved, an acoustic signal sounds",
2106
+ valueOn: ["ON", 0x01],
2107
+ valueOff: ["OFF", 0x00],
2108
+ entityCategory: "config",
2109
+ }),
2110
+ testMode: () => exports.boschGeneralSensorDeviceExtend.testMode({
2111
+ testModeDescription: "Activates the test mode. In this mode, the device acts like it would when " +
2112
+ "detecting any water to verify the installation. Please keep in mind " +
2113
+ "that it can take up to 10 seconds for the test mode to be activated.",
2114
+ supportTimeout: true,
2115
+ }),
1932
2116
  };
1933
2117
  exports.boschSmartPlugExtend = {
1934
2118
  smartPlugCluster: () => m.deviceAddCustomCluster("boschEnergyDevice", {
@@ -2068,5 +2252,735 @@ exports.boschSmartPlugExtend = {
2068
2252
  ...args,
2069
2253
  }),
2070
2254
  };
2255
+ const boschThermostatLookup = {
2256
+ systemModes: {
2257
+ heat: 0x04,
2258
+ cool: 0x03,
2259
+ },
2260
+ raRunningStates: ["idle", "heat"],
2261
+ heaterType: {
2262
+ underfloor_heating: 0x00,
2263
+ radiator: 0x02,
2264
+ central_heating: 0x01,
2265
+ manual_control: 0x03,
2266
+ },
2267
+ };
2268
+ exports.boschThermostatExtend = {
2269
+ customThermostatCluster: () => m.deviceAddCustomCluster("hvacThermostat", {
2270
+ ID: zigbee_herdsman_1.Zcl.Clusters.hvacThermostat.ID,
2271
+ attributes: {
2272
+ operatingMode: { ID: 0x4007, type: zigbee_herdsman_1.Zcl.DataType.ENUM8, manufacturerCode: exports.manufacturerOptions.manufacturerCode },
2273
+ heatingDemand: { ID: 0x4020, type: zigbee_herdsman_1.Zcl.DataType.ENUM8, manufacturerCode: exports.manufacturerOptions.manufacturerCode },
2274
+ valveAdaptStatus: { ID: 0x4022, type: zigbee_herdsman_1.Zcl.DataType.ENUM8, manufacturerCode: exports.manufacturerOptions.manufacturerCode },
2275
+ unknownAttribute0: { ID: 0x4025, type: zigbee_herdsman_1.Zcl.DataType.ENUM8, manufacturerCode: exports.manufacturerOptions.manufacturerCode },
2276
+ remoteTemperature: { ID: 0x4040, type: zigbee_herdsman_1.Zcl.DataType.INT16, manufacturerCode: exports.manufacturerOptions.manufacturerCode },
2277
+ unknownAttribute1: { ID: 0x4041, type: zigbee_herdsman_1.Zcl.DataType.ENUM8, manufacturerCode: exports.manufacturerOptions.manufacturerCode },
2278
+ windowOpenMode: { ID: 0x4042, type: zigbee_herdsman_1.Zcl.DataType.ENUM8, manufacturerCode: exports.manufacturerOptions.manufacturerCode },
2279
+ boostHeating: { ID: 0x4043, type: zigbee_herdsman_1.Zcl.DataType.ENUM8, manufacturerCode: exports.manufacturerOptions.manufacturerCode },
2280
+ cableSensorTemperature: { ID: 0x4052, type: zigbee_herdsman_1.Zcl.DataType.INT16, manufacturerCode: exports.manufacturerOptions.manufacturerCode },
2281
+ valveType: { ID: 0x4060, type: zigbee_herdsman_1.Zcl.DataType.ENUM8, manufacturerCode: exports.manufacturerOptions.manufacturerCode },
2282
+ unknownAttribute2: { ID: 0x4061, type: zigbee_herdsman_1.Zcl.DataType.ENUM8, manufacturerCode: exports.manufacturerOptions.manufacturerCode },
2283
+ cableSensorMode: { ID: 0x4062, type: zigbee_herdsman_1.Zcl.DataType.ENUM8, manufacturerCode: exports.manufacturerOptions.manufacturerCode },
2284
+ heaterType: { ID: 0x4063, type: zigbee_herdsman_1.Zcl.DataType.ENUM8, manufacturerCode: exports.manufacturerOptions.manufacturerCode },
2285
+ errorState: { ID: 0x5000, type: zigbee_herdsman_1.Zcl.DataType.BITMAP8, manufacturerCode: exports.manufacturerOptions.manufacturerCode },
2286
+ automaticValveAdapt: { ID: 0x5010, type: zigbee_herdsman_1.Zcl.DataType.ENUM8, manufacturerCode: exports.manufacturerOptions.manufacturerCode },
2287
+ },
2288
+ commands: {
2289
+ calibrateValve: { ID: 0x41, parameters: [] },
2290
+ },
2291
+ commandsResponse: {},
2292
+ }),
2293
+ customUserInterfaceCfgCluster: () => m.deviceAddCustomCluster("hvacUserInterfaceCfg", {
2294
+ ID: zigbee_herdsman_1.Zcl.Clusters.hvacUserInterfaceCfg.ID,
2295
+ attributes: {
2296
+ displayOrientation: { ID: 0x400b, type: zigbee_herdsman_1.Zcl.DataType.UINT8, manufacturerCode: exports.manufacturerOptions.manufacturerCode },
2297
+ activityLed: { ID: 0x4033, type: zigbee_herdsman_1.Zcl.DataType.ENUM8, manufacturerCode: exports.manufacturerOptions.manufacturerCode },
2298
+ displayedTemperature: { ID: 0x4039, type: zigbee_herdsman_1.Zcl.DataType.ENUM8, manufacturerCode: exports.manufacturerOptions.manufacturerCode },
2299
+ displaySwitchOnDuration: { ID: 0x403a, type: zigbee_herdsman_1.Zcl.DataType.ENUM8, manufacturerCode: exports.manufacturerOptions.manufacturerCode },
2300
+ displayBrightness: { ID: 0x403b, type: zigbee_herdsman_1.Zcl.DataType.ENUM8, manufacturerCode: exports.manufacturerOptions.manufacturerCode },
2301
+ },
2302
+ commands: {},
2303
+ commandsResponse: {},
2304
+ }),
2305
+ relayState: () => m.onOff({ description: "The state of the relay controlling the connected heating/cooling device", powerOnBehavior: false }),
2306
+ cableSensorMode: () => m.enumLookup({
2307
+ name: "cable_sensor_mode",
2308
+ cluster: "hvacThermostat",
2309
+ attribute: "cableSensorMode",
2310
+ description: 'Select a configuration for the sensor connection. If you select "with_regulation", ' +
2311
+ "the measured temperature on the cable sensor is used by the heating/cooling algorithm " +
2312
+ "instead of the local temperature.",
2313
+ lookup: { not_used: 0x00, cable_sensor_without_regulation: 0xb0, cable_sensor_with_regulation: 0xb1 },
2314
+ reporting: false,
2315
+ entityCategory: "config",
2316
+ }),
2317
+ cableSensorTemperature: () => m.numeric({
2318
+ name: "cable_sensor_temperature",
2319
+ cluster: "hvacThermostat",
2320
+ attribute: "cableSensorTemperature",
2321
+ description: "Measured temperature value on the cable sensor (if enabled)",
2322
+ unit: "°C",
2323
+ scale: 100,
2324
+ reporting: { min: 30, max: "MAX", change: 20 },
2325
+ access: "STATE_GET",
2326
+ }),
2327
+ heaterType: () => m.enumLookup({
2328
+ name: "heater_type",
2329
+ cluster: "hvacThermostat",
2330
+ attribute: "heaterType",
2331
+ description: "Select the connected heater type or 'manual_control' if you like to activate the relay manually when necessary",
2332
+ lookup: boschThermostatLookup.heaterType,
2333
+ reporting: false,
2334
+ entityCategory: "config",
2335
+ }),
2336
+ valveType: () => m.enumLookup({
2337
+ name: "valve_type",
2338
+ cluster: "hvacThermostat",
2339
+ attribute: "valveType",
2340
+ description: "Select the connected valve type",
2341
+ lookup: { normally_closed: 0x00, normally_open: 0x01 },
2342
+ reporting: false,
2343
+ entityCategory: "config",
2344
+ }),
2345
+ humidity: () => m.humidity({ reporting: false }),
2346
+ windowOpenMode: (args) => m.binary({
2347
+ name: "window_open_mode",
2348
+ cluster: "hvacThermostat",
2349
+ attribute: "windowOpenMode",
2350
+ description: "Activates the window open mode, where the thermostat disables any heating/cooling " +
2351
+ "to prevent unnecessary energy consumption. Please keep in mind that the device " +
2352
+ "itself does not detect any open windows!",
2353
+ valueOn: ["ON", 0x01],
2354
+ valueOff: ["OFF", 0x00],
2355
+ reporting: args?.enableReporting ? { min: "MIN", max: "MAX", change: null } : false,
2356
+ }),
2357
+ childLock: () => m.binary({
2358
+ name: "child_lock",
2359
+ cluster: "hvacUserInterfaceCfg",
2360
+ attribute: "keypadLockout",
2361
+ description: "Enables/disables physical input on the thermostat",
2362
+ valueOn: ["LOCK", 0x01],
2363
+ valueOff: ["UNLOCK", 0x00],
2364
+ reporting: { min: "MIN", max: "MAX", change: null },
2365
+ }),
2366
+ displayBrightness: () => m.numeric({
2367
+ name: "display_brightness",
2368
+ cluster: "hvacUserInterfaceCfg",
2369
+ attribute: "displayBrightness",
2370
+ description: "Sets brightness of the display",
2371
+ valueMin: 0,
2372
+ valueMax: 100,
2373
+ valueStep: 10,
2374
+ unit: "%",
2375
+ scale: 0.1,
2376
+ reporting: false,
2377
+ entityCategory: "config",
2378
+ }),
2379
+ displaySwitchOnDuration: () => m.numeric({
2380
+ name: "display_switch_on_duration",
2381
+ cluster: "hvacUserInterfaceCfg",
2382
+ attribute: "displaySwitchOnDuration",
2383
+ label: "Display switch-on duration",
2384
+ description: "Sets the time before the display is automatically switched off",
2385
+ valueMin: 5,
2386
+ valueMax: 30,
2387
+ unit: "s",
2388
+ reporting: false,
2389
+ entityCategory: "config",
2390
+ }),
2391
+ displayOrientation: () => m.enumLookup({
2392
+ name: "display_orientation",
2393
+ cluster: "hvacUserInterfaceCfg",
2394
+ attribute: "displayOrientation",
2395
+ description: "You can rotate the display content by 180° here. This is recommended if your thermostat is fitted vertically, for instance.",
2396
+ lookup: { standard_arrangement: 0x00, rotated_by_180_degrees: 0x01 },
2397
+ reporting: false,
2398
+ entityCategory: "config",
2399
+ }),
2400
+ displayedTemperature: () => m.enumLookup({
2401
+ name: "displayed_temperature",
2402
+ cluster: "hvacUserInterfaceCfg",
2403
+ attribute: "displayedTemperature",
2404
+ description: "Select which temperature should be displayed on your radiator thermostat display",
2405
+ lookup: { set_temperature: 0x00, measured_temperature: 0x01 },
2406
+ reporting: false,
2407
+ entityCategory: "config",
2408
+ }),
2409
+ activityLedState: () => m.enumLookup({
2410
+ name: "activity_led",
2411
+ cluster: "hvacUserInterfaceCfg",
2412
+ attribute: "activityLed",
2413
+ label: "Activity LED state",
2414
+ description: "Determines the state of the little dot on the display next to the heating/cooling symbol",
2415
+ lookup: { off: 0x00, auto: 0x01, on: 0x02 },
2416
+ reporting: false,
2417
+ entityCategory: "config",
2418
+ }),
2419
+ remoteTemperature: () => m.numeric({
2420
+ name: "remote_temperature",
2421
+ cluster: "hvacThermostat",
2422
+ attribute: "remoteTemperature",
2423
+ description: "Input for remote temperature sensor. Required at least every 30 minutes to prevent fallback to the internal sensor!",
2424
+ valueMin: 0.0,
2425
+ valueMax: 35.0,
2426
+ valueStep: 0.2,
2427
+ unit: "°C",
2428
+ scale: 100,
2429
+ reporting: false,
2430
+ entityCategory: "config",
2431
+ }),
2432
+ setpointChangeSource: (args) => m.enumLookup({
2433
+ name: "setpoint_change_source",
2434
+ cluster: "hvacThermostat",
2435
+ attribute: "setpointChangeSource",
2436
+ description: "Source of the current setpoint temperature",
2437
+ lookup: { manual: 0x00, schedule: 0x01, externally: 0x02 },
2438
+ access: "STATE_GET",
2439
+ reporting: args?.enableReporting ? { min: "MIN", max: "MAX", change: null } : false,
2440
+ entityCategory: "diagnostic",
2441
+ }),
2442
+ customHeatingDemand: () => m.numeric({
2443
+ name: "pi_heating_demand",
2444
+ cluster: "hvacThermostat",
2445
+ attribute: "heatingDemand",
2446
+ label: "PI heating demand",
2447
+ description: "Position of the valve (= demanded heat) where 0% is fully closed and 100% is fully open",
2448
+ unit: "%",
2449
+ valueMin: 0,
2450
+ valueMax: 100,
2451
+ access: "ALL",
2452
+ reporting: { min: "MIN", max: "MAX", change: null },
2453
+ }),
2454
+ rmBattery: () => m.battery({
2455
+ percentage: true,
2456
+ percentageReporting: false,
2457
+ voltage: true,
2458
+ voltageReporting: true,
2459
+ voltageReportingConfig: false,
2460
+ voltageToPercentage: { min: 4400, max: 6400 },
2461
+ lowStatus: true,
2462
+ lowStatusReportingConfig: { min: "MIN", max: "MAX", change: null },
2463
+ }),
2464
+ rmThermostat: () => {
2465
+ const thermostat = m.thermostat({
2466
+ localTemperature: {
2467
+ configure: { reporting: false },
2468
+ },
2469
+ localTemperatureCalibration: {
2470
+ values: { min: -5, max: 5, step: 0.1 },
2471
+ configure: { reporting: false },
2472
+ },
2473
+ setpoints: {
2474
+ values: {
2475
+ occupiedHeatingSetpoint: { min: 5, max: 30, step: 0.5 },
2476
+ occupiedCoolingSetpoint: { min: 5, max: 30, step: 0.5 },
2477
+ },
2478
+ configure: { reporting: false },
2479
+ },
2480
+ systemMode: {
2481
+ values: ["heat", "cool"],
2482
+ toZigbee: { skip: true },
2483
+ configure: { skip: true },
2484
+ },
2485
+ runningState: {
2486
+ values: ["idle", "heat", "cool"],
2487
+ configure: { reporting: false },
2488
+ },
2489
+ });
2490
+ const expose = (device, options) => {
2491
+ const returnedThermostat = thermostat.exposes;
2492
+ if (utils.isDummyDevice(device)) {
2493
+ return returnedThermostat;
2494
+ }
2495
+ let currentSystemMode;
2496
+ try {
2497
+ currentSystemMode = utils.getFromLookupByValue(device.getEndpoint(1).getClusterAttributeValue("hvacThermostat", "systemMode"), boschThermostatLookup.systemModes);
2498
+ }
2499
+ catch {
2500
+ currentSystemMode = "heat";
2501
+ }
2502
+ // The thermostat is a singleton, thus the values must be set
2503
+ // manually as filtering will lead to an array without
2504
+ // heat/cool in them after two systemMode changes.
2505
+ returnedThermostat[0].features.forEach((exposedAttribute, index, array) => {
2506
+ if (exposedAttribute.type === "enum") {
2507
+ if (exposedAttribute.name === "system_mode") {
2508
+ exposedAttribute.label = "Active system mode";
2509
+ exposedAttribute.description =
2510
+ "Currently used system mode by the thermostat. This field is primarily " +
2511
+ "used to configure the thermostat in Home Assistant correctly.";
2512
+ exposedAttribute.values = [currentSystemMode];
2513
+ exposedAttribute.access = ea.STATE;
2514
+ }
2515
+ if (exposedAttribute.name === "running_state") {
2516
+ exposedAttribute.values = ["idle", currentSystemMode];
2517
+ }
2518
+ }
2519
+ });
2520
+ return returnedThermostat;
2521
+ };
2522
+ return {
2523
+ exposes: [expose],
2524
+ fromZigbee: thermostat.fromZigbee,
2525
+ toZigbee: thermostat.toZigbee,
2526
+ configure: thermostat.configure,
2527
+ isModernExtend: true,
2528
+ };
2529
+ },
2530
+ customSystemMode: () => {
2531
+ const exposes = [
2532
+ e
2533
+ .enum("custom_system_mode", ea.ALL, Object.keys(boschThermostatLookup.systemModes))
2534
+ .withLabel("Available system modes")
2535
+ .withDescription("Select if the thermostat is connected to a heating or a cooling device")
2536
+ .withCategory("config"),
2537
+ ];
2538
+ const fromZigbee = [
2539
+ {
2540
+ cluster: "hvacThermostat",
2541
+ type: ["attributeReport", "readResponse"],
2542
+ convert: (model, msg, publish, options, meta) => {
2543
+ const result = {};
2544
+ const data = msg.data;
2545
+ if (data.systemMode !== undefined) {
2546
+ result.custom_system_mode = utils.getFromLookupByValue(data.systemMode, boschThermostatLookup.systemModes);
2547
+ meta.deviceExposesChanged();
2548
+ }
2549
+ return result;
2550
+ },
2551
+ },
2552
+ ];
2553
+ const toZigbee = [
2554
+ {
2555
+ key: ["custom_system_mode"],
2556
+ convertSet: async (entity, key, value, meta) => {
2557
+ await entity.write("hvacThermostat", {
2558
+ systemMode: utils.toNumber(utils.getFromLookup(value, boschThermostatLookup.systemModes)),
2559
+ });
2560
+ return { state: { custom_system_mode: value } };
2561
+ },
2562
+ convertGet: async (entity, key, meta) => {
2563
+ await entity.read("hvacThermostat", ["systemMode"]);
2564
+ },
2565
+ },
2566
+ ];
2567
+ const configure = [
2568
+ m.setupConfigureForReporting("hvacThermostat", "systemMode", {
2569
+ config: false,
2570
+ access: ea.ALL,
2571
+ }),
2572
+ ];
2573
+ return {
2574
+ exposes,
2575
+ fromZigbee,
2576
+ toZigbee,
2577
+ configure,
2578
+ isModernExtend: true,
2579
+ };
2580
+ },
2581
+ raThermostat: () => {
2582
+ // Native thermostat
2583
+ const thermostat = m.thermostat({
2584
+ localTemperature: {
2585
+ values: {
2586
+ description: "Temperature used by the heating algorithm. This is the " +
2587
+ "temperature measured on the device (by default) or the " +
2588
+ "remote temperature (if set within the last 30 min).",
2589
+ },
2590
+ configure: {
2591
+ reporting: { min: 30, max: 900, change: 20 },
2592
+ },
2593
+ },
2594
+ localTemperatureCalibration: {
2595
+ values: { min: -5, max: 5, step: 0.1 },
2596
+ configure: { reporting: false },
2597
+ },
2598
+ setpoints: {
2599
+ values: {
2600
+ occupiedHeatingSetpoint: { min: 5, max: 30, step: 0.5 },
2601
+ },
2602
+ configure: {
2603
+ reporting: { min: "MIN", max: "MAX", change: 1 },
2604
+ },
2605
+ },
2606
+ systemMode: {
2607
+ values: ["heat"],
2608
+ configure: {
2609
+ reporting: false,
2610
+ },
2611
+ },
2612
+ runningState: {
2613
+ values: boschThermostatLookup.raRunningStates,
2614
+ toZigbee: {
2615
+ skip: true,
2616
+ },
2617
+ configure: {
2618
+ reporting: false,
2619
+ },
2620
+ },
2621
+ piHeatingDemand: {
2622
+ values: ea.ALL,
2623
+ toZigbee: {
2624
+ skip: true,
2625
+ },
2626
+ configure: {
2627
+ skip: true,
2628
+ },
2629
+ },
2630
+ });
2631
+ const exposes = thermostat.exposes;
2632
+ const fromZigbee = thermostat.fromZigbee;
2633
+ const toZigbee = thermostat.toZigbee;
2634
+ let configure = thermostat.configure;
2635
+ // Add converters for custom running state
2636
+ const runningState = exports.boschThermostatExtend.customRunningState();
2637
+ fromZigbee.push(...runningState.fromZigbee);
2638
+ toZigbee.push(...runningState.toZigbee);
2639
+ // Add converters and configure for custom heating demand
2640
+ const piHeatingDemand = exports.boschThermostatExtend.customHeatingDemand();
2641
+ fromZigbee.push(...piHeatingDemand.fromZigbee);
2642
+ toZigbee.push(...piHeatingDemand.toZigbee);
2643
+ configure = [...configure, ...piHeatingDemand.configure];
2644
+ return {
2645
+ exposes,
2646
+ fromZigbee,
2647
+ toZigbee,
2648
+ configure,
2649
+ isModernExtend: true,
2650
+ };
2651
+ },
2652
+ customRunningState: () => {
2653
+ const fromZigbee = [
2654
+ {
2655
+ cluster: "hvacThermostat",
2656
+ type: ["attributeReport", "readResponse"],
2657
+ convert: (model, msg, publish, options, meta) => {
2658
+ const result = {};
2659
+ const data = msg.data;
2660
+ if (data.heatingDemand !== undefined) {
2661
+ result.running_state =
2662
+ utils.toNumber(data.heatingDemand) > 0
2663
+ ? boschThermostatLookup.raRunningStates[1]
2664
+ : boschThermostatLookup.raRunningStates[0];
2665
+ }
2666
+ return result;
2667
+ },
2668
+ },
2669
+ ];
2670
+ const toZigbee = [
2671
+ {
2672
+ key: ["running_state"],
2673
+ convertGet: async (entity, key, meta) => {
2674
+ await entity.read("hvacThermostat", ["heatingDemand"]);
2675
+ },
2676
+ },
2677
+ ];
2678
+ return {
2679
+ fromZigbee,
2680
+ toZigbee,
2681
+ isModernExtend: true,
2682
+ };
2683
+ },
2684
+ operatingMode: (args) => {
2685
+ const operatingModeLookup = { schedule: 0x00, manual: 0x01, pause: 0x05 };
2686
+ const operatingMode = m.enumLookup({
2687
+ name: "operating_mode",
2688
+ cluster: "hvacThermostat",
2689
+ attribute: "operatingMode",
2690
+ description: "Bosch-specific operating mode. This is being used as mode on the exposed thermostat when using Home Assistant.",
2691
+ lookup: operatingModeLookup,
2692
+ reporting: args?.enableReporting ? { min: "MIN", max: "MAX", change: null } : false,
2693
+ entityCategory: "config",
2694
+ });
2695
+ const exposes = operatingMode.exposes;
2696
+ const fromZigbee = operatingMode.fromZigbee;
2697
+ const toZigbee = operatingMode.toZigbee;
2698
+ const configure = operatingMode.configure;
2699
+ const removeLowAndHighTemperatureFields = (payload) => {
2700
+ payload.temperature_high_command_topic = undefined;
2701
+ payload.temperature_low_command_topic = undefined;
2702
+ payload.temperature_high_state_template = undefined;
2703
+ payload.temperature_low_state_template = undefined;
2704
+ payload.temperature_high_state_topic = undefined;
2705
+ payload.temperature_low_state_topic = undefined;
2706
+ };
2707
+ // Override the payload send to Home Assistant to achieve the following:
2708
+ // 1. Use the Bosch operating mode instead of system modes
2709
+ // See: https://github.com/Koenkk/zigbee2mqtt/pull/23075#issue-2355829475
2710
+ // 2. Remove setpoints not compatible with the currently used system mode
2711
+ // See: https://github.com/Koenkk/zigbee2mqtt/issues/28892
2712
+ const meta = {
2713
+ overrideHaDiscoveryPayload: (payload) => {
2714
+ if (payload.modes !== undefined) {
2715
+ if (payload.modes.includes("heat")) {
2716
+ payload.mode_command_template =
2717
+ `{% set values = { 'auto':'schedule', 'heat':'manual', 'off':'pause' } %}` +
2718
+ `{"operating_mode": "{{ values[value] if value in values.keys() else 'pause' }}"}`;
2719
+ payload.mode_state_template =
2720
+ `{% set values = { 'schedule':'auto', 'manual':'heat', 'pause':'off' } %}` +
2721
+ "{% set value = value_json.operating_mode %}" +
2722
+ `{{ values[value] if value in values.keys() else 'off' }}`;
2723
+ if (payload.temperature_low_command_topic !== undefined) {
2724
+ payload.temperature_command_topic = payload.temperature_low_command_topic;
2725
+ payload.temperature_state_template = payload.temperature_low_state_template;
2726
+ payload.temperature_state_topic = payload.temperature_low_state_topic;
2727
+ removeLowAndHighTemperatureFields(payload);
2728
+ }
2729
+ }
2730
+ else if (payload.modes.includes("cool")) {
2731
+ payload.mode_command_template =
2732
+ `{% set values = { 'auto':'schedule', 'cool':'manual', 'off':'pause' } %}` +
2733
+ `{"operating_mode": "{{ values[value] if value in values.keys() else 'pause' }}"}`;
2734
+ payload.mode_state_template =
2735
+ `{% set values = { 'schedule':'auto', 'manual':'cool', 'pause':'off' } %}` +
2736
+ "{% set value = value_json.operating_mode %}" +
2737
+ `{{ values[value] if value in values.keys() else 'off' }}`;
2738
+ if (payload.temperature_high_command_topic !== undefined) {
2739
+ payload.temperature_command_topic = payload.temperature_high_command_topic;
2740
+ payload.temperature_state_template = payload.temperature_high_state_template;
2741
+ payload.temperature_state_topic = payload.temperature_high_state_topic;
2742
+ removeLowAndHighTemperatureFields(payload);
2743
+ }
2744
+ }
2745
+ payload.modes = ["off", ...payload.modes, "auto"];
2746
+ payload.mode_command_topic = payload.mode_command_topic.replace("/system_mode", "");
2747
+ }
2748
+ },
2749
+ };
2750
+ return {
2751
+ exposes,
2752
+ fromZigbee,
2753
+ toZigbee,
2754
+ configure,
2755
+ meta,
2756
+ isModernExtend: true,
2757
+ };
2758
+ },
2759
+ boostHeating: (args) => {
2760
+ const boostHeatingLookup = {
2761
+ OFF: 0x00,
2762
+ ON: 0x01,
2763
+ };
2764
+ const exposes = [
2765
+ e
2766
+ .binary("boost_heating", ea.ALL, utils.getFromLookupByValue(0x01, boostHeatingLookup), utils.getFromLookupByValue(0x00, boostHeatingLookup))
2767
+ .withLabel("Activate boost heating")
2768
+ .withDescription("Activate boost heating (opens TRV for 5 minutes)"),
2769
+ ];
2770
+ const fromZigbee = [
2771
+ {
2772
+ cluster: "hvacThermostat",
2773
+ type: ["attributeReport", "readResponse"],
2774
+ convert: (model, msg, publish, options, meta) => {
2775
+ const result = {};
2776
+ const data = msg.data;
2777
+ if (data.boostHeating !== undefined) {
2778
+ result.boost_heating = utils.getFromLookupByValue(data.boostHeating, boostHeatingLookup);
2779
+ }
2780
+ return result;
2781
+ },
2782
+ },
2783
+ ];
2784
+ const toZigbee = [
2785
+ {
2786
+ key: ["boost_heating"],
2787
+ convertSet: async (entity, key, value, meta) => {
2788
+ const enableBoostHeating = value === utils.getFromLookupByValue(boostHeatingLookup.ON, boostHeatingLookup);
2789
+ if (enableBoostHeating) {
2790
+ const systemModeNotSetToHeat = "system_mode" in meta.state && meta.state.system_mode !== "heat";
2791
+ if (systemModeNotSetToHeat) {
2792
+ throw new Error("Boost heating is only possible when system mode is set to 'heat'!");
2793
+ }
2794
+ const heaterTypeNotSetToRadiator = "heater_type" in meta.state &&
2795
+ meta.state.heater_type !==
2796
+ utils.getFromLookupByValue(boschThermostatLookup.heaterType.radiator, boschThermostatLookup.heaterType);
2797
+ if (heaterTypeNotSetToRadiator) {
2798
+ throw new Error("Boost heating is only possible when heater type is set to 'radiator'!");
2799
+ }
2800
+ }
2801
+ await entity.write("hvacThermostat", {
2802
+ boostHeating: utils.toNumber(utils.getFromLookup(value, boostHeatingLookup)),
2803
+ });
2804
+ return { state: { boost_heating: value } };
2805
+ },
2806
+ convertGet: async (entity, key, meta) => {
2807
+ await entity.read("hvacThermostat", ["boostHeating"]);
2808
+ },
2809
+ },
2810
+ ];
2811
+ const configure = [
2812
+ m.setupConfigureForReporting("hvacThermostat", "boostHeating", {
2813
+ config: args?.enableReporting ? { min: "MIN", max: "MAX", change: null } : false,
2814
+ access: ea.ALL,
2815
+ }),
2816
+ ];
2817
+ return {
2818
+ exposes,
2819
+ fromZigbee,
2820
+ toZigbee,
2821
+ configure,
2822
+ isModernExtend: true,
2823
+ };
2824
+ },
2825
+ errorState: (args) => {
2826
+ const exposes = [
2827
+ e
2828
+ .text("error_state", ea.STATE_GET)
2829
+ .withDescription("Indicates whether the device encounters any errors or not")
2830
+ .withCategory("diagnostic"),
2831
+ ];
2832
+ const fromZigbee = [
2833
+ {
2834
+ cluster: "hvacThermostat",
2835
+ type: ["attributeReport", "readResponse"],
2836
+ convert: (model, msg, publish, options, meta) => {
2837
+ const result = {};
2838
+ const data = msg.data;
2839
+ if (data.errorState !== undefined) {
2840
+ const receivedErrorState = data.errorState;
2841
+ if (receivedErrorState === 0) {
2842
+ result.error_state = "ok";
2843
+ }
2844
+ else {
2845
+ result.error_state = "";
2846
+ const bitmapLength = (receivedErrorState >>> 0).toString(2).length;
2847
+ for (let errorNumber = 0; errorNumber < bitmapLength; errorNumber++) {
2848
+ if ((receivedErrorState >> errorNumber) & 1) {
2849
+ if (String(result.error_state).length > 0) {
2850
+ result.error_state += " - ";
2851
+ }
2852
+ result.error_state += `E${String(errorNumber + 1).padStart(2, "0")}`;
2853
+ }
2854
+ }
2855
+ }
2856
+ }
2857
+ return result;
2858
+ },
2859
+ },
2860
+ ];
2861
+ const toZigbee = [
2862
+ {
2863
+ key: ["error_state"],
2864
+ convertGet: async (entity, key, meta) => {
2865
+ await entity.read("hvacThermostat", ["errorState"]);
2866
+ },
2867
+ },
2868
+ ];
2869
+ const configure = [
2870
+ m.setupConfigureForReporting("hvacThermostat", "errorState", {
2871
+ config: args?.enableReporting ? { min: "MIN", max: "MAX", change: null } : false,
2872
+ access: ea.STATE_GET,
2873
+ }),
2874
+ ];
2875
+ return {
2876
+ exposes,
2877
+ fromZigbee,
2878
+ toZigbee,
2879
+ configure,
2880
+ isModernExtend: true,
2881
+ };
2882
+ },
2883
+ valveAdaptation: () => {
2884
+ const valveAdaptStatusLookup = {
2885
+ none: 0x00,
2886
+ ready_to_calibrate: 0x01,
2887
+ calibration_in_progress: 0x02,
2888
+ error: 0x03,
2889
+ success: 0x04,
2890
+ };
2891
+ const triggerValveAdaptation = async (state, endpoint, throwError = true) => {
2892
+ let adaptStatus;
2893
+ try {
2894
+ adaptStatus = utils.getFromLookup(state.valve_adapt_status, valveAdaptStatusLookup);
2895
+ }
2896
+ catch {
2897
+ adaptStatus = valveAdaptStatusLookup.none;
2898
+ }
2899
+ switch (adaptStatus) {
2900
+ case valveAdaptStatusLookup.ready_to_calibrate:
2901
+ case valveAdaptStatusLookup.error:
2902
+ await endpoint.command("hvacThermostat", "calibrateValve", {}, exports.manufacturerOptions);
2903
+ break;
2904
+ default:
2905
+ if (throwError) {
2906
+ throw new Error("Valve adaptation process not possible right now!");
2907
+ }
2908
+ }
2909
+ };
2910
+ const exposes = [
2911
+ e
2912
+ .enum("valve_adapt_status", ea.STATE_GET, Object.keys(valveAdaptStatusLookup))
2913
+ .withLabel("Valve adaptation status")
2914
+ .withDescription("Specifies the current status of the valve adaptation")
2915
+ .withCategory("diagnostic"),
2916
+ e
2917
+ .binary("automatic_valve_adapt", ea.STATE_GET, true, false)
2918
+ .withLabel("Automatic valve adaptation requested")
2919
+ .withDescription("Specifies if an automatic valve adaptation is being requested by the thermostat " +
2920
+ "(for example after a successful firmware upgrade). If this is the case, the " +
2921
+ "valve adaptation will be automatically started as soon as the adaptation status " +
2922
+ "is 'ready_to_calibrate' or 'error'.")
2923
+ .withCategory("diagnostic"),
2924
+ e
2925
+ .enum("valve_adapt_process", ea.SET, ["adapt"])
2926
+ .withLabel("Trigger adaptation process")
2927
+ .withDescription("Trigger the valve adaptation process. Only possible when the adaptation status is 'ready_to_calibrate' or 'error'.")
2928
+ .withCategory("config"),
2929
+ ];
2930
+ const fromZigbee = [
2931
+ {
2932
+ cluster: "hvacThermostat",
2933
+ type: ["attributeReport", "readResponse"],
2934
+ convert: async (model, msg, publish, options, meta) => {
2935
+ const result = {};
2936
+ const data = msg.data;
2937
+ if (data.valveAdaptStatus !== undefined) {
2938
+ result.valve_adapt_status = utils.getFromLookupByValue(data.valveAdaptStatus, valveAdaptStatusLookup);
2939
+ const automaticValveAdapt = meta.state.automatic_valve_adapt ?? false;
2940
+ if (automaticValveAdapt === true) {
2941
+ await triggerValveAdaptation(meta.state, msg.endpoint, false);
2942
+ }
2943
+ }
2944
+ if (data.automaticValveAdapt !== undefined) {
2945
+ result.automatic_valve_adapt = !!data.automaticValveAdapt;
2946
+ }
2947
+ return result;
2948
+ },
2949
+ },
2950
+ ];
2951
+ const toZigbee = [
2952
+ {
2953
+ key: ["valve_adapt_status", "automatic_valve_adapt", "valve_adapt_process"],
2954
+ convertSet: async (entity, key, value, meta) => {
2955
+ if (key === "valve_adapt_process") {
2956
+ await triggerValveAdaptation(meta.state, entity);
2957
+ }
2958
+ },
2959
+ convertGet: async (entity, key, meta) => {
2960
+ if (key === "valve_adapt_status") {
2961
+ await entity.read("hvacThermostat", ["valveAdaptStatus"]);
2962
+ }
2963
+ if (key === "automatic_valve_adapt") {
2964
+ await entity.read("hvacThermostat", ["automaticValveAdapt"]);
2965
+ }
2966
+ },
2967
+ },
2968
+ ];
2969
+ const configure = [
2970
+ m.setupConfigureForReporting("hvacThermostat", "valveAdaptStatus", {
2971
+ config: { min: "MIN", max: "MAX", change: null },
2972
+ access: ea.STATE_GET,
2973
+ }),
2974
+ m.setupConfigureForReading("hvacThermostat", ["automaticValveAdapt"]),
2975
+ ];
2976
+ return {
2977
+ exposes,
2978
+ fromZigbee,
2979
+ toZigbee,
2980
+ configure,
2981
+ isModernExtend: true,
2982
+ };
2983
+ },
2984
+ };
2071
2985
  //endregion
2072
2986
  //# sourceMappingURL=bosch.js.map