@homebridge-plugins/homebridge-tado 9.2.1 → 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,11 +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
+
3
13
  # v9.2.1 - 2026-06-06
4
14
  - Fix: HeaterCooler temperature limits are no longer overridden by an unset or zero `minValue`/`maxValue` from config (#169)
5
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)
6
16
 
7
17
  # v9.2.0 - 2026-06-05
8
- - 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)
9
19
  - Fix: Config UI: show AC-specific fields for AIR_CONDITIONING zones (#173)
10
20
  - Update dependencies
11
21
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@homebridge-plugins/homebridge-tado",
3
- "version": "9.2.1",
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,53 +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
- //only override temperature limits if config value is explicitly and correctly set
171
- if (
172
- this.accessory.context.config.minValue != null &&
173
- this.accessory.context.config.minValue > 0 &&
174
- this.accessory.context.config.minValue < maxValue
175
- ) {
176
- minValue = this.accessory.context.config.minValue;
177
- }
178
- if (
179
- this.accessory.context.config.maxValue != null &&
180
- this.accessory.context.config.maxValue > 0 &&
181
- this.accessory.context.config.maxValue > minValue
182
- ) {
183
- maxValue = this.accessory.context.config.maxValue;
184
- }
185
-
186
- console.log('Before MINSTEP: ' + this.accessory.context.config.minStep, this.accessory.displayName);
187
-
188
- let minStep = parseFloat(
189
- (this.accessory.context.config.minStep &&
190
- !isNaN(this.accessory.context.config.minStep) &&
191
- this.accessory.context.config.minStep > 0 &&
192
- this.accessory.context.config.minStep <= 1
193
- ? parseFloat(this.accessory.context.config.minStep)
194
- : 1
195
- ).toFixed(2)
196
- );
197
-
198
- console.log('After MINSTEP: ' + minStep, this.accessory.displayName);
212
+ const { minValue, maxValue, minStep } = this.getTemperatureConfig();
199
213
 
200
214
  let maxState = this.accessory.context.config.type === 'HOT_WATER' ? 2 : 3;
201
215
 
@@ -232,30 +246,38 @@ export default class HeaterCoolerAccessory {
232
246
  maxValue: 255,
233
247
  });
234
248
 
235
- if (service.getCharacteristic(this.api.hap.Characteristic.HeatingThresholdTemperature).value < minValue)
236
- service.getCharacteristic(this.api.hap.Characteristic.HeatingThresholdTemperature).updateValue(minValue);
237
-
238
- if (service.getCharacteristic(this.api.hap.Characteristic.HeatingThresholdTemperature).value > maxValue)
239
- service.getCharacteristic(this.api.hap.Characteristic.HeatingThresholdTemperature).updateValue(maxValue);
249
+ const heatingThreshold = service.getCharacteristic(this.api.hap.Characteristic.HeatingThresholdTemperature);
240
250
 
241
- service.getCharacteristic(this.api.hap.Characteristic.HeatingThresholdTemperature).setProps({
251
+ heatingThreshold.setProps({
242
252
  minValue: minValue,
243
253
  maxValue: maxValue,
244
254
  minStep: minStep,
245
255
  });
246
256
 
247
- if (this.accessory.context.config.type === 'HEATING') {
248
- if (service.getCharacteristic(this.api.hap.Characteristic.CoolingThresholdTemperature).value < minValue)
249
- service.getCharacteristic(this.api.hap.Characteristic.CoolingThresholdTemperature).updateValue(minValue);
257
+ let heatingThresholdValue = Number(heatingThreshold.value);
250
258
 
251
- if (service.getCharacteristic(this.api.hap.Characteristic.CoolingThresholdTemperature).value > maxValue)
252
- service.getCharacteristic(this.api.hap.Characteristic.CoolingThresholdTemperature).updateValue(maxValue);
259
+ if (!Number.isFinite(heatingThresholdValue) || heatingThresholdValue < minValue)
260
+ heatingThreshold.updateValue(minValue);
253
261
 
254
- service.getCharacteristic(this.api.hap.Characteristic.CoolingThresholdTemperature).setProps({
262
+ if (Number.isFinite(heatingThresholdValue) && heatingThresholdValue > maxValue)
263
+ heatingThreshold.updateValue(maxValue);
264
+
265
+ if (this.accessory.context.config.type === 'HEATING') {
266
+ const coolingThreshold = service.getCharacteristic(this.api.hap.Characteristic.CoolingThresholdTemperature);
267
+
268
+ coolingThreshold.setProps({
255
269
  minValue: minValue,
256
270
  maxValue: maxValue,
257
271
  minStep: minStep,
258
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);
259
281
  }
260
282
 
261
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
 
@@ -879,7 +888,7 @@ export default {
879
888
  config.name = name;
880
889
  config.subtype = 'extra-cntrlswitch';
881
890
  config.runningInformation = home.extras.runningInformation;
882
- config.rooms = home.zones.filter((zne) => zne && zne.id);
891
+ config.rooms = home.zones.filter((zne) => zne && hasZoneId(zne.id));
883
892
  config.switches = validSwitches;
884
893
  config.model = 'Central Switch';
885
894
  config.serialNumber = hashCode(name);