From 24290e5d08ab6cffc93be7e2b462b0e581e4e8ad Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Mon, 29 May 2023 17:24:15 -0400 Subject: [PATCH] Add `datetime` platform (#81943) Co-authored-by: Franck Nijhof Co-authored-by: Franck Nijhof --- .core_files.yaml | 1 + CODEOWNERS | 2 + homeassistant/bootstrap.py | 8 +- homeassistant/components/datetime/__init__.py | 126 ++++++++++++++++++ homeassistant/components/datetime/const.py | 7 + .../components/datetime/manifest.json | 8 ++ .../components/datetime/services.yaml | 14 ++ .../components/datetime/strings.json | 8 ++ homeassistant/components/demo/__init__.py | 1 + homeassistant/components/demo/datetime.py | 77 +++++++++++ .../components/input_datetime/__init__.py | 2 +- homeassistant/const.py | 6 + tests/components/datetime/__init__.py | 1 + tests/components/datetime/test_init.py | 98 ++++++++++++++ tests/components/demo/test_datetime.py | 35 +++++ tests/components/input_datetime/test_init.py | 54 ++++---- .../custom_components/test/datetime.py | 50 +++++++ 17 files changed, 468 insertions(+), 30 deletions(-) create mode 100644 homeassistant/components/datetime/__init__.py create mode 100644 homeassistant/components/datetime/const.py create mode 100644 homeassistant/components/datetime/manifest.json create mode 100644 homeassistant/components/datetime/services.yaml create mode 100644 homeassistant/components/datetime/strings.json create mode 100644 homeassistant/components/demo/datetime.py create mode 100644 tests/components/datetime/__init__.py create mode 100644 tests/components/datetime/test_init.py create mode 100644 tests/components/demo/test_datetime.py create mode 100644 tests/testing_config/custom_components/test/datetime.py diff --git a/.core_files.yaml b/.core_files.yaml index 118346408f8fa..9af81c599348a 100644 --- a/.core_files.yaml +++ b/.core_files.yaml @@ -21,6 +21,7 @@ base_platforms: &base_platforms - homeassistant/components/climate/** - homeassistant/components/cover/** - homeassistant/components/date/** + - homeassistant/components/datetime/** - homeassistant/components/device_tracker/** - homeassistant/components/diagnostics/** - homeassistant/components/fan/** diff --git a/CODEOWNERS b/CODEOWNERS index 3ca8df7ec16b9..44b7e4bce367d 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -240,6 +240,8 @@ build.json @home-assistant/supervisor /tests/components/daikin/ @fredrike /homeassistant/components/date/ @home-assistant/core /tests/components/date/ @home-assistant/core +/homeassistant/components/datetime/ @home-assistant/core +/tests/components/datetime/ @home-assistant/core /homeassistant/components/debugpy/ @frenck /tests/components/debugpy/ @frenck /homeassistant/components/deconz/ @Kane610 diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 46bd4b5d881eb..67b62da94d56c 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -19,6 +19,7 @@ from . import config as conf_util, config_entries, core, loader from .components import http from .const import ( + FORMAT_DATETIME, REQUIRED_NEXT_PYTHON_HA_RELEASE, REQUIRED_NEXT_PYTHON_VER, SIGNAL_BOOTSTRAP_INTEGRATIONS, @@ -347,7 +348,6 @@ def async_enable_logging( fmt = ( "%(asctime)s.%(msecs)03d %(levelname)s (%(threadName)s) [%(name)s] %(message)s" ) - datefmt = "%Y-%m-%d %H:%M:%S" if not log_no_color: try: @@ -362,7 +362,7 @@ def async_enable_logging( logging.getLogger().handlers[0].setFormatter( ColoredFormatter( colorfmt, - datefmt=datefmt, + datefmt=FORMAT_DATETIME, reset=True, log_colors={ "DEBUG": "cyan", @@ -378,7 +378,7 @@ def async_enable_logging( # If the above initialization failed for any reason, setup the default # formatting. If the above succeeds, this will result in a no-op. - logging.basicConfig(format=fmt, datefmt=datefmt, level=logging.INFO) + logging.basicConfig(format=fmt, datefmt=FORMAT_DATETIME, level=logging.INFO) # Capture warnings.warn(...) and friends messages in logs. # The standard destination for them is stderr, which may end up unnoticed. @@ -435,7 +435,7 @@ def async_enable_logging( _LOGGER.error("Error rolling over log file: %s", err) err_handler.setLevel(logging.INFO if verbose else logging.WARNING) - err_handler.setFormatter(logging.Formatter(fmt, datefmt=datefmt)) + err_handler.setFormatter(logging.Formatter(fmt, datefmt=FORMAT_DATETIME)) logger = logging.getLogger("") logger.addHandler(err_handler) diff --git a/homeassistant/components/datetime/__init__.py b/homeassistant/components/datetime/__init__.py new file mode 100644 index 0000000000000..fb67f4b1ffb68 --- /dev/null +++ b/homeassistant/components/datetime/__init__.py @@ -0,0 +1,126 @@ +"""Component to allow setting date/time as platforms.""" +from __future__ import annotations + +from dataclasses import dataclass +from datetime import datetime, timedelta, timezone +import logging +from typing import final + +import voluptuous as vol + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, ServiceCall +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.config_validation import ( # noqa: F401 + ENTITY_SERVICE_FIELDS, + PLATFORM_SCHEMA, + PLATFORM_SCHEMA_BASE, +) +from homeassistant.helpers.entity import Entity, EntityDescription +from homeassistant.helpers.entity_component import EntityComponent +from homeassistant.helpers.typing import ConfigType +from homeassistant.util import dt as dt_util + +from .const import ATTR_DATETIME, DOMAIN, SERVICE_SET_VALUE + +SCAN_INTERVAL = timedelta(seconds=30) + +ENTITY_ID_FORMAT = DOMAIN + ".{}" + +_LOGGER = logging.getLogger(__name__) + +__all__ = ["ATTR_DATETIME", "DOMAIN", "DateTimeEntity", "DateTimeEntityDescription"] + + +async def _async_set_value(entity: DateTimeEntity, service_call: ServiceCall) -> None: + """Service call wrapper to set a new date/time.""" + value: datetime = service_call.data[ATTR_DATETIME] + if value.tzinfo is None: + value = value.replace( + tzinfo=dt_util.get_time_zone(entity.hass.config.time_zone) + ) + return await entity.async_set_value(value) + + +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up Date/Time entities.""" + component = hass.data[DOMAIN] = EntityComponent[DateTimeEntity]( + _LOGGER, DOMAIN, hass, SCAN_INTERVAL + ) + await component.async_setup(config) + + component.async_register_entity_service( + SERVICE_SET_VALUE, + { + vol.Required(ATTR_DATETIME): cv.datetime, + **ENTITY_SERVICE_FIELDS, + }, + _async_set_value, + ) + + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up a config entry.""" + component: EntityComponent[DateTimeEntity] = hass.data[DOMAIN] + return await component.async_setup_entry(entry) + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + component: EntityComponent[DateTimeEntity] = hass.data[DOMAIN] + return await component.async_unload_entry(entry) + + +@dataclass +class DateTimeEntityDescription(EntityDescription): + """A class that describes date/time entities.""" + + +class DateTimeEntity(Entity): + """Representation of a Date/time entity.""" + + entity_description: DateTimeEntityDescription + _attr_device_class: None = None + _attr_state: None = None + _attr_native_value: datetime | None + + @property + @final + def device_class(self) -> None: + """Return entity device class.""" + return None + + @property + @final + def state_attributes(self) -> None: + """Return the state attributes.""" + return None + + @property + @final + def state(self) -> str | None: + """Return the entity state.""" + if (value := self.native_value) is None: + return None + if value.tzinfo is None: + raise ValueError( + f"Invalid datetime: {self.entity_id} provides state '{value}', " + "which is missing timezone information" + ) + + return value.astimezone(timezone.utc).isoformat(timespec="seconds") + + @property + def native_value(self) -> datetime | None: + """Return the value reported by the datetime.""" + return self._attr_native_value + + def set_value(self, value: datetime) -> None: + """Change the date/time.""" + raise NotImplementedError() + + async def async_set_value(self, value: datetime) -> None: + """Change the date/time.""" + await self.hass.async_add_executor_job(self.set_value, value) diff --git a/homeassistant/components/datetime/const.py b/homeassistant/components/datetime/const.py new file mode 100644 index 0000000000000..f9a5c4e538cf4 --- /dev/null +++ b/homeassistant/components/datetime/const.py @@ -0,0 +1,7 @@ +"""Provides the constants needed for the component.""" + +DOMAIN = "datetime" + +ATTR_DATETIME = "datetime" + +SERVICE_SET_VALUE = "set_value" diff --git a/homeassistant/components/datetime/manifest.json b/homeassistant/components/datetime/manifest.json new file mode 100644 index 0000000000000..469d9a8bd988c --- /dev/null +++ b/homeassistant/components/datetime/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "datetime", + "name": "Date/Time", + "codeowners": ["@home-assistant/core"], + "documentation": "https://www.home-assistant.io/integrations/datetime", + "integration_type": "entity", + "quality_scale": "internal" +} diff --git a/homeassistant/components/datetime/services.yaml b/homeassistant/components/datetime/services.yaml new file mode 100644 index 0000000000000..b5cce19e88b70 --- /dev/null +++ b/homeassistant/components/datetime/services.yaml @@ -0,0 +1,14 @@ +set_value: + name: Set Date/Time + description: Set the date/time for a datetime entity. + target: + entity: + domain: datetime + fields: + datetime: + name: Date & Time + description: The date/time to set. The time zone of the Home Assistant instance is assumed. + required: true + example: "2022/11/01 22:15" + selector: + datetime: diff --git a/homeassistant/components/datetime/strings.json b/homeassistant/components/datetime/strings.json new file mode 100644 index 0000000000000..3b97559018c77 --- /dev/null +++ b/homeassistant/components/datetime/strings.json @@ -0,0 +1,8 @@ +{ + "title": "Date/Time", + "entity_component": { + "_": { + "name": "[%key:component::datetime::title%]" + } + } +} diff --git a/homeassistant/components/demo/__init__.py b/homeassistant/components/demo/__init__.py index 3d271b54bc934..a84d7bf4f0b83 100644 --- a/homeassistant/components/demo/__init__.py +++ b/homeassistant/components/demo/__init__.py @@ -29,6 +29,7 @@ Platform.CLIMATE, Platform.COVER, Platform.DATE, + Platform.DATETIME, Platform.FAN, Platform.HUMIDIFIER, Platform.LIGHT, diff --git a/homeassistant/components/demo/datetime.py b/homeassistant/components/demo/datetime.py new file mode 100644 index 0000000000000..88027f58b9224 --- /dev/null +++ b/homeassistant/components/demo/datetime.py @@ -0,0 +1,77 @@ +"""Demo platform that offers a fake date/time entity.""" +from __future__ import annotations + +from datetime import datetime, timezone + +from homeassistant.components.datetime import DateTimeEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import DEVICE_DEFAULT_NAME +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType + +from . import DOMAIN + + +async def async_setup_platform( + hass: HomeAssistant, + config: ConfigType, + async_add_entities: AddEntitiesCallback, + discovery_info: DiscoveryInfoType | None = None, +) -> None: + """Set up the Demo date/time entity.""" + async_add_entities( + [ + DemoDateTime( + "datetime", + "Date and Time", + datetime(2020, 1, 1, 12, 0, 0, tzinfo=timezone.utc), + "mdi:calendar-clock", + False, + ), + ] + ) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Demo config entry.""" + await async_setup_platform(hass, {}, async_add_entities) + + +class DemoDateTime(DateTimeEntity): + """Representation of a Demo date/time entity.""" + + _attr_should_poll = False + + def __init__( + self, + unique_id: str, + name: str, + state: datetime, + icon: str, + assumed_state: bool, + ) -> None: + """Initialize the Demo date/time entity.""" + self._attr_assumed_state = assumed_state + self._attr_icon = icon + self._attr_name = name or DEVICE_DEFAULT_NAME + self._attr_native_value = state + self._attr_unique_id = unique_id + + self._attr_device_info = DeviceInfo( + identifiers={ + # Serial numbers are unique identifiers within a specific domain + (DOMAIN, unique_id) + }, + name=self.name, + ) + + async def async_set_value(self, value: datetime) -> None: + """Update the date/time.""" + self._attr_native_value = value + self.async_write_ha_state() diff --git a/homeassistant/components/input_datetime/__init__.py b/homeassistant/components/input_datetime/__init__.py index c51c0fdd67c4c..769b2d794d019 100644 --- a/homeassistant/components/input_datetime/__init__.py +++ b/homeassistant/components/input_datetime/__init__.py @@ -272,7 +272,7 @@ async def async_added_to_hass(self): if self.state is not None: return - default_value = py_datetime.datetime.today().strftime("%Y-%m-%d 00:00:00") + default_value = py_datetime.datetime.today().strftime(f"{FMT_DATE} 00:00:00") # Priority 2: Old state if (old_state := await self.async_get_last_state()) is None: diff --git a/homeassistant/const.py b/homeassistant/const.py index 46fc8b57568b7..f150938347d6f 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -32,6 +32,7 @@ class Platform(StrEnum): CLIMATE = "climate" COVER = "cover" DATE = "date" + DATETIME = "datetime" DEVICE_TRACKER = "device_tracker" FAN = "fan" GEO_LOCATION = "geo_location" @@ -1165,6 +1166,11 @@ class UnitOfDataRate(StrEnum): SIGNAL_BOOTSTRAP_INTEGRATIONS = "bootstrap_integrations" +# Date/Time formats +FORMAT_DATE: Final = "%Y-%m-%d" +FORMAT_TIME: Final = "%H:%M:%S" +FORMAT_DATETIME: Final = f"{FORMAT_DATE} {FORMAT_TIME}" + class EntityCategory(StrEnum): """Category of an entity. diff --git a/tests/components/datetime/__init__.py b/tests/components/datetime/__init__.py new file mode 100644 index 0000000000000..728e6e4a99ddf --- /dev/null +++ b/tests/components/datetime/__init__.py @@ -0,0 +1 @@ +"""Tests for the datetime component.""" diff --git a/tests/components/datetime/test_init.py b/tests/components/datetime/test_init.py new file mode 100644 index 0000000000000..66390c8d90f4d --- /dev/null +++ b/tests/components/datetime/test_init.py @@ -0,0 +1,98 @@ +"""The tests for the datetime component.""" +from datetime import datetime, timezone +from zoneinfo import ZoneInfo + +import pytest + +from homeassistant.components.datetime import ( + ATTR_DATETIME, + DOMAIN, + SERVICE_SET_VALUE, + DateTimeEntity, +) +from homeassistant.const import ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, CONF_PLATFORM +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +DEFAULT_VALUE = datetime(2020, 1, 1, 12, 0, 0, tzinfo=timezone.utc) + + +class MockDateTimeEntity(DateTimeEntity): + """Mock datetime device to use in tests.""" + + def __init__(self, native_value: datetime | None = DEFAULT_VALUE) -> None: + """Initialize mock datetime entity.""" + self._attr_native_value = native_value + + async def async_set_value(self, value: datetime) -> None: + """Change the date/time.""" + self._attr_native_value = value + + +async def test_datetime(hass: HomeAssistant, enable_custom_integrations: None) -> None: + """Test date/time entity.""" + hass.config.set_time_zone("UTC") + platform = getattr(hass.components, f"test.{DOMAIN}") + platform.init() + + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + await hass.async_block_till_done() + + state = hass.states.get("datetime.test") + assert state.state == "2020-01-01T01:02:03+00:00" + assert state.attributes == {ATTR_FRIENDLY_NAME: "test"} + + # Test updating datetime + await hass.services.async_call( + DOMAIN, + SERVICE_SET_VALUE, + {ATTR_DATETIME: datetime(2022, 3, 3, 3, 4, 5), ATTR_ENTITY_ID: "datetime.test"}, + blocking=True, + ) + await hass.async_block_till_done() + + state = hass.states.get("datetime.test") + assert state.state == "2022-03-03T03:04:05+00:00" + + # Test updating datetime with UTC timezone + await hass.services.async_call( + DOMAIN, + SERVICE_SET_VALUE, + {ATTR_DATETIME: "2022-03-03T03:04:05+00:00", ATTR_ENTITY_ID: "datetime.test"}, + blocking=True, + ) + await hass.async_block_till_done() + + state = hass.states.get("datetime.test") + assert state.state == "2022-03-03T03:04:05+00:00" + + # Test updating datetime with non UTC timezone + await hass.services.async_call( + DOMAIN, + SERVICE_SET_VALUE, + {ATTR_DATETIME: "2022-03-03T03:04:05-05:00", ATTR_ENTITY_ID: "datetime.test"}, + blocking=True, + ) + await hass.async_block_till_done() + + state = hass.states.get("datetime.test") + assert state.state == "2022-03-03T08:04:05+00:00" + + # Test that non UTC timezone gets converted to UTC + assert ( + MockDateTimeEntity( + native_value=datetime(2020, 1, 2, 3, 4, 5, tzinfo=ZoneInfo("US/Eastern")) + ).state + == "2020-01-02T08:04:05+00:00" + ) + + # Test None state + date_entity = MockDateTimeEntity(native_value=None) + assert date_entity.state is None + assert date_entity.state_attributes is None + + # Test that timezone is required to process state + with pytest.raises(ValueError): + assert MockDateTimeEntity( + native_value=datetime(2020, 1, 2, 3, 4, 5, tzinfo=None) + ).state diff --git a/tests/components/demo/test_datetime.py b/tests/components/demo/test_datetime.py new file mode 100644 index 0000000000000..90019f46af5d9 --- /dev/null +++ b/tests/components/demo/test_datetime.py @@ -0,0 +1,35 @@ +"""The tests for the demo datetime component.""" +import pytest + +from homeassistant.components.datetime import ATTR_DATETIME, DOMAIN, SERVICE_SET_VALUE +from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +ENTITY_DATETIME = "datetime.date_and_time" + + +@pytest.fixture(autouse=True) +async def setup_demo_datetime(hass: HomeAssistant) -> None: + """Initialize setup demo datetime.""" + assert await async_setup_component(hass, DOMAIN, {"datetime": {"platform": "demo"}}) + await hass.async_block_till_done() + + +def test_setup_params(hass: HomeAssistant) -> None: + """Test the initial parameters.""" + state = hass.states.get(ENTITY_DATETIME) + assert state.state == "2020-01-01T12:00:00+00:00" + + +async def test_set_datetime(hass: HomeAssistant) -> None: + """Test set datetime service.""" + hass.config.set_time_zone("UTC") + await hass.services.async_call( + DOMAIN, + SERVICE_SET_VALUE, + {ATTR_ENTITY_ID: ENTITY_DATETIME, ATTR_DATETIME: "2021-02-03 01:02:03"}, + blocking=True, + ) + state = hass.states.get(ENTITY_DATETIME) + assert state.state == "2021-02-03T01:02:03+00:00" diff --git a/tests/components/input_datetime/test_init.py b/tests/components/input_datetime/test_init.py index 834da847b7a2a..e9f9458611a93 100644 --- a/tests/components/input_datetime/test_init.py +++ b/tests/components/input_datetime/test_init.py @@ -19,12 +19,16 @@ CONFIG_SCHEMA, DEFAULT_TIME, DOMAIN, - FMT_DATE, - FMT_DATETIME, - FMT_TIME, SERVICE_RELOAD, ) -from homeassistant.const import ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, ATTR_NAME +from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_FRIENDLY_NAME, + ATTR_NAME, + FORMAT_DATE, + FORMAT_DATETIME, + FORMAT_TIME, +) from homeassistant.core import Context, CoreState, HomeAssistant, State from homeassistant.exceptions import Unauthorized from homeassistant.helpers import entity_registry as er @@ -136,7 +140,7 @@ async def test_set_datetime(hass: HomeAssistant) -> None: await async_set_date_and_time(hass, entity_id, dt_obj) state = hass.states.get(entity_id) - assert state.state == dt_obj.strftime(FMT_DATETIME) + assert state.state == dt_obj.strftime(FORMAT_DATETIME) assert state.attributes["has_time"] assert state.attributes["has_date"] @@ -164,7 +168,7 @@ async def test_set_datetime_2(hass: HomeAssistant) -> None: await async_set_datetime(hass, entity_id, dt_obj) state = hass.states.get(entity_id) - assert state.state == dt_obj.strftime(FMT_DATETIME) + assert state.state == dt_obj.strftime(FORMAT_DATETIME) assert state.attributes["has_time"] assert state.attributes["has_date"] @@ -192,7 +196,7 @@ async def test_set_datetime_3(hass: HomeAssistant) -> None: await async_set_timestamp(hass, entity_id, dt_util.as_utc(dt_obj).timestamp()) state = hass.states.get(entity_id) - assert state.state == dt_obj.strftime(FMT_DATETIME) + assert state.state == dt_obj.strftime(FORMAT_DATETIME) assert state.attributes["has_time"] assert state.attributes["has_date"] @@ -218,7 +222,7 @@ async def test_set_datetime_time(hass: HomeAssistant) -> None: await async_set_date_and_time(hass, entity_id, dt_obj) state = hass.states.get(entity_id) - assert state.state == dt_obj.strftime(FMT_TIME) + assert state.state == dt_obj.strftime(FORMAT_TIME) assert state.attributes["has_time"] assert not state.attributes["has_date"] @@ -337,7 +341,7 @@ async def test_restore_state(hass: HomeAssistant) -> None: "test_bogus_data": { "has_time": True, "has_date": True, - "initial": initial.strftime(FMT_DATETIME), + "initial": initial.strftime(FORMAT_DATETIME), }, "test_was_time": {"has_time": False, "has_date": True}, "test_was_date": {"has_time": True, "has_date": False}, @@ -347,22 +351,22 @@ async def test_restore_state(hass: HomeAssistant) -> None: dt_obj = datetime.datetime(2017, 9, 7, 19, 46) state_time = hass.states.get("input_datetime.test_time") - assert state_time.state == dt_obj.strftime(FMT_TIME) + assert state_time.state == dt_obj.strftime(FORMAT_TIME) state_date = hass.states.get("input_datetime.test_date") - assert state_date.state == dt_obj.strftime(FMT_DATE) + assert state_date.state == dt_obj.strftime(FORMAT_DATE) state_datetime = hass.states.get("input_datetime.test_datetime") - assert state_datetime.state == dt_obj.strftime(FMT_DATETIME) + assert state_datetime.state == dt_obj.strftime(FORMAT_DATETIME) state_bogus = hass.states.get("input_datetime.test_bogus_data") - assert state_bogus.state == initial.strftime(FMT_DATETIME) + assert state_bogus.state == initial.strftime(FORMAT_DATETIME) state_was_time = hass.states.get("input_datetime.test_was_time") - assert state_was_time.state == default.strftime(FMT_DATE) + assert state_was_time.state == default.strftime(FORMAT_DATE) state_was_date = hass.states.get("input_datetime.test_was_date") - assert state_was_date.state == default.strftime(FMT_TIME) + assert state_was_date.state == default.strftime(FORMAT_TIME) async def test_default_value(hass: HomeAssistant) -> None: @@ -381,15 +385,15 @@ async def test_default_value(hass: HomeAssistant) -> None: dt_obj = datetime.datetime.combine(datetime.date.today(), DEFAULT_TIME) state_time = hass.states.get("input_datetime.test_time") - assert state_time.state == dt_obj.strftime(FMT_TIME) + assert state_time.state == dt_obj.strftime(FORMAT_TIME) assert state_time.attributes.get("timestamp") is not None state_date = hass.states.get("input_datetime.test_date") - assert state_date.state == dt_obj.strftime(FMT_DATE) + assert state_date.state == dt_obj.strftime(FORMAT_DATE) assert state_date.attributes.get("timestamp") is not None state_datetime = hass.states.get("input_datetime.test_datetime") - assert state_datetime.state == dt_obj.strftime(FMT_DATETIME) + assert state_datetime.state == dt_obj.strftime(FORMAT_DATETIME) assert state_datetime.attributes.get("timestamp") is not None @@ -446,7 +450,7 @@ async def test_reload( assert state_1 is not None assert state_2 is None assert state_3 is not None - assert dt_obj.strftime(FMT_DATE) == state_1.state + assert dt_obj.strftime(FORMAT_DATE) == state_1.state assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, "dt1") == f"{DOMAIN}.dt1" assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, "dt2") is None assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, "dt3") == f"{DOMAIN}.dt3" @@ -484,10 +488,10 @@ async def test_reload( assert state_1 is not None assert state_2 is not None assert state_3 is None - assert state_1.state == DEFAULT_TIME.strftime(FMT_TIME) + assert state_1.state == DEFAULT_TIME.strftime(FORMAT_TIME) assert state_2.state == datetime.datetime.combine( datetime.date.today(), DEFAULT_TIME - ).strftime(FMT_DATETIME) + ).strftime(FORMAT_DATETIME) assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, "dt1") == f"{DOMAIN}.dt1" assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, "dt2") == f"{DOMAIN}.dt2" @@ -705,7 +709,7 @@ async def test_timestamp(hass: HomeAssistant) -> None: assert ( dt_util.as_local( dt_util.utc_from_timestamp(state_with_tz.attributes[ATTR_TIMESTAMP]) - ).strftime(FMT_DATETIME) + ).strftime(FORMAT_DATETIME) == "2020-12-13 01:00:00" ) @@ -719,13 +723,13 @@ async def test_timestamp(hass: HomeAssistant) -> None: assert ( dt_util.utc_from_timestamp( state_without_tz.attributes[ATTR_TIMESTAMP] - ).strftime(FMT_DATETIME) + ).strftime(FORMAT_DATETIME) == "2020-12-13 18:00:00" ) assert ( dt_util.as_local( dt_util.utc_from_timestamp(state_without_tz.attributes[ATTR_TIMESTAMP]) - ).strftime(FMT_DATETIME) + ).strftime(FORMAT_DATETIME) == "2020-12-13 10:00:00" ) # Use datetime.datetime.fromtimestamp @@ -734,7 +738,7 @@ async def test_timestamp(hass: HomeAssistant) -> None: datetime.datetime.fromtimestamp( state_without_tz.attributes[ATTR_TIMESTAMP], datetime.timezone.utc ) - ).strftime(FMT_DATETIME) + ).strftime(FORMAT_DATETIME) == "2020-12-13 10:00:00" ) diff --git a/tests/testing_config/custom_components/test/datetime.py b/tests/testing_config/custom_components/test/datetime.py new file mode 100644 index 0000000000000..7fca8d5788141 --- /dev/null +++ b/tests/testing_config/custom_components/test/datetime.py @@ -0,0 +1,50 @@ +"""Provide a mock time platform. + +Call init before using it in your tests to ensure clean test data. +""" +from datetime import datetime, timezone + +from homeassistant.components.datetime import DateTimeEntity + +from tests.common import MockEntity + +UNIQUE_DATETIME = "unique_datetime" + +ENTITIES = [] + + +class MockDateTimeEntity(MockEntity, DateTimeEntity): + """Mock date/time class.""" + + @property + def native_value(self): + """Return the native value of this date/time.""" + return self._handle("native_value") + + def set_value(self, value: datetime) -> None: + """Change the time.""" + self._values["native_value"] = value + + +def init(empty=False): + """Initialize the platform with entities.""" + global ENTITIES + + ENTITIES = ( + [] + if empty + else [ + MockDateTimeEntity( + name="test", + unique_id=UNIQUE_DATETIME, + native_value=datetime(2020, 1, 1, 1, 2, 3, tzinfo=timezone.utc), + ), + ] + ) + + +async def async_setup_platform( + hass, config, async_add_entities_callback, discovery_info=None +): + """Return mock entities.""" + async_add_entities_callback(ENTITIES)