Last active
January 14, 2026 15:44
-
-
Save SeidChr/53354482b03748cc8b1ec091247182dc to your computer and use it in GitHub Desktop.
Synchronizing the target temperature of multiple thermostats in and advanced flow using HomeyScript
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ################ | |
| // ### README | |
| // ####### | |
| // Purpose of this script, is to automatically | |
| // synchronize updates of the target temperature | |
| // of multiple specific thermostat devices which are listed | |
| // in an advanced flow as triggers. | |
| // | |
| // When the script runs, it looks for all thermostat | |
| // target_temperature_changed triggers in the flow that | |
| // is passed as an argument, finds the newest temperature | |
| // update between them, and synchronizes the temperature | |
| // to the other devices. All this while allowing for multiple | |
| // updates coming in after each other (When the device sent | |
| // an update while you are still changing the value) and | |
| // avoiding the sync itself causing an additional sync | |
| // process in an infinite loop. | |
| // | |
| // You can use this script by saving it to your | |
| // HomeyScripts as "SyncThermostats" or similar and | |
| // using an "Execute Script with Argument" block | |
| // from your "HomeyScripts" app. | |
| // | |
| // Please read, understand and potentially | |
| // update the sections "ARGS" and "CONFIG" | |
| // ################ | |
| // ### ARGS | |
| // ####### | |
| // this script expects the id of the advanced flow as an argument | |
| // you can copy it from the url of your webbrowser (last url element) | |
| // while editing your advanced flow the const "TESTING_FLOW_ID" will | |
| // be used when no argument is passed: | |
| const TESTING_FLOW_ID = "63fadb39-9493-491b-b9ff-e36c7c5b68b2" | |
| // ################ | |
| // ### CONFIG | |
| // ####### | |
| // time to wait for further updates to come in before starting the sync | |
| const DEBOUNCE_PERIOD_MS = 3000 | |
| // time to wait in between device updates, to not spam the bus | |
| const SYNC_GAP_PERIOD_MS = 100 | |
| // time to wait for all updates to apply after sync, before unlocking the sync process | |
| // this delay avoids that the sync itself triggers another sync | |
| const POST_SYNC_PERIOD_MS = 1000 | |
| // ################ | |
| // ### INIT | |
| // ####### | |
| // override the flow id for testing | |
| if (!args[0]) | |
| { | |
| args[0] = TESTING_FLOW_ID | |
| } | |
| const flowId = args[0] | |
| const SYNC_LOCK_VAR_NAME = "HomeyScript:SyncThermostats:sync_lock" | |
| const GLOBAL_TARGET_TEMP_KEY = `flow:${flowId}:newest_target_temperature` | |
| // ################ | |
| // ### SKIP THIS SCRIPT IF MORE RECENT UPDATES HAPPENED | |
| // ### (DEBOUNCE) | |
| // ####### | |
| const timestamp = (new Date()).toISOString(); | |
| global.set(GLOBAL_TARGET_TEMP_KEY, timestamp) | |
| // debounce period | |
| // wait for more updates and abort this process if more are received | |
| // this means process which last sets the target temp wins | |
| log("Debounce wait START") | |
| await wait(DEBOUNCE_PERIOD_MS); | |
| log("Debounce wait FINISH") | |
| const globalTimestamp = global.get(GLOBAL_TARGET_TEMP_KEY) | |
| if (globalTimestamp != timestamp) | |
| { | |
| // there was an update. meaning another update happened on the device. | |
| // we wont continue with this update, as the value would be old while updates may be received our of order | |
| log("There is a newer sync process running. Stopping this sync.") | |
| log(`MY TIME: ${timestamp} | ITS TIME: ${globalTimestamp}`) | |
| return | |
| } | |
| // ################ | |
| // ### LOCKING SYNC PROCESS | |
| // ### (AVOID TRIGGERING ANOTHER SYNC BY UPDATING THE TARGET TEMP) | |
| // ####### | |
| var lockVar = global.get(SYNC_LOCK_VAR_NAME) | |
| if (lockVar === true) | |
| { | |
| console.log("Another sync is running. Aborting to prevent race condition."); | |
| return; | |
| } | |
| try | |
| { | |
| console.log("Locking sync process."); | |
| global.set(SYNC_LOCK_VAR_NAME, true) | |
| // ################ | |
| // ### FIND THERMOSTATS | |
| // ####### | |
| const flow = await Homey.flow.getAdvancedFlow({ id: flowId }) | |
| log(`Syncing flow triggers for "${flow.name}"`) | |
| const promisedTargetTempTriggerDevices = Object | |
| .values(flow.cards) | |
| .filter(card => card.type == 'trigger' | |
| && card.id.endsWith(":target_temperature_changed")) | |
| .map(card => card.ownerUri.split(':').at(-1)) | |
| .map(deviceId => Homey.devices.getDevice({ id: deviceId })) | |
| const thermostats = await Promise.all(promisedTargetTempTriggerDevices) | |
| log("--- Thermostats ---") | |
| thermostats.forEach(t => log(t.name)) | |
| log("------") | |
| // ################ | |
| // ### FIND LAST UPDATED TARGET TEMPERATURE | |
| // ####### | |
| const newestDeviceTempAggregate = thermostats | |
| .map(device => ({ | |
| id: device.id, | |
| targetTemperature: device.capabilitiesObj.target_temperature | |
| })) | |
| .reduce( | |
| (latest, current) => new Date(current.targetTemperature.lastUpdated) > new Date(latest.targetTemperature.lastUpdated) | |
| ? current | |
| : latest, | |
| { | |
| id: thermostats[0].id, | |
| targetTemperature: thermostats[0].capabilitiesObj.target_temperature | |
| } | |
| ); | |
| const targetTemp = newestDeviceTempAggregate.targetTemperature.value | |
| log(`Newest Thermostat Target Temperature: ${targetTemp}°C`) | |
| // ################ | |
| // ### UPDATING ALL TERMOSTATS WHICH REQUIRE AN UPDATE | |
| // ####### | |
| for (const device of thermostats) { | |
| if (device.id !== newestDeviceTempAggregate.id | |
| && device.capabilitiesObj.target_temperature.value !== targetTemp) | |
| { | |
| log(`Synchronizing ${device.name} ...`); | |
| await device.setCapabilityValue('target_temperature', targetTemp); | |
| // wait a bit after each device to let the network network | |
| await wait(SYNC_GAP_PERIOD_MS); | |
| } | |
| else | |
| { | |
| log(`No need to update ${device.name}`) | |
| } | |
| } | |
| // Artificial delay to let the Zigbee/Z-Wave mesh breathe | |
| // and not get change-events trickle in after updating the devices | |
| await wait(POST_SYNC_PERIOD_MS); | |
| } | |
| catch (err) | |
| { | |
| console.error("Error during sync:", err); | |
| } | |
| finally | |
| { | |
| // ################ | |
| // ### UNLOCKING | |
| // ####### | |
| global.set(SYNC_LOCK_VAR_NAME, false) | |
| console.log("Lock released."); | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment