Add On/Off and Away mode support to template platform

Implemented support for On/Off and Away modes in the template water heater
platform, including optimistic control and lambda-based state reporting.

Refactored the base 'WaterHeaterCall' to replace the 'state_' bitmask with
'optional<bool>' for 'on' and 'away' fields. This change was necessary to
enable partial (delta) updates. The previous bitmask implementation did not
distinguish between a field being "set to false" and "not set at all,"
causing unintended state resets (e.g., turning the device off when only
adjusting temperature).
This commit is contained in:
tronikos
2026-02-07 01:35:37 -08:00
parent eb7aa3420f
commit 52af92d4b5
9 changed files with 144 additions and 24 deletions
@@ -11,6 +11,8 @@ water_heater:
optimistic: true
current_temperature: !lambda "return 45.0f;"
target_temperature: !lambda "return 60.0f;"
on: !lambda "return true;"
away: !lambda "return false;"
# Note: No mode lambda - we want optimistic mode changes to stick
# A mode lambda would override mode changes in loop()
supported_modes:
@@ -64,6 +64,12 @@ async def test_water_heater_template(
f"Expected 4 supported modes, got {len(supported_modes)}: {supported_modes}"
)
# Verify supported features
# WATER_HEATER_SUPPORTS_AWAY_MODE (1 << 3) = 8
# WATER_HEATER_SUPPORTS_ON_OFF (1 << 4) = 16
assert test_water_heater.supported_features & 8
assert test_water_heater.supported_features & 16
# Subscribe with the wrapper that filters initial states
client.subscribe_states(initial_state_helper.on_state_wrapper(on_state))
@@ -88,6 +94,9 @@ async def test_water_heater_template(
assert initial_state.target_temperature == 60.0, (
f"Expected target temp 60.0, got {initial_state.target_temperature}"
)
# Verify On/Away (from lambdas in fixture)
assert initial_state.is_on is True
assert initial_state.away is False
# Test changing to GAS mode
client.water_heater_command(test_water_heater.key, mode=WaterHeaterMode.GAS)
@@ -100,6 +109,38 @@ async def test_water_heater_template(
assert isinstance(gas_state, WaterHeaterState)
assert gas_state.mode == WaterHeaterMode.GAS
# Test changing away mode and power (optimistic)
away_future: asyncio.Future[WaterHeaterState] = loop.create_future()
off_future: asyncio.Future[WaterHeaterState] = loop.create_future()
def on_state_update(state: aioesphomeapi.EntityState) -> None:
if (
isinstance(state, WaterHeaterState)
and state.key == test_water_heater.key
):
if state.away is True and not away_future.done():
away_future.set_result(state)
if state.is_on is False and not off_future.done():
off_future.set_result(state)
client.subscribe_states(on_state_update)
# Change away mode
client.water_heater_command(test_water_heater.key, away=True)
try:
away_state = await asyncio.wait_for(away_future, timeout=5.0)
except TimeoutError:
pytest.fail("Away mode change not received within 5 seconds")
assert away_state.away is True
# Change power
client.water_heater_command(test_water_heater.key, is_on=False)
try:
off_state = await asyncio.wait_for(off_future, timeout=5.0)
except TimeoutError:
pytest.fail("Power off change not received within 5 seconds")
assert off_state.is_on is False
# Test changing to ECO mode (from GAS)
client.water_heater_command(test_water_heater.key, mode=WaterHeaterMode.ECO)