@homebridge-plugins/homebridge-tado 8.6.0-beta.3 → 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
@@ -6,6 +6,7 @@
6
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
8
  - Advanced queue handling for update and persistence tasks
9
+ - Prevent updates during active setStates
9
10
  - Tado API counter now tracks and persists for each authenticated user
10
11
  - Fix: Polling and tasks for multiple homes (#178)
11
12
  - Fix: Corrected zone update handling that could previously cause unintended heating changes (#178)
@@ -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-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)
@@ -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
 
@@ -558,13 +564,14 @@ export default (api, accessories, config, tado, telegram) => {
558
564
  break;
559
565
  }
560
566
  } catch (err) {
567
+ console.log("error at setStates", err);
561
568
  errorHandler(err);
562
569
  } finally {
563
- helpers[config.homeId].settingState = false;
570
+ delete helpers[config.homeId].activeSettingStateRuns[runId];
564
571
  //update zones to ensure correct state in Apple Home
565
572
  const timeSinceLastGetStates = helpers[config.homeId].lastGetStates === 0 ? 0 : (Date.now() - helpers[config.homeId].lastGetStates);
566
573
  const statesIntervalTimeLeft = helpers[config.homeId].statesIntervalTime - timeSinceLastGetStates;
567
- if (zoneUpdated && statesIntervalTimeLeft > (10 * 1000)) await updateZones();
574
+ if (!settingStates() && zoneUpdated && statesIntervalTimeLeft > (10 * 1000)) await updateZones();
568
575
  }
569
576
  }
570
577
 
@@ -797,7 +804,7 @@ export default (api, accessories, config, tado, telegram) => {
797
804
  }
798
805
 
799
806
  async function updateMe() {
800
- if (helpers[config.homeId].settingState) return;
807
+ if (settingStates()) return;
801
808
 
802
809
  Logger.debug('Polling User Info...', config.homeName);
803
810
 
@@ -809,7 +816,7 @@ export default (api, accessories, config, tado, telegram) => {
809
816
  }
810
817
 
811
818
  async function updateHome() {
812
- if (helpers[config.homeId].settingState) return;
819
+ if (settingStates()) return;
813
820
 
814
821
  Logger.debug('Polling Home Info...', config.homeName);
815
822
 
@@ -859,7 +866,7 @@ export default (api, accessories, config, tado, telegram) => {
859
866
  }
860
867
 
861
868
  async function _updateZones() {
862
- if (helpers[config.homeId].settingState) return;
869
+ if (settingStates()) return;
863
870
 
864
871
  Logger.debug('Polling Zones...', config.homeName);
865
872
 
@@ -910,8 +917,11 @@ export default (api, accessories, config, tado, telegram) => {
910
917
  });
911
918
  }
912
919
 
913
- const zoneStates = (await tado.getZoneStates(config.homeId))["zoneStates"] ?? {};
914
- 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
+ }
915
925
 
916
926
  for (const zone of config.zones) {
917
927
  const zoneState = zoneStates[zone.id.toString()];
@@ -1322,7 +1332,7 @@ export default (api, accessories, config, tado, telegram) => {
1322
1332
  }
1323
1333
 
1324
1334
  async function updateMobileDevices() {
1325
- if (helpers[config.homeId].settingState) return;
1335
+ if (settingStates()) return;
1326
1336
 
1327
1337
  Logger.debug('Polling MobileDevices...', config.homeName);
1328
1338
 
@@ -1376,7 +1386,7 @@ export default (api, accessories, config, tado, telegram) => {
1376
1386
  }
1377
1387
 
1378
1388
  async function updateWeather() {
1379
- if (helpers[config.homeId].settingState) return;
1389
+ if (settingStates()) return;
1380
1390
 
1381
1391
  const weatherTemperatureAccessory = accessories.filter(
1382
1392
  (acc) => acc && acc.displayName === acc.context.config.homeName + ' Weather'
@@ -1431,7 +1441,7 @@ export default (api, accessories, config, tado, telegram) => {
1431
1441
  }
1432
1442
 
1433
1443
  async function updatePresence() {
1434
- if (helpers[config.homeId].settingState) return;
1444
+ if (settingStates()) return;
1435
1445
 
1436
1446
  const presenceLockAccessory = accessories.filter(
1437
1447
  (acc) => acc && acc.displayName === acc.context.config.homeName + ' Presence Lock'
@@ -1476,7 +1486,7 @@ export default (api, accessories, config, tado, telegram) => {
1476
1486
  }
1477
1487
 
1478
1488
  async function updateRunningTime() {
1479
- if (helpers[config.homeId].settingState) return;
1489
+ if (settingStates()) return;
1480
1490
 
1481
1491
  const centralSwitchAccessory = accessories.filter(
1482
1492
  (acc) => acc && acc.displayName === acc.context.config.homeName + ' Central Switch'
@@ -1521,7 +1531,7 @@ export default (api, accessories, config, tado, telegram) => {
1521
1531
  }
1522
1532
 
1523
1533
  async function updateDevices() {
1524
- if (helpers[config.homeId].settingState) return;
1534
+ if (settingStates()) return;
1525
1535
 
1526
1536
  Logger.debug('Polling Devices...', config.homeName);
1527
1537
 
@@ -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':