@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 +10 -4
- package/config.schema.json +6 -0
- package/homebridge-ui/public/js/main.js +3 -0
- package/homebridge-ui/public/js/schema.js +6 -0
- package/homebridge-ui/server.js +1 -1
- package/package.json +4 -4
- package/src/accessories/heatercooler.js +9 -31
- package/src/accessories/thermostat.js +9 -21
- package/src/helper/handler.js +96 -80
- package/src/helper/update-buffer.js +69 -0
- package/src/platform.js +2 -2
- package/src/tado/tado-api.js +55 -13
- package/src/tado/tado-config.js +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## v8.6.0 - 2025-10-
|
|
4
|
-
- BREAKING CHANGE: If you use tadoApiUrl or skipAuth,
|
|
5
|
-
- Refactor configuration:
|
|
6
|
-
- Persist
|
|
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
|
package/config.schema.json
CHANGED
|
@@ -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/homebridge-ui/server.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@homebridge-plugins/homebridge-tado",
|
|
3
|
-
"version": "8.6.0-beta.
|
|
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.
|
|
47
|
-
"eslint": "^9.
|
|
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.
|
|
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(
|
|
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(
|
|
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(
|
|
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)
|
|
196
|
-
|
|
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(
|
|
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)
|
package/src/helper/handler.js
CHANGED
|
@@ -1,42 +1,41 @@
|
|
|
1
1
|
import Logger from '../helper/logger.js';
|
|
2
2
|
import moment from 'moment';
|
|
3
|
-
import { writeFile
|
|
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
|
|
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
|
-
|
|
28
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 >
|
|
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
|
-
|
|
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.
|
|
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
|
|
747
|
-
|
|
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
|
|
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
|
|
802
|
+
void refreshHistoryServices();
|
|
815
803
|
}
|
|
816
804
|
}
|
|
817
805
|
|
|
818
806
|
async function updateMe() {
|
|
819
|
-
if (
|
|
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 (
|
|
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 (
|
|
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
|
-
|
|
908
|
-
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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:
|
|
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':
|
package/src/tado/tado-api.js
CHANGED
|
@@ -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
|
-
|
|
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-${
|
|
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
|
-
|
|
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
|
|
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) {
|
package/src/tado/tado-config.js
CHANGED
|
@@ -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,
|