@homebridge-plugins/homebridge-tado 8.3.0 → 8.4.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,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## v8.4.0 - 2025-10-26
4
+ - Improve state handling to ensure Apple Home states always reflect current tado states
5
+
6
+ ## v8.3.1 - 2025-10-25
7
+ - Fix: Persist zone states only if not empty
8
+
3
9
  ## v8.3.0 - 2025-10-25
4
10
  - Add tado API counter
5
11
  - Improve task scheduling and interval handling
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@homebridge-plugins/homebridge-tado",
3
- "version": "8.3.0",
3
+ "version": "8.4.0",
4
4
  "description": "Homebridge plugin for controlling tado° devices.",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -199,17 +199,11 @@ export default class ThermostatAccessory {
199
199
  }
200
200
 
201
201
  this.waitForEndValue = setTimeout(() => {
202
- if (value === 3) {
203
- if (this.timeoutAuto) {
204
- this.deviceHandler.setStates(this.accessory, this.accessories, 'State', value);
205
- clearTimeout(this.timeoutAuto);
206
- this.timeoutAuto = null;
207
- } else {
208
- this.deviceHandler.setStates(this.accessory, this.accessories, 'State', value);
209
- }
210
- } else {
211
- this.deviceHandler.setStates(this.accessory, this.accessories, 'State', value);
202
+ if (this.settingTemperature) {
203
+ this.settingTemperature = false;
204
+ return;
212
205
  }
206
+ this.deviceHandler.setStates(this.accessory, this.accessories, 'State', value);
213
207
  }, 500);
214
208
  });
215
209
 
