From 07b7d514ac8da2d0cc826c652a0527db2566e63f Mon Sep 17 00:00:00 2001 From: Santobert Date: Fri, 1 Nov 2019 21:37:34 +0100 Subject: [PATCH] Add improved scene support to the water_heater integration (#28277) --- homeassistant/components/demo/__init__.py | 1 + .../water_heater/reproduce_state.py | 125 ++++++++++++++++++ .../water_heater/test_reproduce_state.py | 124 +++++++++++++++++ 3 files changed, 250 insertions(+) create mode 100644 homeassistant/components/water_heater/reproduce_state.py create mode 100644 tests/components/water_heater/test_reproduce_state.py diff --git a/homeassistant/components/demo/__init__.py b/homeassistant/components/demo/__init__.py index 967b7852c6f..d93d217caa7 100644 --- a/homeassistant/components/demo/__init__.py +++ b/homeassistant/components/demo/__init__.py @@ -28,6 +28,7 @@ COMPONENTS_WITH_DEMO_PLATFORM = [ "switch", "tts", "mailbox", + "water_heater", ] diff --git a/homeassistant/components/water_heater/reproduce_state.py b/homeassistant/components/water_heater/reproduce_state.py new file mode 100644 index 00000000000..2038b4c237b --- /dev/null +++ b/homeassistant/components/water_heater/reproduce_state.py @@ -0,0 +1,125 @@ +"""Reproduce an Water heater state.""" +import asyncio +import logging +from typing import Iterable, Optional + +from homeassistant.const import ( + ATTR_ENTITY_ID, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_OFF, + STATE_ON, +) +from homeassistant.core import Context, State +from homeassistant.helpers.typing import HomeAssistantType + +from . import ( + ATTR_AWAY_MODE, + ATTR_OPERATION_MODE, + ATTR_TEMPERATURE, + DOMAIN, + SERVICE_SET_AWAY_MODE, + SERVICE_SET_OPERATION_MODE, + SERVICE_SET_TEMPERATURE, + STATE_ECO, + STATE_ELECTRIC, + STATE_GAS, + STATE_HEAT_PUMP, + STATE_HIGH_DEMAND, + STATE_PERFORMANCE, +) + +_LOGGER = logging.getLogger(__name__) + +VALID_STATES = { + STATE_ECO, + STATE_ELECTRIC, + STATE_GAS, + STATE_HEAT_PUMP, + STATE_HIGH_DEMAND, + STATE_OFF, + STATE_ON, + STATE_PERFORMANCE, +} + + +async def _async_reproduce_state( + hass: HomeAssistantType, state: State, context: Optional[Context] = None +) -> None: + """Reproduce a single state.""" + cur_state = hass.states.get(state.entity_id) + + if cur_state is None: + _LOGGER.warning("Unable to find entity %s", state.entity_id) + return + + if state.state not in VALID_STATES: + _LOGGER.warning( + "Invalid state specified for %s: %s", state.entity_id, state.state + ) + return + + # Return if we are already at the right state. + if ( + cur_state.state == state.state + and cur_state.attributes.get(ATTR_TEMPERATURE) + == state.attributes.get(ATTR_TEMPERATURE) + and cur_state.attributes.get(ATTR_AWAY_MODE) + == state.attributes.get(ATTR_AWAY_MODE) + ): + return + + service_data = {ATTR_ENTITY_ID: state.entity_id} + + if state.state != cur_state.state: + if state.state == STATE_ON: + service = SERVICE_TURN_ON + elif state.state == STATE_OFF: + service = SERVICE_TURN_OFF + else: + service = SERVICE_SET_OPERATION_MODE + service_data[ATTR_OPERATION_MODE] = state.state + + await hass.services.async_call( + DOMAIN, service, service_data, context=context, blocking=True + ) + + if ( + state.attributes.get(ATTR_TEMPERATURE) + != cur_state.attributes.get(ATTR_TEMPERATURE) + and state.attributes.get(ATTR_TEMPERATURE) is not None + ): + await hass.services.async_call( + DOMAIN, + SERVICE_SET_TEMPERATURE, + { + ATTR_ENTITY_ID: state.entity_id, + ATTR_TEMPERATURE: state.attributes.get(ATTR_TEMPERATURE), + }, + context=context, + blocking=True, + ) + + if ( + state.attributes.get(ATTR_AWAY_MODE) != cur_state.attributes.get(ATTR_AWAY_MODE) + and state.attributes.get(ATTR_AWAY_MODE) is not None + ): + await hass.services.async_call( + DOMAIN, + SERVICE_SET_AWAY_MODE, + { + ATTR_ENTITY_ID: state.entity_id, + ATTR_AWAY_MODE: state.attributes.get(ATTR_AWAY_MODE), + }, + context=context, + blocking=True, + ) + + +async def async_reproduce_states( + hass: HomeAssistantType, states: Iterable[State], context: Optional[Context] = None +) -> None: + """Reproduce Water heater states.""" + await asyncio.gather( + *(_async_reproduce_state(hass, state, context) for state in states) + ) diff --git a/tests/components/water_heater/test_reproduce_state.py b/tests/components/water_heater/test_reproduce_state.py new file mode 100644 index 00000000000..0c12d8eb54a --- /dev/null +++ b/tests/components/water_heater/test_reproduce_state.py @@ -0,0 +1,124 @@ +"""Test reproduce state for Water heater.""" +from homeassistant.components.water_heater import ( + ATTR_AWAY_MODE, + ATTR_OPERATION_MODE, + ATTR_TEMPERATURE, + SERVICE_SET_AWAY_MODE, + SERVICE_SET_OPERATION_MODE, + SERVICE_SET_TEMPERATURE, + STATE_ECO, + STATE_GAS, +) +from homeassistant.const import SERVICE_TURN_OFF, SERVICE_TURN_ON, STATE_OFF, STATE_ON +from homeassistant.core import State + +from tests.common import async_mock_service + + +async def test_reproducing_states(hass, caplog): + """Test reproducing Water heater states.""" + hass.states.async_set("water_heater.entity_off", STATE_OFF, {}) + hass.states.async_set("water_heater.entity_on", STATE_ON, {ATTR_TEMPERATURE: 45}) + hass.states.async_set("water_heater.entity_away", STATE_ON, {ATTR_AWAY_MODE: True}) + hass.states.async_set("water_heater.entity_gas", STATE_GAS, {}) + hass.states.async_set( + "water_heater.entity_all", + STATE_ECO, + {ATTR_AWAY_MODE: True, ATTR_TEMPERATURE: 45}, + ) + + turn_on_calls = async_mock_service(hass, "water_heater", SERVICE_TURN_ON) + turn_off_calls = async_mock_service(hass, "water_heater", SERVICE_TURN_OFF) + set_op_calls = async_mock_service(hass, "water_heater", SERVICE_SET_OPERATION_MODE) + set_temp_calls = async_mock_service(hass, "water_heater", SERVICE_SET_TEMPERATURE) + set_away_calls = async_mock_service(hass, "water_heater", SERVICE_SET_AWAY_MODE) + + # These calls should do nothing as entities already in desired state + await hass.helpers.state.async_reproduce_state( + [ + State("water_heater.entity_off", STATE_OFF), + State("water_heater.entity_on", STATE_ON, {ATTR_TEMPERATURE: 45}), + State("water_heater.entity_away", STATE_ON, {ATTR_AWAY_MODE: True}), + State("water_heater.entity_gas", STATE_GAS, {}), + State( + "water_heater.entity_all", + STATE_ECO, + {ATTR_AWAY_MODE: True, ATTR_TEMPERATURE: 45}, + ), + ], + blocking=True, + ) + + assert len(turn_on_calls) == 0 + assert len(turn_off_calls) == 0 + assert len(set_op_calls) == 0 + assert len(set_temp_calls) == 0 + assert len(set_away_calls) == 0 + + # Test invalid state is handled + await hass.helpers.state.async_reproduce_state( + [State("water_heater.entity_off", "not_supported")], blocking=True + ) + + assert "not_supported" in caplog.text + assert len(turn_on_calls) == 0 + assert len(turn_off_calls) == 0 + assert len(set_op_calls) == 0 + assert len(set_temp_calls) == 0 + assert len(set_away_calls) == 0 + + # Make sure correct services are called + await hass.helpers.state.async_reproduce_state( + [ + State("water_heater.entity_on", STATE_OFF), + State("water_heater.entity_off", STATE_ON, {ATTR_TEMPERATURE: 45}), + State("water_heater.entity_all", STATE_ECO, {ATTR_AWAY_MODE: False}), + State("water_heater.entity_away", STATE_GAS, {}), + State( + "water_heater.entity_gas", + STATE_ECO, + {ATTR_AWAY_MODE: True, ATTR_TEMPERATURE: 45}, + ), + # Should not raise + State("water_heater.non_existing", "on"), + ], + blocking=True, + ) + + assert len(turn_on_calls) == 1 + assert turn_on_calls[0].domain == "water_heater" + assert turn_on_calls[0].data == {"entity_id": "water_heater.entity_off"} + + assert len(turn_off_calls) == 1 + assert turn_off_calls[0].domain == "water_heater" + assert turn_off_calls[0].data == {"entity_id": "water_heater.entity_on"} + + VALID_OP_CALLS = [ + {"entity_id": "water_heater.entity_away", ATTR_OPERATION_MODE: STATE_GAS}, + {"entity_id": "water_heater.entity_gas", ATTR_OPERATION_MODE: STATE_ECO}, + ] + assert len(set_op_calls) == 2 + for call in set_op_calls: + assert call.domain == "water_heater" + assert call.data in VALID_OP_CALLS + VALID_OP_CALLS.remove(call.data) + + VALID_TEMP_CALLS = [ + {"entity_id": "water_heater.entity_off", ATTR_TEMPERATURE: 45}, + {"entity_id": "water_heater.entity_gas", ATTR_TEMPERATURE: 45}, + ] + assert len(set_temp_calls) == 2 + for call in set_temp_calls: + assert call.domain == "water_heater" + assert call.data in VALID_TEMP_CALLS + VALID_TEMP_CALLS.remove(call.data) + + VALID_AWAY_CALLS = [ + {"entity_id": "water_heater.entity_all", ATTR_AWAY_MODE: False}, + {"entity_id": "water_heater.entity_gas", ATTR_AWAY_MODE: True}, + ] + assert len(set_away_calls) == 2 + for call in set_away_calls: + assert call.domain == "water_heater" + assert call.data in VALID_AWAY_CALLS + VALID_AWAY_CALLS.remove(call.data)