From 703cd961863dc0ea370642bbc5dbb94117644862 Mon Sep 17 00:00:00 2001 From: Santobert Date: Wed, 23 Oct 2019 08:03:38 +0200 Subject: [PATCH] Add improved scene support to the input_datetime integration (#28105) * input_datetime reproduce state * simplify service decision --- .../input_datetime/reproduce_state.py | 111 ++++++++++++++++++ .../input_datetime/test_reproduce_state.py | 69 +++++++++++ 2 files changed, 180 insertions(+) create mode 100644 homeassistant/components/input_datetime/reproduce_state.py create mode 100644 tests/components/input_datetime/test_reproduce_state.py diff --git a/homeassistant/components/input_datetime/reproduce_state.py b/homeassistant/components/input_datetime/reproduce_state.py new file mode 100644 index 00000000000..09a30e65210 --- /dev/null +++ b/homeassistant/components/input_datetime/reproduce_state.py @@ -0,0 +1,111 @@ +"""Reproduce an Input datetime state.""" +import asyncio +import logging +from typing import Iterable, Optional + +from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.core import Context, State +from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.util import dt as dt_util + +from . import ( + ATTR_DATE, + ATTR_DATETIME, + ATTR_TIME, + CONF_HAS_DATE, + CONF_HAS_TIME, + DOMAIN, + SERVICE_SET_DATETIME, +) + +_LOGGER = logging.getLogger(__name__) + + +def is_valid_datetime(string: str) -> bool: + """Test if string dt is a valid datetime.""" + try: + return dt_util.parse_datetime(string) is not None + except ValueError: + return False + + +def is_valid_date(string: str) -> bool: + """Test if string dt is a valid date.""" + try: + return dt_util.parse_date(string) is not None + except ValueError: + return False + + +def is_valid_time(string: str) -> bool: + """Test if string dt is a valid time.""" + try: + return dt_util.parse_time(string) is not None + except ValueError: + return False + + +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 not ( + ( + is_valid_datetime(state.state) + and cur_state.attributes.get(CONF_HAS_DATE) + and cur_state.attributes.get(CONF_HAS_TIME) + ) + or ( + is_valid_date(state.state) + and cur_state.attributes.get(CONF_HAS_DATE) + and not cur_state.attributes.get(CONF_HAS_TIME) + ) + or ( + is_valid_time(state.state) + and cur_state.attributes.get(CONF_HAS_TIME) + and not cur_state.attributes.get(CONF_HAS_DATE) + ) + ): + _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: + return + + service = SERVICE_SET_DATETIME + service_data = {ATTR_ENTITY_ID: state.entity_id} + + has_time = cur_state.attributes.get(CONF_HAS_TIME) + has_date = cur_state.attributes.get(CONF_HAS_DATE) + + if has_time and has_date: + service_data[ATTR_DATETIME] = state.state + elif has_time: + service_data[ATTR_TIME] = state.state + elif has_date: + service_data[ATTR_DATE] = state.state + else: + _LOGGER.warning("input_datetime needs either has_date or has_time or both") + return + + await hass.services.async_call( + DOMAIN, service, service_data, context=context, blocking=True + ) + + +async def async_reproduce_states( + hass: HomeAssistantType, states: Iterable[State], context: Optional[Context] = None +) -> None: + """Reproduce Input datetime states.""" + await asyncio.gather( + *(_async_reproduce_state(hass, state, context) for state in states) + ) diff --git a/tests/components/input_datetime/test_reproduce_state.py b/tests/components/input_datetime/test_reproduce_state.py new file mode 100644 index 00000000000..71f0658923c --- /dev/null +++ b/tests/components/input_datetime/test_reproduce_state.py @@ -0,0 +1,69 @@ +"""Test reproduce state for Input datetime.""" +from homeassistant.core import State + +from tests.common import async_mock_service + + +async def test_reproducing_states(hass, caplog): + """Test reproducing Input datetime states.""" + hass.states.async_set( + "input_datetime.entity_datetime", + "2010-10-10 01:20:00", + {"has_date": True, "has_time": True}, + ) + hass.states.async_set( + "input_datetime.entity_time", "01:20:00", {"has_date": False, "has_time": True} + ) + hass.states.async_set( + "input_datetime.entity_date", + "2010-10-10", + {"has_date": True, "has_time": False}, + ) + + datetime_calls = async_mock_service(hass, "input_datetime", "set_datetime") + + # These calls should do nothing as entities already in desired state + await hass.helpers.state.async_reproduce_state( + [ + State("input_datetime.entity_datetime", "2010-10-10 01:20:00"), + State("input_datetime.entity_time", "01:20:00"), + State("input_datetime.entity_date", "2010-10-10"), + ], + blocking=True, + ) + + assert len(datetime_calls) == 0 + + # Test invalid state is handled + await hass.helpers.state.async_reproduce_state( + [State("input_datetime.entity_datetime", "not_supported")], blocking=True + ) + + assert "not_supported" in caplog.text + assert len(datetime_calls) == 0 + + # Make sure correct services are called + await hass.helpers.state.async_reproduce_state( + [ + State("input_datetime.entity_datetime", "2011-10-10 02:20:00"), + State("input_datetime.entity_time", "02:20:00"), + State("input_datetime.entity_date", "2011-10-10"), + # Should not raise + State("input_datetime.non_existing", "2010-10-10 01:20:00"), + ], + blocking=True, + ) + + valid_calls = [ + { + "entity_id": "input_datetime.entity_datetime", + "datetime": "2011-10-10 02:20:00", + }, + {"entity_id": "input_datetime.entity_time", "time": "02:20:00"}, + {"entity_id": "input_datetime.entity_date", "date": "2011-10-10"}, + ] + assert len(datetime_calls) == 3 + for call in datetime_calls: + assert call.domain == "input_datetime" + assert call.data in valid_calls + valid_calls.remove(call.data)