mirror of
https://github.com/home-assistant/core.git
synced 2025-07-22 04:37:06 +00:00
Add time
platform (#81949)
This commit is contained in:
parent
6f63ed07f9
commit
c0d0c89293
@ -1236,6 +1236,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/tile/ @bachya
|
/tests/components/tile/ @bachya
|
||||||
/homeassistant/components/tilt_ble/ @apt-itude
|
/homeassistant/components/tilt_ble/ @apt-itude
|
||||||
/tests/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
|
/homeassistant/components/time_date/ @fabaff
|
||||||
/tests/components/time_date/ @fabaff
|
/tests/components/time_date/ @fabaff
|
||||||
/homeassistant/components/tmb/ @alemuro
|
/homeassistant/components/tmb/ @alemuro
|
||||||
|
@ -40,6 +40,7 @@ COMPONENTS_WITH_CONFIG_ENTRY_DEMO_PLATFORM = [
|
|||||||
Platform.STT,
|
Platform.STT,
|
||||||
Platform.SWITCH,
|
Platform.SWITCH,
|
||||||
Platform.TEXT,
|
Platform.TEXT,
|
||||||
|
Platform.TIME,
|
||||||
Platform.UPDATE,
|
Platform.UPDATE,
|
||||||
Platform.VACUUM,
|
Platform.VACUUM,
|
||||||
Platform.WATER_HEATER,
|
Platform.WATER_HEATER,
|
||||||
|
63
homeassistant/components/demo/time.py
Normal file
63
homeassistant/components/demo/time.py
Normal file
@ -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()
|
109
homeassistant/components/time/__init__.py
Normal file
109
homeassistant/components/time/__init__.py
Normal file
@ -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)
|
5
homeassistant/components/time/const.py
Normal file
5
homeassistant/components/time/const.py
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
"""Provides the constants needed for the component."""
|
||||||
|
|
||||||
|
DOMAIN = "time"
|
||||||
|
|
||||||
|
SERVICE_SET_VALUE = "set_value"
|
8
homeassistant/components/time/manifest.json
Normal file
8
homeassistant/components/time/manifest.json
Normal file
@ -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"
|
||||||
|
}
|
14
homeassistant/components/time/services.yaml
Normal file
14
homeassistant/components/time/services.yaml
Normal file
@ -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:
|
8
homeassistant/components/time/strings.json
Normal file
8
homeassistant/components/time/strings.json
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"title": "Time",
|
||||||
|
"entity_component": {
|
||||||
|
"_": {
|
||||||
|
"name": "[%key:component::time::title%]"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -51,6 +51,7 @@ class Platform(StrEnum):
|
|||||||
STT = "stt"
|
STT = "stt"
|
||||||
SWITCH = "switch"
|
SWITCH = "switch"
|
||||||
TEXT = "text"
|
TEXT = "text"
|
||||||
|
TIME = "time"
|
||||||
TTS = "tts"
|
TTS = "tts"
|
||||||
VACUUM = "vacuum"
|
VACUUM = "vacuum"
|
||||||
UPDATE = "update"
|
UPDATE = "update"
|
||||||
|
34
tests/components/demo/test_time.py
Normal file
34
tests/components/demo/test_time.py
Normal file
@ -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"
|
1
tests/components/time/__init__.py
Normal file
1
tests/components/time/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
"""Tests for the time component."""
|
52
tests/components/time/test_init.py
Normal file
52
tests/components/time/test_init.py
Normal file
@ -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
|
50
tests/testing_config/custom_components/test/time.py
Normal file
50
tests/testing_config/custom_components/test/time.py
Normal file
@ -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)
|
Loading…
x
Reference in New Issue
Block a user