@@ -223,24 +217,10 @@ export default class ThermostatAccessory {
223
217
  service
224
218
  .getCharacteristic(this.api.hap.Characteristic.TargetTemperature)
225
219
  .onSet((value) => {
226
- /*
227
- *let tempUnit = service.getCharacteristic(this.api.hap.Characteristic.TemperatureDisplayUnits).value;
228
- *
229
- *let cToF = (c) => Math.round((c * 9) / 5 + 32);
230
- *let fToC = (f) => Math.round(((f - 32) * 5) / 9);
231
- *
232
- *let newValue = tempUnit ? (value <= 25 ? cToF(value) : value) : value > 25 ? fToC(value) : value;
233
- *
234
- *service.getCharacteristic(this.api.hap.Characteristic.CurrentTemperature).updateValue(newValue);
235
- */
236
-
237
- this.timeoutAuto = setTimeout(() => {
238
- let targetState = service.getCharacteristic(this.api.hap.Characteristic.TargetHeatingCoolingState).value;
239
-
240
- if (targetState) this.deviceHandler.setStates(this.accessory, this.accessories, 'Temperature', value);
241
-
242
- this.timeoutAuto = null;
243
- }, 600);
220
+ this.settingTemperature = true;
221
+ const targetState = service.getCharacteristic(this.api.hap.Characteristic.TargetHeatingCoolingState).value;
222
+ if (targetState) this.deviceHandler.setStates(this.accessory, this.accessories, 'Temperature', value);
223
+ setTimeout(() => this.settingTemperature = false, 1000);
244
224
  })
245
225
  .on(
246
226
  'change',
@@ -26,6 +26,9 @@ export default (api, accessories, config, tado, telegram) => {
26
26
  const storagePath = api.user.storagePath();
27
27
 
28
28
  async function setStates(accessory, accs, target, value) {
29
+ let zoneUpdated = false;
30
+ let allZonesUpdated = false;
31
+
29
32
  accessories = accs.filter((acc) => acc && acc.context.config.homeName === config.homeName);
30
33
 
31
34
  try {
@@ -75,7 +78,7 @@ export default (api, accessories, config, tado, telegram) => {
75
78
 
76
79
  // Use AC-specific overlay for AIR_CONDITIONING zones
77
80
  if (accessory.context.config.type === 'AIR_CONDITIONING') {
78
-
81
+ zoneUpdated = true;
79
82
  await tado.setACZoneOverlay(
80
83
  config.homeId,
81
84
  accessory.context.config.zoneId,
@@ -88,6 +91,7 @@ export default (api, accessories, config, tado, telegram) => {
88
91
  accessory.context.config.temperatureUnit
89
92
  );
90
93
  } else {
94
+ zoneUpdated = true;
91
95
  await tado.setZoneOverlay(
92
96
  config.homeId,
93
97
  accessory.context.config.zoneId,
@@ -127,8 +131,8 @@ export default (api, accessories, config, tado, telegram) => {
127
131
  accessory.context.config.mode === 'AUTO' &&
128
132
  accessory.context.config.subtype.includes('heatercooler'))
129
133
  ) {
134
+ zoneUpdated = true;
130
135
  await tado.clearZoneOverlay(config.homeId, accessory.context.config.zoneId);
131
- await updateZones(accessory.context.config.zoneId);
132
136
  return;
133
137
  }
134
138
 
@@ -143,6 +147,7 @@ export default (api, accessories, config, tado, telegram) => {
143
147
  if (accessory.context.config.type === 'AIR_CONDITIONING') {
144
148
  let acMode = value === 1 ? 'HEAT' : value === 2 ? 'COOL' : 'COOL';
145
149
 
150
+ zoneUpdated = true;
146
151
  await tado.setACZoneOverlay(
147
152
  config.homeId,
148
153
  accessory.context.config.zoneId,
@@ -155,6 +160,7 @@ export default (api, accessories, config, tado, telegram) => {
155
160
  accessory.context.config.temperatureUnit
156
161
  );
157
162
  } else {
163
+ zoneUpdated = true;
158
164
  await tado.setZoneOverlay(
159
165
  config.homeId,
160
166
  accessory.context.config.zoneId,
@@ -188,8 +194,8 @@ export default (api, accessories, config, tado, telegram) => {
188
194
  accessory.context.config.mode === 'CUSTOM' &&
189
195
  accessory.context.config.subtype.includes('heatercooler'))
190
196
  ) {
197
+ zoneUpdated = true;
191
198
  await tado.clearZoneOverlay(config.homeId, accessory.context.config.zoneId);
192
- await updateZones(accessory.context.config.zoneId);
193
199
  return;
194
200
  }
195
201
 
@@ -251,6 +257,7 @@ export default (api, accessories, config, tado, telegram) => {
251
257
  }
252
258
  }
253
259
 
260
+ zoneUpdated = true;
254
261
  await tado.setACZoneOverlay(
255
262
  config.homeId,
256
263
  accessory.context.config.zoneId,
@@ -263,6 +270,7 @@ export default (api, accessories, config, tado, telegram) => {
263
270
  accessory.context.config.temperatureUnit
264
271
  );
265
272
  } else {
273
+ zoneUpdated = true;
266
274
  await tado.setZoneOverlay(
267
275
  config.homeId,
268
276
  accessory.context.config.zoneId,
@@ -293,7 +301,7 @@ export default (api, accessories, config, tado, telegram) => {
293
301
 
294
302
  // Use AC-specific overlay for AIR_CONDITIONING zones
295
303
  if (accessory.context.config.type === 'AIR_CONDITIONING') {
296
-
304
+ zoneUpdated = true;
297
305
  await tado.setACZoneOverlay(
298
306
  config.homeId,
299
307
  accessory.context.config.zoneId,
@@ -306,6 +314,7 @@ export default (api, accessories, config, tado, telegram) => {
306
314
  accessory.context.config.temperatureUnit
307
315
  );
308
316
  } else {
317
+ zoneUpdated = true;
309
318
  await tado.setZoneOverlay(
310
319
  config.homeId,
311
320
  accessory.context.config.zoneId,
@@ -367,6 +376,7 @@ export default (api, accessories, config, tado, telegram) => {
367
376
  }
368
377
  }
369
378
 
379
+ allZonesUpdated = true;
370
380
  await tado.setPresenceLock(config.homeId, targetState);
371
381
 
372
382
  break;
@@ -418,6 +428,7 @@ export default (api, accessories, config, tado, telegram) => {
418
428
  })
419
429
  .filter((id) => id);
420
430
 
431
+ allZonesUpdated = true;
421
432
  await tado.resumeShedule(config.homeId, roomIds);
422
433
 
423
434
  //Turn all back to AUTO/ON
@@ -542,6 +553,7 @@ export default (api, accessories, config, tado, telegram) => {
542
553
  .updateValue(false);
543
554
  }
544
555
 
556
+ allZonesUpdated = true;
545
557
  await tado.switchAll(config.homeId, rooms);
546
558
 
547
559
  break;
@@ -557,6 +569,8 @@ export default (api, accessories, config, tado, telegram) => {
557
569
  } catch (err) {
558
570
  errorHandler(err);
559
571
  } finally {
572
+ //always update zone to set correct state in Apple Home
573
+ if (zoneUpdated || allZonesUpdated) await updateZones(allZonesUpdated ? undefined : accessory.context.config.zoneId);
560
574
  settingState = false;
561
575
  }
562
576
  }
@@ -717,11 +731,15 @@ export default (api, accessories, config, tado, telegram) => {
717
731
 
718
732
  async function persistStates(homeId, zoneStates) {
719
733
  try {
720
- const homeData = {};
721
- homeData.zoneStates = zoneStates ?? {};
722
- await writeFile(join(storagePath, `tado-states-${homeId}.json`), JSON.stringify(homeData, null, 2), "utf-8");
734
+ if (zoneStates && Object.keys(zoneStates).length) {
735
+ const homeData = {};
736
+ homeData.zoneStates = zoneStates;
737
+ await writeFile(join(storagePath, `tado-states-${homeId}.json`), JSON.stringify(homeData, null, 2), "utf-8");
738
+ } else {
739
+ Logger.info(`Skipping persistence of tado states for home ${homeId}: zone states are empty.`);
740
+ }
723
741
  } catch (error) {
724
- Logger.error(`Error while updating the tado states file for home id ${homeId}: ${error.message || error}`);
742
+ Logger.error(`Error while updating the tado states file for home ${homeId}: ${error.message || error}`);
725
743
  }
726
744
  try {
727
745
  const data = {};
@@ -731,10 +749,10 @@ export default (api, accessories, config, tado, telegram) => {
731
749
  Logger.error(`Error while updating the tado states file: ${error.message || error}`);
732
750
  }
733
751
  try {
734
- //wait for fakegato services to be loaded
752
+ //wait for fakegato history services to be loaded
735
753
  await new Promise(r => setTimeout(r, 4000));
736
- for (const fnRefreshHistory of aRefreshHistoryHandlers) {
737
- fnRefreshHistory();
754
+ for (const refreshHistory of aRefreshHistoryHandlers) {
755
+ refreshHistory();
738
756
  }
739
757
  } catch (error) {
740
758
  Logger.error(`Error while refreshing history: ${error.message || error}`);
@@ -857,34 +875,36 @@ export default (api, accessories, config, tado, telegram) => {
857
875
  }
858
876
  }
859
877
 
860
- const allZones = (await tado.getZones(config.homeId)) || [];
878
+ if (idToUpdate === undefined) {
879
+ const allZones = (await tado.getZones(config.homeId)) || [];
861
880
 
862
- for (const [index, zone] of config.zones.entries()) {
863
- allZones.forEach((zoneWithID) => {
864
- if (zoneWithID.name === zone.name) {
865
- const heatAccessory = accessories.filter(
866
- (acc) => acc && acc.displayName === config.homeName + ' ' + zone.name + ' Heater'
867
- );
881
+ for (const [index, zone] of config.zones.entries()) {
882
+ allZones.forEach((zoneWithID) => {
883
+ if (zoneWithID.name === zone.name) {
884
+ const heatAccessory = accessories.filter(
885
+ (acc) => acc && acc.displayName === config.homeName + ' ' + zone.name + ' Heater'
886
+ );
868
887
 
869
- if (heatAccessory.length) heatAccessory[0].context.config.zoneId = zoneWithID.id;
870
-
871
- config.zones[index].id = zoneWithID.id;
872
- config.zones[index].battery = !config.zones[index].noBattery
873
- ? zoneWithID.devices.filter(
874
- (device) =>
875
- device &&
876
- (zone.type === 'HEATING' || zone.type === 'AIR_CONDITIONING') &&
877
- typeof device.batteryState === 'string' &&
878
- !device.batteryState.includes('NORMAL')
879
- ).length
880
- ? zoneWithID.devices.filter((device) => device && !device.batteryState.includes('NORMAL'))[0]
881
- .batteryState
882
- : zoneWithID.devices.filter((device) => device && device.duties.includes('ZONE_LEADER'))[0].batteryState
883
- : false;
884
- config.zones[index].openWindowEnabled =
885
- zoneWithID.openWindowDetection && zoneWithID.openWindowDetection.enabled ? true : false;
886
- }
887
- });
888
+ if (heatAccessory.length) heatAccessory[0].context.config.zoneId = zoneWithID.id;
889
+
890
+ config.zones[index].id = zoneWithID.id;
891
+ config.zones[index].battery = !config.zones[index].noBattery
892
+ ? zoneWithID.devices.filter(
893
+ (device) =>
894
+ device &&
895
+ (zone.type === 'HEATING' || zone.type === 'AIR_CONDITIONING') &&
896
+ typeof device.batteryState === 'string' &&
897
+ !device.batteryState.includes('NORMAL')
898
+ ).length
899
+ ? zoneWithID.devices.filter((device) => device && !device.batteryState.includes('NORMAL'))[0]
900
+ .batteryState
901
+ : zoneWithID.devices.filter((device) => device && device.duties.includes('ZONE_LEADER'))[0].batteryState
902
+ : false;
903
+ config.zones[index].openWindowEnabled =
904
+ zoneWithID.openWindowDetection && zoneWithID.openWindowDetection.enabled ? true : false;
905
+ }
906
+ });
907
+ }
888
908
  }
889
909
 
890
910
  let zonesToUpdate = [];
@@ -921,19 +941,21 @@ export default (api, accessories, config, tado, telegram) => {
921
941
 
922
942
  tempEqual = Math.round(currentTemp) === Math.round(targetTemp);
923
943
 
924
- currentState = currentTemp <= targetTemp ? 1 : 2;
944
+ //show as currently heating if current temp is lower than target temp, otherwise show as temp set
945
+ currentState = currentTemp < targetTemp ? 1 : 0;
925
946
 
926
- targetState = 1;
947
+ //check if auto mode is enabled
948
+ targetState = zoneState.overlayType === null ? 3 : 1;
927
949
 
928
950
  active = 1;
929
951
  } else {
952
+ //heating is switched off
930
953
  currentState = 0;
931
954
  targetState = 0;
932
955
  active = 0;
933
956
  }
934
957
 
935
- if (zoneState.overlayType === null) {
936
- currentState = 0;
958
+ if (targetState === undefined && zoneState.overlayType === null) {
937
959
  targetState = 3;
938
960
  }
939
961
  }
@@ -1568,7 +1590,7 @@ export default (api, accessories, config, tado, telegram) => {
1568
1590
  error = err;
1569
1591
  }
1570
1592
 
1571
- Logger.error(error, config.homeName);
1593
+ Logger.error("Error:", error, config.homeName);
1572
1594
 
1573
1595
  return;
1574
1596
  }
@@ -72,6 +72,7 @@ export default class Tado {
72
72
  }
73
73
 
74
74
  async getToken() {
75
+ Logger.debug('Get access token...', this.name);
75
76
  if (!this._tadoTokenPromise) {
76
77
  this._tadoTokenPromise = this._getToken().finally(() => {
77
78
  this._tadoTokenPromise = undefined;
@@ -212,7 +213,6 @@ export default class Tado {
212
213
  }
213
214
 
214
215
  async apiCall(path, method = 'GET', data = {}, params = {}, tado_url_dif) {
215
- Logger.debug('Get access token...', this.name);
216
216
  const access_token = this.skipAuth ? undefined : await this.getToken();
217
217
 
218
218
  let tadoLink = tado_url_dif || this.tadoApiUrl;