zigbee-herdsman-converters 25.44.0 → 25.46.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 (64) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/dist/devices/bosch.d.ts.map +1 -1
  3. package/dist/devices/bosch.js +64 -514
  4. package/dist/devices/bosch.js.map +1 -1
  5. package/dist/devices/box.d.ts +3 -0
  6. package/dist/devices/box.d.ts.map +1 -0
  7. package/dist/devices/box.js +370 -0
  8. package/dist/devices/box.js.map +1 -0
  9. package/dist/devices/elko.js +4 -4
  10. package/dist/devices/elko.js.map +1 -1
  11. package/dist/devices/gewiss.js +4 -4
  12. package/dist/devices/gewiss.js.map +1 -1
  13. package/dist/devices/index.d.ts.map +1 -1
  14. package/dist/devices/index.js +2 -0
  15. package/dist/devices/index.js.map +1 -1
  16. package/dist/devices/innr.d.ts.map +1 -1
  17. package/dist/devices/innr.js +8 -1
  18. package/dist/devices/innr.js.map +1 -1
  19. package/dist/devices/lumi.d.ts.map +1 -1
  20. package/dist/devices/lumi.js +13 -6
  21. package/dist/devices/lumi.js.map +1 -1
  22. package/dist/devices/nordtronic.d.ts.map +1 -1
  23. package/dist/devices/nordtronic.js +9 -0
  24. package/dist/devices/nordtronic.js.map +1 -1
  25. package/dist/devices/paulmann.d.ts.map +1 -1
  26. package/dist/devices/paulmann.js +11 -4
  27. package/dist/devices/paulmann.js.map +1 -1
  28. package/dist/devices/philips.d.ts.map +1 -1
  29. package/dist/devices/philips.js +6 -9
  30. package/dist/devices/philips.js.map +1 -1
  31. package/dist/devices/sengled.js +1 -1
  32. package/dist/devices/sengled.js.map +1 -1
  33. package/dist/devices/sonoff.d.ts.map +1 -1
  34. package/dist/devices/sonoff.js +9 -0
  35. package/dist/devices/sonoff.js.map +1 -1
  36. package/dist/devices/third_reality.d.ts.map +1 -1
  37. package/dist/devices/third_reality.js +23 -2
  38. package/dist/devices/third_reality.js.map +1 -1
  39. package/dist/devices/tuya.d.ts.map +1 -1
  40. package/dist/devices/tuya.js +279 -6
  41. package/dist/devices/tuya.js.map +1 -1
  42. package/dist/devices/wirenboard.js +1 -1
  43. package/dist/devices/wirenboard.js.map +1 -1
  44. package/dist/lib/bosch.d.ts +107 -3
  45. package/dist/lib/bosch.d.ts.map +1 -1
  46. package/dist/lib/bosch.js +757 -26
  47. package/dist/lib/bosch.js.map +1 -1
  48. package/dist/lib/exposes.d.ts +0 -1
  49. package/dist/lib/exposes.d.ts.map +1 -1
  50. package/dist/lib/exposes.js +4 -8
  51. package/dist/lib/exposes.js.map +1 -1
  52. package/dist/lib/generateDefinition.js +2 -2
  53. package/dist/lib/generateDefinition.js.map +1 -1
  54. package/dist/lib/modernExtend.d.ts +28 -10
  55. package/dist/lib/modernExtend.d.ts.map +1 -1
  56. package/dist/lib/modernExtend.js +71 -24
  57. package/dist/lib/modernExtend.js.map +1 -1
  58. package/dist/lib/tuya.d.ts.map +1 -1
  59. package/dist/lib/tuya.js +2 -2
  60. package/dist/lib/tuya.js.map +1 -1
  61. package/dist/lib/utils.js +3 -3
  62. package/dist/lib/utils.js.map +1 -1
  63. package/dist/models-index.json +1 -1
  64. package/package.json +4 -4
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.boschBsenExtend = exports.boschDoorWindowContactExtend = exports.boschBsirExtend = exports.boschBmctExtend = 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"));
@@ -98,10 +98,20 @@ exports.boschGeneralExtend = {
98
98
  isModernExtend: true,
99
99
  };
100
100
  },
