@homebridge-plugins/homebridge-tado 8.2.0 → 8.3.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/config.schema.json +6 -0
- package/homebridge-ui/public/js/main.js +3 -0
- package/homebridge-ui/public/js/schema.js +6 -0
- package/package.json +2 -2
- 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 +67 -12
- package/src/platform.js +2 -3
- package/src/tado/tado-api.js +53 -2
package/config.schema.json
CHANGED
|
@@ -563,6 +563,11 @@
|
|
|
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);
|
|
@@ -555,6 +555,11 @@ const schema = {
|
|
|
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.1",
|
|
4
4
|
"description": "Homebridge plugin for controlling tado° devices.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -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,21 @@
|
|
|
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
|
|
|
9
|
+
let statesInterval;
|
|
10
|
+
let counterInterval;
|
|
11
|
+
|
|
7
12
|
const timeout = (ms) => new Promise((res) => setTimeout(res, ms));
|
|
13
|
+
const aRefreshHistoryHandlers = [];
|
|
8
14
|
|
|
9
15
|
export default (api, accessories, config, tado, telegram) => {
|
|
16
|
+
const storagePath = api.user.storagePath();
|
|
17
|
+
initTasks();
|
|
18
|
+
|
|
10
19
|
async function setStates(accessory, accs, target, value) {
|
|
11
20
|
accessories = accs.filter((acc) => acc && acc.context.config.homeName === config.homeName);
|
|
12
21
|
|
|
@@ -567,7 +576,7 @@ export default (api, accessories, config, tado, telegram) => {
|
|
|
567
576
|
? Math.round(targetTemp - currentTemp >= 5 ? 100 : (targetTemp - currentTemp) * 20)
|
|
568
577
|
: 0;
|
|
569
578
|
|
|
570
|
-
historyService.addEntry({
|
|
579
|
+
if (historyService) historyService.addEntry({
|
|
571
580
|
time: moment().unix(),
|
|
572
581
|
currentTemp: currentTemp,
|
|
573
582
|
setTemp: targetTemp,
|
|
@@ -595,7 +604,7 @@ export default (api, accessories, config, tado, telegram) => {
|
|
|
595
604
|
? Math.round(targetTemp - currentTemp >= 5 ? 100 : (targetTemp - currentTemp) * 20)
|
|
596
605
|
: 0;
|
|
597
606
|
|
|
598
|
-
historyService.addEntry({
|
|
607
|
+
if (historyService) historyService.addEntry({
|
|
599
608
|
time: moment().unix(),
|
|
600
609
|
currentTemp: currentTemp,
|
|
601
610
|
setTemp: targetTemp,
|
|
@@ -636,7 +645,7 @@ export default (api, accessories, config, tado, telegram) => {
|
|
|
636
645
|
.updateValue(openDuration);
|
|
637
646
|
}
|
|
638
647
|
|
|
639
|
-
historyService.addEntry({ time: moment().unix(), status: value.newValue ? 1 : 0 });
|
|
648
|
+
if (historyService) historyService.addEntry({ time: moment().unix(), status: value.newValue ? 1 : 0 });
|
|
640
649
|
|
|
641
650
|
let dest = value.newValue ? 'opened' : 'closed';
|
|
642
651
|
|
|
@@ -658,7 +667,7 @@ export default (api, accessories, config, tado, telegram) => {
|
|
|
658
667
|
.getCharacteristic(api.hap.Characteristic.LastActivation)
|
|
659
668
|
.updateValue(lastActivation);
|
|
660
669
|
|
|
661
|
-
historyService.addEntry({ time: moment().unix(), status: value.newValue ? 1 : 0 });
|
|
670
|
+
if (historyService) historyService.addEntry({ time: moment().unix(), status: value.newValue ? 1 : 0 });
|
|
662
671
|
}
|
|
663
672
|
|
|
664
673
|
let dest;
|
|
@@ -679,13 +688,13 @@ export default (api, accessories, config, tado, telegram) => {
|
|
|
679
688
|
|
|
680
689
|
case 'zone-temperature':
|
|
681
690
|
case 'weather-temperature': {
|
|
682
|
-
historyService.addEntry({ time: moment().unix(), temp: value.newValue, humidity: 0, ppm: 0 });
|
|
691
|
+
if (historyService) historyService.addEntry({ time: moment().unix(), temp: value.newValue, humidity: 0, ppm: 0 });
|
|
683
692
|
|
|
684
693
|
break;
|
|
685
694
|
}
|
|
686
695
|
|
|
687
696
|
case 'zone-humidity': {
|
|
688
|
-
historyService.addEntry({ time: moment().unix(), temp: 0, humidity: value.newValue, ppm: 0 });
|
|
697
|
+
if (historyService) historyService.addEntry({ time: moment().unix(), temp: 0, humidity: value.newValue, ppm: 0 });
|
|
689
698
|
|
|
690
699
|
break;
|
|
691
700
|
}
|
|
@@ -697,6 +706,49 @@ export default (api, accessories, config, tado, telegram) => {
|
|
|
697
706
|
}
|
|
698
707
|
}
|
|
699
708
|
|
|
709
|
+
async function refreshHistory(homeId, zoneStates) {
|
|
710
|
+
try {
|
|
711
|
+
const data = {};
|
|
712
|
+
data.counterData = await tado.getCounterData();
|
|
713
|
+
await writeFile(join(storagePath, "tado-counter.json"), JSON.stringify(data, null, 2), "utf-8");
|
|
714
|
+
} catch (error) {
|
|
715
|
+
Logger.error(`Error while updating tado counter file: ${error.message || error}`);
|
|
716
|
+
}
|
|
717
|
+
try {
|
|
718
|
+
const data = {};
|
|
719
|
+
data.zoneStates = zoneStates ?? {};
|
|
720
|
+
await writeFile(join(storagePath, `tado-states-${homeId}.json`), JSON.stringify(data, null, 2), "utf-8");
|
|
721
|
+
} catch (error) {
|
|
722
|
+
Logger.error(`Error while updating tado states file for home id ${homeId}: ${error.message || error}`);
|
|
723
|
+
}
|
|
724
|
+
try {
|
|
725
|
+
for (const fnRefreshHistory of aRefreshHistoryHandlers) {
|
|
726
|
+
fnRefreshHistory();
|
|
727
|
+
}
|
|
728
|
+
} catch (error) {
|
|
729
|
+
Logger.error(`Error while refreshing history: ${error.message || error}`);
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
async function logCounter() {
|
|
734
|
+
try {
|
|
735
|
+
const iCounter = (await tado.getCounterData()).counter;
|
|
736
|
+
Logger.info(`Tado API counter: ${iCounter.toLocaleString('en-US')}`);
|
|
737
|
+
} catch (error) {
|
|
738
|
+
Logger.warn(`Failed to get Tado API counter: ${error.message || error}`);
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
function initTasks() {
|
|
743
|
+
if (statesInterval) clearInterval(statesInterval);
|
|
744
|
+
void getStates();
|
|
745
|
+
statesInterval = setInterval(() => getStates(), Math.max(config.polling, 300) * 1000);
|
|
746
|
+
|
|
747
|
+
if (counterInterval) clearInterval(counterInterval);
|
|
748
|
+
void logCounter();
|
|
749
|
+
counterInterval = setInterval(() => logCounter(), 60 * 60 * 1000);
|
|
750
|
+
}
|
|
751
|
+
|
|
700
752
|
async function getStates() {
|
|
701
753
|
try {
|
|
702
754
|
//ME
|
|
@@ -706,7 +758,12 @@ export default (api, accessories, config, tado, telegram) => {
|
|
|
706
758
|
if (!config.temperatureUnit) await updateHome();
|
|
707
759
|
|
|
708
760
|
//Zones
|
|
709
|
-
|
|
761
|
+
let zoneStates = {};
|
|
762
|
+
try {
|
|
763
|
+
if (config.zones.length) zoneStates = await updateZones();
|
|
764
|
+
} finally {
|
|
765
|
+
void refreshHistory(config.homeId, zoneStates);
|
|
766
|
+
}
|
|
710
767
|
|
|
711
768
|
//MobileDevices
|
|
712
769
|
if (config.presence.length) await updateMobileDevices();
|
|
@@ -724,10 +781,6 @@ export default (api, accessories, config, tado, telegram) => {
|
|
|
724
781
|
if (config.childLock.length) await updateDevices();
|
|
725
782
|
} catch (err) {
|
|
726
783
|
errorHandler(err);
|
|
727
|
-
} finally {
|
|
728
|
-
setTimeout(() => {
|
|
729
|
-
getStates();
|
|
730
|
-
}, Math.max(config.polling, 300) * 1000);
|
|
731
784
|
}
|
|
732
785
|
}
|
|
733
786
|
|
|
@@ -773,6 +826,7 @@ export default (api, accessories, config, tado, telegram) => {
|
|
|
773
826
|
}
|
|
774
827
|
|
|
775
828
|
async function updateZones(idToUpdate) {
|
|
829
|
+
let zoneStates = {};
|
|
776
830
|
if (!settingState || idToUpdate !== undefined) {
|
|
777
831
|
Logger.debug('Polling Zones...', config.homeName);
|
|
778
832
|
|
|
@@ -823,7 +877,6 @@ export default (api, accessories, config, tado, telegram) => {
|
|
|
823
877
|
});
|
|
824
878
|
}
|
|
825
879
|
|
|
826
|
-
let zoneStates = {};
|
|
827
880
|
let zonesToUpdate = [];
|
|
828
881
|
if (idToUpdate !== undefined) {
|
|
829
882
|
zoneStates[idToUpdate] = await tado.getZoneState(config.homeId, idToUpdate);
|
|
@@ -1233,6 +1286,7 @@ export default (api, accessories, config, tado, telegram) => {
|
|
|
1233
1286
|
});
|
|
1234
1287
|
}
|
|
1235
1288
|
}
|
|
1289
|
+
return zoneStates;
|
|
1236
1290
|
}
|
|
1237
1291
|
|
|
1238
1292
|
async function updateMobileDevices() {
|
|
@@ -1513,5 +1567,6 @@ export default (api, accessories, config, tado, telegram) => {
|
|
|
1513
1567
|
getStates: getStates,
|
|
1514
1568
|
setStates: setStates,
|
|
1515
1569
|
changedStates: changedStates,
|
|
1570
|
+
refreshHistoryHandlers: aRefreshHistoryHandlers
|
|
1516
1571
|
};
|
|
1517
1572
|
};
|
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 = [];
|
|
@@ -192,8 +192,7 @@ class TadoPlatform {
|
|
|
192
192
|
|
|
193
193
|
let accessories = this.accessories.filter((acc) => acc && acc.context.config.homeName === name);
|
|
194
194
|
|
|
195
|
-
|
|
196
|
-
deviceHandler.getStates();
|
|
195
|
+
DeviceHandler(this.api, accessories, config, tado, this.telegram);
|
|
197
196
|
}
|
|
198
197
|
}
|
|
199
198
|
|
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";
|
|
@@ -12,6 +12,7 @@ export default class Tado {
|
|
|
12
12
|
this.tadoApiUrl = tadoApiUrl || tado_url;
|
|
13
13
|
this.customTadoApiUrlActive = !!tadoApiUrl;
|
|
14
14
|
this.skipAuth = skipAuth?.toString() === "true";
|
|
15
|
+
this.storagePath = storagePath;
|
|
15
16
|
this.name = name;
|
|
16
17
|
const usesExternalTokenFile = config.username?.toLowerCase().endsWith(".json");
|
|
17
18
|
this._tadoExternalTokenFilePath = usesExternalTokenFile ? config.username : undefined;
|
|
@@ -24,13 +25,59 @@ export default class Tado {
|
|
|
24
25
|
return (hash >>> 0).toString(36).padStart(7, '0');
|
|
25
26
|
};
|
|
26
27
|
this.username = usesExternalTokenFile ? undefined : config.username;
|
|
27
|
-
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`);
|
|
28
29
|
this._tadoApiClientId = tado_client_id;
|
|
29
30
|
this._tadoTokenPromise = undefined;
|
|
30
31
|
this._tadoAuthenticationCallback = undefined;
|
|
32
|
+
this._counterInitPromise = this._initCounter();
|
|
31
33
|
Logger.debug("API successfull initialized", this.name);
|
|
32
34
|
}
|
|
33
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
|
+
|
|
34
81
|
async getToken() {
|
|
35
82
|
if (!this._tadoTokenPromise) {
|
|
36
83
|
this._tadoTokenPromise = this._getToken().finally(() => {
|
|
@@ -95,6 +142,7 @@ export default class Tado {
|
|
|
95
142
|
},
|
|
96
143
|
responseType: "json"
|
|
97
144
|
});
|
|
145
|
+
await this._increaseCounter();
|
|
98
146
|
const { access_token, refresh_token } = response.body;
|
|
99
147
|
if (!access_token || !refresh_token) throw new Error("Empty access/refresh token.");
|
|
100
148
|
await fs.writeFile(this._tadoInternalTokenFilePath, JSON.stringify({ access_token, refresh_token }));
|
|
@@ -114,6 +162,7 @@ export default class Tado {
|
|
|
114
162
|
},
|
|
115
163
|
responseType: "json"
|
|
116
164
|
});
|
|
165
|
+
await this._increaseCounter();
|
|
117
166
|
const { device_code, verification_uri_complete } = authResponse.body;
|
|
118
167
|
if (!device_code) throw new Error("Failed to retrieve device code.");
|
|
119
168
|
Logger.info(`Open the following URL in your browser, click "submit" and log in to your tado° account "${this.username}": ${verification_uri_complete}`);
|
|
@@ -131,6 +180,7 @@ export default class Tado {
|
|
|
131
180
|
},
|
|
132
181
|
responseType: "json"
|
|
133
182
|
});
|
|
183
|
+
await this._increaseCounter();
|
|
134
184
|
} catch (_error) {
|
|
135
185
|
//authentication still pending -> response code 400
|
|
136
186
|
}
|
|
@@ -211,6 +261,7 @@ export default class Tado {
|
|
|
211
261
|
|
|
212
262
|
try {
|
|
213
263
|
const response = await got(tadoLink + path, config);
|
|
264
|
+
await this._increaseCounter();
|
|
214
265
|
return response.body;
|
|
215
266
|
} catch (error) {
|
|
216
267
|
Logger.error(`API Request [${method} ${path}] - FAILED: ${error.message}`, this.name);
|