core/homeassistant/components/starlink/time.py

110 lines
3.6 KiB
Python

"""Contains time pickers exposed by the Starlink integration."""
from __future__ import annotations
from collections.abc import Awaitable, Callable
from dataclasses import dataclass
from datetime import UTC, datetime, time, tzinfo
import math
from homeassistant.components.time import TimeEntity, TimeEntityDescription
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DOMAIN
from .coordinator import StarlinkData, StarlinkUpdateCoordinator
from .entity import StarlinkEntity
async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""Set up all time entities for this entry."""
coordinator = hass.data[DOMAIN][entry.entry_id]
async_add_entities(
StarlinkTimeEntity(coordinator, description) for description in TIMES
)
@dataclass(frozen=True, kw_only=True)
class StarlinkTimeEntityDescription(TimeEntityDescription):
"""Describes a Starlink time entity."""
value_fn: Callable[[StarlinkData, tzinfo], time | None]
update_fn: Callable[[StarlinkUpdateCoordinator, time], Awaitable[None]]
available_fn: Callable[[StarlinkData], bool]
class StarlinkTimeEntity(StarlinkEntity, TimeEntity):
"""A TimeEntity for Starlink devices. Handles creating unique IDs."""
entity_description: StarlinkTimeEntityDescription
@property
def native_value(self) -> time | None:
"""Return the value reported by the time."""
return self.entity_description.value_fn(
self.coordinator.data, self.coordinator.timezone
)
@property
def available(self) -> bool:
"""Return True if entity is available."""
return self.entity_description.available_fn(self.coordinator.data)
async def async_set_value(self, value: time) -> None:
"""Change the time."""
return await self.entity_description.update_fn(self.coordinator, value)
def _utc_minutes_to_time(utc_minutes: int, timezone: tzinfo) -> time:
hour = math.floor(utc_minutes / 60)
if hour > 23:
hour -= 24
minute = utc_minutes % 60
try:
utc = datetime.now(UTC).replace(
hour=hour, minute=minute, second=0, microsecond=0
)
except ValueError as exc:
raise HomeAssistantError from exc
return utc.astimezone(timezone).time()
def _time_to_utc_minutes(t: time, timezone: tzinfo) -> int:
try:
zoned_time = datetime.now(timezone).replace(
hour=t.hour, minute=t.minute, second=0, microsecond=0
)
except ValueError as exc:
raise HomeAssistantError from exc
utc_time = zoned_time.astimezone(UTC).time()
return (utc_time.hour * 60) + utc_time.minute
TIMES = [
StarlinkTimeEntityDescription(
key="sleep_start",
translation_key="sleep_start",
value_fn=lambda data, timezone: _utc_minutes_to_time(data.sleep[0], timezone),
update_fn=lambda coordinator, time: coordinator.async_set_sleep_start(
_time_to_utc_minutes(time, coordinator.timezone)
),
available_fn=lambda data: data.sleep[2],
),
StarlinkTimeEntityDescription(
key="sleep_end",
translation_key="sleep_end",
value_fn=lambda data, timezone: _utc_minutes_to_time(
data.sleep[0] + data.sleep[1], timezone
),
update_fn=lambda coordinator, time: coordinator.async_set_sleep_duration(
_time_to_utc_minutes(time, coordinator.timezone)
),
available_fn=lambda data: data.sleep[2],
),
]