Skip to content

Commit 6b773de

Browse files
committed
[feature] advanced queue handling for update and persistence tasks; move counter to tado-api
1 parent a0e62c6 commit 6b773de

5 files changed

Lines changed: 153 additions & 100 deletions

File tree

CHANGELOG.md

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11
# Changelog
22

3-
## v8.6.0 - 2025-10-30
4-
- BREAKING CHANGE: If you use tadoApiUrl or skipAuth, you must now define them under each corresponding home in your configuration (#176)
5-
- Refactor configuration: moved tadoApiUrl and skipAuth into individual home configs to support multiple API URLs (#176)
6-
- Persist tado zone states after every zone states update
3+
## v8.6.0 - 2025-10-31
4+
- BREAKING CHANGE: If you use tadoApiUrl or skipAuth, they must now be defined under each corresponding home in your configuration (#176)
5+
- Refactor configuration: Moved tadoApiUrl and skipAuth into individual home configs to support multiple API URLs (#176)
6+
- Persist Tado zone states after zone state updates
77
- Improved zone update logic: when setting a state, all zones are now updated immediately if the next scheduled update is more than 10 seconds away
8+
- Advanced queue handling for update and persistence tasks
9+
- Tado API counter now tracks and persists for each authenticated user
10+
- Fix: Polling and tasks for multiple homes (#178)
811
- Fix: Corrected zone update handling that could previously cause unintended heating changes (#178)
12+
- Added additional debug log messages for zone updates
13+
- Note: This update will reset your tado api counter for the current day to zero.
914

1015
## v8.5.0 - 2025-10-27
1116
- Change minimum polling interval to 30s due to improvements made in v8.2.0

homebridge-ui/server.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ class UiServer extends HomebridgePluginUiServer {
2121
username: config.username,
2222
tadoApiUrl: config.tadoApiUrl,
2323
skipAuth: config.skipAuth
24-
}, this.homebridgeStoragePath);
24+
}, this.homebridgeStoragePath, false);
2525

2626
return;
2727
}

src/helper/handler.js

Lines changed: 84 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,35 @@
11
import Logger from '../helper/logger.js';
22
import moment from 'moment';
3-
import { writeFile, access, readFile } from 'fs/promises';
3+
import { writeFile } from 'fs/promises';
44
import { join } from "path";
55

6-
let settingState = false;
7-
let tasksInitialized = false;
8-
let lastGetStates = 0;
9-
const delayTimer = {};
10-
116
const timeout = (ms) => new Promise((res) => setTimeout(res, ms));
12-
const aRefreshHistoryHandlers = [];
13-
14-
export async function getPersistedStates(storagePath) {
15-
try {
16-
const sFilePath = join(storagePath, "tado-states.json");
17-
await access(sFilePath);
18-
const sData = (await readFile(sFilePath, "utf-8"));
19-
if (sData) return JSON.parse(sData);
20-
} catch (error) {
21-
//no states data => ignore
22-
Logger.debug(`Failed to read tado states file: ${error.message || error}`);
23-
}
24-
}
7+
const helpers = {};
258

269
export default (api, accessories, config, tado, telegram) => {
27-
const statesIntervalTime = Math.max(config.polling, 30) * 1000;
28-
const storagePath = api.user.storagePath();
10+
//init helper variables for current home scope
11+
if (!helpers[config.homeId]) {
12+
helpers[config.homeId] = {
13+
settingState: false,
14+
tasksInitialized: false,
15+
lastGetStates: 0,
16+
lastPersistZoneStates: 0,
17+
persistPromise: Promise.resolve(),
18+
updateZonesRunning: false,
19+
updateZonesNextQueued: false,
20+
delayTimer: {},
21+
refreshHistoryHandlers: [],
22+
statesIntervalTime: Math.max(config.polling, 30) * 1000,
23+
storagePath: api.user.storagePath(),
24+
}
25+
}
2926

3027
async function setStates(accessory, accs, target, value) {
3128
let zoneUpdated = false;
32-
3329
accessories = accs.filter((acc) => acc && acc.context.config.homeName === config.homeName);
34-
3530
try {
36-
settingState = true;
37-
31+
helpers[config.homeId].settingState = true;
3832
value = typeof value === 'number' ? parseFloat(value.toFixed(2)) : value;
39-
4033
Logger.info(target + ': ' + value, accessory.displayName);
4134

4235
switch (accessory.context.config.subtype) {
@@ -63,10 +56,10 @@ export default (api, accessories, config, tado, telegram) => {
6356
value < 5
6457
) {
6558
if (value === 0) {
66-
if (delayTimer[accessory.displayName]) {
59+
if (helpers[config.homeId].delayTimer[accessory.displayName]) {
6760
Logger.info('Resetting delay timer', accessory.displayName);
68-
clearTimeout(delayTimer[accessory.displayName]);
69-
delayTimer[accessory.displayName] = null;
61+
clearTimeout(helpers[config.homeId].delayTimer[accessory.displayName]);
62+
helpers[config.homeId].delayTimer[accessory.displayName] = null;
7063
}
7164

7265
power = 'OFF';
@@ -111,14 +104,14 @@ export default (api, accessories, config, tado, telegram) => {
111104
let timer = accessory.context.delayTimer;
112105
let tarState = value === 1 ? 'HEAT' : 'AUTO';
113106

114-
if (delayTimer[accessory.displayName]) {
115-
clearTimeout(delayTimer[accessory.displayName]);
116-
delayTimer[accessory.displayName] = null;
107+
if (helpers[config.homeId].delayTimer[accessory.displayName]) {
108+
clearTimeout(helpers[config.homeId].delayTimer[accessory.displayName]);
109+
helpers[config.homeId].delayTimer[accessory.displayName] = null;
117110
}
118111

119112
Logger.info('Wait ' + timer + ' seconds before switching state', accessory.displayName);
120113

121-
delayTimer[accessory.displayName] = setTimeout(async () => {
114+
helpers[config.homeId].delayTimer[accessory.displayName] = setTimeout(async () => {
122115
Logger.info('Delay timer finished, switching state to ' + tarState, accessory.displayName);
123116

124117
//targetState
@@ -172,7 +165,7 @@ export default (api, accessories, config, tado, telegram) => {
172165
);
173166
}
174167

175-
delayTimer[accessory.displayName] = null;
168+
helpers[config.homeId].delayTimer[accessory.displayName] = null;
176169
}, timer * 1000);
177170
}
178171
} else {
@@ -567,11 +560,11 @@ export default (api, accessories, config, tado, telegram) => {
567560
} catch (err) {
568561
errorHandler(err);
569562
} finally {
570-
settingState = false;
563+
helpers[config.homeId].settingState = false;
571564
//update zones to ensure correct state in Apple Home
572-
const timeSinceLastGetStates = Date.now() - lastGetStates;
573-
const statesIntervalTimeLeft = statesIntervalTime - timeSinceLastGetStates;
574-
if (zoneUpdated && statesIntervalTimeLeft > 10_000) await updateZones();
565+
const timeSinceLastGetStates = helpers[config.homeId].lastGetStates === 0 ? 0 : (Date.now() - helpers[config.homeId].lastGetStates);
566+
const statesIntervalTimeLeft = helpers[config.homeId].statesIntervalTime - timeSinceLastGetStates;
567+
if (zoneUpdated && statesIntervalTimeLeft > (10 * 1000)) await updateZones();
575568
}
576569
}
577570

@@ -729,61 +722,49 @@ export default (api, accessories, config, tado, telegram) => {
729722
}
730723
}
731724

732-
async function persistZoneStates(homeId, zoneStates) {
725+
function persistZoneStates(homeId, zoneStates) {
726+
helpers[config.homeId].persistPromise = helpers[config.homeId].persistPromise.then(() => _persistZoneStates(homeId, zoneStates));
727+
}
728+
729+
async function _persistZoneStates(homeId, zoneStates) {
730+
if ((Date.now() - helpers[config.homeId].lastPersistZoneStates) < (10 * 1000)) return;
733731
try {
734732
if (zoneStates && Object.keys(zoneStates).length) {
735733
const homeData = {};
736734
homeData.zoneStates = zoneStates;
737-
await writeFile(join(storagePath, `tado-states-${homeId}.json`), JSON.stringify(homeData, null, 2), "utf-8");
735+
await writeFile(join(helpers[config.homeId].storagePath, `tado-states-${homeId}.json`), JSON.stringify(homeData, null, 2), "utf-8");
736+
helpers[config.homeId].lastPersistZoneStates = Date.now();
738737
} else {
739-
Logger.info(`Skipping persistence of tado states for home ${homeId}: zone states are empty.`);
738+
Logger.warn(`Skipping persistence of tado states file for home ${homeId}: zone states are empty.`);
740739
}
741740
} catch (error) {
742741
Logger.error(`Error while updating the tado states file for home ${homeId}: ${error.message || error}`);
743742
}
744743
}
745744

746-
async function persistStates() {
747-
try {
748-
const data = {};
749-
data.counterData = await tado.getCounterData();
750-
await writeFile(join(storagePath, "tado-states.json"), JSON.stringify(data, null, 2), "utf-8");
751-
} catch (error) {
752-
Logger.error(`Error while updating the tado states file: ${error.message || error}`);
753-
}
745+
async function refreshHistoryServices() {
746+
if (!helpers[config.homeId].refreshHistoryHandlers.length) return;
754747
try {
755748
//wait for fakegato history services to be loaded
756749
await timeout(4000);
757-
for (const refreshHistory of aRefreshHistoryHandlers) {
750+
for (const refreshHistory of helpers[config.homeId].refreshHistoryHandlers) {
758751
refreshHistory();
759752
}
760753
} catch (error) {
761-
Logger.error(`Error while refreshing history: ${error.message || error}`);
762-
}
763-
}
764-
765-
async function logCounter() {
766-
try {
767-
const iCounter = (await tado.getCounterData()).counter;
768-
Logger.info(`Tado API counter: ${iCounter.toLocaleString('en-US')}`);
769-
} catch (error) {
770-
Logger.warn(`Failed to get Tado API counter: ${error.message || error}`);
754+
Logger.error(`Error while refreshing history services: ${error.message || error}`);
771755
}
772756
}
773757

774758
function initTasks() {
775-
if (tasksInitialized) return;
776-
tasksInitialized = true;
759+
if (helpers[config.homeId].tasksInitialized) return;
760+
helpers[config.homeId].tasksInitialized = true;
777761

778762
void getStates();
779-
setInterval(() => getStates(), statesIntervalTime);
780-
781-
void logCounter();
782-
setInterval(() => logCounter(), 60 * 60 * 1000);
763+
setInterval(() => void getStates(), helpers[config.homeId].statesIntervalTime);
783764
}
784765

785766
async function getStates() {
786-
lastGetStates = Date.now();
767+
helpers[config.homeId].lastGetStates = Date.now();
787768
try {
788769
//ME
789770
if (!config.homeId) await updateMe();
@@ -811,12 +792,12 @@ export default (api, accessories, config, tado, telegram) => {
811792
} catch (err) {
812793
errorHandler(err);
813794
} finally {
814-
void persistStates();
795+
void refreshHistoryServices();
815796
}
816797
}
817798

818799
async function updateMe() {
819-
if (settingState) return;
800+
if (helpers[config.homeId].settingState) return;
820801

821802
Logger.debug('Polling User Info...', config.homeName);
822803

@@ -828,7 +809,7 @@ export default (api, accessories, config, tado, telegram) => {
828809
}
829810

830811
async function updateHome() {
831-
if (settingState) return;
812+
if (helpers[config.homeId].settingState) return;
832813

833814
Logger.debug('Polling Home Info...', config.homeName);
834815

@@ -853,7 +834,32 @@ export default (api, accessories, config, tado, telegram) => {
853834
}
854835

855836
async function updateZones() {
856-
if (settingState) return;
837+
if (helpers[config.homeId].updateZonesRunning) {
838+
helpers[config.homeId].updateZonesNextQueued = true;
839+
return;
840+
}
841+
helpers[config.homeId].updateZonesRunning = true;
842+
try {
843+
while (true) {
844+
try {
845+
await _updateZones();
846+
} catch (error) {
847+
Logger.error(`Failed to update zones: ${error.message || error}`);
848+
}
849+
if (helpers[config.homeId].updateZonesNextQueued) {
850+
helpers[config.homeId].updateZonesNextQueued = false;
851+
//continue with loop
852+
} else {
853+
break;
854+
}
855+
}
856+
} finally {
857+
helpers[config.homeId].updateZonesRunning = false;
858+
}
859+
}
860+
861+
async function _updateZones() {
862+
if (helpers[config.homeId].settingState) return;
857863

858864
Logger.debug('Polling Zones...', config.homeName);
859865

@@ -1316,7 +1322,7 @@ export default (api, accessories, config, tado, telegram) => {
13161322
}
13171323

13181324
async function updateMobileDevices() {
1319-
if (settingState) return;
1325+
if (helpers[config.homeId].settingState) return;
13201326

13211327
Logger.debug('Polling MobileDevices...', config.homeName);
13221328

@@ -1370,7 +1376,7 @@ export default (api, accessories, config, tado, telegram) => {
13701376
}
13711377

13721378
async function updateWeather() {
1373-
if (settingState) return;
1379+
if (helpers[config.homeId].settingState) return;
13741380

13751381
const weatherTemperatureAccessory = accessories.filter(
13761382
(acc) => acc && acc.displayName === acc.context.config.homeName + ' Weather'
@@ -1425,7 +1431,7 @@ export default (api, accessories, config, tado, telegram) => {
14251431
}
14261432

14271433
async function updatePresence() {
1428-
if (settingState) return;
1434+
if (helpers[config.homeId].settingState) return;
14291435

14301436
const presenceLockAccessory = accessories.filter(
14311437
(acc) => acc && acc.displayName === acc.context.config.homeName + ' Presence Lock'
@@ -1470,7 +1476,7 @@ export default (api, accessories, config, tado, telegram) => {
14701476
}
14711477

14721478
async function updateRunningTime() {
1473-
if (settingState) return;
1479+
if (helpers[config.homeId].settingState) return;
14741480

14751481
const centralSwitchAccessory = accessories.filter(
14761482
(acc) => acc && acc.displayName === acc.context.config.homeName + ' Central Switch'
@@ -1515,7 +1521,7 @@ export default (api, accessories, config, tado, telegram) => {
15151521
}
15161522

15171523
async function updateDevices() {
1518-
if (settingState) return;
1524+
if (helpers[config.homeId].settingState) return;
15191525

15201526
Logger.debug('Polling Devices...', config.homeName);
15211527

@@ -1586,6 +1592,6 @@ export default (api, accessories, config, tado, telegram) => {
15861592
getStates: getStates,
15871593
setStates: setStates,
15881594
changedStates: changedStates,
1589-
refreshHistoryHandlers: aRefreshHistoryHandlers
1595+
refreshHistoryHandlers: helpers[config.homeId].refreshHistoryHandlers
15901596
};
1591-
};
1597+
};

0 commit comments

Comments
 (0)