101
+ /** Some Bosch devices ask the coordinator for their ZCL version
102
+ * during deviceAnnouncement. Without answer, these devices regularly
103
+ * re-join the network. To avoid that, we have to make sure that a readRequest
104
+ * for the zclVersion is always being answered. The answered zclVersion is
105
+ * taken from the Bosch Smart Home Controller II.
106
+ *
107
+ * Exception: BTH-RM and BTH-RM230Z ask the coordinator at regular
108
+ * intervals for their zclVersion (maybe availability check like Z2M does?)
109
+ * and *not* during interview! To avoid code-duplication, we handle that
110
+ * case here as well. */
101
111
  handleZclVersionReadRequest: () => {
102
112
  const onEvent = [
103
113
  (event) => {
104
- if (event.type !== "deviceAnnounce") {
114
+ if (event.type !== "start") {
105
115
  return;
106
116
  }
107
117
  event.data.device.customReadResponse = (frame, endpoint) => {
@@ -124,7 +134,7 @@ exports.boschGeneralExtend = {
124
134
  isModernExtend: true,
125
135
  };
126
136
  },
127
- customSeMeteringCluster: () => m.deviceAddCustomCluster("seMetering", {
137
+ customMeteringCluster: () => m.deviceAddCustomCluster("seMetering", {
128
138
  ID: zigbee_herdsman_1.Zcl.Clusters.seMetering.ID,
129
139
  attributes: {},
130
140
  commands: {
@@ -156,6 +166,13 @@ exports.boschGeneralExtend = {
156
166
  isModernExtend: true,
157
167
  };
158
168
  },
169
+ batteryWithPercentageAndLowStatus: (args) => m.battery({
170
+ percentage: true,
171
+ percentageReportingConfig: false,
172
+ lowStatus: true,
173
+ lowStatusReportingConfig: { min: "MIN", max: "MAX", change: null },
174
+ ...args,
175
+ }),
159
176
  autoOff: (args) => {
160
177
  const { endpoint } = args ?? {};
161
178
  const offOnLookup = {
@@ -1183,20 +1200,6 @@ exports.boschBsirExtend = {
1183
1200
  },
1184
1201
  access: "STATE_GET",
1185
1202
  }),
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
1203
  lightDelay: () => m.numeric({
1201
1204
  name: "light_delay",
1202
1205
  cluster: "ssIasWd",
@@ -1415,11 +1418,6 @@ exports.boschDoorWindowContactExtend = {
1415
1418
  commands: {},
1416
1419
  commandsResponse: {},
1417
1420
  }),
1418
- battery: () => m.battery({
1419
- percentage: true,
1420
- lowStatus: true,
1421
- lowStatusReportingConfig: { min: "MIN", max: "MAX", change: 0 },
1422
- }),
1423
1421
  reportContactState: () => m.iasZoneAlarm({
1424
1422
  zoneType: "contact",
1425
1423
  zoneAttributes: ["alarm_1"],
@@ -1665,9 +1663,12 @@ exports.boschDoorWindowContactExtend = {
1665
1663
  // Bosch Smart Home Controller II as of 19-09-2025. Looks like
1666
1664
  // the default value was too high, and they didn't want to
1667
1665
  // push a firmware update. We mimic it here to avoid complaints.
1668
- await endpoint.write("boschDoorWindowContactCluster", {
1669
- vibrationDetectionSensitivity: vibrationDetectionSensitivityLookup.medium,
1670
- });
1666
+ if (device.meta.newDefaultSensitivityApplied === undefined) {
1667
+ await endpoint.write("boschDoorWindowContactCluster", {
1668
+ vibrationDetectionSensitivity: vibrationDetectionSensitivityLookup.medium,
1669
+ });
1670
+ device.meta.newDefaultSensitivityApplied = true;
1671
+ }
1671
1672
  // The write request is made when using the proprietary
1672
1673
  // Bosch Smart Home Controller II as of 19-09-2025. I have
1673
1674
  // no idea what it does, but we mimic it here in case it
@@ -1709,7 +1710,7 @@ exports.boschBsenExtend = {
1709
1710
  voltageReporting: true,
1710
1711
  voltageToPercentage: { min: 2500, max: 3000 },
1711
1712
  lowStatus: true,
1712
- lowStatusReportingConfig: { min: "MIN", max: "MAX", change: 0 },
1713
+ lowStatusReportingConfig: { min: "MIN", max: "MAX", change: null },
1713
1714
  }),
1714
1715
  illuminance: () => m.illuminance({ reporting: { min: "1_SECOND", max: 600, change: 3522 } }),
1715
1716
  // The temperature sensor isn't used at all by Bosch on the BSEN-M.
@@ -2065,5 +2066,735 @@ exports.boschSmartPlugExtend = {
2065
2066
  ...args,
2066
2067
  }),
2067
2068
  };
2069
+ const boschThermostatLookup = {
2070
+ systemModes: {
2071
+ heat: 0x04,
2072
+ cool: 0x03,
2073
+ },
2074
+ raRunningStates: ["idle", "heat"],
2075
+ heaterType: {
2076
+ underfloor_heating: 0x00,
2077
+ radiator: 0x02,
2078
+ central_heating: 0x01,
2079
+ manual_control: 0x03,
2080
+ },
2081
+ };
2082
+ exports.boschThermostatExtend = {
2083
+ customThermostatCluster: () => m.deviceAddCustomCluster("hvacThermostat", {
2084
+ ID: zigbee_herdsman_1.Zcl.Clusters.hvacThermostat.ID,
2085
+ attributes: {
2086
+ operatingMode: { ID: 0x4007, type: zigbee_herdsman_1.Zcl.DataType.ENUM8, manufacturerCode: exports.manufacturerOptions.manufacturerCode },
2087
+ heatingDemand: { ID: 0x4020, type: zigbee_herdsman_1.Zcl.DataType.ENUM8, manufacturerCode: exports.manufacturerOptions.manufacturerCode },
2088
+ valveAdaptStatus: { ID: 0x4022, type: zigbee_herdsman_1.Zcl.DataType.ENUM8, manufacturerCode: exports.manufacturerOptions.manufacturerCode },
2089
+ unknownAttribute0: { ID: 0x4025, type: zigbee_herdsman_1.Zcl.DataType.ENUM8, manufacturerCode: exports.manufacturerOptions.manufacturerCode },
2090
+ remoteTemperature: { ID: 0x4040, type: zigbee_herdsman_1.Zcl.DataType.INT16, manufacturerCode: exports.manufacturerOptions.manufacturerCode },
2091
+ unknownAttribute1: { ID: 0x4041, type: zigbee_herdsman_1.Zcl.DataType.ENUM8, manufacturerCode: exports.manufacturerOptions.manufacturerCode },
2092
+ windowOpenMode: { ID: 0x4042, type: zigbee_herdsman_1.Zcl.DataType.ENUM8, manufacturerCode: exports.manufacturerOptions.manufacturerCode },
2093
+ boostHeating: { ID: 0x4043, type: zigbee_herdsman_1.Zcl.DataType.ENUM8, manufacturerCode: exports.manufacturerOptions.manufacturerCode },
2094
+ cableSensorTemperature: { ID: 0x4052, type: zigbee_herdsman_1.Zcl.DataType.INT16, manufacturerCode: exports.manufacturerOptions.manufacturerCode },
2095
+ valveType: { ID: 0x4060, type: zigbee_herdsman_1.Zcl.DataType.ENUM8, manufacturerCode: exports.manufacturerOptions.manufacturerCode },
2096
+ unknownAttribute2: { ID: 0x4061, type: zigbee_herdsman_1.Zcl.DataType.ENUM8, manufacturerCode: exports.manufacturerOptions.manufacturerCode },
2097
+ cableSensorMode: { ID: 0x4062, type: zigbee_herdsman_1.Zcl.DataType.ENUM8, manufacturerCode: exports.manufacturerOptions.manufacturerCode },
2098
+ heaterType: { ID: 0x4063, type: zigbee_herdsman_1.Zcl.DataType.ENUM8, manufacturerCode: exports.manufacturerOptions.manufacturerCode },
2099
+ errorState: { ID: 0x5000, type: zigbee_herdsman_1.Zcl.DataType.BITMAP8, manufacturerCode: exports.manufacturerOptions.manufacturerCode },
2100
+ automaticValveAdapt: { ID: 0x5010, type: zigbee_herdsman_1.Zcl.DataType.ENUM8, manufacturerCode: exports.manufacturerOptions.manufacturerCode },
2101
+ },
2102
+ commands: {
2103
+ calibrateValve: { ID: 0x41, parameters: [] },
2104
+ },
2105
+ commandsResponse: {},
2106
+ }),
2107
+ customUserInterfaceCfgCluster: () => m.deviceAddCustomCluster("hvacUserInterfaceCfg", {
2108
+ ID: zigbee_herdsman_1.Zcl.Clusters.hvacUserInterfaceCfg.ID,
2109
+ attributes: {
2110
+ displayOrientation: { ID: 0x400b, type: zigbee_herdsman_1.Zcl.DataType.UINT8, manufacturerCode: exports.manufacturerOptions.manufacturerCode },
2111
+ activityLed: { ID: 0x4033, type: zigbee_herdsman_1.Zcl.DataType.ENUM8, manufacturerCode: exports.manufacturerOptions.manufacturerCode },
2112
+ displayedTemperature: { ID: 0x4039, type: zigbee_herdsman_1.Zcl.DataType.ENUM8, manufacturerCode: exports.manufacturerOptions.manufacturerCode },
2113
+ displaySwitchOnDuration: { ID: 0x403a, type: zigbee_herdsman_1.Zcl.DataType.ENUM8, manufacturerCode: exports.manufacturerOptions.manufacturerCode },
2114
+ displayBrightness: { ID: 0x403b, type: zigbee_herdsman_1.Zcl.DataType.ENUM8, manufacturerCode: exports.manufacturerOptions.manufacturerCode },
2115
+ },
2116
+ commands: {},
2117
+ commandsResponse: {},
2118
+ }),
2119
+ relayState: () => m.onOff({ description: "The state of the relay controlling the connected heating/cooling device", powerOnBehavior: false }),
2120
+ cableSensorMode: () => m.enumLookup({
2121
+ name: "cable_sensor_mode",
2122
+ cluster: "hvacThermostat",
2123
+ attribute: "cableSensorMode",
2124
+ description: 'Select a configuration for the sensor connection. If you select "with_regulation", ' +
2125
+ "the measured temperature on the cable sensor is used by the heating/cooling algorithm " +
2126
+ "instead of the local temperature.",
2127
+ lookup: { not_used: 0x00, cable_sensor_without_regulation: 0xb0, cable_sensor_with_regulation: 0xb1 },
2128
+ reporting: false,
2129
+ entityCategory: "config",
2130
+ }),
2131
+ cableSensorTemperature: () => m.numeric({
2132
+ name: "cable_sensor_temperature",
2133
+ cluster: "hvacThermostat",
2134
+ attribute: "cableSensorTemperature",
2135
+ description: "Measured temperature value on the cable sensor (if enabled)",
2136
+ unit: "°C",
2137
+ scale: 100,
2138
+ reporting: { min: 30, max: "MAX", change: 20 },
2139
+ access: "STATE_GET",
2140
+ }),
2141
+ heaterType: () => m.enumLookup({
2142
+ name: "heater_type",
2143
+ cluster: "hvacThermostat",
2144
+ attribute: "heaterType",
2145
+ description: "Select the connected heater type or 'manual_control' if you like to activate the relay manually when necessary",
2146
+ lookup: boschThermostatLookup.heaterType,
2147
+ reporting: false,
2148
+ entityCategory: "config",
2149
+ }),
2150
+ valveType: () => m.enumLookup({
2151
+ name: "valve_type",
2152
+ cluster: "hvacThermostat",
2153
+ attribute: "valveType",
2154
+ description: "Select the connected valve type",
2155
+ lookup: { normally_closed: 0x00, normally_open: 0x01 },
2156
+ reporting: false,
2157
+ entityCategory: "config",
2158
+ }),
2159
+ humidity: () => m.humidity({ reporting: false }),
2160
+ windowOpenMode: (args) => m.binary({
2161
+ name: "window_open_mode",
2162
+ cluster: "hvacThermostat",
2163
+ attribute: "windowOpenMode",
2164
+ description: "Activates the window open mode, where the thermostat disables any heating/cooling " +
2165
+ "to prevent unnecessary energy consumption. Please keep in mind that the device " +
2166
+ "itself does not detect any open windows!",
2167
+ valueOn: ["ON", 0x01],
2168
+ valueOff: ["OFF", 0x00],
2169
+ reporting: args?.enableReporting ? { min: "MIN", max: "MAX", change: null } : false,
2170
+ }),
2171
+ childLock: () => m.binary({
2172
+ name: "child_lock",
2173
+ cluster: "hvacUserInterfaceCfg",
2174
+ attribute: "keypadLockout",
2175
+ description: "Enables/disables physical input on the thermostat",
2176
+ valueOn: ["LOCK", 0x01],
2177
+ valueOff: ["UNLOCK", 0x00],
2178
+ reporting: { min: "MIN", max: "MAX", change: null },
2179
+ }),
2180
+ displayBrightness: () => m.numeric({
2181
+ name: "display_brightness",
2182
+ cluster: "hvacUserInterfaceCfg",
2183
+ attribute: "displayBrightness",
2184
+ description: "Sets brightness of the display",
2185
+ valueMin: 0,
2186
+ valueMax: 100,
2187
+ valueStep: 10,
2188
+ unit: "%",
2189
+ scale: 0.1,
2190
+ reporting: false,
2191
+ entityCategory: "config",
2192
+ }),
2193
+ displaySwitchOnDuration: () => m.numeric({
2194
+ name: "display_switch_on_duration",
2195
+ cluster: "hvacUserInterfaceCfg",
2196
+ attribute: "displaySwitchOnDuration",
2197
+ label: "Display switch-on duration",
2198
+ description: "Sets the time before the display is automatically switched off",
2199
+ valueMin: 5,
2200
+ valueMax: 30,
2201
+ unit: "s",
2202
+ reporting: false,
2203
+ entityCategory: "config",
2204
+ }),
2205
+ displayOrientation: () => m.enumLookup({
2206
+ name: "display_orientation",
2207
+ cluster: "hvacUserInterfaceCfg",
2208
+ attribute: "displayOrientation",
2209
+ description: "You can rotate the display content by 180° here. This is recommended if your thermostat is fitted vertically, for instance.",
2210
+ lookup: { standard_arrangement: 0x00, rotated_by_180_degrees: 0x01 },
2211
+ reporting: false,
2212
+ entityCategory: "config",
2213
+ }),
2214
+ displayedTemperature: () => m.enumLookup({
2215
+ name: "displayed_temperature",
2216
+ cluster: "hvacUserInterfaceCfg",
2217
+ attribute: "displayedTemperature",
2218
+ description: "Select which temperature should be displayed on your radiator thermostat display",
2219
+ lookup: { set_temperature: 0x00, measured_temperature: 0x01 },
2220
+ reporting: false,
2221
+ entityCategory: "config",
2222
+ }),
2223
+ activityLedState: () => m.enumLookup({
2224
+ name: "activity_led",
2225
+ cluster: "hvacUserInterfaceCfg",
2226
+ attribute: "activityLed",
2227
+ label: "Activity LED state",
2228
+ description: "Determines the state of the little dot on the display next to the heating/cooling symbol",
2229
+ lookup: { off: 0x00, auto: 0x01, on: 0x02 },
2230
+ reporting: false,
2231
+ entityCategory: "config",
2232
+ }),
2233
+ remoteTemperature: () => m.numeric({
2234
+ name: "remote_temperature",
2235
+ cluster: "hvacThermostat",
2236
+ attribute: "remoteTemperature",
2237
+ description: "Input for remote temperature sensor. Required at least every 30 minutes to prevent fallback to the internal sensor!",
2238
+ valueMin: 0.0,
2239
+ valueMax: 35.0,
2240
+ valueStep: 0.2,
2241
+ unit: "°C",
2242
+ scale: 100,
2243
+ reporting: false,
2244
+ entityCategory: "config",
2245
+ }),
2246
+ setpointChangeSource: (args) => m.enumLookup({
2247
+ name: "setpoint_change_source",
2248
+ cluster: "hvacThermostat",
2249
+ attribute: "setpointChangeSource",
2250
+ description: "Source of the current setpoint temperature",
2251
+ lookup: { manual: 0x00, schedule: 0x01, externally: 0x02 },
2252
+ access: "STATE_GET",
2253
+ reporting: args?.enableReporting ? { min: "MIN", max: "MAX", change: null } : false,
2254
+ entityCategory: "diagnostic",
2255
+ }),
2256
+ customHeatingDemand: () => m.numeric({
2257
+ name: "pi_heating_demand",
2258
+ cluster: "hvacThermostat",
2259
+ attribute: "heatingDemand",
2260
+ label: "PI heating demand",
2261
+ description: "Position of the valve (= demanded heat) where 0% is fully closed and 100% is fully open",
2262
+ unit: "%",
2263
+ valueMin: 0,
2264
+ valueMax: 100,
2265
+ access: "ALL",
2266
+ reporting: { min: "MIN", max: "MAX", change: null },
2267
+ }),
2268
+ rmBattery: () => m.battery({
2269
+ percentage: true,
2270
+ percentageReporting: false,
2271
+ voltage: true,
2272
+ voltageReporting: true,
2273
+ voltageReportingConfig: false,
2274
+ voltageToPercentage: { min: 4400, max: 6400 },
2275
+ lowStatus: true,
2276
+ lowStatusReportingConfig: { min: "MIN", max: "MAX", change: null },
2277
+ }),
2278
+ rmThermostat: () => {
2279
+ const thermostat = m.thermostat({
2280
+ localTemperature: {
2281
+ configure: { reporting: false },
2282
+ },
2283
+ localTemperatureCalibration: {
2284
+ values: { min: -5, max: 5, step: 0.1 },
2285
+ configure: { reporting: false },
2286
+ },
2287
+ setpoints: {
2288
+ values: {
2289
+ occupiedHeatingSetpoint: { min: 5, max: 30, step: 0.5 },
2290
+ occupiedCoolingSetpoint: { min: 5, max: 30, step: 0.5 },
2291
+ },
2292
+ configure: { reporting: false },
2293
+ },
2294
+ systemMode: {
2295
+ values: ["heat", "cool"],
2296
+ toZigbee: { skip: true },
2297
+ configure: { skip: true },
2298
+ },
2299
+ runningState: {
2300
+ values: ["idle", "heat", "cool"],
2301
+ configure: { reporting: false },
2302
+ },
2303
+ });
2304
+ const expose = (device, options) => {
2305
+ const returnedThermostat = thermostat.exposes;
2306
+ if (utils.isDummyDevice(device)) {
2307
+ return returnedThermostat;
2308
+ }
2309
+ let currentSystemMode;
2310
+ try {
2311
+ currentSystemMode = utils.getFromLookupByValue(device.getEndpoint(1).getClusterAttributeValue("hvacThermostat", "systemMode"), boschThermostatLookup.systemModes);
2312
+ }
2313
+ catch {
2314
+ currentSystemMode = "heat";
2315
+ }
2316
+ // The thermostat is a singleton, thus the values must be set
2317
+ // manually as filtering will lead to an array without
2318
+ // heat/cool in them after two systemMode changes.
2319
+ returnedThermostat[0].features.forEach((exposedAttribute, index, array) => {
2320
+ if (exposedAttribute.type === "enum") {
2321
+ if (exposedAttribute.name === "system_mode") {
2322
+ exposedAttribute.label = "Active system mode";
2323
+ exposedAttribute.description =
2324
+ "Currently used system mode by the thermostat. This field is primarily " +
2325
+ "used to configure the thermostat in Home Assistant correctly.";
2326
+ exposedAttribute.values = [currentSystemMode];
2327
+ exposedAttribute.access = ea.STATE;
2328
+ }
2329
+ if (exposedAttribute.name === "running_state") {
2330
+ exposedAttribute.values = ["idle", currentSystemMode];
2331
+ }
2332
+ }
2333
+ });
2334
+ return returnedThermostat;
2335
+ };
2336
+ return {
2337
+ exposes: [expose],
2338
+ fromZigbee: thermostat.fromZigbee,
2339
+ toZigbee: thermostat.toZigbee,
2340
+ configure: thermostat.configure,
2341
+ isModernExtend: true,
2342
+ };
2343
+ },
2344
+ customSystemMode: () => {
2345
+ const exposes = [
2346
+ e
2347
+ .enum("custom_system_mode", ea.ALL, Object.keys(boschThermostatLookup.systemModes))
2348
+ .withLabel("Available system modes")
2349
+ .withDescription("Select if the thermostat is connected to a heating or a cooling device")
2350
+ .withCategory("config"),
2351
+ ];
2352
+ const fromZigbee = [
2353
+ {
2354
+ cluster: "hvacThermostat",
2355
+ type: ["attributeReport", "readResponse"],
2356
+ convert: (model, msg, publish, options, meta) => {
2357
+ const result = {};
2358
+ const data = msg.data;
2359
+ if (data.systemMode !== undefined) {
2360
+ result.custom_system_mode = utils.getFromLookupByValue(data.systemMode, boschThermostatLookup.systemModes);
2361
+ meta.deviceExposesChanged();
2362
+ }
2363
+ return result;
2364
+ },
2365
+ },
2366
+ ];
2367
+ const toZigbee = [
2368
+ {
2369
+ key: ["custom_system_mode"],
2370
+ convertSet: async (entity, key, value, meta) => {
2371
+ await entity.write("hvacThermostat", {
2372
+ systemMode: utils.toNumber(utils.getFromLookup(value, boschThermostatLookup.systemModes)),
2373
+ });
2374
+ return { state: { custom_system_mode: value } };
2375
+ },
2376
+ convertGet: async (entity, key, meta) => {
2377
+ await entity.read("hvacThermostat", ["systemMode"]);
2378
+ },
2379
+ },
2380
+ ];
2381
+ const configure = [
2382
+ m.setupConfigureForReporting("hvacThermostat", "systemMode", {
2383
+ config: false,
2384
+ access: ea.ALL,
2385
+ }),
2386
+ ];
2387
+ return {
2388
+ exposes,
2389
+ fromZigbee,
2390
+ toZigbee,
2391
+ configure,
2392
+ isModernExtend: true,
2393
+ };
2394
+ },
2395
+ raThermostat: () => {
2396
+ // Native thermostat
2397
+ const thermostat = m.thermostat({
2398
+ localTemperature: {
2399
+ values: {
2400
+ description: "Temperature used by the heating algorithm. This is the " +
2401
+ "temperature measured on the device (by default) or the " +
2402
+ "remote temperature (if set within the last 30 min).",
2403
+ },
2404
+ configure: {
2405
+ reporting: { min: 30, max: 900, change: 20 },
2406
+ },
2407
+ },
2408
+ localTemperatureCalibration: {
2409
+ values: { min: -5, max: 5, step: 0.1 },
2410
+ configure: { reporting: false },
2411
+ },
2412
+ setpoints: {
2413
+ values: {
2414
+ occupiedHeatingSetpoint: { min: 5, max: 30, step: 0.5 },
2415
+ },
2416
+ configure: {
2417
+ reporting: { min: "MIN", max: "MAX", change: 1 },
2418
+ },
2419
+ },
2420
+ systemMode: {
2421
+ values: ["heat"],
2422
+ configure: {
2423
+ reporting: false,
2424
+ },
2425
+ },
2426
+ runningState: {
2427
+ values: boschThermostatLookup.raRunningStates,
2428
+ toZigbee: {
2429
+ skip: true,
2430
+ },
2431
+ configure: {
2432
+ reporting: false,
2433
+ },
2434
+ },
2435
+ piHeatingDemand: {
2436
+ values: ea.ALL,
2437
+ toZigbee: {
2438
+ skip: true,
2439
+ },
2440
+ configure: {
2441
+ skip: true,
2442
+ },
2443
+ },
2444
+ });
2445
+ const exposes = thermostat.exposes;
2446
+ const fromZigbee = thermostat.fromZigbee;
2447
+ const toZigbee = thermostat.toZigbee;
2448
+ let configure = thermostat.configure;
2449
+ // Add converters for custom running state
2450
+ const runningState = exports.boschThermostatExtend.customRunningState();
2451
+ fromZigbee.push(...runningState.fromZigbee);
2452
+ toZigbee.push(...runningState.toZigbee);
2453
+ // Add converters and configure for custom heating demand
2454
+ const piHeatingDemand = exports.boschThermostatExtend.customHeatingDemand();
2455
+ fromZigbee.push(...piHeatingDemand.fromZigbee);
2456
+ toZigbee.push(...piHeatingDemand.toZigbee);
2457
+ configure = [...configure, ...piHeatingDemand.configure];
2458
+ return {
2459
+ exposes,
2460
+ fromZigbee,
2461
+ toZigbee,
2462
+ configure,
2463
+ isModernExtend: true,
2464
+ };
2465
+ },
2466
+ customRunningState: () => {
2467
+ const fromZigbee = [
2468
+ {
2469
+ cluster: "hvacThermostat",
2470
+ type: ["attributeReport", "readResponse"],
2471
+ convert: (model, msg, publish, options, meta) => {
2472
+ const result = {};
2473
+ const data = msg.data;
2474
+ if (data.heatingDemand !== undefined) {
2475
+ result.running_state =
2476
+ utils.toNumber(data.heatingDemand) > 0
2477
+ ? boschThermostatLookup.raRunningStates[1]
2478
+ : boschThermostatLookup.raRunningStates[0];
2479
+ }
2480
+ return result;
2481
+ },
2482
+ },
2483
+ ];
2484
+ const toZigbee = [
2485
+ {
2486
+ key: ["running_state"],
2487
+ convertGet: async (entity, key, meta) => {
2488
+ await entity.read("hvacThermostat", ["heatingDemand"]);
2489
+ },
2490
+ },
2491
+ ];
2492
+ return {
2493
+ fromZigbee,
2494
+ toZigbee,
2495
+ isModernExtend: true,
2496
+ };
2497
+ },
2498
+ operatingMode: (args) => {
2499
+ const operatingModeLookup = { schedule: 0x00, manual: 0x01, pause: 0x05 };
2500
+ const operatingMode = m.enumLookup({
2501
+ name: "operating_mode",
2502
+ cluster: "hvacThermostat",
2503
+ attribute: "operatingMode",
2504
+ description: "Bosch-specific operating mode. This is being used as mode on the exposed thermostat when using Home Assistant.",
2505
+ lookup: operatingModeLookup,
2506
+ reporting: args?.enableReporting ? { min: "MIN", max: "MAX", change: null } : false,
2507
+ entityCategory: "config",
2508
+ });
2509
+ const exposes = operatingMode.exposes;
2510
+ const fromZigbee = operatingMode.fromZigbee;
2511
+ const toZigbee = operatingMode.toZigbee;
2512
+ const configure = operatingMode.configure;
2513
+ const removeLowAndHighTemperatureFields = (payload) => {
2514
+ payload.temperature_high_command_topic = undefined;
2515
+ payload.temperature_low_command_topic = undefined;
2516
+ payload.temperature_high_state_template = undefined;
2517
+ payload.temperature_low_state_template = undefined;
2518
+ payload.temperature_high_state_topic = undefined;
2519
+ payload.temperature_low_state_topic = undefined;
2520
+ };
2521
+ // Override the payload send to Home Assistant to achieve the following:
2522
+ // 1. Use the Bosch operating mode instead of system modes
2523
+ // See: https://github.com/Koenkk/zigbee2mqtt/pull/23075#issue-2355829475
2524
+ // 2. Remove setpoints not compatible with the currently used system mode
2525
+ // See: https://github.com/Koenkk/zigbee2mqtt/issues/28892
2526
+ const meta = {
2527
+ overrideHaDiscoveryPayload: (payload) => {
2528
+ if (payload.modes !== undefined) {
2529
+ if (payload.modes.includes("heat")) {
2530
+ payload.mode_command_template =
2531
+ `{% set values = { 'auto':'schedule', 'heat':'manual', 'off':'pause' } %}` +
2532
+ `{"operating_mode": "{{ values[value] if value in values.keys() else 'pause' }}"}`;
2533
+ payload.mode_state_template =
2534
+ `{% set values = { 'schedule':'auto', 'manual':'heat', 'pause':'off' } %}` +
2535
+ "{% set value = value_json.operating_mode %}" +
2536
+ `{{ values[value] if value in values.keys() else 'off' }}`;
2537
+ if (payload.temperature_low_command_topic !== undefined) {
2538
+ payload.temperature_command_topic = payload.temperature_low_command_topic;
2539
+ payload.temperature_state_template = payload.temperature_low_state_template;
2540
+ payload.temperature_state_topic = payload.temperature_low_state_topic;
2541
+ removeLowAndHighTemperatureFields(payload);
2542
+ }
2543
+ }
2544
+ else if (payload.modes.includes("cool")) {
2545
+ payload.mode_command_template =
2546
+ `{% set values = { 'auto':'schedule', 'cool':'manual', 'off':'pause' } %}` +
2547
+ `{"operating_mode": "{{ values[value] if value in values.keys() else 'pause' }}"}`;
2548
+ payload.mode_state_template =
2549
+ `{% set values = { 'schedule':'auto', 'manual':'cool', 'pause':'off' } %}` +
2550
+ "{% set value = value_json.operating_mode %}" +
2551
+ `{{ values[value] if value in values.keys() else 'off' }}`;
2552
+ if (payload.temperature_high_command_topic !== undefined) {
2553
+ payload.temperature_command_topic = payload.temperature_high_command_topic;
2554
+ payload.temperature_state_template = payload.temperature_high_state_template;
2555
+ payload.temperature_state_topic = payload.temperature_high_state_topic;
2556
+ removeLowAndHighTemperatureFields(payload);
2557
+ }
2558
+ }
2559
+ payload.modes = ["off", ...payload.modes, "auto"];
2560
+ payload.mode_command_topic = payload.mode_command_topic.replace("/system_mode", "");
2561
+ }
2562
+ },
2563
+ };
2564
+ return {
2565
+ exposes,
2566
+ fromZigbee,
2567
+ toZigbee,
2568
+ configure,
2569
+ meta,
2570
+ isModernExtend: true,
2571
+ };
2572
+ },
2573
+ boostHeating: (args) => {
2574
+ const boostHeatingLookup = {
2575
+ OFF: 0x00,
2576
+ ON: 0x01,
2577
+ };
2578
+ const exposes = [
2579
+ e
2580
+ .binary("boost_heating", ea.ALL, utils.getFromLookupByValue(0x01, boostHeatingLookup), utils.getFromLookupByValue(0x00, boostHeatingLookup))
2581
+ .withLabel("Activate boost heating")
2582
+ .withDescription("Activate boost heating (opens TRV for 5 minutes)"),
2583
+ ];
2584
+ const fromZigbee = [
2585
+ {
2586
+ cluster: "hvacThermostat",
2587
+ type: ["attributeReport", "readResponse"],
2588
+ convert: (model, msg, publish, options, meta) => {
2589
+ const result = {};
2590
+ const data = msg.data;
2591
+ if (data.boostHeating !== undefined) {
2592
+ result.boost_heating = utils.getFromLookupByValue(data.boostHeating, boostHeatingLookup);
2593
+ }
2594
+ return result;
2595
+ },
2596
+ },
2597
+ ];
2598
+ const toZigbee = [
2599
+ {
2600
+ key: ["boost_heating"],
2601
+ convertSet: async (entity, key, value, meta) => {
2602
+ const enableBoostHeating = value === utils.getFromLookupByValue(boostHeatingLookup.ON, boostHeatingLookup);
2603
+ if (enableBoostHeating) {
2604
+ const systemModeNotSetToHeat = "system_mode" in meta.state && meta.state.system_mode !== "heat";
2605
+ if (systemModeNotSetToHeat) {
2606
+ throw new Error("Boost heating is only possible when system mode is set to 'heat'!");
2607
+ }
2608
+ const heaterTypeNotSetToRadiator = "heater_type" in meta.state &&
2609
+ meta.state.heater_type !==
2610
+ utils.getFromLookupByValue(boschThermostatLookup.heaterType.radiator, boschThermostatLookup.heaterType);
2611
+ if (heaterTypeNotSetToRadiator) {
2612
+ throw new Error("Boost heating is only possible when heater type is set to 'radiator'!");
2613
+ }
2614
+ }
2615
+ await entity.write("hvacThermostat", {
2616
+ boostHeating: utils.toNumber(utils.getFromLookup(value, boostHeatingLookup)),
2617
+ });
2618
+ return { state: { boost_heating: value } };
2619
+ },
2620
+ convertGet: async (entity, key, meta) => {
2621
+ await entity.read("hvacThermostat", ["boostHeating"]);
2622
+ },
2623
+ },
2624
+ ];
2625
+ const configure = [
2626
+ m.setupConfigureForReporting("hvacThermostat", "boostHeating", {
2627
+ config: args?.enableReporting ? { min: "MIN", max: "MAX", change: null } : false,
2628
+ access: ea.ALL,
2629
+ }),
2630
+ ];
2631
+ return {
2632
+ exposes,
2633
+ fromZigbee,
2634
+ toZigbee,
2635
+ configure,
2636
+ isModernExtend: true,
2637
+ };
2638
+ },
2639
+ errorState: (args) => {
2640
+ const exposes = [
2641
+ e
2642
+ .text("error_state", ea.STATE_GET)
2643
+ .withDescription("Indicates whether the device encounters any errors or not")
2644
+ .withCategory("diagnostic"),
2645
+ ];
2646
+ const fromZigbee = [
2647
+ {
2648
+ cluster: "hvacThermostat",
2649
+ type: ["attributeReport", "readResponse"],
2650
+ convert: (model, msg, publish, options, meta) => {
2651
+ const result = {};
2652
+ const data = msg.data;
2653
+ if (data.errorState !== undefined) {
2654
+ const receivedErrorState = data.errorState;
2655
+ if (receivedErrorState === 0) {
2656
+ result.error_state = "ok";
2657
+ }
2658
+ else {
2659
+ result.error_state = "";
2660
+ const bitmapLength = (receivedErrorState >>> 0).toString(2).length;
2661
+ for (let errorNumber = 0; errorNumber < bitmapLength; errorNumber++) {
2662
+ if ((receivedErrorState >> errorNumber) & 1) {
2663
+ if (String(result.error_state).length > 0) {
2664
+ result.error_state += " - ";
2665
+ }
2666
+ result.error_state += `E${String(errorNumber + 1).padStart(2, "0")}`;
2667
+ }
2668
+ }
2669
+ }
2670
+ }
2671
+ return result;
2672
+ },
2673
+ },
2674
+ ];
2675
+ const toZigbee = [
2676
+ {
2677
+ key: ["error_state"],
2678
+ convertGet: async (entity, key, meta) => {
2679
+ await entity.read("hvacThermostat", ["errorState"]);
2680
+ },
2681
+ },
2682
+ ];
2683
+ const configure = [
2684
+ m.setupConfigureForReporting("hvacThermostat", "errorState", {
2685
+ config: args?.enableReporting ? { min: "MIN", max: "MAX", change: null } : false,
2686
+ access: ea.STATE_GET,
2687
+ }),
2688
+ ];
2689
+ return {
2690
+ exposes,
2691
+ fromZigbee,
2692
+ toZigbee,
2693
+ configure,
2694
+ isModernExtend: true,
2695
+ };
2696
+ },
2697
+ valveAdaptation: () => {
2698
+ const valveAdaptStatusLookup = {
2699
+ none: 0x00,
2700
+ ready_to_calibrate: 0x01,
2701
+ calibration_in_progress: 0x02,
2702
+ error: 0x03,
2703
+ success: 0x04,
2704
+ };
2705
+ const triggerValveAdaptation = async (state, endpoint, throwError = true) => {
2706
+ let adaptStatus;
2707
+ try {
2708
+ adaptStatus = utils.getFromLookup(state.valve_adapt_status, valveAdaptStatusLookup);
2709
+ }
2710
+ catch {
2711
+ adaptStatus = valveAdaptStatusLookup.none;
2712
+ }
2713
+ switch (adaptStatus) {
2714
+ case valveAdaptStatusLookup.ready_to_calibrate:
2715
+ case valveAdaptStatusLookup.error:
2716
+ await endpoint.command("hvacThermostat", "calibrateValve", {}, exports.manufacturerOptions);
2717
+ break;
2718
+ default:
2719
+ if (throwError) {
2720
+ throw new Error("Valve adaptation process not possible right now!");
2721
+ }
2722
+ }
2723
+ };
2724
+ const exposes = [
2725
+ e
2726
+ .enum("valve_adapt_status", ea.STATE_GET, Object.keys(valveAdaptStatusLookup))
2727
+ .withLabel("Valve adaptation status")
2728
+ .withDescription("Specifies the current status of the valve adaptation")
2729
+ .withCategory("diagnostic"),
2730
+ e
2731
+ .binary("automatic_valve_adapt", ea.STATE_GET, true, false)
2732
+ .withLabel("Automatic valve adaptation requested")
2733
+ .withDescription("Specifies if an automatic valve adaptation is being requested by the thermostat " +
2734
+ "(for example after a successful firmware upgrade). If this is the case, the " +
2735
+ "valve adaptation will be automatically started as soon as the adaptation status " +
2736
+ "is 'ready_to_calibrate' or 'error'.")
2737
+ .withCategory("diagnostic"),
2738
+ e
2739
+ .enum("valve_adapt_process", ea.SET, ["adapt"])
2740
+ .withLabel("Trigger adaptation process")
2741
+ .withDescription("Trigger the valve adaptation process. Only possible when the adaptation status is 'ready_to_calibrate' or 'error'.")
2742
+ .withCategory("config"),
2743
+ ];
2744
+ const fromZigbee = [
2745
+ {
2746
+ cluster: "hvacThermostat",
2747
+ type: ["attributeReport", "readResponse"],
2748
+ convert: async (model, msg, publish, options, meta) => {
2749
+ const result = {};
2750
+ const data = msg.data;
2751
+ if (data.valveAdaptStatus !== undefined) {
2752
+ result.valve_adapt_status = utils.getFromLookupByValue(data.valveAdaptStatus, valveAdaptStatusLookup);
2753
+ const automaticValveAdapt = meta.state.automatic_valve_adapt ?? false;
2754
+ if (automaticValveAdapt === true) {
2755
+ await triggerValveAdaptation(meta.state, msg.endpoint, false);
2756
+ }
2757
+ }
2758
+ if (data.automaticValveAdapt !== undefined) {
2759
+ result.automatic_valve_adapt = !!data.automaticValveAdapt;
2760
+ }
2761
+ return result;
2762
+ },
2763
+ },
2764
+ ];
2765
+ const toZigbee = [
2766
+ {
2767
+ key: ["valve_adapt_status", "automatic_valve_adapt", "valve_adapt_process"],
2768
+ convertSet: async (entity, key, value, meta) => {
2769
+ if (key === "valve_adapt_process") {
2770
+ await triggerValveAdaptation(meta.state, entity);
2771
+ }
2772
+ },
2773
+ convertGet: async (entity, key, meta) => {
2774
+ if (key === "valve_adapt_status") {
2775
+ await entity.read("hvacThermostat", ["valveAdaptStatus"]);
2776
+ }
2777
+ if (key === "automatic_valve_adapt") {
2778
+ await entity.read("hvacThermostat", ["automaticValveAdapt"]);
2779
+ }
2780
+ },
2781
+ },
2782
+ ];
2783
+ const configure = [
2784
+ m.setupConfigureForReporting("hvacThermostat", "valveAdaptStatus", {
2785
+ config: { min: "MIN", max: "MAX", change: null },
2786
+ access: ea.STATE_GET,
2787
+ }),
2788
+ m.setupConfigureForReading("hvacThermostat", ["automaticValveAdapt"]),
2789
+ ];
2790
+ return {
2791
+ exposes,
2792
+ fromZigbee,
2793
+ toZigbee,
2794
+ configure,
2795
+ isModernExtend: true,
2796
+ };
2797
+ },
2798
+ };
2068
2799
  //endregion
2069
2800
  //# sourceMappingURL=bosch.js.map