@homebridge-plugins/homebridge-tado 8.7.8 → 8.8.1

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,17 @@
1
1
  # Changelog
2
2
 
3
+ ## v8.8.1 - 2026-02-14
4
+ - Update dependencies
5
+ - Update qs due to vulnerability
6
+ - Update eslint and drop obsolete dev dependencies
7
+
8
+ ## v8.8.0 - 2026-01-31
9
+ - Add `addJitter` option to introduce a ±10% random variation in the polling interval
10
+ - Add `nightPolling` option to optionally use a longer interval overnight, lowering polling activity between 00:00–06:00
11
+ - Change: Schedule the next polling cycle after the previous poll completes, using the configured interval
12
+ - Change: Set timeout for tado api requests to 15 seconds
13
+ - Update dependencies
14
+
3
15
  ## v8.7.8 - 2026-01-13
4
16
  - Fix: Match zones by name and type when updating zone states (#192)
5
17
  - Add more debug logs for accessory state updates
package/README.md CHANGED
@@ -38,6 +38,7 @@ After [Homebridge](https://github.com/nfarina/homebridge) has been installed:
38
38
  - [Configuration](#configuration)
39
39
  - [Authentication](#authentication)
40
40
  - [Custom API Configuration](#custom-api-configuration)
41
+ - [Polling Behavior Configuration](#polling-behavior-configuration)
41
42
  - [Thermostat](#thermostat)
42
43
  - [Open Window](#open-window)
43
44
  - [Air Conditioning](#air-conditioning)
@@ -124,6 +125,14 @@ Additionally, it is possible to disable the built-in authentication flow entirel
124
125
  For further details and configuration examples, refer to the related discussion:
125
126
  [homebridge-tado issue #176 – Authentication Options](https://github.com/homebridge-plugins/homebridge-tado/issues/176#issuecomment-3419839118)
126
127
 
128
+ ## Polling Behavior Configuration
129
+
130
+ To better control API usage and reduce unnecessary load, the plugin provides additional polling-related options:
131
+
132
+ When `addJitter` is enabled, a random ±10% jitter is applied to the polling interval.
133
+
134
+ The setting `nightPolling` optionally defines a longer polling interval during nighttime hours (00:00–06:00, local time). This reduces overnight API activity while maintaining normal polling behavior during the day.
135
+
127
136
  ## Thermostat
128
137
 
129
138
  Each zone in the config.json with ``"type": "HEATING"`` and ``"easyMode": false`` is exposed to Apple Home as a thermostat accessory with the following features:
@@ -51,6 +51,11 @@
51
51
  "type": "boolean",
52
52
  "description": "Optional: Skip authentication for tado API."
53
53
  },
54
+ "addJitter": {
55
+ "title": "Add Jitter",
56
+ "type": "boolean",
57
+ "description": "Optional: Add a ±10% random variation in the polling interval."
58
+ },
54
59
  "polling": {
55
60
  "title": "Polling",
56
61
  "description": "The polling interval in seconds (recommended value: 300).",
@@ -58,6 +63,13 @@
58
63
  "default": 300,
59
64
  "minimum": 30
60
65
  },
66
+ "nightPolling": {
67
+ "title": "Night Polling",
68
+ "description": "Optional: The polling interval in seconds used during night between 00:00 - 06:00.",
69
+ "type": "integer",
70
+ "default": 300,
71
+ "minimum": 30
72
+ },
61
73
  "temperatureUnit": {
62
74
  "title": "Temperature Unit",
63
75
  "description": "Temperature Unit for accessories in Apple Home exposed by this plugin.",
@@ -595,6 +607,7 @@
595
607
  "items": [
596
608
  "homes[].name",
597
609
  "homes[].polling",
610
+ "homes[].nightPolling",
598
611
  "homes[].temperatureUnit",
599
612
  {
600
613
  "key": "homes[]",
@@ -607,6 +620,7 @@
607
620
  "homes[].id",
608
621
  "homes[].tadoApiUrl",
609
622
  "homes[].skipAuth",
623
+ "homes[].addJitter",
610
624
  "homes[].username"
611
625
  ]
612
626
  },
@@ -1107,7 +1107,9 @@ async function fetchDevices(auth, refresh, resync) {
1107
1107
  username: foundHome.username,
1108
1108
  tadoApiUrl: foundHome.tadoApiUrl,
1109
1109
  skipAuth: foundHome.skipAuth,
1110
+ addJitter: false,
1110
1111
  polling: 300,
1112
+ nightPolling: 300,
1111
1113
  zones: [],
1112
1114
  presence: {
1113
1115
  anyone: false,
@@ -1265,7 +1267,9 @@ async function fetchDevices(auth, refresh, resync) {
1265
1267
  username: auth.username,
1266
1268
  tadoApiUrl: auth.tadoApiUrl,
1267
1269
  skipAuth: auth.skipAuth,
1270
+ addJitter: false,
1268
1271
  polling: 300,
1272
+ nightPolling: 300,
1269
1273
  zones: [],
1270
1274
  presence: {
1271
1275
  anyone: false,
@@ -1435,6 +1439,7 @@ async function fetchDevices(auth, refresh, resync) {
1435
1439
  try {
1436
1440
 
1437
1441
  //check version before load ui
1442
+ //eslint-disable-next-line no-undef
1438
1443
  if (window.homebridge.serverEnv.env && window.compareVersions(window.homebridge.serverEnv.env.packageVersion, '4.34.0') < 0) {
1439
1444
  await showOldSchema(true);
1440
1445
  return;
@@ -44,6 +44,11 @@ const schema = {
44
44
  'type': 'boolean',
45
45
  'description': 'Optional: Skip authentication for tado API.'
46
46
  },
47
+ 'addJitter': {
48
+ 'title': 'Add Jitter',
49
+ 'type': 'boolean',
50
+ 'description': 'Optional: Add a ±10% random variation in the polling interval.'
51
+ },
47
52
  'polling': {
48
53
  'title': 'Polling',
49
54
  'description': 'The polling interval in seconds (recommended value: 300).',
@@ -51,6 +56,13 @@ const schema = {
51
56
  'default': 300,
52
57
  'minimum': 30
53
58
  },
59
+ 'nightPolling': {
60
+ 'title': 'Night Polling',
61
+ 'description': 'Optional: The polling interval in seconds used during night between 00:00 - 06:00.',
62
+ 'type': 'integer',
63
+ 'default': 300,
64
+ 'minimum': 30
65
+ },
54
66
  'temperatureUnit': {
55
67
  'title': 'Temperature Unit',
56
68
  'description': 'Temperature Unit for accessories in Apple Home exposed by this plugin.',
@@ -580,6 +592,7 @@ const schema = {
580
592
  'preferSiriTemperature',
581
593
  'homes.name',
582
594
  'homes.polling',
595
+ 'homes.nightPolling',
583
596
  'homes.temperatureUnit',
584
597
  {
585
598
  'key': 'homes',
@@ -592,6 +605,7 @@ const schema = {
592
605
  'homes.id',
593
606
  'homes.tadoApiUrl',
594
607
  'homes.skipAuth',
608
+ 'homes.addJitter',
595
609
  'homes.username'
596
610
  ]
597
611
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@homebridge-plugins/homebridge-tado",
3
- "version": "8.7.8",
3
+ "version": "8.8.1",
4
4
  "description": "Homebridge plugin for controlling tado° devices.",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -33,7 +33,7 @@
33
33
  "homebridge": "^1.6.0||^2.0.0-beta.0"
34
34
  },
35
35
  "dependencies": {
36
- "@homebridge/plugin-ui-utils": "^2.1.2",
36
+ "@homebridge/plugin-ui-utils": "^2.2.0",
37
37
  "fakegato-history": "^0.6.7",
38
38
  "form-data": "^4.0.5",
39
39
  "fs-extra": "^11.3.3",
@@ -41,15 +41,9 @@
41
41
  "moment": "^2.30.1"
42
42
  },
43
43
  "devDependencies": {
44
- "@babel/core": "7.28.6",
45
- "@babel/eslint-parser": "7.28.6",
46
- "@babel/eslint-plugin": "7.27.1",
47
- "@eslint/js": "^9.39.2",
48
- "eslint": "^9.39.2",
49
- "eslint-config-prettier": "^10.1.8",
50
- "eslint-plugin-import": "^2.32.0",
51
- "eslint-plugin-prettier": "^5.5.4",
52
- "globals": "^17.0.0",
53
- "prettier": "^3.7.4"
44
+ "eslint": "^10.0.0",
45
+ "@eslint/js": "^10.0.1",
46
+ "globals": "^17.3.0",
47
+ "prettier": "^3.8.1"
54
48
  }
55
49
  }
@@ -27,6 +27,7 @@ export default class SolarLightsensorAccessory {
27
27
 
28
28
  if (!service) {
29
29
  Logger.info('Adding LightSensor service', this.accessory.displayName);
30
+ // eslint-disable-next-line no-useless-assignment
30
31
  service = this.accessory.addService(
31
32
  this.api.hap.Service.LightSensor,
32
33
  this.accessory.displayName,
@@ -34,4 +35,4 @@ export default class SolarLightsensorAccessory {
34
35
  );
35
36
  }
36
37
  }
37
- }
38
+ }
@@ -13,7 +13,6 @@ export default (api, accessories, config, tado, telegram) => {
13
13
  helpers[config.homeId] = {
14
14
  activeSettingStateRuns: {},
15
15
  tasksInitialized: false,
16
- lastGetStates: 0,
17
16
  lastPersistZoneStates: 0,
18
17
  persistPromise: Promise.resolve(),
19
18
  updateZonesRunning: false,
@@ -21,6 +20,8 @@ export default (api, accessories, config, tado, telegram) => {
21
20
  delayTimer: {},
22
21
  refreshHistoryHandlers: [],
23
22
  statesIntervalTime: Math.max(config.polling, 30) * 1000,
23
+ statesIntervalTimeNight: config.nightPolling ? Math.max(config.nightPolling, config.polling, 30) * 1000 : undefined,
24
+ nextPollingTime: 0,
24
25
  storagePath: api.user.storagePath(),
25
26
  }
26
27
  }
@@ -568,9 +569,7 @@ export default (api, accessories, config, tado, telegram) => {
568
569
  } finally {
569
570
  delete helpers[config.homeId].activeSettingStateRuns[runId];
570
571
  //update zones to ensure correct state in Apple Home
571
- const timeSinceLastGetStates = helpers[config.homeId].lastGetStates === 0 ? 0 : (Date.now() - helpers[config.homeId].lastGetStates);
572
- const statesIntervalTimeLeft = helpers[config.homeId].statesIntervalTime - timeSinceLastGetStates;
573
- if (!settingStates() && zoneUpdated && statesIntervalTimeLeft > (10 * 1000)) await updateZones();
572
+ if (!settingStates() && zoneUpdated && _getTimeUntilNextPolling() > (10 * 1000)) await updateZones();
574
573
  }
575
574
  }
576
575
 
@@ -761,16 +760,42 @@ export default (api, accessories, config, tado, telegram) => {
761
760
  }
762
761
  }
763
762
 
763
+ function _getTimeUntilNextPolling() {
764
+ return Math.max(0, (helpers[config.homeId].nextPollingTime ?? 0) - Date.now());
765
+ }
766
+
767
+ function _getPollingInterval() {
768
+ const hour = helpers[config.homeId].statesIntervalTimeNight ? new Date().getHours() : undefined;
769
+ let interval;
770
+ if (helpers[config.homeId].statesIntervalTimeNight && hour >= 0 && hour < 6) {
771
+ interval = helpers[config.homeId].statesIntervalTimeNight;
772
+ } else {
773
+ interval = helpers[config.homeId].statesIntervalTime;
774
+ }
775
+ const addJitter = config.addJitter?.toString() === "true";
776
+ if (!addJitter) return interval;
777
+ //use 10 percent jitter
778
+ const jitter = interval * 0.1;
779
+ const randomOffset = Math.round((Math.random() * 2 - 1) * jitter);
780
+ return interval + randomOffset;
781
+ }
782
+
764
783
  function initTasks() {
765
784
  if (helpers[config.homeId].tasksInitialized) return;
766
785
  helpers[config.homeId].tasksInitialized = true;
767
-
768
- void getStates();
769
- setInterval(() => void getStates(), helpers[config.homeId].statesIntervalTime);
786
+ async function poll() {
787
+ try {
788
+ await getStates();
789
+ } finally {
790
+ const interval = _getPollingInterval();
791
+ helpers[config.homeId].nextPollingTime = Date.now() + interval;
792
+ setTimeout(poll, interval);
793
+ }
794
+ }
795
+ void poll();
770
796
  }
771
797
 
772
798
  async function getStates() {
773
- helpers[config.homeId].lastGetStates = Date.now();
774
799
  try {
775
800
  //ME
776
801
  if (!config.homeId) await updateMe();
@@ -258,13 +258,15 @@ export default class Tado {
258
258
  const access_token = this.skipAuth ? undefined : await this.getToken();
259
259
  const url = `${tado_url_dif || this.tadoApiUrl}${path}`;
260
260
 
261
- const config = {
261
+ const options = {
262
262
  method,
263
263
  responseType: 'json',
264
264
  headers: access_token ?
265
265
  { Authorization: `Bearer ${access_token}` } :
266
266
  undefined,
267
- timeout: { request: 30000 },
267
+ timeout: {
268
+ request: 15000
269
+ },
268
270
  retry: {
269
271
  limit: 2,
270
272
  statusCodes: [408, 429, 503, 504],
@@ -272,12 +274,12 @@ export default class Tado {
272
274
  },
273
275
  };
274
276
 
275
- // Only add data to config for non-GET methods and when data has content
277
+ //only add data to options for non-GET methods and when data has content
276
278
  if (data && typeof data === 'object' && Object.keys(data).length > 0 && method !== 'GET') {
277
- config.json = data;
279
+ options.json = data;
278
280
  }
279
281
 
280
- if (Object.keys(params).length) config.searchParams = params;
282
+ if (Object.keys(params).length) options.searchParams = params;
281
283
 
282
284
  Logger.debug('API request start', {
283
285
  name: this.name,
@@ -288,7 +290,7 @@ export default class Tado {
288
290
  });
289
291
 
290
292
  try {
291
- const response = await got(url, config);
293
+ const response = await got(url, options);
292
294
  await this._increaseCounter();
293
295
  Logger.debug('API request success', {
294
296
  name: this.name,
@@ -42,7 +42,9 @@ export default {
42
42
  username: auth.username,
43
43
  tadoApiUrl: auth.tadoApiUrl,
44
44
  skipAuth: auth.skipAuth,
45
+ addJitter: false,
45
46
  polling: 300,
47
+ nightPolling: 300,
46
48
  zones: [],
47
49
  presence: {
48
50
  anyone: false,
@@ -513,6 +515,7 @@ export default {
513
515
  username: home.username,
514
516
  tadoApiUrl: home.tadoApiUrl,
515
517
  skipAuth: home.skipAuth,
518
+ addJitter: home.addJitter,
516
519
  temperatureUnit: home.temperatureUnit || 'CELSIUS',
517
520
  geolocation: home.geolocation,
518
521
  tado: tado,
@@ -527,7 +530,8 @@ export default {
527
530
  home.extras && home.extras.childLockSwitches
528
531
  ? home.extras.childLockSwitches.filter((childLockSwitch) => childLockSwitch && childLockSwitch.active)
529
532
  : [],
530
- polling: Number.isInteger(home.polling) ? (home.polling < 30 ? 30 : home.polling) : 300,
533
+ polling: Number.isInteger(home.polling) ? Math.max(home.polling, 30) : 300,
534
+ nightPolling: home.nightPolling && Number.isInteger(home.nightPolling) ? Math.max(home.nightPolling, home.polling, 30) : undefined
531
535
  };
532
536
 
533
537
  if (home.zones && home.zones.length) {