@homebridge-plugins/homebridge-tado 8.1.2 → 8.3.0-beta.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.2.0 - 2025-10-23
4
+ - Query all zone states in a single API request to significantly reduce the number of API calls to tado during polling
5
+ - Update zone state after clearing a zone overlay
6
+ - Skip getRunningTime call when a custom URL is used and remove obsolete getWeatherAirComfort (#176)
7
+ - Add example for tadoApiUrl in config (#176)
8
+
3
9
  ## v8.1.2 - 2025-10-20
4
10
  - Fix: Skip auth also for config endpoints if enabled (#176)
5
11
 
@@ -557,12 +557,17 @@
557
557
  "tadoApiUrl": {
558
558
  "title": "Tado API URL",
559
559
  "type": "string",
560
- "description": "Optional: Use a custom tado api url."
560
+ "description": "Optional: Use a custom tado api url (e.g. http://localhost:8080)."
561
561
  },
562
562
  "skipAuth": {
563
563
  "title": "Skip Authentication",
564
564
  "type": "boolean",
565
565
  "description": "Optional: Skip authentication for tado api."
566
+ },
567
+ "skipFakeGatoHistory": {
568
+ "title": "Skip FakeGato History Service",
569
+ "type": "boolean",
570
+ "description": "Optional: Skip creation of FakeGato history service."
566
571
  }
567
572
  }
568
573
  },
@@ -571,6 +576,7 @@
571
576
  "debug",
572
577
  "tadoApiUrl",
573
578
  "skipAuth",
579
+ "skipFakeGatoHistory",
574
580
  {
575
581
  "key": "homes",
576
582
  "type": "array",
@@ -134,6 +134,7 @@ async function createCustomSchema(home) {
134
134
  debug: pluginConfig[0].debug,
135
135
  tadoApiUrl: pluginConfig[0].tadoApiUrl,
136
136
  skipAuth: pluginConfig[0].skipAuth,
137
+ skipFakeGatoHistory: pluginConfig[0].skipFakeGatoHistory,
137
138
  homes: home
138
139
  });
139
140
 
@@ -143,6 +144,7 @@ async function createCustomSchema(home) {
143
144
  pluginConfig[0].debug = config.debug;
144
145
  pluginConfig[0].tadoApiUrl = config.tadoApiUrl;
145
146
  pluginConfig[0].skipAuth = config.skipAuth;
147
+ pluginConfig[0].skipFakeGatoHistory = config.skipFakeGatoHistory;
146
148
  pluginConfig[0].homes = pluginConfig[0].homes.map(myHome => {
147
149
  if (myHome.name === config.homes.name) {
148
150
  myHome = config.homes;
@@ -285,6 +287,7 @@ async function removeDeviceFromConfig(name) {
285
287
  delete pluginConfig[0].debug;
286
288
  delete pluginConfig[0].tadoApiUrl;
287
289
  delete pluginConfig[0].skipAuth;
290
+ delete pluginConfig[0].skipFakeGatoHistory;
288
291
  }
289
292
 
290
293
  await homebridge.updatePluginConfig(pluginConfig);
@@ -549,12 +549,17 @@ const schema = {
549
549
  'tadoApiUrl': {
550
550
  'title': 'Tado API URL',
551
551
  'type': 'string',
552
- 'description': 'Optional: Use a custom tado api url.'
552
+ 'description': 'Optional: Use a custom tado api url (e.g. http://localhost:8080).'
553
553
  },
554
554
  'skipAuth': {
555
555
  'title': 'Skip Authentication',
556
556
  'type': 'boolean',
557
557
  'description': 'Optional: Skip authentication for tado api.'
558
+ },
559
+ 'skipFakeGatoHistory': {
560
+ 'title': 'Skip FakeGato History Service',
561
+ 'type': 'boolean',
562
+ 'description': 'Optional: Skip creation of FakeGato history service.'
558
563
  }
559
564
  },
560
565
  'layout': [
@@ -562,6 +567,7 @@ const schema = {
562
567
  'debug',
563
568
  'tadoApiUrl',
564
569
  'skipAuth',
570
+ 'skipFakeGatoHistory',
565
571
  'homes.name',
566
572
  'homes.polling',
567
573
  'homes.temperatureUnit',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@homebridge-plugins/homebridge-tado",
3
- "version": "8.1.2",
3
+ "version": "8.3.0-beta.0",
4
4
  "description": "Homebridge plugin for controlling tado° devices.",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -40,8 +40,8 @@
40
40
  "moment": "^2.30.1"
41
41
  },
42
42
  "devDependencies": {
43
- "@babel/core": "7.28.4",
44
- "@babel/eslint-parser": "7.28.4",
43
+ "@babel/core": "7.28.5",
44
+ "@babel/eslint-parser": "7.28.5",
45
45
  "@babel/eslint-plugin": "7.27.1",
46
46
  "@eslint/js": "^9.38.0",
47
47
  "eslint": "^9.38.0",
@@ -51,4 +51,4 @@
51
51
  "globals": "^16.4.0",
52
52
  "prettier": "^3.6.2"
53
53
  }
54
- }
54
+ }
@@ -86,11 +86,11 @@ export default class ContactAccessory {
86
86
  .updateValue(this.accessory.context.timesOpened);
87
87
  });
88
88
 
89
- this.historyService = new this.FakeGatoHistoryService('door', this.accessory, {
89
+ this.historyService = this.FakeGatoHistoryService ? new this.FakeGatoHistoryService('door', this.accessory, {
90
90
  storage: 'fs',
91
91
  path: this.api.user.storagePath(),
92
92
  disableTimer: true,
93
- });
93
+ }) : undefined;
94
94
 
95
95
  await timeout(250); //wait for historyService to load
96
96
 
@@ -101,19 +101,18 @@ export default class ContactAccessory {
101
101
  this.deviceHandler.changedStates.bind(this, this.accessory, this.historyService, this.accessory.displayName)
102
102
  );
103
103
 
104
- this.refreshHistory(service);
104
+ if (!this.refreshHistoryHandlerRegistered) {
105
+ this.deviceHandler.refreshHistoryHandlers.push(() => this.refreshHistory(service));
106
+ this.refreshHistoryHandlerRegistered = true;
107
+ }
105
108
  }
106
109
 
107
110
  refreshHistory(service) {
108
111
  let state = service.getCharacteristic(this.api.hap.Characteristic.ContactSensorState).value;
109
112
 
110
- this.historyService.addEntry({
113
+ if (this.historyService) this.historyService.addEntry({
111
114
  time: moment().unix(),
112
115
  status: state ? 1 : 0,
113
116
  });
114
-
115
- setTimeout(() => {
116
- this.refreshHistory(service);
117
- }, 10 * 60 * 1000);
118
117
  }
119
118
  }
@@ -248,11 +248,11 @@ export default class HeaterCoolerAccessory {
248
248
  if (service.testCharacteristic(this.api.hap.Characteristic.RotationSpeed))
249
249
  service.removeCharacteristic(service.getCharacteristic(this.api.hap.Characteristic.RotationSpeed));
250
250
 
251
- this.historyService = new this.FakeGatoHistoryService('thermo', this.accessory, {
251
+ this.historyService = this.FakeGatoHistoryService ? new this.FakeGatoHistoryService('thermo', this.accessory, {
252
252
  storage: 'fs',
253
253
  path: this.api.user.storagePath(),
254
254
  disableTimer: true,
255
- });
255
+ }) : undefined;
256
256
 
257
257
  await timeout(250); //wait for historyService to load
258
258
 
@@ -324,7 +324,10 @@ export default class HeaterCoolerAccessory {
324
324
  this.deviceHandler.changedStates.bind(this, this.accessory, this.historyService, this.accessory.displayName)
325
325
  );
326
326
 
327
- this.refreshHistory(service);
327
+ if (!this.refreshHistoryHandlerRegistered) {
328
+ this.deviceHandler.refreshHistoryHandlers.push(() => this.refreshHistory(service));
329
+ this.refreshHistoryHandlerRegistered = true;
330
+ }
328
331
  }
329
332
 
330
333
  refreshHistory(service) {
@@ -338,15 +341,11 @@ export default class HeaterCoolerAccessory {
338
341
  : 0;
339
342
 
340
343
  //Thermo
341
- this.historyService.addEntry({
344
+ if (this.historyService) this.historyService.addEntry({
342
345
  time: moment().unix(),
343
346
  currentTemp: currentTemp,
344
347
  setTemp: targetTemp,
345
348
  valvePosition: valvePos,
346
349
  });
347
-
348
- setTimeout(() => {
349
- this.refreshHistory(service);
350
- }, 10 * 60 * 1000);
351
350
  }
352
351
  }
@@ -50,11 +50,11 @@ export default class HumidityAccessory {
50
50
  }
51
51
  }
52
52
 
53
- this.historyService = new this.FakeGatoHistoryService('room', this.accessory, {
53
+ this.historyService = this.FakeGatoHistoryService ? new this.FakeGatoHistoryService('room', this.accessory, {
54
54
  storage: 'fs',
55
55
  path: this.api.user.storagePath(),
56
56
  disableTimer: true,
57
- });
57
+ }) : undefined;
58
58
 
59
59
  await timeout(250); //wait for historyService to load
60
60
 
@@ -65,21 +65,20 @@ export default class HumidityAccessory {
65
65
  this.deviceHandler.changedStates.bind(this, this.accessory, this.historyService, this.accessory.displayName)
66
66
  );
67
67
 
68
- this.refreshHistory(service);
68
+ if (!this.refreshHistoryHandlerRegistered) {
69
+ this.deviceHandler.refreshHistoryHandlers.push(() => this.refreshHistory(service));
70
+ this.refreshHistoryHandlerRegistered = true;
71
+ }
69
72
  }
70
73
 
71
74
  refreshHistory(service) {
72
75
  let state = service.getCharacteristic(this.api.hap.Characteristic.CurrentRelativeHumidity).value;
73
76
 
74
- this.historyService.addEntry({
77
+ if (this.historyService) this.historyService.addEntry({
75
78
  time: moment().unix(),
76
79
  temp: 0,
77
80
  humidity: state,
78
81
  ppm: 0,
79
82
  });
80
-
81
- setTimeout(() => {
82
- this.refreshHistory(service);
83
- }, 10 * 60 * 1000);
84
83
  }
85
84
  }
@@ -41,11 +41,11 @@ export default class MotionAccessory {
41
41
  if (!service.testCharacteristic(this.api.hap.Characteristic.LastActivation))
42
42
  service.addCharacteristic(this.api.hap.Characteristic.LastActivation);
43
43
 
44
- this.historyService = new this.FakeGatoHistoryService('motion', this.accessory, {
44
+ this.historyService = this.FakeGatoHistoryService ? new this.FakeGatoHistoryService('motion', this.accessory, {
45
45
  storage: 'fs',
46
46
  path: this.api.user.storagePath(),
47
47
  disableTimer: true,
48
- });
48
+ }) : undefined;
49
49
 
50
50
  await timeout(250); //wait for historyService to load
51
51
 
@@ -56,19 +56,18 @@ export default class MotionAccessory {
56
56
  this.deviceHandler.changedStates.bind(this, this.accessory, this.historyService, this.accessory.displayName)
57
57
  );
58
58
 
59
- this.refreshHistory(service);
59
+ if (!this.refreshHistoryHandlerRegistered) {
60
+ this.deviceHandler.refreshHistoryHandlers.push(() => this.refreshHistory(service));
61
+ this.refreshHistoryHandlerRegistered = true;
62
+ }
60
63
  }
61
64
 
62
- async refreshHistory(service) {
65
+ refreshHistory(service) {
63
66
  let state = service.getCharacteristic(this.api.hap.Characteristic.MotionDetected).value;
64
67
 
65
- this.historyService.addEntry({
68
+ if (this.historyService) this.historyService.addEntry({
66
69
  time: moment().unix(),
67
70
  status: state ? 1 : 0,
68
71
  });
69
-
70
- setTimeout(() => {
71
- this.refreshHistory(service);
72
- }, 10 * 60 * 1000);
73
72
  }
74
73
  }
@@ -55,11 +55,11 @@ export default class TemperatureAccessory {
55
55
  maxValue: 100,
56
56
  });
57
57
 
58
- this.historyService = new this.FakeGatoHistoryService('room', this.accessory, {
58
+ this.historyService = this.FakeGatoHistoryService ? new this.FakeGatoHistoryService('room', this.accessory, {
59
59
  storage: 'fs',
60
60
  path: this.api.user.storagePath(),
61
61
  disableTimer: true,
62
- });
62
+ }) : undefined;
63
63
 
64
64
  await timeout(250); //wait for historyService to load
65
65
 
@@ -70,21 +70,20 @@ export default class TemperatureAccessory {
70
70
  this.deviceHandler.changedStates.bind(this, this.accessory, this.historyService, this.accessory.displayName)
71
71
  );
72
72
 
73
- this.refreshHistory(service);
73
+ if (!this.refreshHistoryHandlerRegistered) {
74
+ this.deviceHandler.refreshHistoryHandlers.push(() => this.refreshHistory(service));
75
+ this.refreshHistoryHandlerRegistered = true;
76
+ }
74
77
  }
75
78
 
76
79
  refreshHistory(service) {
77
80
  let state = service.getCharacteristic(this.api.hap.Characteristic.CurrentTemperature).value;
78
81
 
79
- this.historyService.addEntry({
82
+ if (this.historyService) this.historyService.addEntry({
80
83
  time: moment().unix(),
81
84
  temp: state,
82
85
  humidity: 0,
83
86
  ppm: 0,
84
87
  });
85
-
86
- setTimeout(() => {
87
- this.refreshHistory(service);
88
- }, 10 * 60 * 1000);
89
88
  }
90
89
  }
@@ -180,11 +180,11 @@ export default class ThermostatAccessory {
180
180
  if (!service.testCharacteristic(this.api.hap.Characteristic.ValvePosition))
181
181
  service.addCharacteristic(this.api.hap.Characteristic.ValvePosition);
182
182
 
183
- this.historyService = new this.FakeGatoHistoryService('thermo', this.accessory, {
183
+ this.historyService = this.FakeGatoHistoryService ? new this.FakeGatoHistoryService('thermo', this.accessory, {
184
184
  storage: 'fs',
185
185
  path: this.api.user.storagePath(),
186
186
  disableTimer: true,
187
- });
187
+ }) : undefined;
188
188
 
189
189
  await timeout(250); //wait for historyService to load
190
190
 
@@ -254,7 +254,10 @@ export default class ThermostatAccessory {
254
254
  this.deviceHandler.changedStates.bind(this, this.accessory, this.historyService, this.accessory.displayName)
255
255
  );
256
256
 
257
- this.refreshHistory(service);
257
+ if (!this.refreshHistoryHandlerRegistered) {
258
+ this.deviceHandler.refreshHistoryHandlers.push(() => this.refreshHistory(service));
259
+ this.refreshHistoryHandlerRegistered = true;
260
+ }
258
261
  }
259
262
 
260
263
  refreshHistory(service) {
@@ -268,16 +271,12 @@ export default class ThermostatAccessory {
268
271
  ? Math.round(targetTemp - currentTemp >= 5 ? 100 : (targetTemp - currentTemp) * 20)
269
272
  : 0;
270
273
 
271
- this.historyService.addEntry({
274
+ if (this.historyService) this.historyService.addEntry({
272
275
  time: moment().unix(),
273
276
  currentTemp: currentTemp,
274
277
  setTemp: targetTemp,
275
278
  valvePosition: valvePos,
276
279
  });
277
-
278
- setTimeout(() => {
279
- this.refreshHistory(service);
280
- }, 10 * 60 * 1000);
281
280
  }
282
281
 
283
282
  async changeUnit(service, value) {
@@ -1,12 +1,17 @@
1
1
  import Logger from '../helper/logger.js';
2
2
  import moment from 'moment';
3
+ import { writeFile } from 'fs/promises';
4
+ import { join } from "path";
3
5
 
4
6
  var settingState = false;
5
7
  var delayTimer = {};
6
8
 
7
9
  const timeout = (ms) => new Promise((res) => setTimeout(res, ms));
10
+ const aRefreshHistoryHandlers = [];
8
11
 
9
12
  export default (api, accessories, config, tado, telegram) => {
13
+ const storagePath = api.user.storagePath();
14
+
10
15
  async function setStates(accessory, accs, target, value) {
11
16
  accessories = accs.filter((acc) => acc && acc.context.config.homeName === config.homeName);
12
17
 
@@ -110,6 +115,7 @@ export default (api, accessories, config, tado, telegram) => {
110
115
  accessory.context.config.subtype.includes('heatercooler'))
111
116
  ) {
112
117
  await tado.clearZoneOverlay(config.homeId, accessory.context.config.zoneId);
118
+ await updateZones(accessory.context.config.zoneId);
113
119
  return;
114
120
  }
115
121
 
@@ -170,6 +176,7 @@ export default (api, accessories, config, tado, telegram) => {
170
176
  accessory.context.config.subtype.includes('heatercooler'))
171
177
  ) {
172
178
  await tado.clearZoneOverlay(config.homeId, accessory.context.config.zoneId);
179
+ await updateZones(accessory.context.config.zoneId);
173
180
  return;
174
181
  }
175
182
 
@@ -565,7 +572,7 @@ export default (api, accessories, config, tado, telegram) => {
565
572
  ? Math.round(targetTemp - currentTemp >= 5 ? 100 : (targetTemp - currentTemp) * 20)
566
573
  : 0;
567
574
 
568
- historyService.addEntry({
575
+ if (historyService) historyService.addEntry({
569
576
  time: moment().unix(),
570
577
  currentTemp: currentTemp,
571
578
  setTemp: targetTemp,
@@ -593,7 +600,7 @@ export default (api, accessories, config, tado, telegram) => {
593
600
  ? Math.round(targetTemp - currentTemp >= 5 ? 100 : (targetTemp - currentTemp) * 20)
594
601
  : 0;
595
602
 
596
- historyService.addEntry({
603
+ if (historyService) historyService.addEntry({
597
604
  time: moment().unix(),
598
605
  currentTemp: currentTemp,
599
606
  setTemp: targetTemp,
@@ -634,7 +641,7 @@ export default (api, accessories, config, tado, telegram) => {
634
641
  .updateValue(openDuration);
635
642
  }
636
643
 
637
- historyService.addEntry({ time: moment().unix(), status: value.newValue ? 1 : 0 });
644
+ if (historyService) historyService.addEntry({ time: moment().unix(), status: value.newValue ? 1 : 0 });
638
645
 
639
646
  let dest = value.newValue ? 'opened' : 'closed';
640
647
 
@@ -656,7 +663,7 @@ export default (api, accessories, config, tado, telegram) => {
656
663
  .getCharacteristic(api.hap.Characteristic.LastActivation)
657
664
  .updateValue(lastActivation);
658
665
 
659
- historyService.addEntry({ time: moment().unix(), status: value.newValue ? 1 : 0 });
666
+ if (historyService) historyService.addEntry({ time: moment().unix(), status: value.newValue ? 1 : 0 });
660
667
  }
661
668
 
662
669
  let dest;
@@ -677,13 +684,13 @@ export default (api, accessories, config, tado, telegram) => {
677
684
 
678
685
  case 'zone-temperature':
679
686
  case 'weather-temperature': {
680
- historyService.addEntry({ time: moment().unix(), temp: value.newValue, humidity: 0, ppm: 0 });
687
+ if (historyService) historyService.addEntry({ time: moment().unix(), temp: value.newValue, humidity: 0, ppm: 0 });
681
688
 
682
689
  break;
683
690
  }
684
691
 
685
692
  case 'zone-humidity': {
686
- historyService.addEntry({ time: moment().unix(), temp: 0, humidity: value.newValue, ppm: 0 });
693
+ if (historyService) historyService.addEntry({ time: moment().unix(), temp: 0, humidity: value.newValue, ppm: 0 });
687
694
 
688
695
  break;
689
696
  }
@@ -695,7 +702,32 @@ export default (api, accessories, config, tado, telegram) => {
695
702
  }
696
703
  }
697
704
 
705
+ async function refreshHistory(homeId, zoneStates) {
706
+ try {
707
+ const data = {};
708
+ data.counterData = await tado.getCounterData();
709
+ await writeFile(join(storagePath, "tado-counter.json"), JSON.stringify(data, null, 2), "utf-8");
710
+ } catch (error) {
711
+ Logger.error(`Error while updating tado counter file: ${error.message || error}`);
712
+ }
713
+ try {
714
+ const data = {};
715
+ data.zoneStates = zoneStates ?? {};
716
+ await writeFile(join(storagePath, `tado-states-${homeId}.json`), JSON.stringify(data, null, 2), "utf-8");
717
+ } catch (error) {
718
+ Logger.error(`Error while updating tado states file for home id ${homeId}: ${error.message || error}`);
719
+ }
720
+ try {
721
+ for (const fnRefreshHistory of aRefreshHistoryHandlers) {
722
+ fnRefreshHistory();
723
+ }
724
+ } catch (error) {
725
+ Logger.error(`Error while refreshing history: ${error.message || error}`);
726
+ }
727
+ }
728
+
698
729
  async function getStates() {
730
+ let zoneStates = {};
699
731
  try {
700
732
  //ME
701
733
  if (!config.homeId) await updateMe();
@@ -704,7 +736,7 @@ export default (api, accessories, config, tado, telegram) => {
704
736
  if (!config.temperatureUnit) await updateHome();
705
737
 
706
738
  //Zones
707
- if (config.zones.length) await updateZones();
739
+ if (config.zones.length) zoneStates = await updateZones();
708
740
 
709
741
  //MobileDevices
710
742
  if (config.presence.length) await updateMobileDevices();
@@ -723,10 +755,23 @@ export default (api, accessories, config, tado, telegram) => {
723
755
  } catch (err) {
724
756
  errorHandler(err);
725
757
  } finally {
758
+ refreshHistory(config.homeId, zoneStates);
726
759
  setTimeout(() => {
727
760
  getStates();
728
761
  }, Math.max(config.polling, 300) * 1000);
729
762
  }
763
+
764
+ //log tado api counter every hour
765
+ async function logCounter() {
766
+ try {
767
+ const iCounter = (await tado.getCounterData()).counter;
768
+ Logger.info(`Tado API counter: ${iCounter.toLocaleString('en-US')}`);
769
+ } catch (error) {
770
+ Logger.warn(`Failed to get Tado API counter: ${error.message || error}`);
771
+ }
772
+ }
773
+ void logCounter();
774
+ setInterval(logCounter, 60 * 60 * 1000);
730
775
  }
731
776
 
732
777
  async function updateMe() {
@@ -770,8 +815,9 @@ export default (api, accessories, config, tado, telegram) => {
770
815
  return;
771
816
  }
772
817
 
773
- async function updateZones() {
774
- if (!settingState) {
818
+ async function updateZones(idToUpdate) {
819
+ let zoneStates = {};
820
+ if (!settingState || idToUpdate !== undefined) {
775
821
  Logger.debug('Polling Zones...', config.homeName);
776
822
 
777
823
  //CentralSwitch
@@ -821,8 +867,17 @@ export default (api, accessories, config, tado, telegram) => {
821
867
  });
822
868
  }
823
869
 
824
- for (const zone of config.zones) {
825
- const zoneState = await tado.getZoneState(config.homeId, zone.id);
870
+ let zonesToUpdate = [];
871
+ if (idToUpdate !== undefined) {
872
+ zoneStates[idToUpdate] = await tado.getZoneState(config.homeId, idToUpdate);
873
+ zonesToUpdate = config.zones.filter(zone => zone.id === idToUpdate);
874
+ } else {
875
+ zoneStates = (await tado.getZoneStates(config.homeId))["zoneStates"];
876
+ zonesToUpdate = config.zones;
877
+ }
878
+
879
+ for (const zone of zonesToUpdate) {
880
+ const zoneState = zoneStates[zone.id];
826
881
 
827
882
  let currentState, targetState, currentTemp, targetTemp, humidity, active, battery, tempEqual;
828
883
 
@@ -1221,6 +1276,7 @@ export default (api, accessories, config, tado, telegram) => {
1221
1276
  });
1222
1277
  }
1223
1278
  }
1279
+ return zoneStates = {};
1224
1280
  }
1225
1281
 
1226
1282
  async function updateMobileDevices() {
@@ -1501,5 +1557,6 @@ export default (api, accessories, config, tado, telegram) => {
1501
1557
  getStates: getStates,
1502
1558
  setStates: setStates,
1503
1559
  changedStates: changedStates,
1560
+ refreshHistoryHandlers: aRefreshHistoryHandlers
1504
1561
  };
1505
1562
  };
package/src/platform.js CHANGED
@@ -41,7 +41,7 @@ class TadoPlatform {
41
41
  Logger.init(log, config.debug);
42
42
  CustomTypes.registerWith(api.hap);
43
43
  EveTypes.registerWith(api.hap);
44
- FakeGatoHistoryService = fakeGatoHistory(api);
44
+ FakeGatoHistoryService = config.skipFakeGatoHistory ? undefined : fakeGatoHistory(api);
45
45
 
46
46
  this.api = api;
47
47
  this.accessories = [];
@@ -1,7 +1,7 @@
1
1
  import Logger from '../helper/logger.js';
2
2
  import got from 'got';
3
3
  import path from 'path';
4
- import fs from 'fs/promises';
4
+ import fs, { access, readFile } from 'fs/promises';
5
5
 
6
6
  const tado_url = "https://my.tado.com";
7
7
  const tado_auth_url = "https://login.tado.com/oauth2";
@@ -10,7 +10,9 @@ const tado_client_id = "1bb50063-6b0c-4d11-bd99-387f4a91cc46";
10
10
  export default class Tado {
11
11
  constructor(name, config, storagePath, tadoApiUrl, skipAuth) {
12
12
  this.tadoApiUrl = tadoApiUrl || tado_url;
13
+ this.customTadoApiUrlActive = !!tadoApiUrl;
13
14
  this.skipAuth = skipAuth?.toString() === "true";
15
+ this.storagePath = storagePath;
14
16
  this.name = name;
15
17
  const usesExternalTokenFile = config.username?.toLowerCase().endsWith(".json");
16
18
  this._tadoExternalTokenFilePath = usesExternalTokenFile ? config.username : undefined;
@@ -23,13 +25,59 @@ export default class Tado {
23
25
  return (hash >>> 0).toString(36).padStart(7, '0');
24
26
  };
25
27
  this.username = usesExternalTokenFile ? undefined : config.username;
26
- this._tadoInternalTokenFilePath = usesExternalTokenFile ? undefined : path.join(storagePath, `.tado-token-${fnSimpleHash(config.username)}.json`);
28
+ this._tadoInternalTokenFilePath = usesExternalTokenFile ? undefined : path.join(this.storagePath, `.tado-token-${fnSimpleHash(config.username)}.json`);
27
29
  this._tadoApiClientId = tado_client_id;
28
30
  this._tadoTokenPromise = undefined;
29
31
  this._tadoAuthenticationCallback = undefined;
32
+ this._counterInitPromise = this._initCounter();
30
33
  Logger.debug("API successfull initialized", this.name);
31
34
  }
32
35
 
36
+ async _initCounter() {
37
+ let counterData;
38
+ try {
39
+ const sFilePath = path.join(this.storagePath, `tado-counter.json`);
40
+ await access(sFilePath);
41
+ const sData = (await readFile(sFilePath, "utf-8"));
42
+ counterData = JSON.parse(sData)?.counterData;
43
+ } catch (_err) {
44
+ //no counter data => ignore
45
+ }
46
+ this._counter = counterData?.counter ?? 0;
47
+ this._counterTimestamp = counterData?.counterTimestamp ?? new Date().toISOString();
48
+ this._checkCounterMidnightReset();
49
+ }
50
+
51
+ _checkCounterMidnightReset() {
52
+ const timezone = "Europe/Berlin";
53
+ const now = new Date();
54
+ const last = new Date(this._counterTimestamp || 0);
55
+ const formatDate = (date) => date.toLocaleDateString("en-US", { timeZone: timezone });
56
+ if (formatDate(now) !== formatDate(last)) {
57
+ this._counter = 0;
58
+ this._counterTimestamp = new Date().toISOString();
59
+ }
60
+ }
61
+
62
+ async _increaseCounter() {
63
+ try {
64
+ await this._counterInitPromise;
65
+ this._checkCounterMidnightReset();
66
+ this._counter++;
67
+ this._counterTimestamp = new Date().toISOString();
68
+ } catch (error) {
69
+ Logger.warn(`Failed to increase tado api counter: ${error.message || error}`);
70
+ }
71
+ }
72
+
73
+ async getCounterData() {
74
+ await this._counterInitPromise;
75
+ return {
76
+ counter: this._counter,
77
+ counterTimestamp: this._counterTimestamp
78
+ };
79
+ }
80
+
33
81
  async getToken() {
34
82
  if (!this._tadoTokenPromise) {
35
83
  this._tadoTokenPromise = this._getToken().finally(() => {
@@ -94,6 +142,7 @@ export default class Tado {
94
142
  },
95
143
  responseType: "json"
96
144
  });
145
+ await this._increaseCounter();
97
146
  const { access_token, refresh_token } = response.body;
98
147
  if (!access_token || !refresh_token) throw new Error("Empty access/refresh token.");
99
148
  await fs.writeFile(this._tadoInternalTokenFilePath, JSON.stringify({ access_token, refresh_token }));
@@ -113,6 +162,7 @@ export default class Tado {
113
162
  },
114
163
  responseType: "json"
115
164
  });
165
+ await this._increaseCounter();
116
166
  const { device_code, verification_uri_complete } = authResponse.body;
117
167
  if (!device_code) throw new Error("Failed to retrieve device code.");
118
168
  Logger.info(`Open the following URL in your browser, click "submit" and log in to your tado° account "${this.username}": ${verification_uri_complete}`);
@@ -130,6 +180,7 @@ export default class Tado {
130
180
  },
131
181
  responseType: "json"
132
182
  });
183
+ await this._increaseCounter();
133
184
  } catch (_error) {
134
185
  //authentication still pending -> response code 400
135
186
  }
@@ -210,6 +261,7 @@ export default class Tado {
210
261
 
211
262
  try {
212
263
  const response = await got(tadoLink + path, config);
264
+ await this._increaseCounter();
213
265
  return response.body;
214
266
  } catch (error) {
215
267
  Logger.error(`API Request [${method} ${path}] - FAILED: ${error.message}`, this.name);
@@ -308,6 +360,10 @@ export default class Tado {
308
360
  return this.apiCall(`/api/v2/homes/${home_id}/zones/${zone_id}/state`);
309
361
  }
310
362
 
363
+ async getZoneStates(home_id) {
364
+ return this.apiCall(`/api/v2/homes/${home_id}/zoneStates`);
365
+ }
366
+
311
367
  async getZoneCapabilities(home_id, zone_id) {
312
368
  return this.apiCall(`/api/v2/homes/${home_id}/zones/${zone_id}/capabilities`);
313
369
  }
@@ -543,20 +599,6 @@ export default class Tado {
543
599
  return this.apiCall(`/api/v2/homes/${home_id}/airComfort`);
544
600
  }
545
601
 
546
- async getWeatherAirComfort(home_id, longitude, latitude) {
547
- let geoLocation = {
548
- longitude: longitude,
549
- latitude: latitude,
550
- };
551
-
552
- if (!geoLocation.longitude || !geoLocation.latitude) {
553
- const data = await this.getHome(home_id);
554
- geoLocation = data.geolocation;
555
- }
556
-
557
- return this.apiCall(`/v1/homes/${home_id}/airComfort`, 'GET', {}, geoLocation, 'https://acme.tado.com');
558
- }
559
-
560
602
  async setChildLock(serialNumber, state) {
561
603
  if (!serialNumber) {
562
604
  throw new Error('Cannot change child lock state. No serialNumber is given.');
@@ -614,6 +656,8 @@ export default class Tado {
614
656
  }
615
657
 
616
658
  async getRunningTime(home_id, time, from, to) {
659
+ if (this.customTadoApiUrlActive) return;
660
+
617
661
  const period = {
618
662
  aggregate: time || 'day',
619
663
  summary_only: true,
@@ -623,6 +667,6 @@ export default class Tado {
623
667
 
624
668
  if (to) period.to = to;
625
669
 
626
- return this.apiCall(`/v1/homes/${home_id}/runningTimes`, 'GET', {}, period, 'https://minder.tado.com', false);
670
+ return this.apiCall(`/v1/homes/${home_id}/runningTimes`, 'GET', {}, period, 'https://minder.tado.com');
627
671
  }
628
672
  }