Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 12 additions & 6 deletions nexia/zone.py
Original file line number Diff line number Diff line change
Expand Up @@ -558,12 +558,18 @@ 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)

Expand Down
47 changes: 47 additions & 0 deletions tests/test_home.py
Original file line number Diff line number Diff line change
Expand Up @@ -2490,3 +2490,50 @@ 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)
Loading