From 282df3da51d2fa9863d4b094ca459a8cec0bc64a Mon Sep 17 00:00:00 2001 From: Bluetooth Devices Bot Date: Tue, 23 Jun 2026 08:04:07 +0000 Subject: [PATCH 1/2] fix: keep auto-mode setpoint band on the unit grid (Celsius) set_heat_cool_temp's auto/off-mode branch split the deadband with math.ceil(deadband / 2), forcing an integer half-offset. On Celsius systems this over-widened the band off the 0.5 grid: a 1.0 deadband yielded +/-1.0 instead of the minimal +/-0.5. Round the half-deadband up to the unit's grid step (0.5 C, 1 F) instead. Fahrenheit output is unchanged. Adds coverage for the previously untested branch. --- nexia/zone.py | 20 ++++++++++++++----- tests/test_home.py | 49 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 5 deletions(-) diff --git a/nexia/zone.py b/nexia/zone.py index 874cd99..a821cc4 100644 --- a/nexia/zone.py +++ b/nexia/zone.py @@ -558,12 +558,22 @@ async def set_heat_cool_temp( self.round_temp(heat_temperature) + deadband, ) else: - cool_temperature = self.round_temp(set_temperature) + math.ceil( - deadband / 2, - ) - heat_temperature = self.round_temp(set_temperature) - math.ceil( - deadband / 2, + # Split the deadband symmetrically around the target. The + # offset must land on the unit's setpoint grid (0.5° on + # Celsius, 1° on Fahrenheit) and be wide enough that the + # resulting band still satisfies the deadband, so round the + # half-deadband up to the next grid step. Using math.ceil on + # the raw half over-widened the band on Celsius systems + # (deadband 1.0 yielded ±1.0 instead of the minimal ±0.5). + step = ( + 0.5 + if self.thermostat.get_unit() == UNIT_CELSIUS + else 1.0 ) + half_deadband = math.ceil(deadband / 2 / step) * step + target = self.round_temp(set_temperature) + cool_temperature = target + half_deadband + heat_temperature = target - half_deadband await self._set_setpoints(cool_temperature, heat_temperature) diff --git a/tests/test_home.py b/tests/test_home.py index 029a3d4..ef58c6e 100644 --- a/tests/test_home.py +++ b/tests/test_home.py @@ -2490,3 +2490,52 @@ async def test_check_heat_cool_setpoints_accepts_in_range( zone.check_heat_cool_setpoints(heat_temperature=69, cool_temperature=78) # Boundary values (exactly at the limits) are allowed. zone.check_heat_cool_setpoints(heat_temperature=55, cool_temperature=99) + + +async def test_set_heat_cool_temp_auto_mode_fahrenheit_band( + aiohttp_session: aiohttp.ClientSession, +) -> None: + """Auto-mode set_temperature splits the deadband on the Fahrenheit grid. + + The zone is in AUTO mode, so a single set_temperature is expanded into a + heat/cool band centered on the target. On Fahrenheit (deadband 3) the + half-deadband rounds up to 2, giving a ±2° band. + """ + nexia = NexiaHome(aiohttp_session) + devices_json = json.loads(await load_fixture("single_zone_xl1050.json")) + nexia.update_from_json(devices_json) + zone = nexia.get_thermostat_by_id(345678).get_zone_by_id(234567) + assert zone.get_current_mode() == "AUTO" + + with patch.object( + zone, "_set_setpoints", new=AsyncMock() + ) as set_setpoints: + await zone.set_heat_cool_temp(set_temperature=70) + + cool, heat = set_setpoints.call_args.args + assert (cool, heat) == (72, 68) + + +async def test_set_heat_cool_temp_auto_mode_celsius_half_degree_grid( + aiohttp_session: aiohttp.ClientSession, +) -> None: + """Auto-mode set_temperature stays on the 0.5° grid for Celsius systems. + + With a 1.0° Celsius deadband the half-deadband is exactly 0.5°, so the band + is the minimal ±0.5° around the target rather than the over-wide ±1.0° that + integer ceil produced. + """ + nexia = NexiaHome(aiohttp_session) + devices_json = json.loads(await load_fixture("single_zone_xl1050.json")) + nexia.update_from_json(devices_json) + zone = nexia.get_thermostat_by_id(345678).get_zone_by_id(234567) + + with ( + patch.object(zone.thermostat, "get_unit", return_value="C"), + patch.object(zone.thermostat, "get_deadband", return_value=1.0), + patch.object(zone, "_set_setpoints", new=AsyncMock()) as set_setpoints, + ): + await zone.set_heat_cool_temp(set_temperature=21.5) + + cool, heat = set_setpoints.call_args.args + assert (cool, heat) == (22.0, 21.0) From 38f708dc8f6a2438fa6f2739c760b7833f306a29 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 23 Jun 2026 08:05:07 +0000 Subject: [PATCH 2/2] chore(pre-commit.ci): auto fixes --- nexia/zone.py | 6 +----- tests/test_home.py | 4 +--- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/nexia/zone.py b/nexia/zone.py index a821cc4..3135d73 100644 --- a/nexia/zone.py +++ b/nexia/zone.py @@ -565,11 +565,7 @@ async def set_heat_cool_temp( # half-deadband up to the next grid step. Using math.ceil on # the raw half over-widened the band on Celsius systems # (deadband 1.0 yielded ±1.0 instead of the minimal ±0.5). - step = ( - 0.5 - if self.thermostat.get_unit() == UNIT_CELSIUS - else 1.0 - ) + step = 0.5 if self.thermostat.get_unit() == UNIT_CELSIUS else 1.0 half_deadband = math.ceil(deadband / 2 / step) * step target = self.round_temp(set_temperature) cool_temperature = target + half_deadband diff --git a/tests/test_home.py b/tests/test_home.py index ef58c6e..5086646 100644 --- a/tests/test_home.py +++ b/tests/test_home.py @@ -2507,9 +2507,7 @@ async def test_set_heat_cool_temp_auto_mode_fahrenheit_band( zone = nexia.get_thermostat_by_id(345678).get_zone_by_id(234567) assert zone.get_current_mode() == "AUTO" - with patch.object( - zone, "_set_setpoints", new=AsyncMock() - ) as set_setpoints: + with patch.object(zone, "_set_setpoints", new=AsyncMock()) as set_setpoints: await zone.set_heat_cool_temp(set_temperature=70) cool, heat = set_setpoints.call_args.args