From c0d0c89293df8e15b4288e0a4064291f77495aca Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Sun, 30 Apr 2023 16:33:51 -0400 Subject: [PATCH] Add `time` platform (#81949) --- CODEOWNERS | 2 + homeassistant/components/demo/__init__.py | 1 + homeassistant/components/demo/time.py | 63 ++++++++++ homeassistant/components/time/__init__.py | 109 ++++++++++++++++++ homeassistant/components/time/const.py | 5 + homeassistant/components/time/manifest.json | 8 ++ homeassistant/components/time/services.yaml | 14 +++ homeassistant/components/time/strings.json | 8 ++ homeassistant/const.py | 1 + tests/components/demo/test_time.py | 34 ++++++ tests/components/time/__init__.py | 1 + tests/components/time/test_init.py | 52 +++++++++ .../custom_components/test/time.py | 50 ++++++++ 13 files changed, 348 insertions(+) create mode 100644 homeassistant/components/demo/time.py create mode 100644 homeassistant/components/time/__init__.py create mode 100644 homeassistant/components/time/const.py create mode 100644 homeassistant/components/time/manifest.json create mode 100644 homeassistant/components/time/services.yaml create mode 100644 homeassistant/components/time/strings.json create mode 100644 tests/components/demo/test_time.py create mode 100644 tests/components/time/__init__.py create mode 100644 tests/components/time/test_init.py create mode 100644 tests/testing_config/custom_components/test/time.py diff --git a/CODEOWNERS b/CODEOWNERS index 11cdf013aae..ca0475f25ee 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1236,6 +1236,8 @@ build.json @home-assistant/supervisor /tests/components/tile/ @bachya /homeassistant/components/tilt_ble/ @apt-itude /tests/components/tilt_ble/ @apt-itude +/homeassistant/components/time/ @home-assistant/core +/tests/components/time/ @home-assistant/core /homeassistant/components/time_date/ @fabaff /tests/components/time_date/ @fabaff /homeassistant/components/tmb/ @alemuro diff --git a/homeassistant/components/demo/__init__.py b/homeassistant/components/demo/__init__.py index 45ec00e4846..85f7b89483f 100644 --- a/homeassistant/components/demo/__init__.py +++ b/homeassistant/components/demo/__init__.py @@ -40,6 +40,7 @@ COMPONENTS_WITH_CONFIG_ENTRY_DEMO_PLATFORM = [ Platform.STT, Platform.SWITCH, Platform.TEXT, + Platform.TIME, Platform.UPDATE, Platform.VACUUM, Platform.WATER_HEATER, diff --git a/homeassistant/components/demo/time.py b/homeassistant/components/demo/time.py new file mode 100644 index 00000000000..aafd425a024 --- /dev/null +++ b/homeassistant/components/demo/time.py @@ -0,0 +1,63 @@ +"""Demo platform that offers a fake time entity.""" +from __future__ import annotations + +from datetime import time + +from homeassistant.components.time import TimeEntity +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 time entity.""" + async_add_entities([DemoTime("time", "Time", time(12, 0, 0), "mdi: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 DemoTime(TimeEntity): + """Representation of a Demo time entity.""" + + _attr_should_poll = False + + def __init__( + self, + unique_id: str, + name: str, + state: time, + icon: str, + assumed_state: bool, + ) -> None: + """Initialize the Demo 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={(DOMAIN, unique_id)}, name=self.name + ) + + async def async_set_value(self, value: time) -> None: + """Update the time.""" + self._attr_native_value = value + self.async_write_ha_state() diff --git a/homeassistant/components/time/__init__.py b/homeassistant/components/time/__init__.py new file mode 100644 index 00000000000..26d40191fb9 --- /dev/null +++ b/homeassistant/components/time/__init__.py @@ -0,0 +1,109 @@ +"""Component to allow setting time as platforms.""" +from __future__ import annotations + +from dataclasses import dataclass +from datetime import time, timedelta +import logging +from typing import final + +import voluptuous as vol + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ATTR_TIME +from homeassistant.core import HomeAssistant, ServiceCall +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.config_validation import ( # noqa: F401 + 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 .const import DOMAIN, SERVICE_SET_VALUE + +SCAN_INTERVAL = timedelta(seconds=30) + +ENTITY_ID_FORMAT = DOMAIN + ".{}" + +_LOGGER = logging.getLogger(__name__) + +__all__ = ["DOMAIN", "TimeEntity", "TimeEntityDescription"] + + +async def _async_set_value(entity: TimeEntity, service_call: ServiceCall) -> None: + """Service call wrapper to set a new date.""" + return await entity.async_set_value(service_call.data[ATTR_TIME]) + + +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up Time entities.""" + component = hass.data[DOMAIN] = EntityComponent[TimeEntity]( + _LOGGER, DOMAIN, hass, SCAN_INTERVAL + ) + await component.async_setup(config) + + component.async_register_entity_service( + SERVICE_SET_VALUE, {vol.Required(ATTR_TIME): cv.time}, _async_set_value + ) + + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up a config entry.""" + component: EntityComponent[TimeEntity] = 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[TimeEntity] = hass.data[DOMAIN] + return await component.async_unload_entry(entry) + + +@dataclass +class TimeEntityDescription(EntityDescription): + """A class that describes time entities.""" + + +class TimeEntity(Entity): + """Representation of a Time entity.""" + + entity_description: TimeEntityDescription + _attr_native_value: time | None + _attr_device_class: None = None + _attr_state: None = None + + @property + @final + def device_class(self) -> None: + """Return the device class for the entity.""" + 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 self.native_value is None: + return None + return self.native_value.isoformat() + + @property + def native_value(self) -> time | None: + """Return the value reported by the time.""" + return self._attr_native_value + + def set_value(self, value: time) -> None: + """Change the time.""" + raise NotImplementedError() + + async def async_set_value(self, value: time) -> None: + """Change the time.""" + await self.hass.async_add_executor_job(self.set_value, value) diff --git a/homeassistant/components/time/const.py b/homeassistant/components/time/const.py new file mode 100644 index 00000000000..23901915c9f --- /dev/null +++ b/homeassistant/components/time/const.py @@ -0,0 +1,5 @@ +"""Provides the constants needed for the component.""" + +DOMAIN = "time" + +SERVICE_SET_VALUE = "set_value" diff --git a/homeassistant/components/time/manifest.json b/homeassistant/components/time/manifest.json new file mode 100644 index 00000000000..f3c243e0005 --- /dev/null +++ b/homeassistant/components/time/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "time", + "name": "Time", + "codeowners": ["@home-assistant/core"], + "documentation": "https://www.home-assistant.io/integrations/time", + "integration_type": "entity", + "quality_scale": "internal" +} diff --git a/homeassistant/components/time/services.yaml b/homeassistant/components/time/services.yaml new file mode 100644 index 00000000000..a8c843ab55a --- /dev/null +++ b/homeassistant/components/time/services.yaml @@ -0,0 +1,14 @@ +set_value: + name: Set Time + description: Set the time for a time entity. + target: + entity: + domain: time + fields: + time: + name: Time + description: The time to set. + required: true + example: "22:15" + selector: + time: diff --git a/homeassistant/components/time/strings.json b/homeassistant/components/time/strings.json new file mode 100644 index 00000000000..e8d92a30e2e --- /dev/null +++ b/homeassistant/components/time/strings.json @@ -0,0 +1,8 @@ +{ + "title": "Time", + "entity_component": { + "_": { + "name": "[%key:component::time::title%]" + } + } +} diff --git a/homeassistant/const.py b/homeassistant/const.py index ad6a2576099..46fc8b57568 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -51,6 +51,7 @@ class Platform(StrEnum): STT = "stt" SWITCH = "switch" TEXT = "text" + TIME = "time" TTS = "tts" VACUUM = "vacuum" UPDATE = "update" diff --git a/tests/components/demo/test_time.py b/tests/components/demo/test_time.py new file mode 100644 index 00000000000..555cfe3ffc9 --- /dev/null +++ b/tests/components/demo/test_time.py @@ -0,0 +1,34 @@ +"""The tests for the demo time component.""" +import pytest + +from homeassistant.components.time import ATTR_TIME, DOMAIN, SERVICE_SET_VALUE +from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +ENTITY_TIME = "time.time" + + +@pytest.fixture(autouse=True) +async def setup_demo_datetime(hass: HomeAssistant) -> None: + """Initialize setup demo time.""" + assert await async_setup_component(hass, DOMAIN, {"time": {"platform": "demo"}}) + await hass.async_block_till_done() + + +def test_setup_params(hass: HomeAssistant) -> None: + """Test the initial parameters.""" + state = hass.states.get(ENTITY_TIME) + assert state.state == "12:00:00" + + +async def test_set_value(hass: HomeAssistant) -> None: + """Test set value service.""" + await hass.services.async_call( + DOMAIN, + SERVICE_SET_VALUE, + {ATTR_ENTITY_ID: ENTITY_TIME, ATTR_TIME: "01:02:03"}, + blocking=True, + ) + state = hass.states.get(ENTITY_TIME) + assert state.state == "01:02:03" diff --git a/tests/components/time/__init__.py b/tests/components/time/__init__.py new file mode 100644 index 00000000000..dc24f4e5328 --- /dev/null +++ b/tests/components/time/__init__.py @@ -0,0 +1 @@ +"""Tests for the time component.""" diff --git a/tests/components/time/test_init.py b/tests/components/time/test_init.py new file mode 100644 index 00000000000..a3248c96361 --- /dev/null +++ b/tests/components/time/test_init.py @@ -0,0 +1,52 @@ +"""The tests for the time component.""" +from datetime import time + +from homeassistant.components.time import DOMAIN, SERVICE_SET_VALUE, TimeEntity +from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_FRIENDLY_NAME, + ATTR_TIME, + CONF_PLATFORM, +) +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + + +class MockTimeEntity(TimeEntity): + """Mock time device to use in tests.""" + + def __init__(self, native_value=time(12, 0, 0)) -> None: + """Initialize mock time entity.""" + self._attr_native_value = native_value + + async def async_set_value(self, value: time) -> None: + """Set the value of the time.""" + self._attr_native_value = value + + +async def test_date(hass: HomeAssistant, enable_custom_integrations: None) -> None: + """Test time entity.""" + 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("time.test") + assert state.state == "01:02:03" + assert state.attributes == {ATTR_FRIENDLY_NAME: "test"} + + await hass.services.async_call( + DOMAIN, + SERVICE_SET_VALUE, + {ATTR_TIME: time(2, 3, 4), ATTR_ENTITY_ID: "time.test"}, + blocking=True, + ) + await hass.async_block_till_done() + + state = hass.states.get("time.test") + assert state.state == "02:03:04" + + date_entity = MockTimeEntity(native_value=None) + assert date_entity.state is None + assert date_entity.state_attributes is None diff --git a/tests/testing_config/custom_components/test/time.py b/tests/testing_config/custom_components/test/time.py new file mode 100644 index 00000000000..9c2f991d694 --- /dev/null +++ b/tests/testing_config/custom_components/test/time.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 time + +from homeassistant.components.time import TimeEntity + +from tests.common import MockEntity + +UNIQUE_TIME = "unique_time" + +ENTITIES = [] + + +class MockTimeEntity(MockEntity, TimeEntity): + """Mock time class.""" + + @property + def native_value(self): + """Return the native value of this time.""" + return self._handle("native_value") + + def set_value(self, value: time) -> None: + """Change the time.""" + self._values["native_value"] = value + + +def init(empty=False): + """Initialize the platform with entities.""" + global ENTITIES + + ENTITIES = ( + [] + if empty + else [ + MockTimeEntity( + name="test", + unique_id=UNIQUE_TIME, + native_value=time(1, 2, 3), + ), + ] + ) + + +async def async_setup_platform( + hass, config, async_add_entities_callback, discovery_info=None +): + """Return mock entities.""" + async_add_entities_callback(ENTITIES)