@homebridge-plugins/homebridge-tado 9.0.0 → 9.1.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/homebridge-ui/public/index.html +16 -0
- package/homebridge-ui/public/js/main.js +150 -109
- package/homebridge-ui/server.js +41 -28
- package/package.json +2 -2
- package/src/tado/tado-api.js +43 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
# v9.1.0 - 2026-05-06
|
|
4
|
+
- Add support for multiple bridges in Config UI
|
|
5
|
+
- Validate tado account after authentication
|
|
6
|
+
- Improve browser authentication instructions
|
|
7
|
+
- Update got
|
|
8
|
+
|
|
3
9
|
## v9.0.0 - 2026-05-03
|
|
4
10
|
- Require Node.js 22
|
|
5
11
|
- Update dependencies
|
|
@@ -33,6 +33,22 @@
|
|
|
33
33
|
<h6 class="m-0 p-0">Custom UI cannot be displayed. The used Config UI X version is not supported!</h6>
|
|
34
34
|
</div>
|
|
35
35
|
|
|
36
|
+
<!-- selectBridge: shown only when this plugin has multiple platform entries
|
|
37
|
+
(e.g. several child bridges). Lets the user pick which one this UI
|
|
38
|
+
session will configure so that newly added homes don't all funnel into
|
|
39
|
+
platforms[0]. -->
|
|
40
|
+
<div id="selectBridge" style="display:none;">
|
|
41
|
+
<img src="images/tado_logo.png" alt="tado meets Homebridge" width="150px" class="center-it mt-2 tadoLogo">
|
|
42
|
+
<h6 class="text-center">You have multiple Tado bridge instances configured.<br>Pick the one you want to configure now.</h6>
|
|
43
|
+
<div class="container mt-4" style="max-width: 360px;">
|
|
44
|
+
<div class="form-group">
|
|
45
|
+
<label for="bridgeSelectChoice">Bridge</label>
|
|
46
|
+
<select class="custom-select" id="bridgeSelectChoice"></select>
|
|
47
|
+
</div>
|
|
48
|
+
<button id="bridgeSelectContinue" type="submit" class="btn btn-primary float-right mt-3 mr-0">Continue</button>
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
|
|
36
52
|
<!-- isConfigured -->
|
|
37
53
|
<div id="isConfigured" style="display:none;">
|
|
38
54
|
<button type="submit" class="btn btn-elegant oldConfig" style="position: absolute;">
|
|
@@ -7,8 +7,39 @@ const pageNavigation = {
|
|
|
7
7
|
|
|
8
8
|
let customSchemaActive = false;
|
|
9
9
|
let pluginConfig = false;
|
|
10
|
+
let activeIndex = 0;
|
|
10
11
|
let currentHome = false;
|
|
11
12
|
|
|
13
|
+
function bridgeLabel(entry, i) {
|
|
14
|
+
const name = entry && entry.name;
|
|
15
|
+
const childUsername = entry && entry._bridge && entry._bridge.username;
|
|
16
|
+
if (name && childUsername) return `${name} (child bridge ${childUsername})`;
|
|
17
|
+
if (name) return name;
|
|
18
|
+
if (childUsername) return `Child bridge ${childUsername}`;
|
|
19
|
+
return `Bridge ${i + 1}`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function chooseActiveBridge() {
|
|
23
|
+
if (!pluginConfig || pluginConfig.length <= 1) return;
|
|
24
|
+
const $select = $('#bridgeSelectChoice');
|
|
25
|
+
$select.empty();
|
|
26
|
+
pluginConfig.forEach((entry, i) => {
|
|
27
|
+
const homeCount = (entry && entry.homes && entry.homes.length) || 0;
|
|
28
|
+
const homesLabel = `${homeCount} home${homeCount === 1 ? '' : 's'}`;
|
|
29
|
+
$select.append(`<option value="${i}">${bridgeLabel(entry, i)} — ${homesLabel}</option>`);
|
|
30
|
+
});
|
|
31
|
+
$select.val(String(activeIndex));
|
|
32
|
+
await new Promise(resolve => {
|
|
33
|
+
$('#bridgeSelectContinue').off('click').on('click', () => {
|
|
34
|
+
const parsed = parseInt($select.val(), 10);
|
|
35
|
+
activeIndex = Number.isInteger(parsed) ? parsed : 0;
|
|
36
|
+
$('#selectBridge').hide();
|
|
37
|
+
resolve();
|
|
38
|
+
});
|
|
39
|
+
$('#selectBridge').fadeIn(250);
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
12
43
|
const TIMEOUT = (ms) => new Promise((res) => setTimeout(res, ms));
|
|
13
44
|
|
|
14
45
|
function toggleContent() {
|
|
@@ -130,20 +161,20 @@ async function createCustomSchema(home) {
|
|
|
130
161
|
//schema.layout.homes.forEach()
|
|
131
162
|
|
|
132
163
|
customSchemaActive = homebridge.createForm(schema, {
|
|
133
|
-
name: pluginConfig[
|
|
134
|
-
debug: pluginConfig[
|
|
135
|
-
disableHistoryService: pluginConfig[
|
|
136
|
-
preferSiriTemperature: pluginConfig[
|
|
164
|
+
name: pluginConfig[activeIndex].name,
|
|
165
|
+
debug: pluginConfig[activeIndex].debug,
|
|
166
|
+
disableHistoryService: pluginConfig[activeIndex].disableHistoryService,
|
|
167
|
+
preferSiriTemperature: pluginConfig[activeIndex].preferSiriTemperature,
|
|
137
168
|
homes: home
|
|
138
169
|
});
|
|
139
170
|
|
|
140
171
|
customSchemaActive.onChange(async config => {
|
|
141
172
|
|
|
142
|
-
pluginConfig[
|
|
143
|
-
pluginConfig[
|
|
144
|
-
pluginConfig[
|
|
145
|
-
pluginConfig[
|
|
146
|
-
pluginConfig[
|
|
173
|
+
pluginConfig[activeIndex].name = config.name;
|
|
174
|
+
pluginConfig[activeIndex].debug = config.debug;
|
|
175
|
+
pluginConfig[activeIndex].disableHistoryService = config.disableHistoryService;
|
|
176
|
+
pluginConfig[activeIndex].preferSiriTemperature = config.preferSiriTemperature;
|
|
177
|
+
pluginConfig[activeIndex].homes = pluginConfig[activeIndex].homes.map(myHome => {
|
|
147
178
|
if (myHome.name === config.homes.name) {
|
|
148
179
|
myHome = config.homes;
|
|
149
180
|
}
|
|
@@ -228,21 +259,21 @@ async function addNewDeviceToConfig(config, refresh, resync) {
|
|
|
228
259
|
|
|
229
260
|
try {
|
|
230
261
|
|
|
231
|
-
for (const i in config[
|
|
262
|
+
for (const i in config[activeIndex].homes) {
|
|
232
263
|
|
|
233
264
|
let found = false;
|
|
234
265
|
|
|
235
|
-
for (const j in pluginConfig[
|
|
236
|
-
if (config[
|
|
266
|
+
for (const j in pluginConfig[activeIndex].homes)
|
|
267
|
+
if (config[activeIndex].homes[i].name === pluginConfig[activeIndex].homes[j].name)
|
|
237
268
|
found = true;
|
|
238
269
|
|
|
239
270
|
if (!found) {
|
|
240
|
-
addDeviceToList(config[
|
|
241
|
-
homebridge.toast.success(config[
|
|
271
|
+
addDeviceToList(config[activeIndex].homes[i]);
|
|
272
|
+
homebridge.toast.success(config[activeIndex].homes[i].name + ' added to config!', 'Success');
|
|
242
273
|
} else if (refresh) {
|
|
243
|
-
homebridge.toast.success(config[
|
|
274
|
+
homebridge.toast.success(config[activeIndex].homes[i].name + ' refreshed!', 'Success');
|
|
244
275
|
} else if (resync) {
|
|
245
|
-
homebridge.toast.success(config[
|
|
276
|
+
homebridge.toast.success(config[activeIndex].homes[i].name + ' resynchronized!', 'Success');
|
|
246
277
|
}
|
|
247
278
|
|
|
248
279
|
}
|
|
@@ -270,7 +301,7 @@ async function removeDeviceFromConfig(name) {
|
|
|
270
301
|
let foundIndex;
|
|
271
302
|
let pluginConfigBkp = JSON.parse(JSON.stringify(pluginConfig));
|
|
272
303
|
|
|
273
|
-
pluginConfig[
|
|
304
|
+
pluginConfig[activeIndex].homes.forEach((home, index) => {
|
|
274
305
|
if (home.name === currentHome) {
|
|
275
306
|
foundIndex = index;
|
|
276
307
|
}
|
|
@@ -280,13 +311,13 @@ async function removeDeviceFromConfig(name) {
|
|
|
280
311
|
|
|
281
312
|
try {
|
|
282
313
|
|
|
283
|
-
pluginConfig[
|
|
314
|
+
pluginConfig[activeIndex].homes.splice(foundIndex, 1);
|
|
284
315
|
removeDeviceFromList(currentHome);
|
|
285
316
|
|
|
286
|
-
if (!pluginConfig[
|
|
287
|
-
delete pluginConfig[
|
|
288
|
-
delete pluginConfig[
|
|
289
|
-
delete pluginConfig[
|
|
317
|
+
if (!pluginConfig[activeIndex].homes.length) {
|
|
318
|
+
delete pluginConfig[activeIndex].debug;
|
|
319
|
+
delete pluginConfig[activeIndex].disableHistoryService;
|
|
320
|
+
delete pluginConfig[activeIndex].preferSiriTemperature;
|
|
290
321
|
}
|
|
291
322
|
|
|
292
323
|
await homebridge.updatePluginConfig(pluginConfig);
|
|
@@ -324,9 +355,13 @@ async function fetchDevices(auth, refresh, resync) {
|
|
|
324
355
|
homebridge.request('/authenticate', params);
|
|
325
356
|
const instructionsURL = await homebridge.request('/exec', { dest: 'fullAuthentication' });
|
|
326
357
|
if (instructionsURL && instructionsURL !== "") {
|
|
327
|
-
$("#fetchDevices #authenticationInstructions").html(
|
|
358
|
+
$("#fetchDevices #authenticationInstructions").html(
|
|
359
|
+
`Sign in as <strong>${params.username}</strong> and click "Submit":` +
|
|
360
|
+
`<br><a href="${instructionsURL}" target="_blank" rel="noopener noreferrer">${instructionsURL}</a>` +
|
|
361
|
+
`<br><span style="font-size:smaller;color:#666;">Tip: if your browser is signed in to tado.com with a different account, open the link in a private/incognito window.</span>`
|
|
362
|
+
);
|
|
328
363
|
$("#fetchDevices #authenticationInstructions").css("display", "block");
|
|
329
|
-
homebridge.toast.info("
|
|
364
|
+
homebridge.toast.info("Open the link and confirm your login.");
|
|
330
365
|
const authenticationSuccessful = await homebridge.request('/exec', { dest: 'waitForAuthentication' });
|
|
331
366
|
$("#fetchDevices #authenticationInstructions").html("");
|
|
332
367
|
$("#fetchDevices #authenticationInstructions").css("display", "none");
|
|
@@ -348,7 +383,7 @@ async function fetchDevices(auth, refresh, resync) {
|
|
|
348
383
|
//refresh selected home
|
|
349
384
|
|
|
350
385
|
//Home Informations
|
|
351
|
-
let home = config[
|
|
386
|
+
let home = config[activeIndex].homes.find(home => home && home.name === currentHome);
|
|
352
387
|
|
|
353
388
|
if (!home)
|
|
354
389
|
return homebridge.toast.error('Cannot refresh ' + currentHome + '. Not found in config!', 'Error');
|
|
@@ -371,26 +406,26 @@ async function fetchDevices(auth, refresh, resync) {
|
|
|
371
406
|
|
|
372
407
|
const homeInfo = await homebridge.request('/exec', { dest: 'getHome', data: home.id });
|
|
373
408
|
|
|
374
|
-
for (let [i, home] of config[
|
|
409
|
+
for (let [i, home] of config[activeIndex].homes.entries()) {
|
|
375
410
|
|
|
376
|
-
if (config[
|
|
411
|
+
if (config[activeIndex].homes[i].name === homeInfo.name) {
|
|
377
412
|
|
|
378
|
-
config[
|
|
379
|
-
config[
|
|
380
|
-
config[
|
|
381
|
-
config[
|
|
382
|
-
config[
|
|
383
|
-
config[
|
|
413
|
+
config[activeIndex].homes[i].id = homeInfo.id;
|
|
414
|
+
config[activeIndex].homes[i].username = auth.username;
|
|
415
|
+
config[activeIndex].homes[i].tadoApiUrl = auth.tadoApiUrl;
|
|
416
|
+
config[activeIndex].homes[i].skipAuth = auth.skipAuth;
|
|
417
|
+
config[activeIndex].homes[i].temperatureUnit = homeInfo.temperatureUnit || 'CELSIUS';
|
|
418
|
+
config[activeIndex].homes[i].zones = config[activeIndex].homes[i].zones || [];
|
|
384
419
|
|
|
385
420
|
if (homeInfo.geolocation)
|
|
386
|
-
config[
|
|
421
|
+
config[activeIndex].homes[i].geolocation = {
|
|
387
422
|
longitude: homeInfo.geolocation.longitude.toString(),
|
|
388
423
|
latitude: homeInfo.geolocation.latitude.toString()
|
|
389
424
|
};
|
|
390
425
|
|
|
391
426
|
//init devices for childLock
|
|
392
|
-
config[
|
|
393
|
-
config[
|
|
427
|
+
config[activeIndex].homes[i].extras = config[activeIndex].homes[i].extras || {};
|
|
428
|
+
config[activeIndex].homes[i].extras.childLockSwitches = config[activeIndex].homes[i].extras.childLockSwitches || [];
|
|
394
429
|
|
|
395
430
|
let allFoundDevices = [];
|
|
396
431
|
|
|
@@ -401,15 +436,15 @@ async function fetchDevices(auth, refresh, resync) {
|
|
|
401
436
|
//Mobile Devices Informations
|
|
402
437
|
const mobileDevices = await homebridge.request('/exec', { dest: 'getMobileDevices', data: home.id });
|
|
403
438
|
|
|
404
|
-
if (!config[
|
|
405
|
-
config[
|
|
439
|
+
if (!config[activeIndex].homes[i].presence)
|
|
440
|
+
config[activeIndex].homes[i].presence = {
|
|
406
441
|
anyone: false,
|
|
407
442
|
accTypeAnyone: 'OCCUPANCY',
|
|
408
443
|
user: []
|
|
409
444
|
};
|
|
410
445
|
|
|
411
446
|
//Remove not registred devices
|
|
412
|
-
config[
|
|
447
|
+
config[activeIndex].homes[i].presence.user.forEach((user, index) => {
|
|
413
448
|
let found = false;
|
|
414
449
|
mobileDevices.forEach(foundUser => {
|
|
415
450
|
if (foundUser.name === user.name) {
|
|
@@ -418,21 +453,21 @@ async function fetchDevices(auth, refresh, resync) {
|
|
|
418
453
|
});
|
|
419
454
|
if (!found) {
|
|
420
455
|
homebridge.toast.info(user.name + ' removed from config!', auth.username);
|
|
421
|
-
config[
|
|
456
|
+
config[activeIndex].homes[i].presence.user.splice(index, 1);
|
|
422
457
|
}
|
|
423
458
|
});
|
|
424
459
|
|
|
425
460
|
//Check for new registred devices
|
|
426
|
-
if (config[
|
|
461
|
+
if (config[activeIndex].homes[i].presence.user.length) {
|
|
427
462
|
for (const foundUser of mobileDevices) {
|
|
428
463
|
let userIndex;
|
|
429
|
-
config[
|
|
464
|
+
config[activeIndex].homes[i].presence.user.forEach((user, index) => {
|
|
430
465
|
if (user.name === foundUser.name) {
|
|
431
466
|
userIndex = index;
|
|
432
467
|
}
|
|
433
468
|
});
|
|
434
469
|
if (userIndex === undefined) {
|
|
435
|
-
config[
|
|
470
|
+
config[activeIndex].homes[i].presence.user.push({
|
|
436
471
|
active: false,
|
|
437
472
|
name: foundUser.name,
|
|
438
473
|
accType: 'OCCUPANCY'
|
|
@@ -440,7 +475,7 @@ async function fetchDevices(auth, refresh, resync) {
|
|
|
440
475
|
}
|
|
441
476
|
}
|
|
442
477
|
} else {
|
|
443
|
-
config[
|
|
478
|
+
config[activeIndex].homes[i].presence.user = mobileDevices.map(user => {
|
|
444
479
|
return {
|
|
445
480
|
active: false,
|
|
446
481
|
name: user.name,
|
|
@@ -457,7 +492,7 @@ async function fetchDevices(auth, refresh, resync) {
|
|
|
457
492
|
const zones = await homebridge.request('/exec', { dest: 'getZones', data: home.id });
|
|
458
493
|
|
|
459
494
|
//Remove not available zones
|
|
460
|
-
config[
|
|
495
|
+
config[activeIndex].homes[i].zones.forEach((zone, index) => {
|
|
461
496
|
let found = false;
|
|
462
497
|
zones.forEach(foundZone => {
|
|
463
498
|
if (foundZone.name === zone.name) {
|
|
@@ -466,12 +501,12 @@ async function fetchDevices(auth, refresh, resync) {
|
|
|
466
501
|
});
|
|
467
502
|
if (!found) {
|
|
468
503
|
homebridge.toast.info(zone.name + ' removed from config!', auth.username);
|
|
469
|
-
config[
|
|
504
|
+
config[activeIndex].homes[i].zones.splice(index, 1);
|
|
470
505
|
}
|
|
471
506
|
});
|
|
472
507
|
|
|
473
508
|
//Check for new zones or refresh exist one
|
|
474
|
-
if (config[
|
|
509
|
+
if (config[activeIndex].homes[i].zones.length) {
|
|
475
510
|
for (const foundZone of zones) {
|
|
476
511
|
|
|
477
512
|
const capabilities = await homebridge.request('/exec', { dest: 'getZoneCapabilities', data: [home.id, foundZone.id] }) || {};
|
|
@@ -516,19 +551,19 @@ async function fetchDevices(auth, refresh, resync) {
|
|
|
516
551
|
});
|
|
517
552
|
|
|
518
553
|
let zoneIndex;
|
|
519
|
-
config[
|
|
554
|
+
config[activeIndex].homes[i].zones.forEach((zone, index) => {
|
|
520
555
|
if (zone.name === foundZone.name) {
|
|
521
556
|
zoneIndex = index;
|
|
522
557
|
}
|
|
523
558
|
});
|
|
524
559
|
if (zoneIndex !== undefined) {
|
|
525
|
-
config[
|
|
526
|
-
config[
|
|
527
|
-
config[
|
|
528
|
-
config[
|
|
529
|
-
config[
|
|
560
|
+
config[activeIndex].homes[i].zones[zoneIndex].id = foundZone.id;
|
|
561
|
+
config[activeIndex].homes[i].zones[zoneIndex].type = foundZone.type;
|
|
562
|
+
config[activeIndex].homes[i].zones[zoneIndex].minValue = minTempValue;
|
|
563
|
+
config[activeIndex].homes[i].zones[zoneIndex].maxValue = maxTempValue;
|
|
564
|
+
config[activeIndex].homes[i].zones[zoneIndex].minStep = minTempStep;
|
|
530
565
|
} else {
|
|
531
|
-
config[
|
|
566
|
+
config[activeIndex].homes[i].zones.push({
|
|
532
567
|
active: true,
|
|
533
568
|
id: foundZone.id,
|
|
534
569
|
name: foundZone.name,
|
|
@@ -595,7 +630,7 @@ async function fetchDevices(auth, refresh, resync) {
|
|
|
595
630
|
});
|
|
596
631
|
});
|
|
597
632
|
|
|
598
|
-
config[
|
|
633
|
+
config[activeIndex].homes[i].zones.push({
|
|
599
634
|
active: true,
|
|
600
635
|
id: zone.id,
|
|
601
636
|
name: zone.name,
|
|
@@ -621,7 +656,7 @@ async function fetchDevices(auth, refresh, resync) {
|
|
|
621
656
|
}
|
|
622
657
|
|
|
623
658
|
//remove non existing childLockSwitches
|
|
624
|
-
config[
|
|
659
|
+
config[activeIndex].homes[i].extras.childLockSwitches.forEach((childLockSwitch, index) => {
|
|
625
660
|
let found = false;
|
|
626
661
|
allFoundDevices.forEach(foundDevice => {
|
|
627
662
|
if (foundDevice.serialNumber === childLockSwitch.serialNumber) {
|
|
@@ -630,21 +665,21 @@ async function fetchDevices(auth, refresh, resync) {
|
|
|
630
665
|
});
|
|
631
666
|
if (!found) {
|
|
632
667
|
homebridge.toast.info(childLockSwitch.name + ' removed from config!', auth.username);
|
|
633
|
-
config[
|
|
668
|
+
config[activeIndex].homes[i].extras.childLockSwitches.splice(index, 1);
|
|
634
669
|
}
|
|
635
670
|
});
|
|
636
671
|
|
|
637
672
|
//check for new childLockSwitches
|
|
638
|
-
if (config[
|
|
673
|
+
if (config[activeIndex].homes[i].extras.childLockSwitches.length) {
|
|
639
674
|
for (const foundDevice of allFoundDevices) {
|
|
640
675
|
let found = false;
|
|
641
|
-
config[
|
|
676
|
+
config[activeIndex].homes[i].extras.childLockSwitches.forEach(childLockSwitch => {
|
|
642
677
|
if (childLockSwitch.serialNumber === foundDevice.serialNumber) {
|
|
643
678
|
found = true;
|
|
644
679
|
}
|
|
645
680
|
});
|
|
646
681
|
if (!found) {
|
|
647
|
-
config[
|
|
682
|
+
config[activeIndex].homes[i].extras.childLockSwitches.push({
|
|
648
683
|
active: false,
|
|
649
684
|
name: foundDevice.name,
|
|
650
685
|
serialNumber: foundDevice.serialNumber
|
|
@@ -652,7 +687,7 @@ async function fetchDevices(auth, refresh, resync) {
|
|
|
652
687
|
}
|
|
653
688
|
}
|
|
654
689
|
} else {
|
|
655
|
-
config[
|
|
690
|
+
config[activeIndex].homes[i].extras.childLockSwitches = allFoundDevices.map(device => {
|
|
656
691
|
return {
|
|
657
692
|
active: false,
|
|
658
693
|
name: device.name,
|
|
@@ -671,7 +706,7 @@ async function fetchDevices(auth, refresh, resync) {
|
|
|
671
706
|
|
|
672
707
|
const availableHomesInApis = [];
|
|
673
708
|
|
|
674
|
-
for (let home of config[
|
|
709
|
+
for (let home of config[activeIndex].homes) {
|
|
675
710
|
|
|
676
711
|
if (home.name && home.username) {
|
|
677
712
|
|
|
@@ -710,7 +745,7 @@ async function fetchDevices(auth, refresh, resync) {
|
|
|
710
745
|
let removedHomes = 0;
|
|
711
746
|
|
|
712
747
|
//remove non exist homes from config that doesnt exist in api
|
|
713
|
-
for (let [i, home] of config[
|
|
748
|
+
for (let [i, home] of config[activeIndex].homes.entries()) {
|
|
714
749
|
|
|
715
750
|
if (home.name && home.username) {
|
|
716
751
|
|
|
@@ -734,7 +769,7 @@ async function fetchDevices(auth, refresh, resync) {
|
|
|
734
769
|
homebridge.toast.info(home.name + ' removed from config!', home.username);
|
|
735
770
|
|
|
736
771
|
await removeDeviceFromConfig(home.name);
|
|
737
|
-
config[
|
|
772
|
+
config[activeIndex].homes.splice(i, 1);
|
|
738
773
|
|
|
739
774
|
removedHomes += 1;
|
|
740
775
|
|
|
@@ -754,7 +789,7 @@ async function fetchDevices(auth, refresh, resync) {
|
|
|
754
789
|
await TIMEOUT(2000);
|
|
755
790
|
|
|
756
791
|
//refresh existing homes
|
|
757
|
-
for (let [i, home] of config[
|
|
792
|
+
for (let [i, home] of config[activeIndex].homes.entries()) {
|
|
758
793
|
|
|
759
794
|
if (home.name && home.username) {
|
|
760
795
|
|
|
@@ -780,37 +815,37 @@ async function fetchDevices(auth, refresh, resync) {
|
|
|
780
815
|
|
|
781
816
|
const homeInfo = await homebridge.request('/exec', { dest: 'getHome', data: home.id });
|
|
782
817
|
|
|
783
|
-
config[
|
|
784
|
-
config[
|
|
785
|
-
config[
|
|
786
|
-
config[
|
|
787
|
-
config[
|
|
788
|
-
config[
|
|
818
|
+
config[activeIndex].homes[i].id = homeInfo.id;
|
|
819
|
+
config[activeIndex].homes[i].username = foundHome.username;
|
|
820
|
+
config[activeIndex].homes[i].tadoApiUrl = foundHome.tadoApiUrl;
|
|
821
|
+
config[activeIndex].homes[i].skipAuth = foundHome.skipAuth;
|
|
822
|
+
config[activeIndex].homes[i].temperatureUnit = homeInfo.temperatureUnit || 'CELSIUS';
|
|
823
|
+
config[activeIndex].homes[i].zones = config[activeIndex].homes[i].zones || [];
|
|
789
824
|
|
|
790
825
|
if (homeInfo.geolocation)
|
|
791
|
-
config[
|
|
826
|
+
config[activeIndex].homes[i].geolocation = {
|
|
792
827
|
longitude: homeInfo.geolocation.longitude.toString(),
|
|
793
828
|
latitude: homeInfo.geolocation.latitude.toString()
|
|
794
829
|
};
|
|
795
830
|
|
|
796
831
|
//init devices for childLock
|
|
797
|
-
config[
|
|
798
|
-
config[
|
|
832
|
+
config[activeIndex].homes[i].extras = config[activeIndex].homes[i].extras || {};
|
|
833
|
+
config[activeIndex].homes[i].extras.childLockSwitches = config[activeIndex].homes[i].extras.childLockSwitches || [];
|
|
799
834
|
|
|
800
835
|
let allFoundDevices = [];
|
|
801
836
|
|
|
802
837
|
//Mobile Devices Informations
|
|
803
838
|
const mobileDevices = await homebridge.request('/exec', { dest: 'getMobileDevices', data: home.id });
|
|
804
839
|
|
|
805
|
-
if (!config[
|
|
806
|
-
config[
|
|
840
|
+
if (!config[activeIndex].homes[i].presence)
|
|
841
|
+
config[activeIndex].homes[i].presence = {
|
|
807
842
|
anyone: false,
|
|
808
843
|
accTypeAnyone: 'OCCUPANCY',
|
|
809
844
|
user: []
|
|
810
845
|
};
|
|
811
846
|
|
|
812
847
|
//Remove not registred devices
|
|
813
|
-
config[
|
|
848
|
+
config[activeIndex].homes[i].presence.user.forEach((user, index) => {
|
|
814
849
|
let found = false;
|
|
815
850
|
mobileDevices.forEach(foundUser => {
|
|
816
851
|
if (foundUser.name === user.name) {
|
|
@@ -819,21 +854,21 @@ async function fetchDevices(auth, refresh, resync) {
|
|
|
819
854
|
});
|
|
820
855
|
if (!found) {
|
|
821
856
|
homebridge.toast.info(user.name + ' removed from config!', home.username);
|
|
822
|
-
config[
|
|
857
|
+
config[activeIndex].homes[i].presence.user.splice(index, 1);
|
|
823
858
|
}
|
|
824
859
|
});
|
|
825
860
|
|
|
826
861
|
//Check for new registred devices
|
|
827
|
-
if (config[
|
|
862
|
+
if (config[activeIndex].homes[i].presence.user.length) {
|
|
828
863
|
for (const foundUser of mobileDevices) {
|
|
829
864
|
let userIndex;
|
|
830
|
-
config[
|
|
865
|
+
config[activeIndex].homes[i].presence.user.forEach((user, index) => {
|
|
831
866
|
if (user.name === foundUser.name) {
|
|
832
867
|
userIndex = index;
|
|
833
868
|
}
|
|
834
869
|
});
|
|
835
870
|
if (userIndex === undefined) {
|
|
836
|
-
config[
|
|
871
|
+
config[activeIndex].homes[i].presence.user.push({
|
|
837
872
|
active: false,
|
|
838
873
|
name: foundUser.name,
|
|
839
874
|
accType: 'OCCUPANCY'
|
|
@@ -841,7 +876,7 @@ async function fetchDevices(auth, refresh, resync) {
|
|
|
841
876
|
}
|
|
842
877
|
}
|
|
843
878
|
} else {
|
|
844
|
-
config[
|
|
879
|
+
config[activeIndex].homes[i].presence.user = mobileDevices.map(user => {
|
|
845
880
|
return {
|
|
846
881
|
active: false,
|
|
847
882
|
name: user.name,
|
|
@@ -854,7 +889,7 @@ async function fetchDevices(auth, refresh, resync) {
|
|
|
854
889
|
const zones = await homebridge.request('/exec', { dest: 'getZones', data: home.id });
|
|
855
890
|
|
|
856
891
|
//Remove not available zones
|
|
857
|
-
config[
|
|
892
|
+
config[activeIndex].homes[i].zones.forEach((zone, index) => {
|
|
858
893
|
let found = false;
|
|
859
894
|
zones.forEach(foundZone => {
|
|
860
895
|
if (foundZone.name === zone.name) {
|
|
@@ -863,12 +898,12 @@ async function fetchDevices(auth, refresh, resync) {
|
|
|
863
898
|
});
|
|
864
899
|
if (!found) {
|
|
865
900
|
homebridge.toast.info(zone.name + ' removed from config!', home.username);
|
|
866
|
-
config[
|
|
901
|
+
config[activeIndex].homes[i].zones.splice(index, 1);
|
|
867
902
|
}
|
|
868
903
|
});
|
|
869
904
|
|
|
870
905
|
//Check for new zones or refresh exist one
|
|
871
|
-
if (config[
|
|
906
|
+
if (config[activeIndex].homes[i].zones.length) {
|
|
872
907
|
for (const foundZone of zones) {
|
|
873
908
|
|
|
874
909
|
const capabilities = await homebridge.request('/exec', { dest: 'getZoneCapabilities', data: [home.id, foundZone.id] }) || {};
|
|
@@ -913,19 +948,19 @@ async function fetchDevices(auth, refresh, resync) {
|
|
|
913
948
|
});
|
|
914
949
|
|
|
915
950
|
let zoneIndex;
|
|
916
|
-
config[
|
|
951
|
+
config[activeIndex].homes[i].zones.forEach((zone, index) => {
|
|
917
952
|
if (zone.name === foundZone.name) {
|
|
918
953
|
zoneIndex = index;
|
|
919
954
|
}
|
|
920
955
|
});
|
|
921
956
|
if (zoneIndex !== undefined) {
|
|
922
|
-
config[
|
|
923
|
-
config[
|
|
924
|
-
config[
|
|
925
|
-
config[
|
|
926
|
-
config[
|
|
957
|
+
config[activeIndex].homes[i].zones[zoneIndex].id = foundZone.id;
|
|
958
|
+
config[activeIndex].homes[i].zones[zoneIndex].type = foundZone.type;
|
|
959
|
+
config[activeIndex].homes[i].zones[zoneIndex].minValue = minTempValue;
|
|
960
|
+
config[activeIndex].homes[i].zones[zoneIndex].maxValue = maxTempValue;
|
|
961
|
+
config[activeIndex].homes[i].zones[zoneIndex].minStep = minTempStep;
|
|
927
962
|
} else {
|
|
928
|
-
config[
|
|
963
|
+
config[activeIndex].homes[i].zones.push({
|
|
929
964
|
active: true,
|
|
930
965
|
id: foundZone.id,
|
|
931
966
|
name: foundZone.name,
|
|
@@ -993,7 +1028,7 @@ async function fetchDevices(auth, refresh, resync) {
|
|
|
993
1028
|
});
|
|
994
1029
|
});
|
|
995
1030
|
|
|
996
|
-
config[
|
|
1031
|
+
config[activeIndex].homes[i].zones.push({
|
|
997
1032
|
active: true,
|
|
998
1033
|
id: zone.id,
|
|
999
1034
|
name: zone.name,
|
|
@@ -1019,7 +1054,7 @@ async function fetchDevices(auth, refresh, resync) {
|
|
|
1019
1054
|
}
|
|
1020
1055
|
|
|
1021
1056
|
//remove non existing childLockSwitches
|
|
1022
|
-
config[
|
|
1057
|
+
config[activeIndex].homes[i].extras.childLockSwitches.forEach((childLockSwitch, index) => {
|
|
1023
1058
|
let found = false;
|
|
1024
1059
|
allFoundDevices.forEach(foundDevice => {
|
|
1025
1060
|
if (foundDevice.serialNumber === childLockSwitch.serialNumber) {
|
|
@@ -1028,21 +1063,21 @@ async function fetchDevices(auth, refresh, resync) {
|
|
|
1028
1063
|
});
|
|
1029
1064
|
if (!found) {
|
|
1030
1065
|
homebridge.toast.info(childLockSwitch.serialNumber + ' removed from config!', home.username);
|
|
1031
|
-
config[
|
|
1066
|
+
config[activeIndex].homes[i].extras.childLockSwitches.splice(index, 1);
|
|
1032
1067
|
}
|
|
1033
1068
|
});
|
|
1034
1069
|
|
|
1035
1070
|
//check for new childLockSwitches
|
|
1036
|
-
if (config[
|
|
1071
|
+
if (config[activeIndex].homes[i].extras.childLockSwitches.length) {
|
|
1037
1072
|
for (const foundDevice of allFoundDevices) {
|
|
1038
1073
|
let found = false;
|
|
1039
|
-
config[
|
|
1074
|
+
config[activeIndex].homes[i].extras.childLockSwitches.forEach(childLockSwitch => {
|
|
1040
1075
|
if (childLockSwitch.serialNumber === foundDevice.serialNumber) {
|
|
1041
1076
|
found = true;
|
|
1042
1077
|
}
|
|
1043
1078
|
});
|
|
1044
1079
|
if (!found) {
|
|
1045
|
-
config[
|
|
1080
|
+
config[activeIndex].homes[i].extras.childLockSwitches.push({
|
|
1046
1081
|
active: false,
|
|
1047
1082
|
name: foundDevice.name,
|
|
1048
1083
|
serialNumber: foundDevice.serialNumber
|
|
@@ -1050,7 +1085,7 @@ async function fetchDevices(auth, refresh, resync) {
|
|
|
1050
1085
|
}
|
|
1051
1086
|
}
|
|
1052
1087
|
} else {
|
|
1053
|
-
config[
|
|
1088
|
+
config[activeIndex].homes[i].extras.childLockSwitches = allFoundDevices.map(device => {
|
|
1054
1089
|
return {
|
|
1055
1090
|
active: false,
|
|
1056
1091
|
name: device.name,
|
|
@@ -1083,7 +1118,7 @@ async function fetchDevices(auth, refresh, resync) {
|
|
|
1083
1118
|
|
|
1084
1119
|
let found = false;
|
|
1085
1120
|
|
|
1086
|
-
config[
|
|
1121
|
+
config[activeIndex].homes.forEach(home => {
|
|
1087
1122
|
if (home.name === foundHome.name || home.id === foundHome.id)
|
|
1088
1123
|
found = true;
|
|
1089
1124
|
});
|
|
@@ -1227,7 +1262,7 @@ async function fetchDevices(auth, refresh, resync) {
|
|
|
1227
1262
|
|
|
1228
1263
|
}
|
|
1229
1264
|
|
|
1230
|
-
config[
|
|
1265
|
+
config[activeIndex].homes.push(homeConfig);
|
|
1231
1266
|
|
|
1232
1267
|
await TIMEOUT(2000);
|
|
1233
1268
|
|
|
@@ -1253,7 +1288,7 @@ async function fetchDevices(auth, refresh, resync) {
|
|
|
1253
1288
|
for (const foundHome of me.homes) {
|
|
1254
1289
|
|
|
1255
1290
|
let homeIndex;
|
|
1256
|
-
config[
|
|
1291
|
+
config[activeIndex].homes.forEach((home, index) => {
|
|
1257
1292
|
if (home.name === foundHome.name || home.id === foundHome.id) {
|
|
1258
1293
|
homeIndex = index;
|
|
1259
1294
|
}
|
|
@@ -1400,7 +1435,7 @@ async function fetchDevices(auth, refresh, resync) {
|
|
|
1400
1435
|
}
|
|
1401
1436
|
|
|
1402
1437
|
|
|
1403
|
-
config[
|
|
1438
|
+
config[activeIndex].homes.push(homeConfig);
|
|
1404
1439
|
|
|
1405
1440
|
}
|
|
1406
1441
|
|
|
@@ -1459,12 +1494,18 @@ async function fetchDevices(auth, refresh, resync) {
|
|
|
1459
1494
|
|
|
1460
1495
|
} else {
|
|
1461
1496
|
|
|
1462
|
-
|
|
1463
|
-
|
|
1497
|
+
// When the user runs this plugin as multiple platform entries (typical
|
|
1498
|
+
// with child bridges), let them pick which one this UI session will
|
|
1499
|
+
// configure. Otherwise every "+" still goes to platforms[0] and the
|
|
1500
|
+
// other entries silently stay empty.
|
|
1501
|
+
await chooseActiveBridge();
|
|
1502
|
+
|
|
1503
|
+
if (!pluginConfig[activeIndex].homes || (pluginConfig[activeIndex].homes && !pluginConfig[activeIndex].homes.length)) {
|
|
1504
|
+
pluginConfig[activeIndex].homes = [];
|
|
1464
1505
|
return transPage(false, $('#notConfigured'));
|
|
1465
1506
|
}
|
|
1466
1507
|
|
|
1467
|
-
pluginConfig[
|
|
1508
|
+
pluginConfig[activeIndex].homes.forEach(home => {
|
|
1468
1509
|
$('#deviceSelect').append('<option value="' + home.name + '">' + home.name + ' <' + home.username + '></option>');
|
|
1469
1510
|
});
|
|
1470
1511
|
|
|
@@ -1561,7 +1602,7 @@ $('#editDevice').on('click', () => {
|
|
|
1561
1602
|
resetUI();
|
|
1562
1603
|
|
|
1563
1604
|
currentHome = $('#deviceSelect option:selected').val();
|
|
1564
|
-
let home = pluginConfig[
|
|
1605
|
+
let home = pluginConfig[activeIndex].homes.find(home => home.name === currentHome);
|
|
1565
1606
|
|
|
1566
1607
|
if (!home)
|
|
1567
1608
|
return homebridge.toast.error('Can not find selected home!', 'Error');
|
|
@@ -1578,7 +1619,7 @@ $('#refreshDevice').on('click', async () => {
|
|
|
1578
1619
|
|
|
1579
1620
|
resetSchema();
|
|
1580
1621
|
|
|
1581
|
-
let home = pluginConfig[
|
|
1622
|
+
let home = pluginConfig[activeIndex].homes.find(home => home.name === currentHome);
|
|
1582
1623
|
|
|
1583
1624
|
if (!home)
|
|
1584
1625
|
return homebridge.toast.error('Can not find home in config!', 'Error');
|
|
@@ -1613,7 +1654,7 @@ $('#removeDevice').on('click', async () => {
|
|
|
1613
1654
|
|
|
1614
1655
|
resetUI();
|
|
1615
1656
|
|
|
1616
|
-
transPage(false, pluginConfig[
|
|
1657
|
+
transPage(false, pluginConfig[activeIndex].homes.length ? $('#isConfigured') : $('#notConfigured'));
|
|
1617
1658
|
|
|
1618
1659
|
} catch (err) {
|
|
1619
1660
|
|
package/homebridge-ui/server.js
CHANGED
|
@@ -10,64 +10,77 @@ class UiServer extends HomebridgePluginUiServer {
|
|
|
10
10
|
this.onRequest('/exec', this.exec.bind(this));
|
|
11
11
|
this.onRequest('/reset', this.reset.bind(this));
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
// Track instances per username so two concurrent settings panels
|
|
14
|
+
// (or two child-bridge configurations) don't clobber each other's API.
|
|
15
|
+
this.tadoInstances = new Map();
|
|
16
|
+
this.activeUsername = undefined;
|
|
14
17
|
|
|
15
18
|
this.ready();
|
|
16
19
|
}
|
|
17
20
|
|
|
18
21
|
authenticate(config) {
|
|
19
22
|
|
|
20
|
-
|
|
21
|
-
|
|
23
|
+
const username = config?.username;
|
|
24
|
+
if (!username) throw new RequestError('Username is required for authentication.');
|
|
25
|
+
|
|
26
|
+
const instance = new TadoApi('Config UI X', {
|
|
27
|
+
username: username,
|
|
22
28
|
tadoApiUrl: config.tadoApiUrl,
|
|
23
29
|
skipAuth: config.skipAuth
|
|
24
30
|
}, this.homebridgeStoragePath, false);
|
|
25
31
|
|
|
32
|
+
this.tadoInstances.set(username, instance);
|
|
33
|
+
this.activeUsername = username;
|
|
34
|
+
|
|
26
35
|
return;
|
|
27
36
|
}
|
|
28
37
|
|
|
29
|
-
reset() {
|
|
38
|
+
reset(payload) {
|
|
30
39
|
|
|
31
|
-
|
|
40
|
+
const username = payload?.username;
|
|
41
|
+
if (username) {
|
|
42
|
+
this.tadoInstances.delete(username);
|
|
43
|
+
if (this.activeUsername === username) this.activeUsername = undefined;
|
|
44
|
+
} else {
|
|
45
|
+
this.tadoInstances.clear();
|
|
46
|
+
this.activeUsername = undefined;
|
|
47
|
+
}
|
|
32
48
|
|
|
33
49
|
return;
|
|
34
50
|
}
|
|
35
51
|
|
|
36
52
|
async exec(payload) {
|
|
37
53
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
try {
|
|
54
|
+
const username = payload?.username || this.activeUsername;
|
|
55
|
+
const tado = username ? this.tadoInstances.get(username) : undefined;
|
|
41
56
|
|
|
42
|
-
|
|
57
|
+
if (!tado) throw new RequestError('API not initialized!');
|
|
43
58
|
|
|
44
|
-
|
|
59
|
+
try {
|
|
45
60
|
|
|
46
|
-
|
|
47
|
-
if (typeof payload.data === 'object') {
|
|
48
|
-
value1 = payload.data[0];
|
|
49
|
-
value2 = payload.data[1];
|
|
50
|
-
value3 = payload.data[2];
|
|
51
|
-
} else {
|
|
52
|
-
value1 = payload.data;
|
|
53
|
-
}
|
|
54
|
-
}
|
|
61
|
+
console.log('Executing /' + payload.dest + (username ? ` for ${username}` : ''));
|
|
55
62
|
|
|
56
|
-
|
|
63
|
+
let value1, value2, value3;
|
|
57
64
|
|
|
58
|
-
|
|
65
|
+
if (payload.data) {
|
|
66
|
+
if (typeof payload.data === 'object') {
|
|
67
|
+
value1 = payload.data[0];
|
|
68
|
+
value2 = payload.data[1];
|
|
69
|
+
value3 = payload.data[2];
|
|
70
|
+
} else {
|
|
71
|
+
value1 = payload.data;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
59
74
|
|
|
60
|
-
|
|
75
|
+
const data = await tado[payload.dest](value1, value2, value3);
|
|
61
76
|
|
|
62
|
-
|
|
77
|
+
return data;
|
|
63
78
|
|
|
64
|
-
|
|
79
|
+
} catch (err) {
|
|
65
80
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
} else {
|
|
81
|
+
console.log(err);
|
|
69
82
|
|
|
70
|
-
throw new RequestError(
|
|
83
|
+
throw new RequestError(err.message);
|
|
71
84
|
|
|
72
85
|
}
|
|
73
86
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@homebridge-plugins/homebridge-tado",
|
|
3
|
-
"version": "9.
|
|
3
|
+
"version": "9.1.0",
|
|
4
4
|
"description": "Homebridge plugin for controlling tado° devices.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"fakegato-history": "^0.6.7",
|
|
38
38
|
"form-data": "^4.0.5",
|
|
39
39
|
"fs-extra": "^11.3.4",
|
|
40
|
-
"got": "^15.0.
|
|
40
|
+
"got": "^15.0.5",
|
|
41
41
|
"moment": "^2.30.1"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
package/src/tado/tado-api.js
CHANGED
|
@@ -145,7 +145,6 @@ export default class Tado {
|
|
|
145
145
|
await access(this._tadoInternalTokenFilePath);
|
|
146
146
|
const refresh_token = await this._retrieveRefreshTokenFromInternalFile();
|
|
147
147
|
return this._refreshToken(refresh_token);
|
|
148
|
-
|
|
149
148
|
} catch (_err) {
|
|
150
149
|
return this._authenticateUser();
|
|
151
150
|
}
|
|
@@ -201,7 +200,10 @@ export default class Tado {
|
|
|
201
200
|
await this._increaseCounter();
|
|
202
201
|
const { device_code, verification_uri_complete } = authResponse.body;
|
|
203
202
|
if (!device_code) throw new Error("Failed to retrieve device code.");
|
|
204
|
-
Logger.info(
|
|
203
|
+
Logger.info(
|
|
204
|
+
`Open the following URL and sign in as "${this.username}" to authorize the plugin (tip: if your browser is signed in to tado.com with a different account, use a private/incognito window). ` +
|
|
205
|
+
`URL: ${verification_uri_complete}`
|
|
206
|
+
);
|
|
205
207
|
if (this._tadoAuthenticationCallback) this._tadoAuthenticationCallback(verification_uri_complete);
|
|
206
208
|
const maxRetries = 30;
|
|
207
209
|
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
@@ -223,6 +225,7 @@ export default class Tado {
|
|
|
223
225
|
if (tokenResponse?.body) {
|
|
224
226
|
const { access_token, refresh_token } = tokenResponse.body;
|
|
225
227
|
if (access_token && refresh_token) {
|
|
228
|
+
await this._verifyAuthenticatedIdentity();
|
|
226
229
|
await writeFile(this._tadoInternalTokenFilePath, JSON.stringify({ access_token, refresh_token }));
|
|
227
230
|
this._tadoBearerToken = { access_token, refresh_token, timestamp: Date.now() };
|
|
228
231
|
Logger.info("Authentication successful!");
|
|
@@ -234,6 +237,44 @@ export default class Tado {
|
|
|
234
237
|
throw new Error(`Failed to authenticate after ${maxRetries} attempts.`);
|
|
235
238
|
}
|
|
236
239
|
|
|
240
|
+
/*
|
|
241
|
+
* Tado's device-code "Submit" page silently confirms whichever account is
|
|
242
|
+
* already signed in to tado.com, so a user trying to authenticate account B
|
|
243
|
+
* can end up granting tokens for account A without noticing. Verify the
|
|
244
|
+
* identity that actually came back matches the one we asked for, and abort
|
|
245
|
+
* if it doesn't - better a loud failure than a silent account mix-up that
|
|
246
|
+
* poisons a token file. Uses got directly because apiCall -> getToken would
|
|
247
|
+
* re-enter the in-flight token promise and deadlock.
|
|
248
|
+
*/
|
|
249
|
+
async _verifyAuthenticatedIdentity() {
|
|
250
|
+
if (!this.username) return;
|
|
251
|
+
const access_token = this._tadoBearerToken?.access_token;
|
|
252
|
+
if (!access_token) throw new Error('No access token available for identity verification.');
|
|
253
|
+
const url = `${this.tadoApiUrl}/api/v2/me`;
|
|
254
|
+
let me;
|
|
255
|
+
try {
|
|
256
|
+
const response = await got(url, {
|
|
257
|
+
method: 'GET',
|
|
258
|
+
responseType: 'json',
|
|
259
|
+
headers: { Authorization: `Bearer ${access_token}` },
|
|
260
|
+
timeout: { request: 15000 }
|
|
261
|
+
});
|
|
262
|
+
await this._increaseCounter();
|
|
263
|
+
me = response.body;
|
|
264
|
+
} catch (error) {
|
|
265
|
+
throw new Error(`Could not verify identity after authentication: ${error.message || JSON.stringify(error)}`);
|
|
266
|
+
}
|
|
267
|
+
const actual = (me?.email || me?.username || '').toLowerCase();
|
|
268
|
+
const expected = this.username.toLowerCase();
|
|
269
|
+
if (actual && actual !== expected) {
|
|
270
|
+
throw new Error(
|
|
271
|
+
`Authenticated identity "${actual}" does not match the configured username "${this.username}". ` +
|
|
272
|
+
`This usually means tado.com was logged in as a different account when you clicked "Submit". ` +
|
|
273
|
+
`Sign out of tado.com (or use a private/incognito window) and try again.`
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
237
278
|
async _retrieveTokenFromExternalFile() {
|
|
238
279
|
const maxRetries = 3;
|
|
239
280
|
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|