@homebridge-plugins/homebridge-tado 9.2.0 → 9.3.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.
package/CHANGELOG.md CHANGED
@@ -1,7 +1,21 @@
1
1
  # Changelog
2
2
 
3
+ # v9.3.0 - 2026-06-18
4
+ - Fix: Treat tado zone id `0` as a valid zone id when resolving and updating configured zones (#169)
5
+ - Fix: Resolve missing `HOT_WATER` zone ids for temperature-controlled boiler `HeaterCooler` accessories before sending overlays (#169)
6
+ - Fix: Prevent invalid overlay requests from being sent when a zone id cannot be resolved (#169)
7
+ - Fix: Use a valid configured `HOT_WATER` target temperature when Apple Home turns the accessory back on without providing one (#169)
8
+ - Fix: Clamp only `HOT_WATER` target temperatures to the configured supported range before sending overlays (#169)
9
+ - Fix: Apply `HeaterCooler` temperature limits before restoring cached threshold values to avoid HomeKit range warnings for `HOT_WATER` zones (#169)
10
+ - Improve: Refine update buffer handling for accessory state and temperature updates
11
+ - Update dependencies
12
+
13
+ # v9.2.1 - 2026-06-06
14
+ - Fix: HeaterCooler temperature limits are no longer overridden by an unset or zero `minValue`/`maxValue` from config (#169)
15
+ - Fix: `accTypeBoiler` is now hidden in the UI when `boilerTempSupport` is enabled, as temperature-controlled hot water devices are always exposed as HeaterCooler (#169)
16
+
3
17
  # v9.2.0 - 2026-06-05
4
- - Fix: Incorrect warning "CoolingThresholdTemperature exceeded maximum of 35" for HOT_WATER zones with `boilerTempSupport: true` (#169)
18
+ - Fix: Incorrect warning "CoolingThresholdTemperature exceeded maximum of 35" for `HOT_WATER` zones with `boilerTempSupport: true` (#169)
5
19
  - Fix: Config UI: show AC-specific fields for AIR_CONDITIONING zones (#173)
6
20
  - Update dependencies
7
21
 
@@ -682,7 +682,12 @@
682
682
  },
683
683
  "items": [
684
684
  "homes[].zones[].boilerTempSupport",
685
- "homes[].zones[].accTypeBoiler"
685
+ {
686
+ "key": "homes[].zones[].accTypeBoiler",
687
+ "condition": {
688
+ "functionBody": "try { return !model.homes[arrayIndices[0]].zones[arrayIndices[1]].boilerTempSupport } catch(e){ return false }"
689
+ }
690
+ }
686
691
  ]
687
692
  },
688
693
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@homebridge-plugins/homebridge-tado",
3
- "version": "9.2.0",
3
+ "version": "9.3.0",
4
4
  "description": "Homebridge plugin for controlling tado° devices.",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -35,15 +35,15 @@
35
35
  "dependencies": {
36
36
  "@homebridge/plugin-ui-utils": "^2.2.4",
37
37
  "fakegato-history": "^0.6.7",
38
- "form-data": "^4.0.5",
38
+ "form-data": "^4.0.6",
39
39
  "fs-extra": "^11.3.5",
40
40
  "got": "^15.0.5",
41
41
  "moment": "^2.30.1"
42
42
  },
43
43
  "devDependencies": {
44
- "eslint": "^10.4.1",
44
+ "eslint": "^10.5.0",
45
45
  "@eslint/js": "^10.0.1",
46
46
  "globals": "^17.6.0",
47
- "prettier": "^3.8.3"
47
+ "prettier": "^3.8.4"
48
48
  }
49
49
  }
@@ -16,13 +16,73 @@ export default class HeaterCoolerAccessory {
16
16
 
17
17
  this.autoDelayTimeout = null;
18
18
 
19
- this.updateBuffer = new TadoUpdateBuffer((target, value) => {
20
- return this.deviceHandler.setStates(this.accessory, this.accessories, target, value);
21
- }, preferSiriTemperature);
19
+ const temperatureConfig = this.getTemperatureConfig();
20
+ const isHotWater = this.accessory.context.config.type === 'HOT_WATER';
21
+ const defaultTemperature = isHotWater ? temperatureConfig.minValue : 20;
22
+ const clampToRange = isHotWater;
23
+
24
+ this.updateBuffer = new TadoUpdateBuffer(
25
+ (target, value) => {
26
+ return this.deviceHandler.setStates(this.accessory, this.accessories, target, value);
27
+ },
28
+ preferSiriTemperature,
29
+ defaultTemperature,
30
+ temperatureConfig.minValue,
31
+ temperatureConfig.maxValue,
32
+ clampToRange
33
+ );
22
34
 
23
35
  this.getService();
24
36
  }
25
37
 
38
+ getTemperatureConfig() {
39
+ let minValue =
40
+ this.accessory.context.config.type === 'HOT_WATER'
41
+ ? this.accessory.context.config.temperatureUnit === 'FAHRENHEIT'
42
+ ? 86
43
+ : 30
44
+ : this.accessory.context.config.temperatureUnit === 'FAHRENHEIT'
45
+ ? 41
46
+ : 5;
47
+
48
+ let maxValue =
49
+ this.accessory.context.config.type === 'HOT_WATER'
50
+ ? this.accessory.context.config.temperatureUnit === 'FAHRENHEIT'
51
+ ? 149
52
+ : 65
53
+ : this.accessory.context.config.temperatureUnit === 'FAHRENHEIT'
54
+ ? 77
55
+ : 25;
56
+
57
+ //only override temperature limits if config value is explicitly and correctly set
58
+ if (
59
+ this.accessory.context.config.minValue != null &&
60
+ this.accessory.context.config.minValue > 0 &&
61
+ this.accessory.context.config.minValue < maxValue
62
+ ) {
63
+ minValue = this.accessory.context.config.minValue;
64
+ }
65
+ if (
66
+ this.accessory.context.config.maxValue != null &&
67
+ this.accessory.context.config.maxValue > 0 &&
68
+ this.accessory.context.config.maxValue > minValue
69
+ ) {
70
+ maxValue = this.accessory.context.config.maxValue;
71
+ }
72
+
73
+ const minStep = parseFloat(
74
+ (this.accessory.context.config.minStep &&
75
+ !isNaN(this.accessory.context.config.minStep) &&
76
+ this.accessory.context.config.minStep > 0 &&
77
+ this.accessory.context.config.minStep <= 1
78
+ ? parseFloat(this.accessory.context.config.minStep)
79
+ : 1
80
+ ).toFixed(2)
81
+ );
82
+
83
+ return { minValue, maxValue, minStep };
84
+ }
85
+
26
86
  //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~//
27
87
  // Services
28
88
  //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~//
@@ -149,41 +209,7 @@ export default class HeaterCoolerAccessory {
149
209
  );
150
210
  }
151
211
 
152
- let minValue =
153
- this.accessory.context.config.type === 'HOT_WATER'
154
- ? this.accessory.context.config.temperatureUnit === 'CELSIUS'
155
- ? 30
156
- : 86
157
- : this.accessory.context.config.temperatureUnit === 'CELSIUS'
158
- ? 5
159
- : 41;
160
-
161
- let maxValue =
162
- this.accessory.context.config.type === 'HOT_WATER'
163
- ? this.accessory.context.config.temperatureUnit === 'CELSIUS'
164
- ? 65
165
- : 149
166
- : this.accessory.context.config.temperatureUnit === 'CELSIUS'
167
- ? 25
168
- : 77;
169
-
170
- minValue = this.accessory.context.config.minValue < maxValue ? this.accessory.context.config.minValue : minValue;
171
-
172
- maxValue = this.accessory.context.config.maxValue > minValue ? this.accessory.context.config.maxValue : maxValue;
173
-
174
- console.log('Before MINSTEP: ' + this.accessory.context.config.minStep, this.accessory.displayName);
175
-
176
- let minStep = parseFloat(
177
- (this.accessory.context.config.minStep &&
178
- !isNaN(this.accessory.context.config.minStep) &&
179
- this.accessory.context.config.minStep > 0 &&
180
- this.accessory.context.config.minStep <= 1
181
- ? parseFloat(this.accessory.context.config.minStep)
182
- : 1
183
- ).toFixed(2)
184
- );
185
-
186
- console.log('After MINSTEP: ' + minStep, this.accessory.displayName);
212
+ const { minValue, maxValue, minStep } = this.getTemperatureConfig();
187
213
 
188
214
  let maxState = this.accessory.context.config.type === 'HOT_WATER' ? 2 : 3;
189
215
 
@@ -220,30 +246,38 @@ export default class HeaterCoolerAccessory {
220
246
  maxValue: 255,
221
247
  });
222
248
 
223
- if (service.getCharacteristic(this.api.hap.Characteristic.HeatingThresholdTemperature).value < minValue)
224
- service.getCharacteristic(this.api.hap.Characteristic.HeatingThresholdTemperature).updateValue(minValue);
249
+ const heatingThreshold = service.getCharacteristic(this.api.hap.Characteristic.HeatingThresholdTemperature);
225
250
 
226
- if (service.getCharacteristic(this.api.hap.Characteristic.HeatingThresholdTemperature).value > maxValue)
227
- service.getCharacteristic(this.api.hap.Characteristic.HeatingThresholdTemperature).updateValue(maxValue);
228
-
229
- service.getCharacteristic(this.api.hap.Characteristic.HeatingThresholdTemperature).setProps({
251
+ heatingThreshold.setProps({
230
252
  minValue: minValue,
231
253
  maxValue: maxValue,
232
254
  minStep: minStep,
233
255
  });
234
256
 
235
- if (this.accessory.context.config.type === 'HEATING') {
236
- if (service.getCharacteristic(this.api.hap.Characteristic.CoolingThresholdTemperature).value < minValue)
237
- service.getCharacteristic(this.api.hap.Characteristic.CoolingThresholdTemperature).updateValue(minValue);
257
+ let heatingThresholdValue = Number(heatingThreshold.value);
258
+
259
+ if (!Number.isFinite(heatingThresholdValue) || heatingThresholdValue < minValue)
260
+ heatingThreshold.updateValue(minValue);
238
261
 
239
- if (service.getCharacteristic(this.api.hap.Characteristic.CoolingThresholdTemperature).value > maxValue)
240
- service.getCharacteristic(this.api.hap.Characteristic.CoolingThresholdTemperature).updateValue(maxValue);
262
+ if (Number.isFinite(heatingThresholdValue) && heatingThresholdValue > maxValue)
263
+ heatingThreshold.updateValue(maxValue);
241
264
 
242
- service.getCharacteristic(this.api.hap.Characteristic.CoolingThresholdTemperature).setProps({
265
+ if (this.accessory.context.config.type === 'HEATING') {
266
+ const coolingThreshold = service.getCharacteristic(this.api.hap.Characteristic.CoolingThresholdTemperature);
267
+
268
+ coolingThreshold.setProps({
243
269
  minValue: minValue,
244
270
  maxValue: maxValue,
245
271
  minStep: minStep,
246
272
  });
273
+
274
+ let coolingThresholdValue = Number(coolingThreshold.value);
275
+
276
+ if (!Number.isFinite(coolingThresholdValue) || coolingThresholdValue < minValue)
277
+ coolingThreshold.updateValue(minValue);
278
+
279
+ if (Number.isFinite(coolingThresholdValue) && coolingThresholdValue > maxValue)
280
+ coolingThreshold.updateValue(maxValue);
247
281
  }
248
282
 
249
283
  if (!service.testCharacteristic(this.api.hap.Characteristic.ValvePosition))
@@ -30,6 +30,162 @@ export default (api, accessories, config, tado, telegram) => {
30
30
  return Object.keys(helpers[config.homeId].activeSettingStateRuns).length > 0;
31
31
  }
32
32
 
33
+
34
+ function _hasZoneId(zoneId) {
35
+ return zoneId !== undefined && zoneId !== null;
36
+ }
37
+
38
+ function _getExpectedAccessoryName(zone) {
39
+ return config.homeName + ' ' + zone.name + (zone.type === 'HEATING' ? ' Heater' : zone.type === 'AIR_CONDITIONING' ? ' AC' : ' Boiler');
40
+ }
41
+
42
+ function _getAccessoryNameSuffix(zone) {
43
+ return zone.name + (zone.type === 'HEATING' ? ' Heater' : zone.type === 'AIR_CONDITIONING' ? ' AC' : ' Boiler');
44
+ }
45
+
46
+ function _isAccessoryForZone(accessory, zone) {
47
+ return (
48
+ accessory &&
49
+ zone &&
50
+ accessory.context.config.type === zone.type &&
51
+ (accessory.displayName === _getExpectedAccessoryName(zone) ||
52
+ accessory.displayName === _getAccessoryNameSuffix(zone) ||
53
+ accessory.displayName.endsWith(' ' + _getAccessoryNameSuffix(zone)))
54
+ );
55
+ }
56
+
57
+ function _findConfiguredZoneForAccessory(accessory) {
58
+ if (!config.zones || !config.zones.length) return;
59
+
60
+ return config.zones.find((zone) => _isAccessoryForZone(accessory, zone));
61
+ }
62
+
63
+ async function _getZoneIdForAccessory(accessory) {
64
+ if (_hasZoneId(accessory.context.config.zoneId)) return accessory.context.config.zoneId;
65
+
66
+ let configuredZone = _findConfiguredZoneForAccessory(accessory);
67
+
68
+ if (configuredZone && _hasZoneId(configuredZone.id)) {
69
+ accessory.context.config.zoneId = configuredZone.id;
70
+ return configuredZone.id;
71
+ }
72
+
73
+ const allZones = (await tado.getZones(config.homeId)) || [];
74
+
75
+ for (const [index, zone] of config.zones.entries()) {
76
+ const foundZone = allZones.find((zoneWithID) => zoneWithID.name === zone.name && zoneWithID.type === zone.type);
77
+
78
+ if (foundZone && _hasZoneId(foundZone.id)) {
79
+ config.zones[index].id = foundZone.id;
80
+ }
81
+ }
82
+
83
+ configuredZone = _findConfiguredZoneForAccessory(accessory);
84
+
85
+ if (configuredZone && _hasZoneId(configuredZone.id)) {
86
+ accessory.context.config.zoneId = configuredZone.id;
87
+ return configuredZone.id;
88
+ }
89
+
90
+ Logger.error(`Cannot set state for ${accessory.displayName}: missing tado zone id. Please run reconfigure or add the correct zone id to the zone config.`);
91
+ return null;
92
+ }
93
+
94
+ function _getTemperatureLimits(accessory) {
95
+ let min =
96
+ accessory.context.config.type === 'HOT_WATER'
97
+ ? accessory.context.config.temperatureUnit === 'FAHRENHEIT'
98
+ ? 86
99
+ : 30
100
+ : accessory.context.config.temperatureUnit === 'FAHRENHEIT'
101
+ ? 41
102
+ : 5;
103
+
104
+ let max =
105
+ accessory.context.config.type === 'HOT_WATER'
106
+ ? accessory.context.config.temperatureUnit === 'FAHRENHEIT'
107
+ ? 149
108
+ : 65
109
+ : accessory.context.config.temperatureUnit === 'FAHRENHEIT'
110
+ ? 77
111
+ : 25;
112
+
113
+ if (
114
+ accessory.context.config.minValue != null &&
115
+ accessory.context.config.minValue > 0 &&
116
+ accessory.context.config.minValue < max
117
+ ) {
118
+ min = accessory.context.config.minValue;
119
+ }
120
+
121
+ if (
122
+ accessory.context.config.maxValue != null &&
123
+ accessory.context.config.maxValue > 0 &&
124
+ accessory.context.config.maxValue > min
125
+ ) {
126
+ max = accessory.context.config.maxValue;
127
+ }
128
+
129
+ return { min, max };
130
+ }
131
+
132
+ function _normalizeTemperatureForAccessory(accessory, temp) {
133
+ if (temp === null || temp === undefined) return temp;
134
+
135
+ const numericTemp = Number(temp);
136
+ if (!Number.isFinite(numericTemp)) return temp;
137
+
138
+ if (accessory.context.config.type !== 'HOT_WATER') return parseFloat(numericTemp.toFixed(2));
139
+
140
+ const { min, max } = _getTemperatureLimits(accessory);
141
+
142
+ return parseFloat(Math.min(Math.max(numericTemp, min), max).toFixed(2));
143
+ }
144
+
145
+ async function _setZoneOverlay(accessory, power, temp, mode) {
146
+ const zoneId = await _getZoneIdForAccessory(accessory);
147
+ if (!_hasZoneId(zoneId)) return false;
148
+
149
+ await tado.setZoneOverlay(
150
+ config.homeId,
151
+ zoneId,
152
+ power,
153
+ _normalizeTemperatureForAccessory(accessory, temp),
154
+ mode,
155
+ accessory.context.config.temperatureUnit
156
+ );
157
+
158
+ return true;
159
+ }
160
+
161
+ async function _setACZoneOverlay(accessory, power, acMode, temp, fanSpeed, swing, mode) {
162
+ const zoneId = await _getZoneIdForAccessory(accessory);
163
+ if (!_hasZoneId(zoneId)) return false;
164
+
165
+ await tado.setACZoneOverlay(
166
+ config.homeId,
167
+ zoneId,
168
+ power,
169
+ acMode,
170
+ _normalizeTemperatureForAccessory(accessory, temp),
171
+ fanSpeed,
172
+ swing,
173
+ mode,
174
+ accessory.context.config.temperatureUnit
175
+ );
176
+
177
+ return true;
178
+ }
179
+
180
+ async function _clearZoneOverlay(accessory) {
181
+ const zoneId = await _getZoneIdForAccessory(accessory);
182
+ if (!_hasZoneId(zoneId)) return false;
183
+
184
+ await tado.clearZoneOverlay(config.homeId, zoneId);
185
+
186
+ return true;
187
+ }
188
+
33
189
  async function setStates(accessory, accs, target, value) {
34
190
  let zoneUpdated = false;
35
191
  accessories = accs.filter((acc) => acc && acc.context.config.homeName === config.homeName);
@@ -80,27 +236,18 @@ export default (api, accessories, config, tado, telegram) => {
80
236
  // Use AC-specific overlay for AIR_CONDITIONING zones
81
237
  if (accessory.context.config.type === 'AIR_CONDITIONING') {
82
238
  zoneUpdated = true;
83
- await tado.setACZoneOverlay(
84
- config.homeId,
85
- accessory.context.config.zoneId,
239
+ await _setACZoneOverlay(
240
+ accessory,
86
241
  power,
87
242
  'COOL', // Default AC mode for OFF state
88
243
  temp,
89
244
  null, // No fan speed for AC units
90
245
  'OFF', // Default swing
91
- mode,
92
- accessory.context.config.temperatureUnit
246
+ mode
93
247
  );
94
248
  } else {
95
249
  zoneUpdated = true;
96
- await tado.setZoneOverlay(
97
- config.homeId,
98
- accessory.context.config.zoneId,
99
- power,
100
- temp,
101
- mode,
102
- accessory.context.config.temperatureUnit
103
- );
250
+ await _setZoneOverlay(accessory, power, temp, mode);
104
251
  }
105
252
  } else {
106
253
  let mode =
@@ -133,7 +280,7 @@ export default (api, accessories, config, tado, telegram) => {
133
280
  accessory.context.config.subtype.includes('heatercooler'))
134
281
  ) {
135
282
  zoneUpdated = true;
136
- await tado.clearZoneOverlay(config.homeId, accessory.context.config.zoneId);
283
+ await _clearZoneOverlay(accessory);
137
284
  return;
138
285
  }
139
286
 
@@ -149,27 +296,18 @@ export default (api, accessories, config, tado, telegram) => {
149
296
  let acMode = value === 1 ? 'HEAT' : value === 2 ? 'COOL' : 'COOL';
150
297
 
151
298
  zoneUpdated = true;
152
- await tado.setACZoneOverlay(
153
- config.homeId,
154
- accessory.context.config.zoneId,
299
+ await _setACZoneOverlay(
300
+ accessory,
155
301
  power,
156
302
  acMode,
157
303
  temp,
158
304
  null, // No fan speed for AC units
159
305
  'OFF', // Default swing
160
- mode,
161
- accessory.context.config.temperatureUnit
306
+ mode
162
307
  );
163
308
  } else {
164
309
  zoneUpdated = true;
165
- await tado.setZoneOverlay(
166
- config.homeId,
167
- accessory.context.config.zoneId,
168
- power,
169
- temp,
170
- mode,
171
- accessory.context.config.temperatureUnit
172
- );
310
+ await _setZoneOverlay(accessory, power, temp, mode);
173
311
  }
174
312
 
175
313
  helpers[config.homeId].delayTimer[accessory.displayName] = null;
@@ -196,7 +334,7 @@ export default (api, accessories, config, tado, telegram) => {
196
334
  accessory.context.config.subtype.includes('heatercooler'))
197
335
  ) {
198
336
  zoneUpdated = true;
199
- await tado.clearZoneOverlay(config.homeId, accessory.context.config.zoneId);
337
+ await _clearZoneOverlay(accessory);
200
338
  return;
201
339
  }
202
340
 
@@ -259,27 +397,18 @@ export default (api, accessories, config, tado, telegram) => {
259
397
  }
260
398
 
261
399
  zoneUpdated = true;
262
- await tado.setACZoneOverlay(
263
- config.homeId,
264
- accessory.context.config.zoneId,
400
+ await _setACZoneOverlay(
401
+ accessory,
265
402
  power,
266
403
  acMode,
267
404
  temp,
268
405
  null, // No fan speed for AC units
269
406
  'OFF', // Default swing
270
- mode,
271
- accessory.context.config.temperatureUnit
407
+ mode
272
408
  );
273
409
  } else {
274
410
  zoneUpdated = true;
275
- await tado.setZoneOverlay(
276
- config.homeId,
277
- accessory.context.config.zoneId,
278
- power,
279
- temp,
280
- mode,
281
- accessory.context.config.temperatureUnit
282
- );
411
+ await _setZoneOverlay(accessory, power, temp, mode);
283
412
  }
284
413
  }
285
414
 
@@ -293,7 +422,7 @@ export default (api, accessories, config, tado, telegram) => {
293
422
  let temp = null;
294
423
  let power = value ? 'ON' : 'OFF';
295
424
 
296
- if (faucetService) faucetService.getCharacteristic(this.api.hap.Characteristic.InUse).updateValue(value);
425
+ if (faucetService) faucetService.getCharacteristic(api.hap.Characteristic.InUse).updateValue(value);
297
426
 
298
427
  let mode =
299
428
  accessory.context.config.mode === 'TIMER'
@@ -303,27 +432,18 @@ export default (api, accessories, config, tado, telegram) => {
303
432
  // Use AC-specific overlay for AIR_CONDITIONING zones
304
433
  if (accessory.context.config.type === 'AIR_CONDITIONING') {
305
434
  zoneUpdated = true;
306
- await tado.setACZoneOverlay(
307
- config.homeId,
308
- accessory.context.config.zoneId,
435
+ await _setACZoneOverlay(
436
+ accessory,
309
437
  power,
310
438
  'COOL', // Default AC mode for switch/faucet
311
439
  temp,
312
440
  null, // No fan speed for AC units
313
441
  'OFF', // Default swing
314
- mode,
315
- accessory.context.config.temperatureUnit
442
+ mode
316
443
  );
317
444
  } else {
318
445
  zoneUpdated = true;
319
- await tado.setZoneOverlay(
320
- config.homeId,
321
- accessory.context.config.zoneId,
322
- power,
323
- temp,
324
- mode,
325
- accessory.context.config.temperatureUnit
326
- );
446
+ await _setZoneOverlay(accessory, power, temp, mode);
327
447
  }
328
448
 
329
449
  break;
@@ -899,7 +1019,7 @@ export default (api, accessories, config, tado, telegram) => {
899
1019
  let inOffMode = 0;
900
1020
  let inAutoMode = 0;
901
1021
 
902
- let zonesWithoutID = config.zones.filter((zone) => zone && !zone.id);
1022
+ let zonesWithoutID = config.zones.filter((zone) => zone && !_hasZoneId(zone.id));
903
1023
 
904
1024
  if (zonesWithoutID.length) {
905
1025
  const allZones = (await tado.getZones(config.homeId)) || [];
@@ -917,11 +1037,11 @@ export default (api, accessories, config, tado, telegram) => {
917
1037
  for (const [index, zone] of config.zones.entries()) {
918
1038
  allZones.forEach((zoneWithID) => {
919
1039
  if (zoneWithID.name === zone.name && zoneWithID.type === zone.type) {
920
- const heatAccessory = accessories.filter(
921
- (acc) => acc && acc.displayName === config.homeName + ' ' + zone.name + ' Heater'
922
- );
1040
+ const zoneAccessories = accessories.filter((acc) => _isAccessoryForZone(acc, zone));
923
1041
 
924
- if (heatAccessory.length) heatAccessory[0].context.config.zoneId = zoneWithID.id;
1042
+ zoneAccessories.forEach((acc) => {
1043
+ acc.context.config.zoneId = zoneWithID.id;
1044
+ });
925
1045
 
926
1046
  config.zones[index].id = zoneWithID.id;
927
1047
  config.zones[index].battery = !config.zones[index].noBattery
@@ -949,9 +1069,19 @@ export default (api, accessories, config, tado, telegram) => {
949
1069
  }
950
1070
 
951
1071
  for (const zone of config.zones) {
1072
+ if (!_hasZoneId(zone.id)) {
1073
+ Logger.warn(`Skipping zone ${zone.name}: missing tado zone id.`);
1074
+ continue;
1075
+ }
1076
+
952
1077
  const zoneState = zoneStates[zone.id.toString()];
953
1078
  Logger.debug(`Update state of zone ${zone.id} to:`, zoneState);
954
1079
 
1080
+ if (!zoneState || !zoneState.setting) {
1081
+ Logger.warn(`Skipping zone ${zone.name}: missing tado zone state.`);
1082
+ continue;
1083
+ }
1084
+
955
1085
  let currentState, targetState, currentTemp, targetTemp, humidity, active, battery, tempEqual;
956
1086
 
957
1087
  if (zoneState.setting.type === 'HEATING') {
@@ -2,7 +2,7 @@ import { hrtime } from "process";
2
2
  import Logger from '../helper/logger.js';
3
3
 
4
4
  export class TadoUpdateBuffer {
5
- constructor(sendUpdateFn, preferSiriTemperature = false) {
5
+ constructor(sendUpdateFn, preferSiriTemperature = false, defaultTemperature = 20, minTemperature = 5, maxTemperature = null, clampToRange = false) {
6
6
  this.preferSiriTemperature = !!preferSiriTemperature;
7
7
  this.sendUpdateFn = sendUpdateFn;
8
8
  this.delay = 400;
@@ -10,7 +10,12 @@ export class TadoUpdateBuffer {
10
10
  this.pendingState = null;
11
11
  this.pendingTemperature = null;
12
12
  this.lastUpdateTime = null;
13
- this.lastTemperature = 20;
13
+ this.minTemperature = Number.isFinite(Number(minTemperature)) ? Number(minTemperature) : 5;
14
+ this.maxTemperature = Number.isFinite(Number(maxTemperature)) ? Number(maxTemperature) : null;
15
+ this.clampToRange = !!clampToRange;
16
+
17
+ const normalizedDefaultTemperature = this._normalizeTemperature(defaultTemperature);
18
+ this.lastTemperature = normalizedDefaultTemperature ?? (this.clampToRange ? this.minTemperature : 20);
14
19
  }
15
20
 
16
21
  setState(value) {
@@ -25,6 +30,16 @@ export class TadoUpdateBuffer {
25
30
  Logger.debug("[TadoUpdateBuffer] setTemperature", value, hrtime.bigint());
26
31
  }
27
32
 
33
+ _normalizeTemperature(value) {
34
+ const temp = Number(value);
35
+ if (!Number.isFinite(temp)) return null;
36
+
37
+ if (temp < this.minTemperature) return this.clampToRange ? this.minTemperature : null;
38
+ if (this.maxTemperature !== null && temp > this.maxTemperature) return this.clampToRange ? this.maxTemperature : temp;
39
+
40
+ return parseFloat(temp.toFixed(2));
41
+ }
42
+
28
43
  _schedule() {
29
44
  if (this.timer) clearTimeout(this.timer);
30
45
  this.timer = setTimeout(() => this._apply(), this.delay);
@@ -33,15 +48,17 @@ export class TadoUpdateBuffer {
33
48
  _apply() {
34
49
  this.timer = null;
35
50
  const state = this.pendingState;
36
- const temp = this.pendingTemperature;
37
- const tempSet = temp !== null && temp !== undefined && temp >= 5;
51
+ const rawTemp = this.pendingTemperature;
52
+ const siriAutoTemperature = Number(rawTemp) === 5;
53
+ const temp = siriAutoTemperature ? 5 : this._normalizeTemperature(rawTemp);
54
+ const tempSet = temp !== null;
38
55
  this.pendingState = null;
39
56
  this.pendingTemperature = null;
40
- if (tempSet) this.lastTemperature = temp;
57
+ if (tempSet && !siriAutoTemperature) this.lastTemperature = temp;
41
58
 
42
59
  //Siri temperature heuristic
43
60
  if (this.preferSiriTemperature && state === 3 && tempSet) {
44
- if (temp === 5) {
61
+ if (siriAutoTemperature) {
45
62
  //set auto mode on
46
63
  Logger.debug("[TadoUpdateBuffer] preferSiriTemperature active but temp=5 -> treat as auto mode");
47
64
  return this.sendUpdateFn("State", 3);
@@ -15,6 +15,8 @@ const hashCode = (s) =>
15
15
  const devices = new Map();
16
16
  const deviceHandler = new Map();
17
17
 
18
+ const hasZoneId = (zoneId) => zoneId !== undefined && zoneId !== null;
19
+
18
20
  let telegram;
19
21
 
20
22
  export default {
@@ -554,6 +556,13 @@ export default {
554
556
  zone.type = 'HEATING';
555
557
  }
556
558
 
559
+ if (!hasZoneId(zone.id)) {
560
+ Logger.warn(
561
+ 'There is no tado zone id configured for this zone. The plugin will try to resolve it from the tado API at runtime. Run reconfigure to persist it in the config.',
562
+ zone.name
563
+ );
564
+ }
565
+
557
566
  if (!error) {
558
567
  activeZones += 1;
559
568
 
@@ -582,6 +591,7 @@ export default {
582
591
  ? 'zone-faucet'
583
592
  : 'zone-switch';
584
593
 
594
+ // boilerTempSupport always takes precedence over accTypeBoiler
585
595
  config.subtype = zone.boilerTempSupport ? 'zone-heatercooler-boiler' : config.subtype;
586
596
 
587
597
  config.zoneId = zone.id;
@@ -878,7 +888,7 @@ export default {
878
888
  config.name = name;
879
889
  config.subtype = 'extra-cntrlswitch';
880
890
  config.runningInformation = home.extras.runningInformation;
881
- config.rooms = home.zones.filter((zne) => zne && zne.id);
891
+ config.rooms = home.zones.filter((zne) => zne && hasZoneId(zne.id));
882
892
  config.switches = validSwitches;
883
893
  config.model = 'Central Switch';
884
894
  config.serialNumber = hashCode(name);