diff --git a/CODEOWNERS b/CODEOWNERS index b059808408b..11cdf013aae 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -234,6 +234,8 @@ build.json @home-assistant/supervisor /homeassistant/components/cups/ @fabaff /homeassistant/components/daikin/ @fredrike /tests/components/daikin/ @fredrike +/homeassistant/components/date/ @home-assistant/core +/tests/components/date/ @home-assistant/core /homeassistant/components/debugpy/ @frenck /tests/components/debugpy/ @frenck /homeassistant/components/deconz/ @Kane610 diff --git a/homeassistant/components/date/__init__.py b/homeassistant/components/date/__init__.py new file mode 100644 index 00000000000..51f3a492c47 --- /dev/null +++ b/homeassistant/components/date/__init__.py @@ -0,0 +1,109 @@ +"""Component to allow setting date as platforms.""" +from __future__ import annotations + +from dataclasses import dataclass +from datetime import date, timedelta +import logging +from typing import final + +import voluptuous as vol + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ATTR_DATE +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", "DateEntity", "DateEntityDescription"] + + +async def _async_set_value(entity: DateEntity, service_call: ServiceCall) -> None: + """Service call wrapper to set a new date.""" + return await entity.async_set_value(service_call.data[ATTR_DATE]) + + +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up Date entities.""" + component = hass.data[DOMAIN] = EntityComponent[DateEntity]( + _LOGGER, DOMAIN, hass, SCAN_INTERVAL + ) + await component.async_setup(config) + + component.async_register_entity_service( + SERVICE_SET_VALUE, {vol.Required(ATTR_DATE): cv.date}, _async_set_value + ) + + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up a config entry.""" + component: EntityComponent[DateEntity] = 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[DateEntity] = hass.data[DOMAIN] + return await component.async_unload_entry(entry) + + +@dataclass +class DateEntityDescription(EntityDescription): + """A class that describes date entities.""" + + +class DateEntity(Entity): + """Representation of a Date entity.""" + + entity_description: DateEntityDescription + _attr_device_class: None + _attr_native_value: date | 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) -> date | None: + """Return the value reported by the date.""" + return self._attr_native_value + + def set_value(self, value: date) -> None: + """Change the date.""" + raise NotImplementedError() + + async def async_set_value(self, value: date) -> None: + """Change the date.""" + await self.hass.async_add_executor_job(self.set_value, value) diff --git a/homeassistant/components/date/const.py b/homeassistant/components/date/const.py new file mode 100644 index 00000000000..aa87b330e03 --- /dev/null +++ b/homeassistant/components/date/const.py @@ -0,0 +1,5 @@ +"""Provides the constants needed for the component.""" + +DOMAIN = "date" + +SERVICE_SET_VALUE = "set_value" diff --git a/homeassistant/components/date/manifest.json b/homeassistant/components/date/manifest.json new file mode 100644 index 00000000000..f0e51390ebf --- /dev/null +++ b/homeassistant/components/date/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "date", + "name": "Date", + "codeowners": ["@home-assistant/core"], + "documentation": "https://www.home-assistant.io/integrations/date", + "integration_type": "entity", + "quality_scale": "internal" +} diff --git a/homeassistant/components/date/services.yaml b/homeassistant/components/date/services.yaml new file mode 100644 index 00000000000..7ce1210f809 --- /dev/null +++ b/homeassistant/components/date/services.yaml @@ -0,0 +1,14 @@ +set_value: + name: Set Date + description: Set the date for a date entity. + target: + entity: + domain: date + fields: + date: + name: Date + description: The date to set. + required: true + example: "2022/11/01" + selector: + date: diff --git a/homeassistant/components/date/strings.json b/homeassistant/components/date/strings.json new file mode 100644 index 00000000000..110a4cabb92 --- /dev/null +++ b/homeassistant/components/date/strings.json @@ -0,0 +1,8 @@ +{ + "title": "Date", + "entity_component": { + "_": { + "name": "[%key:component::date::title%]" + } + } +} diff --git a/homeassistant/components/demo/__init__.py b/homeassistant/components/demo/__init__.py index 82cb8eff625..45ec00e4846 100644 --- a/homeassistant/components/demo/__init__.py +++ b/homeassistant/components/demo/__init__.py @@ -27,6 +27,7 @@ COMPONENTS_WITH_CONFIG_ENTRY_DEMO_PLATFORM = [ Platform.CAMERA, Platform.CLIMATE, Platform.COVER, + Platform.DATE, Platform.FAN, Platform.HUMIDIFIER, Platform.LIGHT, diff --git a/homeassistant/components/demo/date.py b/homeassistant/components/demo/date.py new file mode 100644 index 00000000000..eb96bc49038 --- /dev/null +++ b/homeassistant/components/demo/date.py @@ -0,0 +1,73 @@ +"""Demo platform that offers a fake Date entity.""" +from __future__ import annotations + +from datetime import date + +from homeassistant.components.date import DateEntity +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 entity.""" + async_add_entities( + [ + DemoDate( + "date", + "Date", + date(2020, 1, 1), + "mdi:calendar", + 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 DemoDate(DateEntity): + """Representation of a Demo date entity.""" + + _attr_should_poll = False + + def __init__( + self, + unique_id: str, + name: str, + state: date, + icon: str, + assumed_state: bool, + ) -> None: + """Initialize the Demo date 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: date) -> None: + """Update the date.""" + self._attr_native_value = value + self.async_write_ha_state() diff --git a/homeassistant/const.py b/homeassistant/const.py index 4f1586fc303..ad6a2576099 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -31,6 +31,7 @@ class Platform(StrEnum): CAMERA = "camera" CLIMATE = "climate" COVER = "cover" + DATE = "date" DEVICE_TRACKER = "device_tracker" FAN = "fan" GEO_LOCATION = "geo_location" diff --git a/tests/components/date/__init__.py b/tests/components/date/__init__.py new file mode 100644 index 00000000000..269734003d9 --- /dev/null +++ b/tests/components/date/__init__.py @@ -0,0 +1 @@ +"""Tests for the date component.""" diff --git a/tests/components/date/test_init.py b/tests/components/date/test_init.py new file mode 100644 index 00000000000..2ae17673119 --- /dev/null +++ b/tests/components/date/test_init.py @@ -0,0 +1,54 @@ +"""The tests for the date component.""" +from datetime import date + +from homeassistant.components.date import DOMAIN, SERVICE_SET_VALUE, DateEntity +from homeassistant.const import ( + ATTR_DATE, + ATTR_ENTITY_ID, + ATTR_FRIENDLY_NAME, + CONF_PLATFORM, +) +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + + +class MockDateEntity(DateEntity): + """Mock date device to use in tests.""" + + _attr_name = "date" + + def __init__(self, native_value=date(2020, 1, 1)) -> None: + """Initialize mock date entity.""" + self._attr_native_value = native_value + + async def async_set_value(self, value: date) -> None: + """Set the value of the date.""" + self._attr_native_value = value + + +async def test_date(hass: HomeAssistant, enable_custom_integrations: None) -> None: + """Test date 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("date.test") + assert state.state == "2020-01-01" + assert state.attributes == {ATTR_FRIENDLY_NAME: "test"} + + await hass.services.async_call( + DOMAIN, + SERVICE_SET_VALUE, + {ATTR_DATE: date(2021, 1, 1), ATTR_ENTITY_ID: "date.test"}, + blocking=True, + ) + await hass.async_block_till_done() + + state = hass.states.get("date.test") + assert state.state == "2021-01-01" + + date_entity = MockDateEntity(native_value=None) + assert date_entity.state is None + assert date_entity.state_attributes is None diff --git a/tests/components/demo/test_date.py b/tests/components/demo/test_date.py new file mode 100644 index 00000000000..c42ba06667e --- /dev/null +++ b/tests/components/demo/test_date.py @@ -0,0 +1,34 @@ +"""The tests for the demo date component.""" +import pytest + +from homeassistant.components.date import ATTR_DATE, DOMAIN, SERVICE_SET_VALUE +from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +ENTITY_DATE = "date.date" + + +@pytest.fixture(autouse=True) +async def setup_demo_date(hass: HomeAssistant) -> None: + """Initialize setup demo date.""" + assert await async_setup_component(hass, DOMAIN, {"date": {"platform": "demo"}}) + await hass.async_block_till_done() + + +def test_setup_params(hass: HomeAssistant) -> None: + """Test the initial parameters.""" + state = hass.states.get(ENTITY_DATE) + assert state.state == "2020-01-01" + + +async def test_set_datetime(hass: HomeAssistant) -> None: + """Test set datetime service.""" + await hass.services.async_call( + DOMAIN, + SERVICE_SET_VALUE, + {ATTR_ENTITY_ID: ENTITY_DATE, ATTR_DATE: "2021-02-03"}, + blocking=True, + ) + state = hass.states.get(ENTITY_DATE) + assert state.state == "2021-02-03" diff --git a/tests/testing_config/custom_components/test/date.py b/tests/testing_config/custom_components/test/date.py new file mode 100644 index 00000000000..b35be6f1919 --- /dev/null +++ b/tests/testing_config/custom_components/test/date.py @@ -0,0 +1,50 @@ +"""Provide a mock date platform. + +Call init before using it in your tests to ensure clean test data. +""" +from datetime import date + +from homeassistant.components.date import DateEntity + +from tests.common import MockEntity + +UNIQUE_DATE = "unique_date" + +ENTITIES = [] + + +class MockDateEntity(MockEntity, DateEntity): + """Mock date class.""" + + @property + def native_value(self): + """Return the native value of this date.""" + return self._handle("native_value") + + def set_value(self, value: date) -> None: + """Change the date.""" + self._values["native_value"] = value + + +def init(empty=False): + """Initialize the platform with entities.""" + global ENTITIES + + ENTITIES = ( + [] + if empty + else [ + MockDateEntity( + name="test", + unique_id=UNIQUE_DATE, + native_value=date(2020, 1, 1), + ), + ] + ) + + +async def async_setup_platform( + hass, config, async_add_entities_callback, discovery_info=None +): + """Return mock entities.""" + async_add_entities_callback(ENTITIES)