@homebridge-plugins/homebridge-tado 8.1.2 → 8.3.0-beta.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 +6 -0
- package/config.schema.json +7 -1
- package/homebridge-ui/public/js/main.js +3 -0
- package/homebridge-ui/public/js/schema.js +7 -1
- package/package.json +4 -4
- package/src/accessories/contact.js +7 -8
- package/src/accessories/heatercooler.js +7 -8
- package/src/accessories/humidity.js +7 -8
- package/src/accessories/motion.js +8 -9
- package/src/accessories/temperature.js +7 -8
- package/src/accessories/thermostat.js +7 -8
- package/src/helper/handler.js +68 -11
- package/src/platform.js +1 -1
- package/src/tado/tado-api.js +61 -17
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v8.2.0 - 2025-10-23
|
|
4
|
+
- Query all zone states in a single API request to significantly reduce the number of API calls to tado during polling
|
|
5
|
+
- Update zone state after clearing a zone overlay
|
|
6
|
+
- Skip getRunningTime call when a custom URL is used and remove obsolete getWeatherAirComfort (#176)
|
|
7
|
+
- Add example for tadoApiUrl in config (#176)
|
|
8
|
+
|
|
3
9
|
## v8.1.2 - 2025-10-20
|
|
4
10
|
- Fix: Skip auth also for config endpoints if enabled (#176)
|
|
5
11
|
|
package/config.schema.json
CHANGED
|
@@ -557,12 +557,17 @@
|
|
|
557
557
|
"tadoApiUrl": {
|
|
558
558
|
"title": "Tado API URL",
|
|
559
559
|
"type": "string",
|
|
560
|
-
"description": "Optional: Use a custom tado api url."
|
|
560
|
+
"description": "Optional: Use a custom tado api url (e.g. http://localhost:8080)."
|
|
561
561
|
},
|
|
562
562
|
"skipAuth": {
|
|
563
563
|
"title": "Skip Authentication",
|
|
564
564
|
"type": "boolean",
|
|
565
565
|
"description": "Optional: Skip authentication for tado api."
|
|
566
|
+
},
|
|
567
|
+
"skipFakeGatoHistory": {
|
|
568
|
+
"title": "Skip FakeGato History Service",
|
|
569
|
+
"type": "boolean",
|
|
570
|
+
"description": "Optional: Skip creation of FakeGato history service."
|
|
566
571
|
}
|
|
567
572
|
}
|
|
568
573
|
},
|
|
@@ -571,6 +576,7 @@
|
|
|
571
576
|
"debug",
|
|
572
577
|
"tadoApiUrl",
|
|
573
578
|
"skipAuth",
|
|
579
|
+
"skipFakeGatoHistory",
|
|
574
580
|
{
|
|
575
581
|
"key": "homes",
|
|
576
582
|
"type": "array",
|
|
@@ -134,6 +134,7 @@ async function createCustomSchema(home) {
|
|
|
134
134
|
debug: pluginConfig[0].debug,
|
|
135
135
|
tadoApiUrl: pluginConfig[0].tadoApiUrl,
|
|
136
136
|
skipAuth: pluginConfig[0].skipAuth,
|
|
137
|
+
skipFakeGatoHistory: pluginConfig[0].skipFakeGatoHistory,
|
|
137
138
|
homes: home
|
|
138
139
|
});
|
|
139
140
|
|
|
@@ -143,6 +144,7 @@ async function createCustomSchema(home) {
|
|
|
143
144
|
pluginConfig[0].debug = config.debug;
|
|
144
145
|
pluginConfig[0].tadoApiUrl = config.tadoApiUrl;
|
|
145
146
|
pluginConfig[0].skipAuth = config.skipAuth;
|
|
147
|
+
pluginConfig[0].skipFakeGatoHistory = config.skipFakeGatoHistory;
|
|
146
148
|
pluginConfig[0].homes = pluginConfig[0].homes.map(myHome => {
|
|
147
149
|
if (myHome.name === config.homes.name) {
|
|
148
150
|
myHome = config.homes;
|
|
@@ -285,6 +287,7 @@ async function removeDeviceFromConfig(name) {
|
|
|
285
287
|
delete pluginConfig[0].debug;
|
|
286
288
|
delete pluginConfig[0].tadoApiUrl;
|
|
287
289
|
delete pluginConfig[0].skipAuth;
|
|
290
|
+
delete pluginConfig[0].skipFakeGatoHistory;
|
|
288
291
|
}
|
|
289
292
|
|
|
290
293
|
await homebridge.updatePluginConfig(pluginConfig);
|
|
@@ -549,12 +549,17 @@ const schema = {
|
|
|
549
549
|
'tadoApiUrl': {
|
|
550
550
|
'title': 'Tado API URL',
|
|
551
551
|
'type': 'string',
|
|
552
|
-
'description': 'Optional: Use a custom tado api url.'
|
|
552
|
+
'description': 'Optional: Use a custom tado api url (e.g. http://localhost:8080).'
|
|
553
553
|
},
|
|
554
554
|
'skipAuth': {
|
|
555
555
|
'title': 'Skip Authentication',
|
|
556
556
|
'type': 'boolean',
|
|
557
557
|
'description': 'Optional: Skip authentication for tado api.'
|
|
558
|
+
},
|
|
559
|
+
'skipFakeGatoHistory': {
|
|
560
|
+
'title': 'Skip FakeGato History Service',
|
|
561
|
+
'type': 'boolean',
|
|
562
|
+
'description': 'Optional: Skip creation of FakeGato history service.'
|
|
558
563
|
}
|
|
559
564
|
},
|
|
560
565
|
'layout': [
|
|
@@ -562,6 +567,7 @@ const schema = {
|
|
|
562
567
|
'debug',
|
|
563
568
|
'tadoApiUrl',
|
|
564
569
|
'skipAuth',
|
|
570
|
+
'skipFakeGatoHistory',
|
|
565
571
|
'homes.name',
|
|
566
572
|
'homes.polling',
|
|
567
573
|
'homes.temperatureUnit',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@homebridge-plugins/homebridge-tado",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.3.0-beta.0",
|
|
4
4
|
"description": "Homebridge plugin for controlling tado° devices.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -40,8 +40,8 @@
|
|
|
40
40
|
"moment": "^2.30.1"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
|
-
"@babel/core": "7.28.
|
|
44
|
-
"@babel/eslint-parser": "7.28.
|
|
43
|
+
"@babel/core": "7.28.5",
|
|
44
|
+
"@babel/eslint-parser": "7.28.5",
|
|
45
45
|
"@babel/eslint-plugin": "7.27.1",
|
|
46
46
|
"@eslint/js": "^9.38.0",
|
|
47
47
|
"eslint": "^9.38.0",
|
|
@@ -51,4 +51,4 @@
|
|
|
51
51
|
"globals": "^16.4.0",
|
|
52
52
|
"prettier": "^3.6.2"
|
|
53
53
|
}
|
|
54
|
-
}
|
|
54
|
+
}
|
|
@@ -86,11 +86,11 @@ export default class ContactAccessory {
|
|
|
86
86
|
.updateValue(this.accessory.context.timesOpened);
|
|
87
87
|
});
|
|
88
88
|
|
|
89
|
-
this.historyService = new this.FakeGatoHistoryService('door', this.accessory, {
|
|
89
|
+
this.historyService = this.FakeGatoHistoryService ? new this.FakeGatoHistoryService('door', this.accessory, {
|
|
90
90
|
storage: 'fs',
|
|
91
91
|
path: this.api.user.storagePath(),
|
|
92
92
|
disableTimer: true,
|
|
93
|
-
});
|
|
93
|
+
}) : undefined;
|
|
94
94
|
|
|
95
95
|
await timeout(250); //wait for historyService to load
|
|
96
96
|
|
|
@@ -101,19 +101,18 @@ export default class ContactAccessory {
|
|
|
101
101
|
this.deviceHandler.changedStates.bind(this, this.accessory, this.historyService, this.accessory.displayName)
|
|
102
102
|
);
|
|
103
103
|
|
|
104
|
-
this.
|
|
104
|
+
if (!this.refreshHistoryHandlerRegistered) {
|
|
105
|
+
this.deviceHandler.refreshHistoryHandlers.push(() => this.refreshHistory(service));
|
|
106
|
+
this.refreshHistoryHandlerRegistered = true;
|
|
107
|
+
}
|
|
105
108
|
}
|
|
106
109
|
|
|
107
110
|
refreshHistory(service) {
|
|
108
111
|
let state = service.getCharacteristic(this.api.hap.Characteristic.ContactSensorState).value;
|
|
109
112
|
|
|
110
|
-
this.historyService.addEntry({
|
|
113
|
+
if (this.historyService) this.historyService.addEntry({
|
|
111
114
|
time: moment().unix(),
|
|
112
115
|
status: state ? 1 : 0,
|
|
113
116
|
});
|
|
114
|
-
|
|
115
|
-
setTimeout(() => {
|
|
116
|
-
this.refreshHistory(service);
|
|
117
|
-
}, 10 * 60 * 1000);
|
|
118
117
|
}
|
|
119
118
|
}
|
|
@@ -248,11 +248,11 @@ export default class HeaterCoolerAccessory {
|
|
|
248
248
|
if (service.testCharacteristic(this.api.hap.Characteristic.RotationSpeed))
|
|
249
249
|
service.removeCharacteristic(service.getCharacteristic(this.api.hap.Characteristic.RotationSpeed));
|
|
250
250
|
|
|
251
|
-
this.historyService = new this.FakeGatoHistoryService('thermo', this.accessory, {
|
|
251
|
+
this.historyService = this.FakeGatoHistoryService ? new this.FakeGatoHistoryService('thermo', this.accessory, {
|
|
252
252
|
storage: 'fs',
|
|
253
253
|
path: this.api.user.storagePath(),
|
|
254
254
|
disableTimer: true,
|
|
255
|
-
});
|
|
255
|
+
}) : undefined;
|
|
256
256
|
|
|
257
257
|
await timeout(250); //wait for historyService to load
|
|
258
258
|
|
|
@@ -324,7 +324,10 @@ export default class HeaterCoolerAccessory {
|
|
|
324
324
|
this.deviceHandler.changedStates.bind(this, this.accessory, this.historyService, this.accessory.displayName)
|
|
325
325
|
);
|
|
326
326
|
|
|
327
|
-
this.
|
|
327
|
+
if (!this.refreshHistoryHandlerRegistered) {
|
|
328
|
+
this.deviceHandler.refreshHistoryHandlers.push(() => this.refreshHistory(service));
|
|
329
|
+
this.refreshHistoryHandlerRegistered = true;
|
|
330
|
+
}
|
|
328
331
|
}
|
|
329
332
|
|
|
330
333
|
refreshHistory(service) {
|
|
@@ -338,15 +341,11 @@ export default class HeaterCoolerAccessory {
|
|
|
338
341
|
: 0;
|
|
339
342
|
|
|
340
343
|
//Thermo
|
|
341
|
-
this.historyService.addEntry({
|
|
344
|
+
if (this.historyService) this.historyService.addEntry({
|
|
342
345
|
time: moment().unix(),
|
|
343
346
|
currentTemp: currentTemp,
|
|
344
347
|
setTemp: targetTemp,
|
|
345
348
|
valvePosition: valvePos,
|
|
346
349
|
});
|
|
347
|
-
|
|
348
|
-
setTimeout(() => {
|
|
349
|
-
this.refreshHistory(service);
|
|
350
|
-
}, 10 * 60 * 1000);
|
|
351
350
|
}
|
|
352
351
|
}
|
|
@@ -50,11 +50,11 @@ export default class HumidityAccessory {
|
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
this.historyService = new this.FakeGatoHistoryService('room', this.accessory, {
|
|
53
|
+
this.historyService = this.FakeGatoHistoryService ? new this.FakeGatoHistoryService('room', this.accessory, {
|
|
54
54
|
storage: 'fs',
|
|
55
55
|
path: this.api.user.storagePath(),
|
|
56
56
|
disableTimer: true,
|
|
57
|
-
});
|
|
57
|
+
}) : undefined;
|
|
58
58
|
|
|
59
59
|
await timeout(250); //wait for historyService to load
|
|
60
60
|
|
|
@@ -65,21 +65,20 @@ export default class HumidityAccessory {
|
|
|
65
65
|
this.deviceHandler.changedStates.bind(this, this.accessory, this.historyService, this.accessory.displayName)
|
|
66
66
|
);
|
|
67
67
|
|
|
68
|
-
this.
|
|
68
|
+
if (!this.refreshHistoryHandlerRegistered) {
|
|
69
|
+
this.deviceHandler.refreshHistoryHandlers.push(() => this.refreshHistory(service));
|
|
70
|
+
this.refreshHistoryHandlerRegistered = true;
|
|
71
|
+
}
|
|
69
72
|
}
|
|
70
73
|
|
|
71
74
|
refreshHistory(service) {
|
|
72
75
|
let state = service.getCharacteristic(this.api.hap.Characteristic.CurrentRelativeHumidity).value;
|
|
73
76
|
|
|
74
|
-
this.historyService.addEntry({
|
|
77
|
+
if (this.historyService) this.historyService.addEntry({
|
|
75
78
|
time: moment().unix(),
|
|
76
79
|
temp: 0,
|
|
77
80
|
humidity: state,
|
|
78
81
|
ppm: 0,
|
|
79
82
|
});
|
|
80
|
-
|
|
81
|
-
setTimeout(() => {
|
|
82
|
-
this.refreshHistory(service);
|
|
83
|
-
}, 10 * 60 * 1000);
|
|
84
83
|
}
|
|
85
84
|
}
|
|
@@ -41,11 +41,11 @@ export default class MotionAccessory {
|
|
|
41
41
|
if (!service.testCharacteristic(this.api.hap.Characteristic.LastActivation))
|
|
42
42
|
service.addCharacteristic(this.api.hap.Characteristic.LastActivation);
|
|
43
43
|
|
|
44
|
-
this.historyService = new this.FakeGatoHistoryService('motion', this.accessory, {
|
|
44
|
+
this.historyService = this.FakeGatoHistoryService ? new this.FakeGatoHistoryService('motion', this.accessory, {
|
|
45
45
|
storage: 'fs',
|
|
46
46
|
path: this.api.user.storagePath(),
|
|
47
47
|
disableTimer: true,
|
|
48
|
-
});
|
|
48
|
+
}) : undefined;
|
|
49
49
|
|
|
50
50
|
await timeout(250); //wait for historyService to load
|
|
51
51
|
|
|
@@ -56,19 +56,18 @@ export default class MotionAccessory {
|
|
|
56
56
|
this.deviceHandler.changedStates.bind(this, this.accessory, this.historyService, this.accessory.displayName)
|
|
57
57
|
);
|
|
58
58
|
|
|
59
|
-
this.
|
|
59
|
+
if (!this.refreshHistoryHandlerRegistered) {
|
|
60
|
+
this.deviceHandler.refreshHistoryHandlers.push(() => this.refreshHistory(service));
|
|
61
|
+
this.refreshHistoryHandlerRegistered = true;
|
|
62
|
+
}
|
|
60
63
|
}
|
|
61
64
|
|
|
62
|
-
|
|
65
|
+
refreshHistory(service) {
|
|
63
66
|
let state = service.getCharacteristic(this.api.hap.Characteristic.MotionDetected).value;
|
|
64
67
|
|
|
65
|
-
this.historyService.addEntry({
|
|
68
|
+
if (this.historyService) this.historyService.addEntry({
|
|
66
69
|
time: moment().unix(),
|
|
67
70
|
status: state ? 1 : 0,
|
|
68
71
|
});
|
|
69
|
-
|
|
70
|
-
setTimeout(() => {
|
|
71
|
-
this.refreshHistory(service);
|
|
72
|
-
}, 10 * 60 * 1000);
|
|
73
72
|
}
|
|
74
73
|
}
|
|
@@ -55,11 +55,11 @@ export default class TemperatureAccessory {
|
|
|
55
55
|
maxValue: 100,
|
|
56
56
|
});
|
|
57
57
|
|
|
58
|
-
this.historyService = new this.FakeGatoHistoryService('room', this.accessory, {
|
|
58
|
+
this.historyService = this.FakeGatoHistoryService ? new this.FakeGatoHistoryService('room', this.accessory, {
|
|
59
59
|
storage: 'fs',
|
|
60
60
|
path: this.api.user.storagePath(),
|
|
61
61
|
disableTimer: true,
|
|
62
|
-
});
|
|
62
|
+
}) : undefined;
|
|
63
63
|
|
|
64
64
|
await timeout(250); //wait for historyService to load
|
|
65
65
|
|
|
@@ -70,21 +70,20 @@ export default class TemperatureAccessory {
|
|
|
70
70
|
this.deviceHandler.changedStates.bind(this, this.accessory, this.historyService, this.accessory.displayName)
|
|
71
71
|
);
|
|
72
72
|
|
|
73
|
-
this.
|
|
73
|
+
if (!this.refreshHistoryHandlerRegistered) {
|
|
74
|
+
this.deviceHandler.refreshHistoryHandlers.push(() => this.refreshHistory(service));
|
|
75
|
+
this.refreshHistoryHandlerRegistered = true;
|
|
76
|
+
}
|
|
74
77
|
}
|
|
75
78
|
|
|
76
79
|
refreshHistory(service) {
|
|
77
80
|
let state = service.getCharacteristic(this.api.hap.Characteristic.CurrentTemperature).value;
|
|
78
81
|
|
|
79
|
-
this.historyService.addEntry({
|
|
82
|
+
if (this.historyService) this.historyService.addEntry({
|
|
80
83
|
time: moment().unix(),
|
|
81
84
|
temp: state,
|
|
82
85
|
humidity: 0,
|
|
83
86
|
ppm: 0,
|
|
84
87
|
});
|
|
85
|
-
|
|
86
|
-
setTimeout(() => {
|
|
87
|
-
this.refreshHistory(service);
|
|
88
|
-
}, 10 * 60 * 1000);
|
|
89
88
|
}
|
|
90
89
|
}
|
|
@@ -180,11 +180,11 @@ export default class ThermostatAccessory {
|
|
|
180
180
|
if (!service.testCharacteristic(this.api.hap.Characteristic.ValvePosition))
|
|
181
181
|
service.addCharacteristic(this.api.hap.Characteristic.ValvePosition);
|
|
182
182
|
|
|
183
|
-
this.historyService = new this.FakeGatoHistoryService('thermo', this.accessory, {
|
|
183
|
+
this.historyService = this.FakeGatoHistoryService ? new this.FakeGatoHistoryService('thermo', this.accessory, {
|
|
184
184
|
storage: 'fs',
|
|
185
185
|
path: this.api.user.storagePath(),
|
|
186
186
|
disableTimer: true,
|
|
187
|
-
});
|
|
187
|
+
}) : undefined;
|
|
188
188
|
|
|
189
189
|
await timeout(250); //wait for historyService to load
|
|
190
190
|
|
|
@@ -254,7 +254,10 @@ export default class ThermostatAccessory {
|
|
|
254
254
|
this.deviceHandler.changedStates.bind(this, this.accessory, this.historyService, this.accessory.displayName)
|
|
255
255
|
);
|
|
256
256
|
|
|
257
|
-
this.
|
|
257
|
+
if (!this.refreshHistoryHandlerRegistered) {
|
|
258
|
+
this.deviceHandler.refreshHistoryHandlers.push(() => this.refreshHistory(service));
|
|
259
|
+
this.refreshHistoryHandlerRegistered = true;
|
|
260
|
+
}
|
|
258
261
|
}
|
|
259
262
|
|
|
260
263
|
refreshHistory(service) {
|
|
@@ -268,16 +271,12 @@ export default class ThermostatAccessory {
|
|
|
268
271
|
? Math.round(targetTemp - currentTemp >= 5 ? 100 : (targetTemp - currentTemp) * 20)
|
|
269
272
|
: 0;
|
|
270
273
|
|
|
271
|
-
this.historyService.addEntry({
|
|
274
|
+
if (this.historyService) this.historyService.addEntry({
|
|
272
275
|
time: moment().unix(),
|
|
273
276
|
currentTemp: currentTemp,
|
|
274
277
|
setTemp: targetTemp,
|
|
275
278
|
valvePosition: valvePos,
|
|
276
279
|
});
|
|
277
|
-
|
|
278
|
-
setTimeout(() => {
|
|
279
|
-
this.refreshHistory(service);
|
|
280
|
-
}, 10 * 60 * 1000);
|
|
281
280
|
}
|
|
282
281
|
|
|
283
282
|
async changeUnit(service, value) {
|
package/src/helper/handler.js
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
import Logger from '../helper/logger.js';
|
|
2
2
|
import moment from 'moment';
|
|
3
|
+
import { writeFile } from 'fs/promises';
|
|
4
|
+
import { join } from "path";
|
|
3
5
|
|
|
4
6
|
var settingState = false;
|
|
5
7
|
var delayTimer = {};
|
|
6
8
|
|
|
7
9
|
const timeout = (ms) => new Promise((res) => setTimeout(res, ms));
|
|
10
|
+
const aRefreshHistoryHandlers = [];
|
|
8
11
|
|
|
9
12
|
export default (api, accessories, config, tado, telegram) => {
|
|
13
|
+
const storagePath = api.user.storagePath();
|
|
14
|
+
|
|
10
15
|
async function setStates(accessory, accs, target, value) {
|
|
11
16
|
accessories = accs.filter((acc) => acc && acc.context.config.homeName === config.homeName);
|
|
12
17
|
|
|
@@ -110,6 +115,7 @@ export default (api, accessories, config, tado, telegram) => {
|
|
|
110
115
|
accessory.context.config.subtype.includes('heatercooler'))
|
|
111
116
|
) {
|
|
112
117
|
await tado.clearZoneOverlay(config.homeId, accessory.context.config.zoneId);
|
|
118
|
+
await updateZones(accessory.context.config.zoneId);
|
|
113
119
|
return;
|
|
114
120
|
}
|
|
115
121
|
|
|
@@ -170,6 +176,7 @@ export default (api, accessories, config, tado, telegram) => {
|
|
|
170
176
|
accessory.context.config.subtype.includes('heatercooler'))
|
|
171
177
|
) {
|
|
172
178
|
await tado.clearZoneOverlay(config.homeId, accessory.context.config.zoneId);
|
|
179
|
+
await updateZones(accessory.context.config.zoneId);
|
|
173
180
|
return;
|
|
174
181
|
}
|
|
175
182
|
|
|
@@ -565,7 +572,7 @@ export default (api, accessories, config, tado, telegram) => {
|
|
|
565
572
|
? Math.round(targetTemp - currentTemp >= 5 ? 100 : (targetTemp - currentTemp) * 20)
|
|
566
573
|
: 0;
|
|
567
574
|
|
|
568
|
-
historyService.addEntry({
|
|
575
|
+
if (historyService) historyService.addEntry({
|
|
569
576
|
time: moment().unix(),
|
|
570
577
|
currentTemp: currentTemp,
|
|
571
578
|
setTemp: targetTemp,
|
|
@@ -593,7 +600,7 @@ export default (api, accessories, config, tado, telegram) => {
|
|
|
593
600
|
? Math.round(targetTemp - currentTemp >= 5 ? 100 : (targetTemp - currentTemp) * 20)
|
|
594
601
|
: 0;
|
|
595
602
|
|
|
596
|
-
historyService.addEntry({
|
|
603
|
+
if (historyService) historyService.addEntry({
|
|
597
604
|
time: moment().unix(),
|
|
598
605
|
currentTemp: currentTemp,
|
|
599
606
|
setTemp: targetTemp,
|
|
@@ -634,7 +641,7 @@ export default (api, accessories, config, tado, telegram) => {
|
|
|
634
641
|
.updateValue(openDuration);
|
|
635
642
|
}
|
|
636
643
|
|
|
637
|
-
historyService.addEntry({ time: moment().unix(), status: value.newValue ? 1 : 0 });
|
|
644
|
+
if (historyService) historyService.addEntry({ time: moment().unix(), status: value.newValue ? 1 : 0 });
|
|
638
645
|
|
|
639
646
|
let dest = value.newValue ? 'opened' : 'closed';
|
|
640
647
|
|
|
@@ -656,7 +663,7 @@ export default (api, accessories, config, tado, telegram) => {
|
|
|
656
663
|
.getCharacteristic(api.hap.Characteristic.LastActivation)
|
|
657
664
|
.updateValue(lastActivation);
|
|
658
665
|
|
|
659
|
-
historyService.addEntry({ time: moment().unix(), status: value.newValue ? 1 : 0 });
|
|
666
|
+
if (historyService) historyService.addEntry({ time: moment().unix(), status: value.newValue ? 1 : 0 });
|
|
660
667
|
}
|
|
661
668
|
|
|
662
669
|
let dest;
|
|
@@ -677,13 +684,13 @@ export default (api, accessories, config, tado, telegram) => {
|
|
|
677
684
|
|
|
678
685
|
case 'zone-temperature':
|
|
679
686
|
case 'weather-temperature': {
|
|
680
|
-
historyService.addEntry({ time: moment().unix(), temp: value.newValue, humidity: 0, ppm: 0 });
|
|
687
|
+
if (historyService) historyService.addEntry({ time: moment().unix(), temp: value.newValue, humidity: 0, ppm: 0 });
|
|
681
688
|
|
|
682
689
|
break;
|
|
683
690
|
}
|
|
684
691
|
|
|
685
692
|
case 'zone-humidity': {
|
|
686
|
-
historyService.addEntry({ time: moment().unix(), temp: 0, humidity: value.newValue, ppm: 0 });
|
|
693
|
+
if (historyService) historyService.addEntry({ time: moment().unix(), temp: 0, humidity: value.newValue, ppm: 0 });
|
|
687
694
|
|
|
688
695
|
break;
|
|
689
696
|
}
|
|
@@ -695,7 +702,32 @@ export default (api, accessories, config, tado, telegram) => {
|
|
|
695
702
|
}
|
|
696
703
|
}
|
|
697
704
|
|
|
705
|
+
async function refreshHistory(homeId, zoneStates) {
|
|
706
|
+
try {
|
|
707
|
+
const data = {};
|
|
708
|
+
data.counterData = await tado.getCounterData();
|
|
709
|
+
await writeFile(join(storagePath, "tado-counter.json"), JSON.stringify(data, null, 2), "utf-8");
|
|
710
|
+
} catch (error) {
|
|
711
|
+
Logger.error(`Error while updating tado counter file: ${error.message || error}`);
|
|
712
|
+
}
|
|
713
|
+
try {
|
|
714
|
+
const data = {};
|
|
715
|
+
data.zoneStates = zoneStates ?? {};
|
|
716
|
+
await writeFile(join(storagePath, `tado-states-${homeId}.json`), JSON.stringify(data, null, 2), "utf-8");
|
|
717
|
+
} catch (error) {
|
|
718
|
+
Logger.error(`Error while updating tado states file for home id ${homeId}: ${error.message || error}`);
|
|
719
|
+
}
|
|
720
|
+
try {
|
|
721
|
+
for (const fnRefreshHistory of aRefreshHistoryHandlers) {
|
|
722
|
+
fnRefreshHistory();
|
|
723
|
+
}
|
|
724
|
+
} catch (error) {
|
|
725
|
+
Logger.error(`Error while refreshing history: ${error.message || error}`);
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
|
|
698
729
|
async function getStates() {
|
|
730
|
+
let zoneStates = {};
|
|
699
731
|
try {
|
|
700
732
|
//ME
|
|
701
733
|
if (!config.homeId) await updateMe();
|
|
@@ -704,7 +736,7 @@ export default (api, accessories, config, tado, telegram) => {
|
|
|
704
736
|
if (!config.temperatureUnit) await updateHome();
|
|
705
737
|
|
|
706
738
|
//Zones
|
|
707
|
-
if (config.zones.length) await updateZones();
|
|
739
|
+
if (config.zones.length) zoneStates = await updateZones();
|
|
708
740
|
|
|
709
741
|
//MobileDevices
|
|
710
742
|
if (config.presence.length) await updateMobileDevices();
|
|
@@ -723,10 +755,23 @@ export default (api, accessories, config, tado, telegram) => {
|
|
|
723
755
|
} catch (err) {
|
|
724
756
|
errorHandler(err);
|
|
725
757
|
} finally {
|
|
758
|
+
refreshHistory(config.homeId, zoneStates);
|
|
726
759
|
setTimeout(() => {
|
|
727
760
|
getStates();
|
|
728
761
|
}, Math.max(config.polling, 300) * 1000);
|
|
729
762
|
}
|
|
763
|
+
|
|
764
|
+
//log tado api counter every hour
|
|
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}`);
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
void logCounter();
|
|
774
|
+
setInterval(logCounter, 60 * 60 * 1000);
|
|
730
775
|
}
|
|
731
776
|
|
|
732
777
|
async function updateMe() {
|
|
@@ -770,8 +815,9 @@ export default (api, accessories, config, tado, telegram) => {
|
|
|
770
815
|
return;
|
|
771
816
|
}
|
|
772
817
|
|
|
773
|
-
async function updateZones() {
|
|
774
|
-
|
|
818
|
+
async function updateZones(idToUpdate) {
|
|
819
|
+
let zoneStates = {};
|
|
820
|
+
if (!settingState || idToUpdate !== undefined) {
|
|
775
821
|
Logger.debug('Polling Zones...', config.homeName);
|
|
776
822
|
|
|
777
823
|
//CentralSwitch
|
|
@@ -821,8 +867,17 @@ export default (api, accessories, config, tado, telegram) => {
|
|
|
821
867
|
});
|
|
822
868
|
}
|
|
823
869
|
|
|
824
|
-
|
|
825
|
-
|
|
870
|
+
let zonesToUpdate = [];
|
|
871
|
+
if (idToUpdate !== undefined) {
|
|
872
|
+
zoneStates[idToUpdate] = await tado.getZoneState(config.homeId, idToUpdate);
|
|
873
|
+
zonesToUpdate = config.zones.filter(zone => zone.id === idToUpdate);
|
|
874
|
+
} else {
|
|
875
|
+
zoneStates = (await tado.getZoneStates(config.homeId))["zoneStates"];
|
|
876
|
+
zonesToUpdate = config.zones;
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
for (const zone of zonesToUpdate) {
|
|
880
|
+
const zoneState = zoneStates[zone.id];
|
|
826
881
|
|
|
827
882
|
let currentState, targetState, currentTemp, targetTemp, humidity, active, battery, tempEqual;
|
|
828
883
|
|
|
@@ -1221,6 +1276,7 @@ export default (api, accessories, config, tado, telegram) => {
|
|
|
1221
1276
|
});
|
|
1222
1277
|
}
|
|
1223
1278
|
}
|
|
1279
|
+
return zoneStates = {};
|
|
1224
1280
|
}
|
|
1225
1281
|
|
|
1226
1282
|
async function updateMobileDevices() {
|
|
@@ -1501,5 +1557,6 @@ export default (api, accessories, config, tado, telegram) => {
|
|
|
1501
1557
|
getStates: getStates,
|
|
1502
1558
|
setStates: setStates,
|
|
1503
1559
|
changedStates: changedStates,
|
|
1560
|
+
refreshHistoryHandlers: aRefreshHistoryHandlers
|
|
1504
1561
|
};
|
|
1505
1562
|
};
|
package/src/platform.js
CHANGED
|
@@ -41,7 +41,7 @@ class TadoPlatform {
|
|
|
41
41
|
Logger.init(log, config.debug);
|
|
42
42
|
CustomTypes.registerWith(api.hap);
|
|
43
43
|
EveTypes.registerWith(api.hap);
|
|
44
|
-
FakeGatoHistoryService = fakeGatoHistory(api);
|
|
44
|
+
FakeGatoHistoryService = config.skipFakeGatoHistory ? undefined : fakeGatoHistory(api);
|
|
45
45
|
|
|
46
46
|
this.api = api;
|
|
47
47
|
this.accessories = [];
|
package/src/tado/tado-api.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import Logger from '../helper/logger.js';
|
|
2
2
|
import got from 'got';
|
|
3
3
|
import path from 'path';
|
|
4
|
-
import fs from 'fs/promises';
|
|
4
|
+
import fs, { access, readFile } from 'fs/promises';
|
|
5
5
|
|
|
6
6
|
const tado_url = "https://my.tado.com";
|
|
7
7
|
const tado_auth_url = "https://login.tado.com/oauth2";
|
|
@@ -10,7 +10,9 @@ const tado_client_id = "1bb50063-6b0c-4d11-bd99-387f4a91cc46";
|
|
|
10
10
|
export default class Tado {
|
|
11
11
|
constructor(name, config, storagePath, tadoApiUrl, skipAuth) {
|
|
12
12
|
this.tadoApiUrl = tadoApiUrl || tado_url;
|
|
13
|
+
this.customTadoApiUrlActive = !!tadoApiUrl;
|
|
13
14
|
this.skipAuth = skipAuth?.toString() === "true";
|
|
15
|
+
this.storagePath = storagePath;
|
|
14
16
|
this.name = name;
|
|
15
17
|
const usesExternalTokenFile = config.username?.toLowerCase().endsWith(".json");
|
|
16
18
|
this._tadoExternalTokenFilePath = usesExternalTokenFile ? config.username : undefined;
|
|
@@ -23,13 +25,59 @@ export default class Tado {
|
|
|
23
25
|
return (hash >>> 0).toString(36).padStart(7, '0');
|
|
24
26
|
};
|
|
25
27
|
this.username = usesExternalTokenFile ? undefined : config.username;
|
|
26
|
-
this._tadoInternalTokenFilePath = usesExternalTokenFile ? undefined : path.join(storagePath, `.tado-token-${fnSimpleHash(config.username)}.json`);
|
|
28
|
+
this._tadoInternalTokenFilePath = usesExternalTokenFile ? undefined : path.join(this.storagePath, `.tado-token-${fnSimpleHash(config.username)}.json`);
|
|
27
29
|
this._tadoApiClientId = tado_client_id;
|
|
28
30
|
this._tadoTokenPromise = undefined;
|
|
29
31
|
this._tadoAuthenticationCallback = undefined;
|
|
32
|
+
this._counterInitPromise = this._initCounter();
|
|
30
33
|
Logger.debug("API successfull initialized", this.name);
|
|
31
34
|
}
|
|
32
35
|
|
|
36
|
+
async _initCounter() {
|
|
37
|
+
let counterData;
|
|
38
|
+
try {
|
|
39
|
+
const sFilePath = path.join(this.storagePath, `tado-counter.json`);
|
|
40
|
+
await access(sFilePath);
|
|
41
|
+
const sData = (await readFile(sFilePath, "utf-8"));
|
|
42
|
+
counterData = JSON.parse(sData)?.counterData;
|
|
43
|
+
} catch (_err) {
|
|
44
|
+
//no counter data => ignore
|
|
45
|
+
}
|
|
46
|
+
this._counter = counterData?.counter ?? 0;
|
|
47
|
+
this._counterTimestamp = counterData?.counterTimestamp ?? new Date().toISOString();
|
|
48
|
+
this._checkCounterMidnightReset();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
_checkCounterMidnightReset() {
|
|
52
|
+
const timezone = "Europe/Berlin";
|
|
53
|
+
const now = new Date();
|
|
54
|
+
const last = new Date(this._counterTimestamp || 0);
|
|
55
|
+
const formatDate = (date) => date.toLocaleDateString("en-US", { timeZone: timezone });
|
|
56
|
+
if (formatDate(now) !== formatDate(last)) {
|
|
57
|
+
this._counter = 0;
|
|
58
|
+
this._counterTimestamp = new Date().toISOString();
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async _increaseCounter() {
|
|
63
|
+
try {
|
|
64
|
+
await this._counterInitPromise;
|
|
65
|
+
this._checkCounterMidnightReset();
|
|
66
|
+
this._counter++;
|
|
67
|
+
this._counterTimestamp = new Date().toISOString();
|
|
68
|
+
} catch (error) {
|
|
69
|
+
Logger.warn(`Failed to increase tado api counter: ${error.message || error}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async getCounterData() {
|
|
74
|
+
await this._counterInitPromise;
|
|
75
|
+
return {
|
|
76
|
+
counter: this._counter,
|
|
77
|
+
counterTimestamp: this._counterTimestamp
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
33
81
|
async getToken() {
|
|
34
82
|
if (!this._tadoTokenPromise) {
|
|
35
83
|
this._tadoTokenPromise = this._getToken().finally(() => {
|
|
@@ -94,6 +142,7 @@ export default class Tado {
|
|
|
94
142
|
},
|
|
95
143
|
responseType: "json"
|
|
96
144
|
});
|
|
145
|
+
await this._increaseCounter();
|
|
97
146
|
const { access_token, refresh_token } = response.body;
|
|
98
147
|
if (!access_token || !refresh_token) throw new Error("Empty access/refresh token.");
|
|
99
148
|
await fs.writeFile(this._tadoInternalTokenFilePath, JSON.stringify({ access_token, refresh_token }));
|
|
@@ -113,6 +162,7 @@ export default class Tado {
|
|
|
113
162
|
},
|
|
114
163
|
responseType: "json"
|
|
115
164
|
});
|
|
165
|
+
await this._increaseCounter();
|
|
116
166
|
const { device_code, verification_uri_complete } = authResponse.body;
|
|
117
167
|
if (!device_code) throw new Error("Failed to retrieve device code.");
|
|
118
168
|
Logger.info(`Open the following URL in your browser, click "submit" and log in to your tado° account "${this.username}": ${verification_uri_complete}`);
|
|
@@ -130,6 +180,7 @@ export default class Tado {
|
|
|
130
180
|
},
|
|
131
181
|
responseType: "json"
|
|
132
182
|
});
|
|
183
|
+
await this._increaseCounter();
|
|
133
184
|
} catch (_error) {
|
|
134
185
|
//authentication still pending -> response code 400
|
|
135
186
|
}
|
|
@@ -210,6 +261,7 @@ export default class Tado {
|
|
|
210
261
|
|
|
211
262
|
try {
|
|
212
263
|
const response = await got(tadoLink + path, config);
|
|
264
|
+
await this._increaseCounter();
|
|
213
265
|
return response.body;
|
|
214
266
|
} catch (error) {
|
|
215
267
|
Logger.error(`API Request [${method} ${path}] - FAILED: ${error.message}`, this.name);
|
|
@@ -308,6 +360,10 @@ export default class Tado {
|
|
|
308
360
|
return this.apiCall(`/api/v2/homes/${home_id}/zones/${zone_id}/state`);
|
|
309
361
|
}
|
|
310
362
|
|
|
363
|
+
async getZoneStates(home_id) {
|
|
364
|
+
return this.apiCall(`/api/v2/homes/${home_id}/zoneStates`);
|
|
365
|
+
}
|
|
366
|
+
|
|
311
367
|
async getZoneCapabilities(home_id, zone_id) {
|
|
312
368
|
return this.apiCall(`/api/v2/homes/${home_id}/zones/${zone_id}/capabilities`);
|
|
313
369
|
}
|
|
@@ -543,20 +599,6 @@ export default class Tado {
|
|
|
543
599
|
return this.apiCall(`/api/v2/homes/${home_id}/airComfort`);
|
|
544
600
|
}
|
|
545
601
|
|
|
546
|
-
async getWeatherAirComfort(home_id, longitude, latitude) {
|
|
547
|
-
let geoLocation = {
|
|
548
|
-
longitude: longitude,
|
|
549
|
-
latitude: latitude,
|
|
550
|
-
};
|
|
551
|
-
|
|
552
|
-
if (!geoLocation.longitude || !geoLocation.latitude) {
|
|
553
|
-
const data = await this.getHome(home_id);
|
|
554
|
-
geoLocation = data.geolocation;
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
return this.apiCall(`/v1/homes/${home_id}/airComfort`, 'GET', {}, geoLocation, 'https://acme.tado.com');
|
|
558
|
-
}
|
|
559
|
-
|
|
560
602
|
async setChildLock(serialNumber, state) {
|
|
561
603
|
if (!serialNumber) {
|
|
562
604
|
throw new Error('Cannot change child lock state. No serialNumber is given.');
|
|
@@ -614,6 +656,8 @@ export default class Tado {
|
|
|
614
656
|
}
|
|
615
657
|
|
|
616
658
|
async getRunningTime(home_id, time, from, to) {
|
|
659
|
+
if (this.customTadoApiUrlActive) return;
|
|
660
|
+
|
|
617
661
|
const period = {
|
|
618
662
|
aggregate: time || 'day',
|
|
619
663
|
summary_only: true,
|
|
@@ -623,6 +667,6 @@ export default class Tado {
|
|
|
623
667
|
|
|
624
668
|
if (to) period.to = to;
|
|
625
669
|
|
|
626
|
-
return this.apiCall(`/v1/homes/${home_id}/runningTimes`, 'GET', {}, period, 'https://minder.tado.com'
|
|
670
|
+
return this.apiCall(`/v1/homes/${home_id}/runningTimes`, 'GET', {}, period, 'https://minder.tado.com');
|
|
627
671
|
}
|
|
628
672
|
}
|