1
0
mirror of https://github.com/home-assistant/core.git synced 2026-05-31 20:54:23 +01:00
Files
core/homeassistant/components/time_date/sensor.py
T
2026-04-30 21:14:48 +02:00

201 lines
6.5 KiB
Python

"""Support for showing the date and the time."""
from collections.abc import Callable, Mapping
from datetime import datetime, timedelta
import logging
from typing import Any
import voluptuous as vol
from homeassistant.components.sensor import (
ENTITY_ID_FORMAT,
PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA,
SensorEntity,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_DISPLAY_OPTIONS, EVENT_CORE_CONFIG_UPDATE
from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, callback
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.entity_platform import (
AddConfigEntryEntitiesCallback,
AddEntitiesCallback,
)
from homeassistant.helpers.event import async_track_point_in_utc_time
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.util import dt as dt_util
from .const import OPTION_TYPES
_LOGGER = logging.getLogger(__name__)
TIME_STR_FORMAT = "%H:%M"
PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend(
{
vol.Optional(CONF_DISPLAY_OPTIONS, default=["time"]): vol.All(
cv.ensure_list, [vol.In(OPTION_TYPES)]
)
}
)
async def async_setup_platform(
hass: HomeAssistant,
config: ConfigType,
async_add_entities: AddEntitiesCallback,
discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""Set up the Time and Date sensor."""
if hass.config.time_zone is None:
_LOGGER.error("Timezone is not set in Home Assistant configuration") # type: ignore[unreachable]
return
async_add_entities(
[TimeDateSensor(variable) for variable in config[CONF_DISPLAY_OPTIONS]]
)
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the Time & Date sensor."""
async_add_entities(
[TimeDateSensor(entry.options[CONF_DISPLAY_OPTIONS], entry.entry_id)]
)
class TimeDateSensor(SensorEntity):
"""Implementation of a Time and Date sensor."""
_attr_should_poll = False
_attr_has_entity_name = True
_state: str | None = None
unsub: CALLBACK_TYPE | None = None
def __init__(self, option_type: str, entry_id: str | None = None) -> None:
"""Initialize the sensor."""
self._attr_translation_key = option_type
self.type = option_type
self.entity_id = ENTITY_ID_FORMAT.format(option_type)
self._attr_unique_id = option_type if entry_id else None
self._update_internal_state(dt_util.utcnow())
@property
def native_value(self) -> str | None:
"""Return the state of the sensor."""
return self._state
@property
def icon(self) -> str:
"""Icon to use in the frontend, if any."""
if "date" in self.type and "time" in self.type:
return "mdi:calendar-clock"
if "date" in self.type:
return "mdi:calendar"
return "mdi:clock"
@callback
def async_start_preview(
self,
preview_callback: Callable[[str, Mapping[str, Any]], None],
) -> CALLBACK_TYPE:
"""Render a preview."""
@callback
def point_in_time_listener(time_date: datetime | None) -> None:
"""Update preview."""
now = dt_util.utcnow()
self._update_internal_state(now)
self.unsub = async_track_point_in_utc_time(
self.hass, point_in_time_listener, self.get_next_interval(now)
)
calculated_state = self._async_calculate_state()
preview_callback(calculated_state.state, calculated_state.attributes)
@callback
def async_stop_preview() -> None:
"""Stop preview."""
if self.unsub:
self.unsub()
self.unsub = None
point_in_time_listener(None)
return async_stop_preview
async def async_added_to_hass(self) -> None:
"""Set up first update."""
async def async_update_config(event: Event) -> None:
"""Handle core config update."""
self._update_state_and_setup_listener()
self.async_write_ha_state()
self.async_on_remove(
self.hass.bus.async_listen(EVENT_CORE_CONFIG_UPDATE, async_update_config)
)
self._update_state_and_setup_listener()
async def async_will_remove_from_hass(self) -> None:
"""Cancel next update."""
if self.unsub:
self.unsub()
self.unsub = None
def get_next_interval(self, time_date: datetime) -> datetime:
"""Compute next time an update should occur."""
if self.type == "date":
tomorrow = dt_util.as_local(time_date) + timedelta(days=1)
return dt_util.start_of_local_day(tomorrow)
timestamp = dt_util.as_timestamp(time_date)
interval = 60
delta = interval - (timestamp % interval)
next_interval = time_date + timedelta(seconds=delta)
_LOGGER.debug("%s + %s -> %s (%s)", time_date, delta, next_interval, self.type)
return next_interval
def _update_internal_state(self, time_date: datetime) -> None:
time = dt_util.as_local(time_date).strftime(TIME_STR_FORMAT)
time_utc = time_date.strftime(TIME_STR_FORMAT)
date = dt_util.as_local(time_date).date().isoformat()
date_utc = time_date.date().isoformat()
if self.type == "time":
self._state = time
elif self.type == "date":
self._state = date
elif self.type == "date_time":
self._state = f"{date}, {time}"
elif self.type == "date_time_utc":
self._state = f"{date_utc}, {time_utc}"
elif self.type == "time_date":
self._state = f"{time}, {date}"
elif self.type == "time_utc":
self._state = time_utc
elif self.type == "date_time_iso":
self._state = dt_util.parse_datetime(
f"{date} {time}", raise_on_error=True
).isoformat()
def _update_state_and_setup_listener(self) -> None:
"""Update state and setup listener for next interval."""
now = dt_util.utcnow()
self._update_internal_state(now)
self.unsub = async_track_point_in_utc_time(
self.hass, self.point_in_time_listener, self.get_next_interval(now)
)
@callback
def point_in_time_listener(self, time_date: datetime) -> None:
"""Get the latest data and update state."""
self._update_state_and_setup_listener()
self.async_write_ha_state()