From 4ff376cdd612f0355db5095de115c2782dc186cd Mon Sep 17 00:00:00 2001 From: Phil Bruckner Date: Wed, 26 Aug 2020 04:28:30 -0500 Subject: [PATCH] Add timestamp option for input_datetime.set_datetime (#39121) --- .../components/input_datetime/__init__.py | 78 +++++++++++-------- .../components/input_datetime/services.yaml | 11 ++- tests/components/input_datetime/test_init.py | 48 +++++++++++- 3 files changed, 96 insertions(+), 41 deletions(-) diff --git a/homeassistant/components/input_datetime/__init__.py b/homeassistant/components/input_datetime/__init__.py index d000f606c58..e95287d2cbe 100644 --- a/homeassistant/components/input_datetime/__init__.py +++ b/homeassistant/components/input_datetime/__init__.py @@ -37,8 +37,34 @@ DEFAULT_DATE = datetime.date(1970, 1, 1) DEFAULT_TIME = datetime.time(0, 0, 0) ATTR_DATETIME = "datetime" +ATTR_TIMESTAMP = "timestamp" + + +def validate_set_datetime_attrs(config): + """Validate set_datetime service attributes.""" + has_date_or_time_attr = any(key in config for key in (ATTR_DATE, ATTR_TIME)) + if ( + sum([has_date_or_time_attr, ATTR_DATETIME in config, ATTR_TIMESTAMP in config]) + > 1 + ): + raise vol.Invalid(f"Cannot use together: {', '.join(config.keys())}") + return config + SERVICE_SET_DATETIME = "set_datetime" +SERVICE_SET_DATETIME_SCHEMA = vol.All( + vol.Schema( + { + vol.Optional(ATTR_DATE): cv.date, + vol.Optional(ATTR_TIME): cv.time, + vol.Optional(ATTR_DATETIME): cv.datetime, + vol.Optional(ATTR_TIMESTAMP): vol.Coerce(float), + }, + extra=vol.ALLOW_EXTRA, + ), + cv.has_at_least_one_key(ATTR_DATE, ATTR_TIME, ATTR_DATETIME, ATTR_TIMESTAMP), + validate_set_datetime_attrs, +) STORAGE_KEY = DOMAIN STORAGE_VERSION = 1 @@ -138,37 +164,29 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: async def async_set_datetime_service(entity, call): """Handle a call to the input datetime 'set datetime' service.""" - time = call.data.get(ATTR_TIME) date = call.data.get(ATTR_DATE) + time = call.data.get(ATTR_TIME) dttm = call.data.get(ATTR_DATETIME) - if ( - dttm - and (date or time) - or entity.has_date - and not (date or dttm) - or entity.has_time - and not (time or dttm) - ): - _LOGGER.error( - "Invalid service data for %s input_datetime.set_datetime: %s", - entity.entity_id, - str(call.data), - ) - return + tmsp = call.data.get(ATTR_TIMESTAMP) + if tmsp: + dttm = dt_util.as_local(dt_util.utc_from_timestamp(tmsp)).replace( + tzinfo=None + ) if dttm: date = dttm.date() time = dttm.time() + if not entity.has_date: + date = None + if not entity.has_time: + time = None + if not date and not time: + raise vol.Invalid("Nothing to set") + entity.async_set_datetime(date, time) component.async_register_entity_service( - SERVICE_SET_DATETIME, - { - vol.Optional(ATTR_DATE): cv.date, - vol.Optional(ATTR_TIME): cv.time, - vol.Optional(ATTR_DATETIME): cv.datetime, - }, - async_set_datetime_service, + SERVICE_SET_DATETIME, SERVICE_SET_DATETIME_SCHEMA, async_set_datetime_service ) return True @@ -338,17 +356,11 @@ class InputDatetime(RestoreEntity): @callback def async_set_datetime(self, date_val, time_val): """Set a new date / time.""" - if self.has_date and self.has_time and date_val and time_val: - self._current_datetime = datetime.datetime.combine(date_val, time_val) - elif self.has_date and not self.has_time and date_val: - self._current_datetime = datetime.datetime.combine( - date_val, self._current_datetime.time() - ) - if self.has_time and not self.has_date and time_val: - self._current_datetime = datetime.datetime.combine( - self._current_datetime.date(), time_val - ) - + if not date_val: + date_val = self._current_datetime.date() + if not time_val: + time_val = self._current_datetime.time() + self._current_datetime = datetime.datetime.combine(date_val, time_val) self.async_write_ha_state() async def async_update_config(self, config: typing.Dict) -> None: diff --git a/homeassistant/components/input_datetime/services.yaml b/homeassistant/components/input_datetime/services.yaml index 26b2d088aea..bcbadc45aad 100644 --- a/homeassistant/components/input_datetime/services.yaml +++ b/homeassistant/components/input_datetime/services.yaml @@ -1,18 +1,21 @@ set_datetime: - description: This can be used to dynamically set the date and/or time. + description: This can be used to dynamically set the date and/or time. Use date/time, datetime or timestamp. fields: entity_id: description: Entity id of the input datetime to set the new value. example: input_datetime.test_date_time date: - description: The target date the entity should be set to. Do not use with datetime. + description: The target date the entity should be set to. example: '"2019-04-20"' time: - description: The target time the entity should be set to. Do not use with datetime. + description: The target time the entity should be set to. example: '"05:04:20"' datetime: - description: The target date & time the entity should be set to. Do not use with date or time. + description: The target date & time the entity should be set to. example: '"2019-04-20 05:04:20"' + timestamp: + description: The target date & time the entity should be set to as expressed by a UNIX timestamp. + example: 1598027400 reload: description: Reload the input_datetime configuration. diff --git a/tests/components/input_datetime/test_init.py b/tests/components/input_datetime/test_init.py index 0eb4d748563..70f0b69d3ef 100644 --- a/tests/components/input_datetime/test_init.py +++ b/tests/components/input_datetime/test_init.py @@ -10,6 +10,7 @@ from homeassistant.components.input_datetime import ( ATTR_DATETIME, ATTR_EDITABLE, ATTR_TIME, + ATTR_TIMESTAMP, CONF_HAS_DATE, CONF_HAS_TIME, CONF_ID, @@ -25,6 +26,7 @@ from homeassistant.core import Context, CoreState, State from homeassistant.exceptions import Unauthorized from homeassistant.helpers import entity_registry from homeassistant.setup import async_setup_component +from homeassistant.util import dt as dt_util from tests.async_mock import patch from tests.common import mock_restore_cache @@ -92,6 +94,16 @@ async def async_set_datetime(hass, entity_id, dt_value): ) +async def async_set_timestamp(hass, entity_id, timestamp): + """Set date and / or time of input_datetime.""" + await hass.services.async_call( + DOMAIN, + SERVICE_SET_DATETIME, + {ATTR_ENTITY_ID: entity_id, ATTR_TIMESTAMP: timestamp}, + blocking=True, + ) + + async def test_invalid_configs(hass): """Test config.""" invalid_configs = [ @@ -156,6 +168,32 @@ async def test_set_datetime_2(hass): assert state.attributes["timestamp"] == dt_obj.timestamp() +async def test_set_datetime_3(hass): + """Test set_datetime method using timestamp.""" + await async_setup_component( + hass, DOMAIN, {DOMAIN: {"test_datetime": {"has_time": True, "has_date": True}}} + ) + + entity_id = "input_datetime.test_datetime" + + dt_obj = datetime.datetime(2017, 9, 7, 19, 46, 30) + + await async_set_timestamp(hass, entity_id, dt_util.as_utc(dt_obj).timestamp()) + + state = hass.states.get(entity_id) + assert state.state == str(dt_obj) + assert state.attributes["has_time"] + assert state.attributes["has_date"] + + assert state.attributes["year"] == 2017 + assert state.attributes["month"] == 9 + assert state.attributes["day"] == 7 + assert state.attributes["hour"] == 19 + assert state.attributes["minute"] == 46 + assert state.attributes["second"] == 30 + assert state.attributes["timestamp"] == dt_obj.timestamp() + + async def test_set_datetime_time(hass): """Test set_datetime method with only time.""" await async_setup_component( @@ -199,7 +237,8 @@ async def test_set_invalid(hass): await hass.services.async_call( "input_datetime", "set_datetime", - {"entity_id": "test_date", "time": time_portion}, + {"entity_id": entity_id, "time": time_portion}, + blocking=True, ) await hass.async_block_till_done() @@ -229,7 +268,8 @@ async def test_set_invalid_2(hass): await hass.services.async_call( "input_datetime", "set_datetime", - {"entity_id": "test_date", "time": time_portion, "datetime": dt_obj}, + {"entity_id": entity_id, "time": time_portion, "datetime": dt_obj}, + blocking=True, ) await hass.async_block_till_done() @@ -358,8 +398,8 @@ async def test_input_datetime_context(hass, hass_admin_user): "input_datetime", "set_datetime", {"entity_id": state.entity_id, "date": "2018-01-02"}, - True, - Context(user_id=hass_admin_user.id), + blocking=True, + context=Context(user_id=hass_admin_user.id), ) state2 = hass.states.get("input_datetime.only_date")