Skip to content

Instantly share code, notes, and snippets.

@SeidChr
Last active January 14, 2026 15:44
Show Gist options
  • Select an option

  • Save SeidChr/53354482b03748cc8b1ec091247182dc to your computer and use it in GitHub Desktop.

Select an option

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
// ################
// ### 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