Developed with Plugwise Emma in mind. Source climates expose
current_temperatureandtemperature; destinations may expose either a singletemperaturetarget or atarget_temp_low/target_temp_highrange.
This release adds a downloadable Home Assistant diagnostics file. It captures the configured source and destination roles, current source health and deltas, demand state, setpoint calculation, timestamps, counters, and the latest error. It contains no credentials and does not change ClimateSync's control behaviour.
This release adds two first-class heating-demand binary sensors. Heating Demand Active follows all configured sources, while Primary Heating Demand Active follows only the user-selected primary subset. Both use the exact same configured activation and deactivation thresholds; the primary scope does not change destination control. The setup and options flows are now a documented four-step wizard with Next buttons between steps and Submit only on the final step.
This release retains the clearer source-health diagnostics from 1.2.4 while restoring the active idle fallback when no usable source remains. That fail-safe withdraws a potentially stale heating request instead of leaving the last destination target untouched.
This release makes partial source failures explicit and safe. A source climate
that is off but still exposes a valid current temperature is treated as a
usable inactive room with zero heating demand. Unavailable or incomplete
sources are reported as degraded_source_data, while healthy rooms continue to
drive the destination. ClimateSync suppresses destination writes only when no
usable source remains.
This release adds configurable heating-demand hysteresis. Small positive room
deltas no longer start the destination thermostat immediately; by default,
demand starts at 0.3 °C and stops at 0.1 °C. The existing destination-setpoint
anti-flap and rounding behaviour remain unchanged.
Heating range destinations are supported by controlling target_temp_low while
preserving their current target_temp_high atomically.
ClimateSync is a HACS-ready Home Assistant custom integration that implements delta-based thermostat synchronisation. It reads the heating demand (delta between target and current temperature) from multiple source climate entities (rooms) and drives a single destination thermostat by continuously adjusting its target temperature.
Instead of simply syncing the highest source setpoint to the destination, ClimateSync uses the maximum delta (largest gap between target and current temperature across all rooms) and adds it to the destination's own current temperature:
destination_target = destination_current_temperature + delta_max
This approach was designed with the Plugwise Emma in mind. The Emma is connected to the central-heating boiler (CV) over OpenTherm and internally calculates a desired CV water temperature based on its own current delta. By feeding the Emma the home-wide maximum delta on top of its own measured temperature, ClimateSync lets the Emma function as a full OpenTherm controller — it sees the "hardest working" room's demand and adjusts the boiler modulation accordingly.
Even without an Emma, this delta-based method is more accurate than copying setpoints because it accounts for how far the destination already is from equilibrium.
- Event-driven updates — reacts immediately to temperature changes.
- Periodic resync (configurable, default 60 s) to recover from missed events.
- Anti-flap: only sends
climate.set_temperaturewhen the change exceeds a configurable threshold (default 0.2 °C). - Configurable demand hysteresis: start heating at 0.3 °C room delta and stop at 0.1 °C by default.
- All-source and primary-source heating-demand binary sensors, both driven by the configured hysteresis.
- Destination target selection: ordinary
temperature(default) or heating-rangetarget_temp_low. - Rate limiting: maximum one service call per 10 seconds (configurable).
- Rich diagnostic sensors plus a downloadable diagnostics file for support and troubleshooting.
- No controllable entities — all control is internal via
climate.set_temperature.
- Open HACS → Integrations → ⋮ → Custom repositories.
- Add
https://github.com/Patrick1610/ClimateSyncas an Integration. - Search for ClimateSync and install.
- Restart Home Assistant.
- Copy
custom_components/climatesync/to your<config>/custom_components/directory. - Restart Home Assistant.
Navigate to Settings → Devices & Services → Add Integration → ClimateSync.
Select one or more climate entities that represent the rooms whose heating demand should be tracked. Each selected entity must expose current_temperature and temperature attributes.
Select the subset of source rooms that may enable other rooms to piggyback on
an existing heating run. General destination control still uses all sources.
A secondary room can therefore start heating when it falls below its own target,
but it does not turn on Primary Heating Demand Active.
Existing installations migrate with every existing source selected as primary, so the upgrade does not silently narrow their demand scope.
| Field | Default | Description |
|---|---|---|
| Destination climate entity | — | The thermostat that ClimateSync will control. |
| Destination target | Target temperature | Select ordinary temperature or lower target target_temp_low for a heating range climate. |
| Idle temperature | 5.0 °C | Target temperature sent to the destination when no room has a positive delta (all rooms are at or above their target). |
| Maximum setpoint | 35.0 °C | Hard ceiling for the destination setpoint. |
| Rounding mode | 1 decimal | Step size used for the computed setpoint. |
| Rounding direction | nearest | Whether the computed setpoint is rounded down, normally, or up within the selected rounding mode. |
| Option | Default | Description |
|---|---|---|
| Resync interval | 60 s | How often ClimateSync checks even without state changes. |
| Minimum destination change | 0.2 °C | Only send a new destination setpoint when the change exceeds this. It does not determine demand state. |
| Heating-demand activation threshold | 0.3 °C | Delta required to switch either demand scope on. |
| Heating-demand deactivation threshold | 0.1 °C | Delta at or below which an active demand scope switches off. Must be lower than activation. |
| Minimum send interval | 10 s | At most one destination service call per this many seconds. |
After setup, open the integration → Configure (⚙ gear icon) to get the same four-step wizard again. Intermediate steps show Next; only the final step shows Submit. You can change:
- Step 1: add or remove source rooms;
- Step 2: choose the primary subset;
- Step 3: change destination and setpoint behaviour;
- Step 4: change hysteresis, resync, anti-flap, and rate limiting.
| Option | Default | Description |
|---|---|---|
| Destination thermostat | — | Change which thermostat is controlled. |
| Destination target | Target temperature | Select ordinary temperature or lower target target_temp_low for a heating range climate. |
| Idle temperature | 5.0 °C | Temperature sent when no room needs heating. |
| Maximum setpoint | 35.0 °C | Hard ceiling for the destination setpoint. |
| Rounding mode | 1 decimal | Step size used for setpoints. |
| Rounding direction | nearest | floor, nearest, or ceiling rounding within the selected mode. |
| Resync interval | 60 s | How often ClimateSync checks even without state changes. |
| Minimum change threshold | 0.2 °C | Only send a new setpoint if the change exceeds this. |
| Heating-demand activation threshold | 0.3 °C | Minimum room delta required to activate the destination thermostat. |
| Heating-demand deactivation threshold | 0.1 °C | Delta at or below which active heating demand stops. Must be lower than the activation threshold. |
| Minimum send interval | 10 s | At most one service call per this many seconds. |
For each source climate entity (room):
current = current_temperature attribute
target = temperature attribute
if state == off and current is valid:
source is usable but inactive; delta = 0
elif current or target is missing/unavailable:
source is degraded; delta = 0
else:
source is active; delta = max(target - current, 0)
If no usable source remains:
apply idle_temperature to withdraw prior heating demand
delta_max = max(all room deltas)
If demand is inactive and delta_max >= demand_activation_threshold:
demand becomes active
If demand is active and delta_max <= demand_deactivation_threshold:
demand becomes inactive
primary_delta_max = max(deltas of configured primary sources)
Apply the same activation/deactivation hysteresis independently to
primary_delta_max to calculate primary_demand_active
If demand is inactive:
setpoint_raw = idle_temperature # No room needs heating
Else:
setpoint_raw = destination_current_temperature + delta_max
# The destination target is set to its own current temperature
# plus the largest demand across all source rooms. This means the
# destination "feels" the same heating gap as the hardest-working
# room, which is critical for OpenTherm controllers like the
# Plugwise Emma that modulate boiler output based on their own
# observed delta.
rounded_setpoint = round_setpoint(setpoint_raw, rounding_mode, rounding_direction)
setpoint_final = min(rounded_setpoint, maximum_setpoint)
If abs(destination_current_target - setpoint_final) > min_change_threshold:
If time_since_last_call >= min_send_interval:
If destination_target == temperature:
climate.set_temperature(destination, temperature=setpoint_final)
Else:
climate.set_temperature(
destination,
target_temp_low=min(setpoint_final, current_target_temp_high),
target_temp_high=current_target_temp_high,
)
Home Assistant requires both range bounds in the same set_temperature action.
ClimateSync therefore reads the upper target again immediately before applying a
lower target, preserves it in the same action, and never controls cooling through
target_temp_high.
The rounding mode determines the step size. The rounding direction determines where the raw setpoint lands within that step size. ClimateSync applies epsilon-safe floor/nearest/ceiling rounding to avoid floating-point edge cases around step boundaries.
nearest is the default and preserves the historical ClimateSync behaviour for existing installations. If an older config entry does not contain rounding_direction, ClimateSync treats it as nearest.
| Mode | Example input | Result |
|---|---|---|
0.5 steps |
19.3 | 19.5 |
1 decimal |
19.33 | 19.3 |
2 decimals |
19.333 | 19.33 |
| Direction | Behaviour |
|---|---|
floor |
Always round down within the selected mode. |
nearest |
Round to the nearest step, matching the old behaviour. |
ceiling |
Always round up within the selected mode. |
Plugwise Emma examples:
| Mode + direction | Raw setpoint | Sent setpoint |
|---|---|---|
0.5 steps + ceiling |
19.1 | 19.5 |
0.5 steps + ceiling |
19.5 | 19.5 |
0.5 steps + ceiling |
19.6 | 20.0 |
0.5 steps + floor |
19.1 | 19.0 |
0.5 steps + floor |
19.9 | 19.5 |
1 decimal + ceiling |
19.11 | 19.2 |
1 decimal + floor |
19.19 | 19.1 |
For Plugwise Emma, 0.5 steps can be useful because Emma commonly accepts half-degree setpoints. Combining 0.5 steps with ceiling can help when small RoomMind deltas would otherwise disappear through normal rounding, for example a raw setpoint of 19.1 becoming 19.5 instead of 19.0.
All entities are attached to a ClimateSync device. Sensors (setpoint, deltas, destination target) are regular entities; the status sensor is classified as diagnostic. Home Assistant can also download a diagnostics file from the integration or device page.
Logical regulation demand across all configured sources. It switches on at the configured activation threshold and remains on until the maximum delta reaches the configured deactivation threshold. It means ClimateSync has a qualified heating request; it does not prove that the boiler is physically firing or that the destination accepted the last write.
The same independently retained hysteresis state, calculated only over the configured primary subset. Use this entity to enable secondary rooms to piggyback without allowing those secondary rooms to trigger one another. It is unavailable when none of the configured primary sources has usable data.
Both entities expose their scope, current maximum delta, leading source, thresholds, source list, and source-health diagnostics as attributes.
| Attribute | Description |
|---|---|
destination_entity_id |
The controlled thermostat |
destination_target |
Selected destination target: temperature or target_temp_low |
destination_current_temperature |
Current measured temperature at destination |
destination_current_target |
Current target temperature at destination |
delta_max |
Max delta used for this computation |
demand_active |
Whether the hysteresis currently considers heating demand active |
demand_activation_threshold |
Configured room delta required to start demand |
demand_deactivation_threshold |
Configured room delta at or below which demand stops |
rounding_mode |
Active rounding mode |
rounding_direction |
Active rounding direction |
raw_setpoint |
Unrounded destination_current_temperature + delta_max, or idle temperature when no room needs heating |
rounded_setpoint |
Raw setpoint after rounding mode and rounding direction |
final_setpoint |
Final setpoint after rounding and maximum-setpoint clamp |
idle_temperature |
Configured idle temperature |
State: the final setpoint that ClimateSync wants to apply (destination_current_temperature + delta_max, rounded and capped by maximum setpoint).
| Attribute | Description |
|---|---|
room_deltas |
Map of {entity_id: delta} for all rooms |
leading_room |
Entity id of the room with the highest delta |
usable_source_count |
Number of sources that can safely participate |
degraded_source_entities |
Unavailable or incomplete sources ignored for demand |
inactive_source_entities |
off sources with a valid measurement and zero demand |
State: the maximum delta across all rooms.
One sensor per source climate entity.
| Attribute | Description |
|---|---|
source_entity_id |
The climate entity this sensor tracks |
current_temperature |
Last known current temperature |
target_temperature |
Last known target temperature |
raw_delta |
target - current (may be negative) |
source_status |
active, inactive_off, unavailable, or missing_temperature |
State: max(raw_delta, 0) — the effective heating demand for this room.
Shows the destination thermostat's actual current target temperature in real time, making it easy to compare against the computed setpoint without switching to the destination device.
| Attribute | Description |
|---|---|
destination_entity_id |
The controlled thermostat |
destination_current_temperature |
Current measured temperature at destination |
State: the destination's current temperature attribute (its active target).
States:
| State | Meaning |
|---|---|
ok |
Everything is in sync, no issues. |
rate_limited |
A setpoint update was suppressed because the last call was too recent. |
destination_unavailable |
The destination climate entity is unavailable or unknown. |
degraded_source_data |
One or more sources are unavailable or incomplete, but at least one usable source remains. Valid rooms continue to control the destination. |
missing_source_data |
No usable source remains. ClimateSync applies the configured idle fallback until a source recovers. |
apply_failed |
The climate.set_temperature service call threw an exception. Check last_error. |
mismatch |
The destination's actual target deviates from the desired setpoint beyond the threshold. ClimateSync will attempt to correct this on the next cycle. |
Attributes:
| Attribute | Description |
|---|---|
last_update_time |
ISO timestamp of the last evaluation |
last_service_call_time |
ISO timestamp of the last climate.set_temperature call |
last_desired_setpoint |
What ClimateSync computed as the ideal setpoint |
last_applied_setpoint |
What was last actually sent to the destination |
current_destination_target |
The destination's actual temperature attribute right now |
rounding_mode |
Active rounding mode |
rounding_direction |
Active rounding direction |
raw_setpoint |
Last unrounded setpoint |
rounded_setpoint |
Last rounded setpoint before maximum-setpoint clamp |
final_setpoint |
Last final setpoint after rounding and clamp |
mismatch_seconds |
How long (seconds) the desired and actual setpoint have been diverging |
mismatch_since |
ISO timestamp of when the mismatch started (null when in sync) |
resync_count |
Number of periodic resyncs since startup |
apply_attempts |
Total service call attempts since startup |
apply_failures |
Total service call failures since startup |
evaluation_count |
Total evaluation cycles since startup |
skipped_anti_flap |
Times a setpoint update was skipped because the change was within the threshold |
skipped_rate_limit |
Times a setpoint update was skipped due to rate limiting |
last_error |
Last exception message, if any |
source_count |
Number of configured source climates |
usable_source_count |
Number of sources currently safe to use |
degraded_source_entities |
Sources currently ignored because data is unavailable or incomplete |
inactive_source_entities |
off sources that remain valid measurements with zero demand |
Some thermostats (e.g. Plugwise Emma) only accept specific temperature steps. Use the 0.5 steps rounding mode in that case. If small RoomMind deltas are rounded away, use Rounding direction = ceiling so a raw setpoint such as 19.1 is sent as 19.5 instead of 19.0.
An external automation or the user may be overriding the destination's target. ClimateSync will keep trying to reapply on every evaluation cycle and resync. Check if another integration or automation is fighting over the thermostat.
The source rooms are changing temperature very rapidly. Increase min_send_interval in the Options Flow to reduce chatter.
No selected source currently provides usable data. ClimateSync actively applies
the configured idle temperature so an earlier heating request cannot remain in
effect. Restore at least one source with a valid current_temperature and
temperature, or an off source with a valid current_temperature.
At least one source is unavailable or lacks a required temperature attribute.
Other usable rooms continue to be processed. Inspect degraded_source_entities
and each delta sensor's source_status attribute.
The destination thermostat is offline. No service calls are made. ClimateSync will recover automatically once the entity becomes available again.
Check last_error in the sensor.climatesync_status attributes. Most likely the climate entity does not support the climate.set_temperature service or the entity id is wrong.
ClimateSync uses only the standard climate.set_temperature service and reads standard climate entity attributes (current_temperature, temperature). It works with any climate integration that follows the standard HA climate platform contract, including but not limited to:
- Plugwise Emma / Smile
- Generic Thermostat
- ESPHome climate components
- Z-Wave thermostats
- Zigbee thermostats (ZHA / Zigbee2MQTT)
- Google Nest (via the Nest integration)
MIT — see LICENSE.