@homebridge-plugins/homebridge-tado 8.6.0-beta.3 → 8.6.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,16 +1,16 @@
1
1
  # Changelog
2
2
 
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
- - 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
- - Tado API counter now tracks and persists for each authenticated user
10
- - Fix: Polling and tasks for multiple homes (#178)
11
- - Fix: Corrected zone update handling that could previously cause unintended heating changes (#178)
12
- - Added additional debug log messages for zone updates
13
- - Note: This update will reset your tado api counter for the current day to zero.
3
+ ## v8.6.0 2025-11-01
4
+ - BREAKING CHANGE: `tadoApiUrl` and `skipAuth` must now be defined under each home configuration for proper multi-home support (#176)
5
+ - New parameter `preferSiriTemperature` for improved Siri handling allows temperature changes via Siri without forcing Auto mode (#178). See [#178 (comment)](https://github.com/homebridge-plugins/homebridge-tado/issues/178#issuecomment-3476646430) for a detailed explanation
6
+ - Restored stable update behavior from v8.3.1 and earlier while keeping Siri compatibility (#178)
7
+ - Reworked thermostat update logic: batches state and temperature updates within 400 ms for more reliable state updates (#178)
8
+ - Improved zone update and persistence handling for faster, more consistent status updates
9
+ - Optimized task queue to prevent overlapping operations and API calls
10
+ - Fixed multi-home polling and individual API handling (#176)
11
+ - Added enhanced debug logs for zone updates and API interactions
12
+ - Note: This update resets the Tado API counter for the current day
13
+ - Apologies for the unexpected behavior introduced in 8.4.x–8.5.x this release restores consistent and reliable behavior, with an optional fix for Siri users. Full statement: [#178 (comment)](https://github.com/homebridge-plugins/homebridge-tado/issues/178#issuecomment-3476646430)
14
14
 
15
15
  ## v8.5.0 - 2025-10-27
16
16
  - 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',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@homebridge-plugins/homebridge-tado",
3
- "version": "8.6.0-beta.3",
3
+ "version": "8.6.0",
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)
@@ -2,6 +2,7 @@ import Logger from '../helper/logger.js';
2
2
  import moment from 'moment';
3
3
  import { writeFile } from 'fs/promises';
4
4
  import { join } from "path";
5
+ import { randomUUID } from 'crypto';
5
6
 
6
7
  const timeout = (ms) => new Promise((res) => setTimeout(res, ms));
7
8
  const helpers = {};
@@ -10,7 +11,7 @@ export default (api, accessories, config, tado, telegram) => {
10
11
  //init helper variables for current home scope
11
12
  if (!helpers[config.homeId]) {
12
13
  helpers[config.homeId] = {
13
- settingState: false,
14
+ activeSettingStateRuns: {},
14
15
  tasksInitialized: false,
15
16
  lastGetStates: 0,
16
17
  lastPersistZoneStates: 0,
@@ -24,11 +25,16 @@ export default (api, accessories, config, tado, telegram) => {
24
25
  }
25
26
  }
26
27
 
28
+ function settingStates() {
29
+ return Object.keys(helpers[config.homeId].activeSettingStateRuns).length > 0;
30
+ }
31
+
27
32
  async function setStates(accessory, accs, target, value) {
28
33
  let zoneUpdated = false;
29
34
  accessories = accs.filter((acc) => acc && acc.context.config.homeName === config.homeName);
35
+ const runId = randomUUID();
30
36
  try {
31
- helpers[config.homeId].settingState = true;
37
+ helpers[config.homeId].activeSettingStateRuns[runId] = true;
32
38
  value = typeof value === 'number' ? parseFloat(value.toFixed(2)) : value;
33
39
  Logger.info(target + ': ' + value, accessory.displayName);
34
40
 
@@ -560,11 +566,11 @@ export default (api, accessories, config, tado, telegram) => {
560
566
  } catch (err) {
561
567
  errorHandler(err);
562
568
  } finally {
563
- helpers[config.homeId].settingState = false;
569
+ delete helpers[config.homeId].activeSettingStateRuns[runId];
564
570
  //update zones to ensure correct state in Apple Home
565
571
  const timeSinceLastGetStates = helpers[config.homeId].lastGetStates === 0 ? 0 : (Date.now() - helpers[config.homeId].lastGetStates);
566
572
  const statesIntervalTimeLeft = helpers[config.homeId].statesIntervalTime - timeSinceLastGetStates;
567
- if (zoneUpdated && statesIntervalTimeLeft > (10 * 1000)) await updateZones();
573
+ if (!settingStates() && zoneUpdated && statesIntervalTimeLeft > (10 * 1000)) await updateZones();
568
574
  }
569
575
  }
570
576
 
@@ -797,7 +803,7 @@ export default (api, accessories, config, tado, telegram) => {
797
803
  }
798
804
 
799
805
  async function updateMe() {
800
- if (helpers[config.homeId].settingState) return;
806
+ if (settingStates()) return;
801
807
 
802
808
  Logger.debug('Polling User Info...', config.homeName);
803
809
 
@@ -809,7 +815,7 @@ export default (api, accessories, config, tado, telegram) => {
809
815
  }
810
816
 
811
817
  async function updateHome() {
812
- if (helpers[config.homeId].settingState) return;
818
+ if (settingStates()) return;
813
819
 
814
820
  Logger.debug('Polling Home Info...', config.homeName);
815
821
 
@@ -859,7 +865,7 @@ export default (api, accessories, config, tado, telegram) => {
859
865
  }
860
866
 
861
867
  async function _updateZones() {
862
- if (helpers[config.homeId].settingState) return;
868
+ if (settingStates()) return;
863
869
 
864
870
  Logger.debug('Polling Zones...', config.homeName);
865
871
 
@@ -910,8 +916,11 @@ export default (api, accessories, config, tado, telegram) => {
910
916
  });
911
917
  }
912
918
 
913
- const zoneStates = (await tado.getZoneStates(config.homeId))["zoneStates"] ?? {};
914
- void persistZoneStates(config.homeId, zoneStates);
919
+ let zoneStates = {};
920
+ if (config.zones?.length) {
921
+ zoneStates = (await tado.getZoneStates(config.homeId))["zoneStates"] ?? {};
922
+ void persistZoneStates(config.homeId, zoneStates);
923
+ }
915
924
 
916
925
  for (const zone of config.zones) {
917
926
  const zoneState = zoneStates[zone.id.toString()];
@@ -1322,7 +1331,7 @@ export default (api, accessories, config, tado, telegram) => {
1322
1331
  }
1323
1332
 
1324
1333
  async function updateMobileDevices() {
1325
- if (helpers[config.homeId].settingState) return;
1334
+ if (settingStates()) return;
1326
1335
 
1327
1336
  Logger.debug('Polling MobileDevices...', config.homeName);
1328
1337
 
@@ -1376,7 +1385,7 @@ export default (api, accessories, config, tado, telegram) => {
1376
1385
  }
1377
1386
 
1378
1387
  async function updateWeather() {
1379
- if (helpers[config.homeId].settingState) return;
1388
+ if (settingStates()) return;
1380
1389
 
1381
1390
  const weatherTemperatureAccessory = accessories.filter(
1382
1391
  (acc) => acc && acc.displayName === acc.context.config.homeName + ' Weather'
@@ -1431,7 +1440,7 @@ export default (api, accessories, config, tado, telegram) => {
1431
1440
  }
1432
1441
 
1433
1442
  async function updatePresence() {
1434
- if (helpers[config.homeId].settingState) return;
1443
+ if (settingStates()) return;
1435
1444
 
1436
1445
  const presenceLockAccessory = accessories.filter(
1437
1446
  (acc) => acc && acc.displayName === acc.context.config.homeName + ' Presence Lock'
@@ -1476,7 +1485,7 @@ export default (api, accessories, config, tado, telegram) => {
1476
1485
  }
1477
1486
 
1478
1487
  async function updateRunningTime() {
1479
- if (helpers[config.homeId].settingState) return;
1488
+ if (settingStates()) return;
1480
1489
 
1481
1490
  const centralSwitchAccessory = accessories.filter(
1482
1491
  (acc) => acc && acc.displayName === acc.context.config.homeName + ' Central Switch'
@@ -1521,7 +1530,7 @@ export default (api, accessories, config, tado, telegram) => {
1521
1530
  }
1522
1531
 
1523
1532
  async function updateDevices() {
1524
- if (helpers[config.homeId].settingState) return;
1533
+ if (settingStates()) return;
1525
1534
 
1526
1535
  Logger.debug('Polling Devices...', config.homeName);
1527
1536
 
@@ -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':