From 1d3a6f3d599aba8592b39b93f16b9994763f7b71 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 18 May 2026 10:34:41 +0000 Subject: [PATCH 1/4] Prepare 1.2.2 recovery release metadata and notes Agent-Logs-Url: https://github.com/Patrick1610/ClimateSync/sessions/d230525c-d9ee-4579-927f-18865905d907 Co-authored-by: Patrick1610 <6585375+Patrick1610@users.noreply.github.com> --- README.md | 5 +++++ custom_components/climatesync/manifest.json | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 531afe5..3ae8caa 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,11 @@ > **Developed with Plugwise Emma in mind, but universally usable with any Home Assistant climate entity that exposes `current_temperature` and `temperature` attributes.** +## Recovery release 1.2.2 + +This release is intentionally built from the stable `v1.2.1` baseline and only includes the rounding improvements from PR #8 (`rounding_direction`). +Unwanted functionality introduced in newer lines is intentionally excluded from this recovery patch. + 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. ### Why delta-based instead of copying the highest setpoint? diff --git a/custom_components/climatesync/manifest.json b/custom_components/climatesync/manifest.json index e610bc6..7ba31d2 100644 --- a/custom_components/climatesync/manifest.json +++ b/custom_components/climatesync/manifest.json @@ -1,7 +1,7 @@ { "domain": "climatesync", "name": "ClimateSync", - "version": "1.2.0", + "version": "1.2.2", "documentation": "https://github.com/Patrick1610/ClimateSync", "issue_tracker": "https://github.com/Patrick1610/ClimateSync/issues", "codeowners": ["@Patrick1610"], From 4058a0e5eac766bf56309d372c23f3914be6819f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 18 May 2026 19:08:12 +0000 Subject: [PATCH 2/4] Add robust rounding direction handling and diagnostics Agent-Logs-Url: https://github.com/Patrick1610/ClimateSync/sessions/7f7407c2-3387-48f5-b836-7311a43f3b38 Co-authored-by: Patrick1610 <6585375+Patrick1610@users.noreply.github.com> --- README.md | 1 + custom_components/climatesync/config_flow.py | 21 +++--- custom_components/climatesync/const.py | 6 +- custom_components/climatesync/coordinator.py | 65 ++++++++++++------- custom_components/climatesync/sensor.py | 10 ++- .../climatesync/translations/en.json | 2 +- .../climatesync/translations/nl.json | 2 +- tests/test_coordinator.py | 19 ++++++ tests/test_sensor.py | 13 ++++ 9 files changed, 100 insertions(+), 39 deletions(-) diff --git a/README.md b/README.md index 3ae8caa..185d881 100644 --- a/README.md +++ b/README.md @@ -124,6 +124,7 @@ If abs(destination_current_target - setpoint_final) > min_change_threshold: ### Rounding mode and direction 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`. diff --git a/custom_components/climatesync/config_flow.py b/custom_components/climatesync/config_flow.py index 965454f..1cacf8b 100644 --- a/custom_components/climatesync/config_flow.py +++ b/custom_components/climatesync/config_flow.py @@ -163,6 +163,13 @@ def _destination_schema( return vol.Schema(fields) +def _normalize_rounding_direction(value: Any) -> str: + """Return a safe rounding direction value for storage/use.""" + if value in ROUNDING_DIRECTIONS: + return value + return DEFAULT_ROUNDING_DIRECTION + + # ────────────────────────────────────────────────────────────────────────────── # Initial config flow (2 steps, no advanced options) # ────────────────────────────────────────────────────────────────────────────── @@ -217,9 +224,8 @@ async def async_step_destination( CONF_IDLE_TEMPERATURE: user_input[CONF_IDLE_TEMPERATURE], CONF_MAX_SETPOINT: user_input[CONF_MAX_SETPOINT], CONF_ROUNDING_MODE: user_input[CONF_ROUNDING_MODE], - CONF_ROUNDING_DIRECTION: user_input.get( - CONF_ROUNDING_DIRECTION, - DEFAULT_ROUNDING_DIRECTION, + CONF_ROUNDING_DIRECTION: _normalize_rounding_direction( + user_input.get(CONF_ROUNDING_DIRECTION) ), }, ) @@ -299,9 +305,8 @@ async def async_step_destination( CONF_IDLE_TEMPERATURE: user_input[CONF_IDLE_TEMPERATURE], CONF_MAX_SETPOINT: user_input[CONF_MAX_SETPOINT], CONF_ROUNDING_MODE: user_input[CONF_ROUNDING_MODE], - CONF_ROUNDING_DIRECTION: user_input.get( - CONF_ROUNDING_DIRECTION, - DEFAULT_ROUNDING_DIRECTION, + CONF_ROUNDING_DIRECTION: _normalize_rounding_direction( + user_input.get(CONF_ROUNDING_DIRECTION) ), CONF_RESYNC_INTERVAL: user_input[CONF_RESYNC_INTERVAL], CONF_MIN_CHANGE_THRESHOLD: user_input[CONF_MIN_CHANGE_THRESHOLD], @@ -316,8 +321,8 @@ async def async_step_destination( default_idle=self._get(CONF_IDLE_TEMPERATURE, DEFAULT_IDLE_TEMPERATURE), default_max_setpoint=self._get(CONF_MAX_SETPOINT, DEFAULT_MAX_SETPOINT), default_rounding=self._get(CONF_ROUNDING_MODE, DEFAULT_ROUNDING_MODE), - default_rounding_direction=self._get( - CONF_ROUNDING_DIRECTION, DEFAULT_ROUNDING_DIRECTION + default_rounding_direction=_normalize_rounding_direction( + self._get(CONF_ROUNDING_DIRECTION, DEFAULT_ROUNDING_DIRECTION) ), default_resync=self._get(CONF_RESYNC_INTERVAL, DEFAULT_RESYNC_INTERVAL), default_threshold=self._get( diff --git a/custom_components/climatesync/const.py b/custom_components/climatesync/const.py index 567cb8d..52438af 100644 --- a/custom_components/climatesync/const.py +++ b/custom_components/climatesync/const.py @@ -23,17 +23,17 @@ ROUNDING_MODE_1DEC = "1_decimal" ROUNDING_MODE_2DEC = "2_decimals" -ROUNDING_MODES = [ROUNDING_MODE_HALF, ROUNDING_MODE_1DEC, ROUNDING_MODE_2DEC] +ROUNDING_MODES = (ROUNDING_MODE_HALF, ROUNDING_MODE_1DEC, ROUNDING_MODE_2DEC) ROUNDING_DIRECTION_FLOOR = "floor" ROUNDING_DIRECTION_NEAREST = "nearest" ROUNDING_DIRECTION_CEILING = "ceiling" -ROUNDING_DIRECTIONS = [ +ROUNDING_DIRECTIONS = ( ROUNDING_DIRECTION_FLOOR, ROUNDING_DIRECTION_NEAREST, ROUNDING_DIRECTION_CEILING, -] +) DEFAULT_ROUNDING_DIRECTION = ROUNDING_DIRECTION_NEAREST diff --git a/custom_components/climatesync/coordinator.py b/custom_components/climatesync/coordinator.py index 90649ec..5e31d8a 100644 --- a/custom_components/climatesync/coordinator.py +++ b/custom_components/climatesync/coordinator.py @@ -33,6 +33,8 @@ DEFAULT_ROUNDING_MODE, ROUNDING_DIRECTION_CEILING, ROUNDING_DIRECTION_FLOOR, + ROUNDING_DIRECTIONS, + ROUNDING_MODES, ROUNDING_MODE_2DEC, ROUNDING_MODE_HALF, STATUS_APPLY_FAILED, @@ -60,16 +62,6 @@ def _safe_float(value: Any) -> float | None: return None -def _apply_nearest_rounding(value: float, mode: str) -> float: - """Apply the historical nearest rounding mode to a temperature value.""" - if mode == ROUNDING_MODE_HALF: - return round(value * 2) / 2 - if mode == ROUNDING_MODE_2DEC: - return round(value, 2) - # Default / ROUNDING_MODE_1DEC - return round(value, 1) - - def _rounding_step(mode: str) -> tuple[float, int]: """Return the quantum and display precision for a rounding mode.""" if mode == ROUNDING_MODE_HALF: @@ -80,24 +72,46 @@ def _rounding_step(mode: str) -> tuple[float, int]: return 0.1, 1 +def _normalize_rounding_mode(mode: str) -> str: + """Return a safe rounding mode.""" + if mode in ROUNDING_MODES: + return mode + return DEFAULT_ROUNDING_MODE + + +def _normalize_rounding_direction(direction: str) -> str: + """Return a safe rounding direction.""" + if direction in ROUNDING_DIRECTIONS: + return direction + return DEFAULT_ROUNDING_DIRECTION + + +def _round_scaled(scaled: float, direction: str) -> float: + """Round a scaled value using epsilon-safe floor/nearest/ceiling logic. + + The default branch handles nearest rounding with an epsilon bump to avoid + boundary artifacts. Floor and nearest use +epsilon for values slightly + below boundaries; ceiling uses -epsilon for values slightly above + boundaries, so float artifacts still land on the expected step. + """ + if direction == ROUNDING_DIRECTION_FLOOR: + return float(math.floor(scaled + _ROUNDING_EPSILON)) + if direction == ROUNDING_DIRECTION_CEILING: + return float(math.ceil(scaled - _ROUNDING_EPSILON)) + return float(round(scaled + _ROUNDING_EPSILON)) + + def round_setpoint( value: float, rounding_mode: str, rounding_direction: str = DEFAULT_ROUNDING_DIRECTION, ) -> float: """Round a setpoint using the selected mode and direction.""" - if rounding_direction not in ( - ROUNDING_DIRECTION_FLOOR, - ROUNDING_DIRECTION_CEILING, - ): - return _apply_nearest_rounding(value, rounding_mode) - - step, precision = _rounding_step(rounding_mode) + mode = _normalize_rounding_mode(rounding_mode) + direction = _normalize_rounding_direction(rounding_direction) + step, precision = _rounding_step(mode) scaled = value / step - if rounding_direction == ROUNDING_DIRECTION_FLOOR: - rounded = math.floor(scaled + _ROUNDING_EPSILON) * step - else: - rounded = math.ceil(scaled - _ROUNDING_EPSILON) * step + rounded = _round_scaled(scaled, direction) * step return round(rounded, precision) @@ -200,9 +214,12 @@ def async_apply_options(self) -> None: CONF_ROUNDING_MODE, data.get(CONF_ROUNDING_MODE, DEFAULT_ROUNDING_MODE), ) - self._rounding_direction = opts.get( - CONF_ROUNDING_DIRECTION, - data.get(CONF_ROUNDING_DIRECTION, DEFAULT_ROUNDING_DIRECTION), + self._rounding_mode = _normalize_rounding_mode(self._rounding_mode) + self._rounding_direction = _normalize_rounding_direction( + opts.get( + CONF_ROUNDING_DIRECTION, + data.get(CONF_ROUNDING_DIRECTION, DEFAULT_ROUNDING_DIRECTION), + ) ) self._resync_interval = int( opts.get(CONF_RESYNC_INTERVAL, DEFAULT_RESYNC_INTERVAL) diff --git a/custom_components/climatesync/sensor.py b/custom_components/climatesync/sensor.py index c4b89e2..5cdbddf 100644 --- a/custom_components/climatesync/sensor.py +++ b/custom_components/climatesync/sensor.py @@ -213,6 +213,9 @@ def native_value(self) -> float | None: @property def extra_state_attributes(self) -> dict[str, Any]: """Return detailed setpoint context.""" + rounded_setpoint = self._coordinator.rounded_setpoint + if rounded_setpoint is None: + rounded_setpoint = self._coordinator.computed_setpoint return { "destination_entity_id": self._coordinator.destination_entity, "destination_current_temperature": self._coordinator.destination_current_temperature, @@ -221,7 +224,7 @@ def extra_state_attributes(self) -> dict[str, Any]: "rounding_mode": self._coordinator.rounding_mode, "rounding_direction": self._coordinator.rounding_direction, "raw_setpoint": self._coordinator.raw_setpoint, - "rounded_setpoint": self._coordinator.rounded_setpoint, + "rounded_setpoint": rounded_setpoint, "final_setpoint": self._coordinator.computed_setpoint, "idle_temperature": self._coordinator.idle_temperature, } @@ -295,6 +298,7 @@ def extra_state_attributes(self) -> dict[str, Any]: else None ) return { + "destination_entity_id": coord.destination_entity, "last_update_time": last_update, "last_service_call_time": last_call, "last_desired_setpoint": coord.last_desired_setpoint, @@ -303,7 +307,9 @@ def extra_state_attributes(self) -> dict[str, Any]: "rounding_mode": coord.rounding_mode, "rounding_direction": coord.rounding_direction, "raw_setpoint": coord.raw_setpoint, - "rounded_setpoint": coord.rounded_setpoint, + "rounded_setpoint": coord.rounded_setpoint + if coord.rounded_setpoint is not None + else coord.computed_setpoint, "final_setpoint": coord.computed_setpoint, "mismatch_seconds": round(coord.mismatch_seconds, 1), "mismatch_since": ( diff --git a/custom_components/climatesync/translations/en.json b/custom_components/climatesync/translations/en.json index a97c016..63a95c2 100644 --- a/custom_components/climatesync/translations/en.json +++ b/custom_components/climatesync/translations/en.json @@ -67,7 +67,7 @@ "rounding_direction": { "options": { "floor": "Round floor / down", - "nearest": "Round abs / normal rounding", + "nearest": "Round nearest (default)", "ceiling": "Round ceiling / up" } } diff --git a/custom_components/climatesync/translations/nl.json b/custom_components/climatesync/translations/nl.json index 0906d3b..31de30b 100644 --- a/custom_components/climatesync/translations/nl.json +++ b/custom_components/climatesync/translations/nl.json @@ -67,7 +67,7 @@ "rounding_direction": { "options": { "floor": "Afronden floor / omlaag", - "nearest": "Afronden abs / normaal afronden", + "nearest": "Afronden nearest (standaard)", "ceiling": "Afronden ceiling / omhoog" } } diff --git a/tests/test_coordinator.py b/tests/test_coordinator.py index 5ab36a5..895bd0c 100644 --- a/tests/test_coordinator.py +++ b/tests/test_coordinator.py @@ -329,6 +329,25 @@ def test_missing_rounding_direction_defaults_to_nearest(self): assert coord.rounding_direction == ROUNDING_DIRECTION_NEAREST assert round_setpoint(19.2, coord.rounding_mode, coord.rounding_direction) == 19.0 + def test_invalid_rounding_direction_falls_back_to_nearest(self): + assert round_setpoint(19.3, ROUNDING_MODE_HALF, "invalid") == 19.5 + + def test_invalid_rounding_mode_falls_back_to_default(self): + assert round_setpoint(19.26, "invalid_mode", ROUNDING_DIRECTION_FLOOR) == 19.2 + + def test_epsilon_safe_floor_and_ceiling(self): + assert round_setpoint(19.5000000001, ROUNDING_MODE_HALF, ROUNDING_DIRECTION_FLOOR) == 19.5 + assert round_setpoint(19.4999999999, ROUNDING_MODE_HALF, ROUNDING_DIRECTION_CEILING) == 19.5 + + +def test_apply_options_invalid_rounding_direction_defaults_to_nearest(): + coord, _ = _build_coordinator(rounding_mode=ROUNDING_MODE_HALF) + coord.entry.options[CONF_ROUNDING_DIRECTION] = "bad_value" + + coord.async_apply_options() + + assert coord.rounding_direction == DEFAULT_ROUNDING_DIRECTION + # --------------------------------------------------------------------------- # Core coordinator tests diff --git a/tests/test_sensor.py b/tests/test_sensor.py index 9ade998..057bfc1 100644 --- a/tests/test_sensor.py +++ b/tests/test_sensor.py @@ -259,12 +259,25 @@ def test_status_attributes_include_rounding_context(self): sensor = StatusSensor(coord, coord.entry, _make_device_info()) attrs = sensor.extra_state_attributes + assert attrs["destination_entity_id"] == "climate.dest" assert attrs["rounding_mode"] == DEFAULT_ROUNDING_MODE assert attrs["rounding_direction"] == DEFAULT_ROUNDING_DIRECTION assert attrs["raw_setpoint"] == 19.2 assert attrs["rounded_setpoint"] == 19.5 assert attrs["final_setpoint"] == 19.5 + def test_rounding_attributes_fallback_to_final_setpoint(self): + coord, _ = _build_coordinator() + coord.raw_setpoint = 19.2 + coord.rounded_setpoint = None + coord.computed_setpoint = 19.5 + + setpoint_sensor = DestinationSetpointSensor(coord, coord.entry, _make_device_info()) + status_sensor = StatusSensor(coord, coord.entry, _make_device_info()) + + assert setpoint_sensor.extra_state_attributes["rounded_setpoint"] == 19.5 + assert status_sensor.extra_state_attributes["rounded_setpoint"] == 19.5 + # --------------------------------------------------------------------------- # Tests: DestinationCurrentTargetSensor From 2933a22bb6b6bf4f15fea08cfeeb2f5b7123dcf6 Mon Sep 17 00:00:00 2001 From: Patrick1610 <6585375+Patrick1610@users.noreply.github.com> Date: Tue, 19 May 2026 16:29:57 +0200 Subject: [PATCH 3/4] Preserve legacy nearest rounding --- custom_components/climatesync/coordinator.py | 10 +++++----- tests/test_coordinator.py | 20 ++++++++++++++++++++ 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/custom_components/climatesync/coordinator.py b/custom_components/climatesync/coordinator.py index 5e31d8a..1eda6bd 100644 --- a/custom_components/climatesync/coordinator.py +++ b/custom_components/climatesync/coordinator.py @@ -89,16 +89,16 @@ def _normalize_rounding_direction(direction: str) -> str: def _round_scaled(scaled: float, direction: str) -> float: """Round a scaled value using epsilon-safe floor/nearest/ceiling logic. - The default branch handles nearest rounding with an epsilon bump to avoid - boundary artifacts. Floor and nearest use +epsilon for values slightly - below boundaries; ceiling uses -epsilon for values slightly above - boundaries, so float artifacts still land on the expected step. + Floor uses +epsilon for values slightly below boundaries; ceiling uses + -epsilon for values slightly above boundaries, so float artifacts still + land on the expected step. Nearest intentionally uses plain Python + round() to preserve the legacy v1.2.1 behavior for existing configs. """ if direction == ROUNDING_DIRECTION_FLOOR: return float(math.floor(scaled + _ROUNDING_EPSILON)) if direction == ROUNDING_DIRECTION_CEILING: return float(math.ceil(scaled - _ROUNDING_EPSILON)) - return float(round(scaled + _ROUNDING_EPSILON)) + return float(round(scaled)) def round_setpoint( diff --git a/tests/test_coordinator.py b/tests/test_coordinator.py index 895bd0c..9245cfa 100644 --- a/tests/test_coordinator.py +++ b/tests/test_coordinator.py @@ -187,6 +187,20 @@ def test_1dec(self): def test_2dec(self): assert _apply_rounding(21.346, "2_decimals") == 21.35 + @pytest.mark.parametrize( + ("value", "mode", "expected"), + [ + (19.25, ROUNDING_MODE_HALF, 19.0), + (20.25, ROUNDING_MODE_HALF, 20.0), + (19.15, ROUNDING_MODE_1DEC, 19.1), + (19.25, ROUNDING_MODE_1DEC, 19.2), + (19.005, ROUNDING_MODE_2DEC, 19.0), + (19.025, ROUNDING_MODE_2DEC, 19.02), + ], + ) + def test_legacy_nearest_tie_cases(self, value: float, mode: str, expected: float): + assert _apply_rounding(value, mode) == expected + class TestRoundSetpoint: """Unit tests for round_setpoint helper.""" @@ -198,6 +212,8 @@ class TestRoundSetpoint: (19.3, 19.5), (19.0, 19.0), (19.5, 19.5), + (19.25, 19.0), + (20.25, 20.0), ], ) def test_half_step_nearest(self, value: float, expected: float): @@ -242,6 +258,8 @@ def test_half_step_ceiling(self, value: float, expected: float): (19.14, 19.1), (19.16, 19.2), (19.10, 19.1), + (19.15, 19.1), + (19.25, 19.2), ], ) def test_1_decimal_nearest(self, value: float, expected: float): @@ -284,6 +302,8 @@ def test_1_decimal_ceiling(self, value: float, expected: float): (19.114, 19.11), (19.116, 19.12), (19.10, 19.1), + (19.005, 19.0), + (19.025, 19.02), ], ) def test_2_decimals_nearest(self, value: float, expected: float): From 35d03705e750e1cd3ff7530c893c6bc3c6db83a9 Mon Sep 17 00:00:00 2001 From: Patrick1610 <6585375+Patrick1610@users.noreply.github.com> Date: Wed, 20 May 2026 07:27:27 +0200 Subject: [PATCH 4/4] Fix HACS validation metadata --- custom_components/climatesync/brand/icon.png | Bin 0 -> 5192 bytes custom_components/climatesync/manifest.json | 10 +++++----- 2 files changed, 5 insertions(+), 5 deletions(-) create mode 100644 custom_components/climatesync/brand/icon.png diff --git a/custom_components/climatesync/brand/icon.png b/custom_components/climatesync/brand/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..bcf7a257d22ad1d3d5545a71d8a52b7e1a460e64 GIT binary patch literal 5192 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 O#2Jh8=4Gayk^ci^wx25i literal 0 HcmV?d00001 diff --git a/custom_components/climatesync/manifest.json b/custom_components/climatesync/manifest.json index 7ba31d2..08d1af1 100644 --- a/custom_components/climatesync/manifest.json +++ b/custom_components/climatesync/manifest.json @@ -1,12 +1,12 @@ { "domain": "climatesync", "name": "ClimateSync", - "version": "1.2.2", - "documentation": "https://github.com/Patrick1610/ClimateSync", - "issue_tracker": "https://github.com/Patrick1610/ClimateSync/issues", "codeowners": ["@Patrick1610"], - "requirements": [], + "config_flow": true, "dependencies": [], + "documentation": "https://github.com/Patrick1610/ClimateSync", "iot_class": "local_push", - "config_flow": true + "issue_tracker": "https://github.com/Patrick1610/ClimateSync/issues", + "requirements": [], + "version": "1.2.2" }