@homebridge-plugins/homebridge-tado 8.6.0-beta.2 → 8.6.0-beta.4

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,17 @@
1
1
  # Changelog
2
2
 
3
- ## v8.6.0 - 2025-10-30
4
- - BREAKING CHANGE: If you use tadoApiUrl or skipAuth, you must now define them under each corresponding home in your configuration (#176)
5
- - Refactor configuration: moved tadoApiUrl and skipAuth into individual home configs to support multiple API URLs (#176)
6
- - Persist tado zone states after every zone states update
3
+ ## v8.6.0 - 2025-10-31
4
+ - BREAKING CHANGE: If you use tadoApiUrl or skipAuth, they must now be defined under each corresponding home in your configuration (#176)
5
+ - Refactor configuration: Moved tadoApiUrl and skipAuth into individual home configs to support multiple API URLs (#176)
6
+ - Persist Tado zone states after zone state updates
7
7
  - Improved zone update logic: when setting a state, all zones are now updated immediately if the next scheduled update is more than 10 seconds away
8
+ - Advanced queue handling for update and persistence tasks
9
+ - Prevent updates during active setStates
10
+ - Tado API counter now tracks and persists for each authenticated user
11
+ - Fix: Polling and tasks for multiple homes (#178)
8
12
  - Fix: Corrected zone update handling that could previously cause unintended heating changes (#178)
13
+ - Added additional debug log messages for zone updates
14
+ - Note: This update will reset your tado api counter for the current day to zero.
9
15
 
10
16
  ## v8.5.0 - 2025-10-27
11
17
  - Change minimum polling interval to 30s due to improvements made in v8.2.0
@@ -568,6 +568,11 @@
568
568
  "title": "Disable History Service",
569
569
  "type": "boolean",
570
570
  "description": "Optional: Skip creation of history service."
571
+ },
572
+ "preferSiriTemperature": {
573
+ "title": "Prefer Siri temperature changes",
574
+ "type": "boolean",
575
+ "description": "Prefers temperature changes when the Auto (state=3) mode is sent simultaneously. Default: false."
571
576
  }
572
577
  }
573
578
  },
@@ -575,6 +580,7 @@
575
580
  "name",
576
581
  "debug",
577
582
  "disableHistoryService",
583
+ "preferSiriTemperature",
578
584
  {
579
585
  "key": "homes",
580
586
  "type": "array",
@@ -133,6 +133,7 @@ async function createCustomSchema(home) {
133
133
  name: pluginConfig[0].name,
134
134
  debug: pluginConfig[0].debug,
135
135
  disableHistoryService: pluginConfig[0].disableHistoryService,
136
+ preferSiriTemperature: pluginConfig[0].preferSiriTemperature,
136
137
  homes: home
137
138
  });
138
139
 
@@ -141,6 +142,7 @@ async function createCustomSchema(home) {
141
142
  pluginConfig[0].name = config.name;
142
143
  pluginConfig[0].debug = config.debug;
143
144
  pluginConfig[0].disableHistoryService = config.disableHistoryService;
145
+ pluginConfig[0].preferSiriTemperature = config.preferSiriTemperature;
144
146
  pluginConfig[0].homes = pluginConfig[0].homes.map(myHome => {
145
147
  if (myHome.name === config.homes.name) {
146
148
  myHome = config.homes;
@@ -284,6 +286,7 @@ async function removeDeviceFromConfig(name) {
284
286
  if (!pluginConfig[0].homes.length) {
285
287
  delete pluginConfig[0].debug;
286
288
  delete pluginConfig[0].disableHistoryService;
289
+ delete pluginConfig[0].preferSiriTemperature;
287
290
  }
288
291
 
289
292
  await homebridge.updatePluginConfig(pluginConfig);
@@ -560,12 +560,18 @@ const schema = {
560
560
  'title': 'Disable History Service',
561
561
  'type': 'boolean',
562
562
  'description': 'Optional: Skip creation of history service.'
563
+ },
564
+ 'preferSiriTemperature': {
565
+ 'title': 'Prefer Siri temperature changes',
566
+ 'type': 'boolean',
567
+ 'description': 'Prefers temperature changes when the Auto (state=3) mode is sent simultaneously. Default: false.'
563
568
  }
564
569
  },
565
570
  'layout': [
566
571
  'name',
567
572
  'debug',
568
573
  'disableHistoryService',
574
+ 'preferSiriTemperature',
569
575
  'homes.name',
570
576
  'homes.polling',
571
577
  'homes.temperatureUnit',
@@ -21,7 +21,7 @@ class UiServer extends HomebridgePluginUiServer {
21
21
  username: config.username,
22
22
  tadoApiUrl: config.tadoApiUrl,
23
23
  skipAuth: config.skipAuth
24
- }, this.homebridgeStoragePath);
24
+ }, this.homebridgeStoragePath, false);
25
25
 
26
26
  return;
27
27
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@homebridge-plugins/homebridge-tado",
3
- "version": "8.6.0-beta.2",
3
+ "version": "8.6.0-beta.4",
4
4
  "description": "Homebridge plugin for controlling tado° devices.",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -43,12 +43,12 @@
43
43
  "@babel/core": "7.28.5",
44
44
  "@babel/eslint-parser": "7.28.5",
45
45
  "@babel/eslint-plugin": "7.27.1",
46
- "@eslint/js": "^9.38.0",
47
- "eslint": "^9.38.0",
46
+ "@eslint/js": "^9.39.0",
47
+ "eslint": "^9.39.0",
48
48
  "eslint-config-prettier": "^10.1.8",
49
49
  "eslint-plugin-import": "^2.32.0",
50
50
  "eslint-plugin-prettier": "^5.5.4",
51
- "globals": "^16.4.0",
51
+ "globals": "^16.5.0",
52
52
  "prettier": "^3.6.2"
53
53
  }
54
54
  }
@@ -1,10 +1,11 @@
1
1
  import Logger from '../helper/logger.js';
2
2
  import moment from 'moment';
3
+ import { TadoUpdateBuffer } from '../helper/update-buffer.js'
3
4
 
4
5
  const timeout = (ms) => new Promise((res) => setTimeout(res, ms));
5
6
 
6
7
  export default class HeaterCoolerAccessory {
7
- constructor(api, accessory, accessories, tado, deviceHandler, FakeGatoHistoryService) {
8
+ constructor(api, accessory, accessories, tado, deviceHandler, preferSiriTemperature, FakeGatoHistoryService) {
8
9
  this.api = api;
9
10
  this.accessory = accessory;
10
11
  this.accessories = accessories;
@@ -15,6 +16,10 @@ export default class HeaterCoolerAccessory {
15
16
 
16
17
  this.autoDelayTimeout = null;
17
18
 
19
+ this.updateBuffer = new TadoUpdateBuffer((target, value) => {
20
+ return this.deviceHandler.setStates(this.accessory, this.accessories, target, value);
21
+ }, preferSiriTemperature);
22
+
18
23
  this.getService();
19
24
  }
20
25
 
@@ -258,16 +263,7 @@ export default class HeaterCoolerAccessory {
258
263
 
259
264
  service
260
265
  .getCharacteristic(this.api.hap.Characteristic.Active)
261
- .onSet((value) => {
262
- if (this.waitForEndValue) {
263
- clearTimeout(this.waitForEndValue);
264
- this.waitForEndValue = null;
265
- }
266
-
267
- this.waitForEndValue = setTimeout(() => {
268
- this.deviceHandler.setStates(this.accessory, this.accessories, 'State', value);
269
- }, 500);
270
- })
266
+ .onSet(value => this.updateBuffer.setState(value))
271
267
  .on(
272
268
  'change',
273
269
  this.deviceHandler.changedStates.bind(this, this.accessory, this.historyService, this.accessory.displayName)
@@ -282,16 +278,7 @@ export default class HeaterCoolerAccessory {
282
278
 
283
279
  service
284
280
  .getCharacteristic(this.api.hap.Characteristic.HeatingThresholdTemperature)
285
- .onSet((value) => {
286
- if (this.waitForEndValue) {
287
- clearTimeout(this.waitForEndValue);
288
- this.waitForEndValue = null;
289
- }
290
-
291
- this.waitForEndValue = setTimeout(() => {
292
- this.deviceHandler.setStates(this.accessory, this.accessories, 'Temperature', value);
293
- }, 250);
294
- })
281
+ .onSet(value => this.updateBuffer.setTemperature(value))
295
282
  .on(
296
283
  'change',
297
284
  this.deviceHandler.changedStates.bind(this, this.accessory, this.historyService, this.accessory.displayName)
@@ -301,16 +288,7 @@ export default class HeaterCoolerAccessory {
301
288
  if (this.accessory.context.config.type === 'AIR_CONDITIONING') {
302
289
  service
303
290
  .getCharacteristic(this.api.hap.Characteristic.CoolingThresholdTemperature)
304
- .onSet((value) => {
305
- if (this.waitForEndValue) {
306
- clearTimeout(this.waitForEndValue);
307
- this.waitForEndValue = null;
308
- }
309
-
310
- this.waitForEndValue = setTimeout(() => {
311
- this.deviceHandler.setStates(this.accessory, this.accessories, 'Temperature', value);
312
- }, 250);
313
- })
291
+ .onSet(value => this.updateBuffer.setTemperature(value))
314
292
  .on(
315
293
  'change',
316
294
  this.deviceHandler.changedStates.bind(this, this.accessory, this.historyService, this.accessory.displayName)
@@ -1,11 +1,12 @@
1
1
  import Logger from '../helper/logger.js';
2
2
  import moment from 'moment';
3
3
  import fs from 'fs-extra';
4
+ import { TadoUpdateBuffer } from '../helper/update-buffer.js'
4
5
 
5
6
  const timeout = (ms) => new Promise((res) => setTimeout(res, ms));
6
7
 
7
8
  export default class ThermostatAccessory {
8
- constructor(api, accessory, accessories, tado, deviceHandler, FakeGatoHistoryService) {
9
+ constructor(api, accessory, accessories, tado, deviceHandler, preferSiriTemperature, FakeGatoHistoryService) {
9
10
  this.api = api;
10
11
  this.accessory = accessory;
11
12
  this.accessories = accessories;
@@ -16,6 +17,10 @@ export default class ThermostatAccessory {
16
17
 
17
18
  this.autoDelayTimeout = null;
18
19
 
20
+ this.updateBuffer = new TadoUpdateBuffer((target, value) => {
21
+ return this.deviceHandler.setStates(this.accessory, this.accessories, target, value);
22
+ }, preferSiriTemperature);
23
+
19
24
  this.getService();
20
25
  }
21
26
 
@@ -192,20 +197,8 @@ export default class ThermostatAccessory {
192
197
  .getCharacteristic(this.api.hap.Characteristic.TemperatureDisplayUnits)
193
198
  .onSet(this.changeUnit.bind(this, service));
194
199
 
195
- service.getCharacteristic(this.api.hap.Characteristic.TargetHeatingCoolingState).onSet((value) => {
196
- if (this.waitForEndValue) {
197
- clearTimeout(this.waitForEndValue);
198
- this.waitForEndValue = null;
199
- }
200
-
201
- this.waitForEndValue = setTimeout(() => {
202
- if (this.settingTemperature) {
203
- this.settingTemperature = false;
204
- return;
205
- }
206
- this.deviceHandler.setStates(this.accessory, this.accessories, 'State', value);
207
- }, 500);
208
- });
200
+ service.getCharacteristic(this.api.hap.Characteristic.TargetHeatingCoolingState)
201
+ .onSet(value => this.updateBuffer.setState(value));
209
202
 
210
203
  service
211
204
  .getCharacteristic(this.api.hap.Characteristic.CurrentTemperature)
@@ -216,12 +209,7 @@ export default class ThermostatAccessory {
216
209
 
217
210
  service
218
211
  .getCharacteristic(this.api.hap.Characteristic.TargetTemperature)
219
- .onSet((value) => {
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);
224
- })
212
+ .onSet(value => this.updateBuffer.setTemperature(value))
225
213
  .on(
226
214
  'change',
227
215
  this.deviceHandler.changedStates.bind(this, this.accessory, this.historyService, this.accessory.displayName)
@@ -1,42 +1,41 @@
1
1
  import Logger from '../helper/logger.js';
2
2
  import moment from 'moment';
3
- import { writeFile, access, readFile } from 'fs/promises';
3
+ import { writeFile } from 'fs/promises';
4
4
  import { join } from "path";
5
-
6
- let settingState = false;
7
- let tasksInitialized = false;
8
- let lastGetStates = 0;
9
- const delayTimer = {};
5
+ import { randomUUID } from 'crypto';
10
6
 
11
7
  const timeout = (ms) => new Promise((res) => setTimeout(res, ms));
12
- const aRefreshHistoryHandlers = [];
13
-
14
- export async function getPersistedStates(storagePath) {
15
- try {
16
- const sFilePath = join(storagePath, "tado-states.json");
17
- await access(sFilePath);
18
- const sData = (await readFile(sFilePath, "utf-8"));
19
- if (sData) return JSON.parse(sData);
20
- } catch (error) {
21
- //no states data => ignore
22
- Logger.debug(`Failed to read tado states file: ${error.message || error}`);
23
- }
24
- }
8
+ const helpers = {};
25
9
 
26
10
  export default (api, accessories, config, tado, telegram) => {
27
- const statesIntervalTime = Math.max(config.polling, 30) * 1000;
28
- const storagePath = api.user.storagePath();
11
+ //init helper variables for current home scope
12
+ if (!helpers[config.homeId]) {
13
+ helpers[config.homeId] = {
14
+ activeSettingStateRuns: {},
15
+ tasksInitialized: false,
16
+ lastGetStates: 0,
17
+ lastPersistZoneStates: 0,
18
+ persistPromise: Promise.resolve(),
19
+ updateZonesRunning: false,
20
+ updateZonesNextQueued: false,
21
+ delayTimer: {},
22
+ refreshHistoryHandlers: [],
23
+ statesIntervalTime: Math.max(config.polling, 30) * 1000,
24
+ storagePath: api.user.storagePath(),
25
+ }
26
+ }
27
+
28
+ function settingStates() {
29
+ return Object.keys(helpers[config.homeId].activeSettingStateRuns).length > 0;
30
+ }
29
31
 
30
32
  async function setStates(accessory, accs, target, value) {
31
33
  let zoneUpdated = false;
32
-
33
34
  accessories = accs.filter((acc) => acc && acc.context.config.homeName === config.homeName);
34
-
35
+ const runId = randomUUID();
35
36
  try {
36
- settingState = true;
37
-
37
+ helpers[config.homeId].activeSettingStateRuns[runId] = true;
38
38
  value = typeof value === 'number' ? parseFloat(value.toFixed(2)) : value;
39
-
40
39
  Logger.info(target + ': ' + value, accessory.displayName);
41
40
 
42
41
  switch (accessory.context.config.subtype) {
@@ -63,10 +62,10 @@ export default (api, accessories, config, tado, telegram) => {
63
62
  value < 5
64
63
  ) {
65
64
  if (value === 0) {
66
- if (delayTimer[accessory.displayName]) {
65
+ if (helpers[config.homeId].delayTimer[accessory.displayName]) {
67
66
  Logger.info('Resetting delay timer', accessory.displayName);
68
- clearTimeout(delayTimer[accessory.displayName]);
69
- delayTimer[accessory.displayName] = null;
67
+ clearTimeout(helpers[config.homeId].delayTimer[accessory.displayName]);
68
+ helpers[config.homeId].delayTimer[accessory.displayName] = null;
70
69
  }
71
70
 
72
71
  power = 'OFF';
@@ -111,14 +110,14 @@ export default (api, accessories, config, tado, telegram) => {
111
110
  let timer = accessory.context.delayTimer;
112
111
  let tarState = value === 1 ? 'HEAT' : 'AUTO';
113
112
 
114
- if (delayTimer[accessory.displayName]) {
115
- clearTimeout(delayTimer[accessory.displayName]);
116
- delayTimer[accessory.displayName] = null;
113
+ if (helpers[config.homeId].delayTimer[accessory.displayName]) {
114
+ clearTimeout(helpers[config.homeId].delayTimer[accessory.displayName]);
115
+ helpers[config.homeId].delayTimer[accessory.displayName] = null;
117
116
  }
118
117
 
119
118
  Logger.info('Wait ' + timer + ' seconds before switching state', accessory.displayName);
120
119
 
121
- delayTimer[accessory.displayName] = setTimeout(async () => {
120
+ helpers[config.homeId].delayTimer[accessory.displayName] = setTimeout(async () => {
122
121
  Logger.info('Delay timer finished, switching state to ' + tarState, accessory.displayName);
123
122
 
124
123
  //targetState
@@ -172,7 +171,7 @@ export default (api, accessories, config, tado, telegram) => {
172
171
  );
173
172
  }
174
173
 
175
- delayTimer[accessory.displayName] = null;
174
+ helpers[config.homeId].delayTimer[accessory.displayName] = null;
176
175
  }, timer * 1000);
177
176
  }
178
177
  } else {
@@ -565,13 +564,14 @@ export default (api, accessories, config, tado, telegram) => {
565
564
  break;
566
565
  }
567
566
  } catch (err) {
567
+ console.log("error at setStates", err);
568
568
  errorHandler(err);
569
569
  } finally {
570
- settingState = false;
570
+ delete helpers[config.homeId].activeSettingStateRuns[runId];
571
571
  //update zones to ensure correct state in Apple Home
572
- const timeSinceLastGetStates = Date.now() - lastGetStates;
573
- const statesIntervalTimeLeft = statesIntervalTime - timeSinceLastGetStates;
574
- if (zoneUpdated && statesIntervalTimeLeft > 10_000) await updateZones();
572
+ const timeSinceLastGetStates = helpers[config.homeId].lastGetStates === 0 ? 0 : (Date.now() - helpers[config.homeId].lastGetStates);
573
+ const statesIntervalTimeLeft = helpers[config.homeId].statesIntervalTime - timeSinceLastGetStates;
574
+ if (!settingStates() && zoneUpdated && statesIntervalTimeLeft > (10 * 1000)) await updateZones();
575
575
  }
576
576
  }
577
577
 
@@ -729,61 +729,49 @@ export default (api, accessories, config, tado, telegram) => {
729
729
  }
730
730
  }
731
731
 
732
- async function persistZoneStates(homeId, zoneStates) {
732
+ function persistZoneStates(homeId, zoneStates) {
733
+ helpers[config.homeId].persistPromise = helpers[config.homeId].persistPromise.then(() => _persistZoneStates(homeId, zoneStates));
734
+ }
735
+
736
+ async function _persistZoneStates(homeId, zoneStates) {
737
+ if ((Date.now() - helpers[config.homeId].lastPersistZoneStates) < (10 * 1000)) return;
733
738
  try {
734
739
  if (zoneStates && Object.keys(zoneStates).length) {
735
740
  const homeData = {};
736
741
  homeData.zoneStates = zoneStates;
737
- await writeFile(join(storagePath, `tado-states-${homeId}.json`), JSON.stringify(homeData, null, 2), "utf-8");
742
+ await writeFile(join(helpers[config.homeId].storagePath, `tado-states-${homeId}.json`), JSON.stringify(homeData, null, 2), "utf-8");
743
+ helpers[config.homeId].lastPersistZoneStates = Date.now();
738
744
  } else {
739
- Logger.info(`Skipping persistence of tado states for home ${homeId}: zone states are empty.`);
745
+ Logger.warn(`Skipping persistence of tado states file for home ${homeId}: zone states are empty.`);
740
746
  }
741
747
  } catch (error) {
742
748
  Logger.error(`Error while updating the tado states file for home ${homeId}: ${error.message || error}`);
743
749
  }
744
750
  }
745
751
 
746
- async function persistStates() {
747
- try {
748
- const data = {};
749
- data.counterData = await tado.getCounterData();
750
- await writeFile(join(storagePath, "tado-states.json"), JSON.stringify(data, null, 2), "utf-8");
751
- } catch (error) {
752
- Logger.error(`Error while updating the tado states file: ${error.message || error}`);
753
- }
752
+ async function refreshHistoryServices() {
753
+ if (!helpers[config.homeId].refreshHistoryHandlers.length) return;
754
754
  try {
755
755
  //wait for fakegato history services to be loaded
756
756
  await timeout(4000);
757
- for (const refreshHistory of aRefreshHistoryHandlers) {
757
+ for (const refreshHistory of helpers[config.homeId].refreshHistoryHandlers) {
758
758
  refreshHistory();
759
759
  }
760
760
  } catch (error) {
761
- Logger.error(`Error while refreshing history: ${error.message || error}`);
762
- }
763
- }
764
-
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}`);
761
+ Logger.error(`Error while refreshing history services: ${error.message || error}`);
771
762
  }
772
763
  }
773
764
 
774
765
  function initTasks() {
775
- if (tasksInitialized) return;
776
- tasksInitialized = true;
766
+ if (helpers[config.homeId].tasksInitialized) return;
767
+ helpers[config.homeId].tasksInitialized = true;
777
768
 
778
769
  void getStates();
779
- setInterval(() => getStates(), statesIntervalTime);
780
-
781
- void logCounter();
782
- setInterval(() => logCounter(), 60 * 60 * 1000);
770
+ setInterval(() => void getStates(), helpers[config.homeId].statesIntervalTime);
783
771
  }
784
772
 
785
773
  async function getStates() {
786
- lastGetStates = Date.now();
774
+ helpers[config.homeId].lastGetStates = Date.now();
787
775
  try {
788
776
  //ME
789
777
  if (!config.homeId) await updateMe();
@@ -811,12 +799,12 @@ export default (api, accessories, config, tado, telegram) => {
811
799
  } catch (err) {
812
800
  errorHandler(err);
813
801
  } finally {
814
- void persistStates();
802
+ void refreshHistoryServices();
815
803
  }
816
804
  }
817
805
 
818
806
  async function updateMe() {
819
- if (settingState) return;
807
+ if (settingStates()) return;
820
808
 
821
809
  Logger.debug('Polling User Info...', config.homeName);
822
810
 
@@ -828,7 +816,7 @@ export default (api, accessories, config, tado, telegram) => {
828
816
  }
829
817
 
830
818
  async function updateHome() {
831
- if (settingState) return;
819
+ if (settingStates()) return;
832
820
 
833
821
  Logger.debug('Polling Home Info...', config.homeName);
834
822
 
@@ -853,7 +841,32 @@ export default (api, accessories, config, tado, telegram) => {
853
841
  }
854
842
 
855
843
  async function updateZones() {
856
- if (settingState) return;
844
+ if (helpers[config.homeId].updateZonesRunning) {
845
+ helpers[config.homeId].updateZonesNextQueued = true;
846
+ return;
847
+ }
848
+ helpers[config.homeId].updateZonesRunning = true;
849
+ try {
850
+ while (true) {
851
+ try {
852
+ await _updateZones();
853
+ } catch (error) {
854
+ Logger.error(`Failed to update zones: ${error.message || error}`);
855
+ }
856
+ if (helpers[config.homeId].updateZonesNextQueued) {
857
+ helpers[config.homeId].updateZonesNextQueued = false;
858
+ //continue with loop
859
+ } else {
860
+ break;
861
+ }
862
+ }
863
+ } finally {
864
+ helpers[config.homeId].updateZonesRunning = false;
865
+ }
866
+ }
867
+
868
+ async function _updateZones() {
869
+ if (settingStates()) return;
857
870
 
858
871
  Logger.debug('Polling Zones...', config.homeName);
859
872
 
@@ -904,8 +917,11 @@ export default (api, accessories, config, tado, telegram) => {
904
917
  });
905
918
  }
906
919
 
907
- const zoneStates = (await tado.getZoneStates(config.homeId))["zoneStates"] ?? {};
908
- void persistZoneStates(config.homeId, zoneStates);
920
+ let zoneStates = {};
921
+ if (config.zones?.length) {
922
+ zoneStates = (await tado.getZoneStates(config.homeId))["zoneStates"] ?? {};
923
+ void persistZoneStates(config.homeId, zoneStates);
924
+ }
909
925
 
910
926
  for (const zone of config.zones) {
911
927
  const zoneState = zoneStates[zone.id.toString()];
@@ -1316,7 +1332,7 @@ export default (api, accessories, config, tado, telegram) => {
1316
1332
  }
1317
1333
 
1318
1334
  async function updateMobileDevices() {
1319
- if (settingState) return;
1335
+ if (settingStates()) return;
1320
1336
 
1321
1337
  Logger.debug('Polling MobileDevices...', config.homeName);
1322
1338
 
@@ -1370,7 +1386,7 @@ export default (api, accessories, config, tado, telegram) => {
1370
1386
  }
1371
1387
 
1372
1388
  async function updateWeather() {
1373
- if (settingState) return;
1389
+ if (settingStates()) return;
1374
1390
 
1375
1391
  const weatherTemperatureAccessory = accessories.filter(
1376
1392
  (acc) => acc && acc.displayName === acc.context.config.homeName + ' Weather'
@@ -1425,7 +1441,7 @@ export default (api, accessories, config, tado, telegram) => {
1425
1441
  }
1426
1442
 
1427
1443
  async function updatePresence() {
1428
- if (settingState) return;
1444
+ if (settingStates()) return;
1429
1445
 
1430
1446
  const presenceLockAccessory = accessories.filter(
1431
1447
  (acc) => acc && acc.displayName === acc.context.config.homeName + ' Presence Lock'
@@ -1470,7 +1486,7 @@ export default (api, accessories, config, tado, telegram) => {
1470
1486
  }
1471
1487
 
1472
1488
  async function updateRunningTime() {
1473
- if (settingState) return;
1489
+ if (settingStates()) return;
1474
1490
 
1475
1491
  const centralSwitchAccessory = accessories.filter(
1476
1492
  (acc) => acc && acc.displayName === acc.context.config.homeName + ' Central Switch'
@@ -1515,7 +1531,7 @@ export default (api, accessories, config, tado, telegram) => {
1515
1531
  }
1516
1532
 
1517
1533
  async function updateDevices() {
1518
- if (settingState) return;
1534
+ if (settingStates()) return;
1519
1535
 
1520
1536
  Logger.debug('Polling Devices...', config.homeName);
1521
1537
 
@@ -1586,6 +1602,6 @@ export default (api, accessories, config, tado, telegram) => {
1586
1602
  getStates: getStates,
1587
1603
  setStates: setStates,
1588
1604
  changedStates: changedStates,
1589
- refreshHistoryHandlers: aRefreshHistoryHandlers
1605
+ refreshHistoryHandlers: helpers[config.homeId].refreshHistoryHandlers
1590
1606
  };
1591
- };
1607
+ };
@@ -0,0 +1,69 @@
1
+ import { hrtime } from "process";
2
+ import Logger from '../helper/logger.js';
3
+
4
+ export class TadoUpdateBuffer {
5
+ constructor(sendUpdateFn, preferSiriTemperature = false) {
6
+ this.preferSiriTemperature = !!preferSiriTemperature;
7
+ this.sendUpdateFn = sendUpdateFn;
8
+ this.delay = 400;
9
+ this.timer = null;
10
+ this.pendingState = null;
11
+ this.pendingTemperature = null;
12
+ this.lastUpdateTime = null;
13
+ this.lastTemperature = 20;
14
+ }
15
+
16
+ setState(value) {
17
+ this.pendingState = value;
18
+ this._schedule();
19
+ Logger.debug("[TadoUpdateBuffer] setState", value, hrtime.bigint());
20
+ }
21
+
22
+ setTemperature(value) {
23
+ this.pendingTemperature = value;
24
+ this._schedule();
25
+ Logger.debug("[TadoUpdateBuffer] setTemperature", value, hrtime.bigint());
26
+ }
27
+
28
+ _schedule() {
29
+ if (this.timer) clearTimeout(this.timer);
30
+ this.timer = setTimeout(() => this._apply(), this.delay);
31
+ }
32
+
33
+ _apply() {
34
+ this.timer = null;
35
+ const state = this.pendingState;
36
+ const temp = this.pendingTemperature;
37
+ const tempSet = temp !== null && temp !== undefined && temp >= 5;
38
+ this.pendingState = null;
39
+ this.pendingTemperature = null;
40
+ if (tempSet) this.lastTemperature = temp;
41
+
42
+ //Siri temperature heuristic
43
+ if (this.preferSiriTemperature && state === 3 && tempSet) {
44
+ if (temp === 5) {
45
+ //set auto mode on
46
+ Logger.debug("[TadoUpdateBuffer] preferSiriTemperature active but temp=5 -> treat as auto mode");
47
+ return this.sendUpdateFn("State", 3);
48
+ }
49
+ //set temperature
50
+ Logger.debug("[TadoUpdateBuffer] Siri temperature change detected -> ignore state 3, apply temperature only");
51
+ return this.sendUpdateFn("Temperature", temp);
52
+ }
53
+
54
+ //default behaviour
55
+ if (state === 0) {
56
+ //set heating off
57
+ return this.sendUpdateFn("State", 0);
58
+ } else if (state === 3) {
59
+ //set auto mode on
60
+ return this.sendUpdateFn("State", 3);
61
+ } else if (tempSet) {
62
+ //set heating on with temperature provided
63
+ return this.sendUpdateFn("Temperature", temp);
64
+ } else if (state === 1) {
65
+ //heating on without temperature provided
66
+ return this.sendUpdateFn("Temperature", this.lastTemperature);
67
+ }
68
+ }
69
+ }
package/src/platform.js CHANGED
@@ -237,12 +237,12 @@ class TadoPlatform {
237
237
 
238
238
  switch (device.subtype) {
239
239
  case 'zone-thermostat':
240
- new ThermostatAccessory(this.api, accessory, this.accessories, tado, deviceHandler, FakeGatoHistoryService);
240
+ new ThermostatAccessory(this.api, accessory, this.accessories, tado, deviceHandler, this.config.preferSiriTemperature, FakeGatoHistoryService);
241
241
  break;
242
242
  case 'zone-heatercooler':
243
243
  case 'zone-heatercooler-boiler':
244
244
  case 'zone-heatercooler-ac':
245
- new HeaterCoolerAccessory(this.api, accessory, this.accessories, tado, deviceHandler, FakeGatoHistoryService);
245
+ new HeaterCoolerAccessory(this.api, accessory, this.accessories, tado, deviceHandler, this.config.preferSiriTemperature, FakeGatoHistoryService);
246
246
  break;
247
247
  case 'zone-switch':
248
248
  case 'zone-window-switch':
@@ -1,5 +1,4 @@
1
1
  import Logger from '../helper/logger.js';
2
- import { getPersistedStates } from '../helper/handler.js';
3
2
  import got from 'got';
4
3
  import { join } from 'path';
5
4
  import { access, readFile, writeFile } from 'fs/promises';
@@ -8,8 +7,17 @@ const tado_url = "https://my.tado.com";
8
7
  const tado_auth_url = "https://login.tado.com/oauth2";
9
8
  const tado_client_id = "1bb50063-6b0c-4d11-bd99-387f4a91cc46";
10
9
 
10
+ function _getSimpleHash(str) {
11
+ let hash = 0;
12
+ for (let i = 0; i < str.length; i++) {
13
+ const char = str.charCodeAt(i);
14
+ hash = (hash << 5) - hash + char;
15
+ }
16
+ return (hash >>> 0).toString(36).padStart(7, '0');
17
+ }
18
+
11
19
  export default class Tado {
12
- constructor(name, auth, storagePath) {
20
+ constructor(name, auth, storagePath, counterActivated) {
13
21
  this.tadoApiUrl = auth.tadoApiUrl || tado_url;
14
22
  this.customTadoApiUrlActive = !!auth.tadoApiUrl;
15
23
  this.skipAuth = auth.skipAuth?.toString() === "true";
@@ -17,28 +25,42 @@ export default class Tado {
17
25
  this.name = name;
18
26
  const usesExternalTokenFile = auth.username?.toLowerCase().endsWith(".json");
19
27
  this._tadoExternalTokenFilePath = usesExternalTokenFile ? auth.username : undefined;
20
- const fnSimpleHash = (str) => {
21
- let hash = 0;
22
- for (let i = 0; i < str.length; i++) {
23
- const char = str.charCodeAt(i);
24
- hash = (hash << 5) - hash + char;
25
- }
26
- return (hash >>> 0).toString(36).padStart(7, '0');
27
- };
28
+ this.hashedUsername = _getSimpleHash(auth.username);
28
29
  this.username = usesExternalTokenFile ? undefined : auth.username;
29
- this._tadoInternalTokenFilePath = usesExternalTokenFile ? undefined : join(this.storagePath, `.tado-token-${fnSimpleHash(auth.username)}.json`);
30
+ this._tadoInternalTokenFilePath = usesExternalTokenFile ? undefined : join(this.storagePath, `.tado-token-${this.hashedUsername}.json`);
30
31
  this._tadoApiClientId = tado_client_id;
31
32
  this._tadoTokenPromise = undefined;
32
33
  this._tadoAuthenticationCallback = undefined;
34
+ this._counterActivated = counterActivated?.toString() === "true";
33
35
  this._counterInitPromise = this._initCounter();
34
36
  Logger.debug("API successfull initialized", this.name);
35
37
  }
36
38
 
37
39
  async _initCounter() {
38
- const persistedCounterData = (await getPersistedStates(this.storagePath))?.counterData;
40
+ if (!this._counterActivated) return;
41
+ const persistedCounterData = (await this._getPersistedCounter())?.counterData;
39
42
  this._counter = persistedCounterData?.counter ?? 0;
40
43
  this._counterTimestamp = persistedCounterData?.counterTimestamp ?? new Date().toISOString();
41
44
  this._checkCounterMidnightReset();
45
+ //wait some seconds to catch recent api calls
46
+ setTimeout(() => {
47
+ void this._logCounter();
48
+ setInterval(() => void this._logCounter(), 60 * 60 * 1000);
49
+ void this._persistCounterData();
50
+ setInterval(() => void this._persistCounterData(), 5 * 60 * 1000);
51
+ }, 4 * 1000);
52
+ }
53
+
54
+ async _getPersistedCounter() {
55
+ try {
56
+ const filePath = join(this.storagePath, `tado-api-${this.hashedUsername}.json`);
57
+ await access(filePath);
58
+ const data = (await readFile(filePath, "utf-8"));
59
+ if (data) return JSON.parse(data);
60
+ } catch (error) {
61
+ //no persisted counter data => ignore
62
+ Logger.debug(`Failed to read tado api file for user ${this.hashedUsername}: ${error.message || error}`);
63
+ }
42
64
  }
43
65
 
44
66
  _checkCounterMidnightReset() {
@@ -53,6 +75,7 @@ export default class Tado {
53
75
  }
54
76
 
55
77
  async _increaseCounter() {
78
+ if (!this._counterActivated) return;
56
79
  try {
57
80
  await this._counterInitPromise;
58
81
  this._checkCounterMidnightReset();
@@ -63,7 +86,7 @@ export default class Tado {
63
86
  }
64
87
  }
65
88
 
66
- async getCounterData() {
89
+ async _getCounterData() {
67
90
  await this._counterInitPromise;
68
91
  return {
69
92
  counter: this._counter,
@@ -71,6 +94,25 @@ export default class Tado {
71
94
  };
72
95
  }
73
96
 
97
+ async _persistCounterData() {
98
+ try {
99
+ const data = {};
100
+ data.counterData = await this._getCounterData();
101
+ await writeFile(join(this.storagePath, `tado-api-${this.hashedUsername}.json`), JSON.stringify(data, null, 2), "utf-8");
102
+ } catch (error) {
103
+ Logger.error(`Error while updating the tado api file for user ${this.hashedUsername}: ${error.message || error}`);
104
+ }
105
+ }
106
+
107
+ async _logCounter() {
108
+ try {
109
+ const counter = (await this._getCounterData()).counter;
110
+ Logger.info(`Tado API counter: ${counter.toLocaleString('en-US')}`);
111
+ } catch (error) {
112
+ Logger.warn(`Failed to get Tado API counter: ${error.message || error}`);
113
+ }
114
+ }
115
+
74
116
  async getToken() {
75
117
  Logger.debug('Get access token...', this.name);
76
118
  if (!this._tadoTokenPromise) {
@@ -23,7 +23,7 @@ export default {
23
23
 
24
24
  for (const auth of auths) {
25
25
 
26
- const tado = new TadoApi('Configuration', auth, storagePath);
26
+ const tado = new TadoApi('Configuration', auth, storagePath, false);
27
27
 
28
28
  const me = await tado.getMe();
29
29
 
@@ -160,7 +160,7 @@ export default {
160
160
  username: auth.username,
161
161
  tadoApiUrl: auth.tadoApiUrl,
162
162
  skipAuth: auth.skipAuth
163
- }, storagePath);
163
+ }, storagePath, false);
164
164
 
165
165
  const me = await tado.getMe();
166
166
 
@@ -208,7 +208,7 @@ export default {
208
208
 
209
209
  refresh: async function (currentHome, config, auth, storagePath) {
210
210
 
211
- const tado = new TadoApi('Configuration', auth, storagePath);
211
+ const tado = new TadoApi('Configuration', auth, storagePath, false);
212
212
 
213
213
  //Home Informations
214
214
  let home = config.homes.find((home) => home && home.name === currentHome);
@@ -505,7 +505,7 @@ export default {
505
505
  username: home.username,
506
506
  tadoApiUrl: home.tadoApiUrl,
507
507
  skipAuth: home.skipAuth
508
- }, storagePath);
508
+ }, storagePath, true);
509
509
 
510
510
  const accessoryConfig = {
511
511
  homeId: home.id,