Add config flow for Time & Date (#104183)
Co-authored-by: Erik <erik@montnemery.com>
This commit is contained in:
parent
eaa32146a6
commit
65581e94ea
15 changed files with 573 additions and 88 deletions
|
@ -1 +1,18 @@
|
||||||
"""The time_date component."""
|
"""The time_date component."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
|
from .const import PLATFORMS
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
|
"""Set up Time & Date from a config entry."""
|
||||||
|
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
|
"""Unload Time & Date config entry."""
|
||||||
|
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||||
|
|
130
homeassistant/components/time_date/config_flow.py
Normal file
130
homeassistant/components/time_date/config_flow.py
Normal file
|
@ -0,0 +1,130 @@
|
||||||
|
"""Adds config flow for Time & Date integration."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Mapping
|
||||||
|
from datetime import timedelta
|
||||||
|
import logging
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant.components import websocket_api
|
||||||
|
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
|
||||||
|
from homeassistant.core import HomeAssistant, callback
|
||||||
|
from homeassistant.helpers.entity_platform import EntityPlatform
|
||||||
|
from homeassistant.helpers.schema_config_entry_flow import (
|
||||||
|
SchemaCommonFlowHandler,
|
||||||
|
SchemaConfigFlowHandler,
|
||||||
|
SchemaFlowError,
|
||||||
|
SchemaFlowFormStep,
|
||||||
|
)
|
||||||
|
from homeassistant.helpers.selector import (
|
||||||
|
SelectSelector,
|
||||||
|
SelectSelectorConfig,
|
||||||
|
SelectSelectorMode,
|
||||||
|
)
|
||||||
|
from homeassistant.setup import async_prepare_setup_platform
|
||||||
|
|
||||||
|
from .const import CONF_DISPLAY_OPTIONS, DOMAIN, OPTION_TYPES
|
||||||
|
from .sensor import TimeDateSensor
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
USER_SCHEMA = vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Required(CONF_DISPLAY_OPTIONS): SelectSelector(
|
||||||
|
SelectSelectorConfig(
|
||||||
|
options=[option for option in OPTION_TYPES if option != "beat"],
|
||||||
|
mode=SelectSelectorMode.DROPDOWN,
|
||||||
|
translation_key="display_options",
|
||||||
|
)
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def validate_input(
|
||||||
|
handler: SchemaCommonFlowHandler, user_input: dict[str, Any]
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""Validate rest setup."""
|
||||||
|
hass = handler.parent_handler.hass
|
||||||
|
if hass.config.time_zone is None:
|
||||||
|
raise SchemaFlowError("timezone_not_exist")
|
||||||
|
return user_input
|
||||||
|
|
||||||
|
|
||||||
|
CONFIG_FLOW = {
|
||||||
|
"user": SchemaFlowFormStep(
|
||||||
|
schema=USER_SCHEMA,
|
||||||
|
preview=DOMAIN,
|
||||||
|
validate_user_input=validate_input,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class TimeDateConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN):
|
||||||
|
"""Handle a config flow for Time & Date."""
|
||||||
|
|
||||||
|
config_flow = CONFIG_FLOW
|
||||||
|
|
||||||
|
def async_config_entry_title(self, options: Mapping[str, Any]) -> str:
|
||||||
|
"""Return config entry title."""
|
||||||
|
return f"Time & Date {options[CONF_DISPLAY_OPTIONS]}"
|
||||||
|
|
||||||
|
def async_config_flow_finished(self, options: Mapping[str, Any]) -> None:
|
||||||
|
"""Abort if instance already exist."""
|
||||||
|
self._async_abort_entries_match(dict(options))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def async_setup_preview(hass: HomeAssistant) -> None:
|
||||||
|
"""Set up preview WS API."""
|
||||||
|
websocket_api.async_register_command(hass, ws_start_preview)
|
||||||
|
|
||||||
|
|
||||||
|
@websocket_api.websocket_command(
|
||||||
|
{
|
||||||
|
vol.Required("type"): "time_date/start_preview",
|
||||||
|
vol.Required("flow_id"): str,
|
||||||
|
vol.Required("flow_type"): vol.Any("config_flow"),
|
||||||
|
vol.Required("user_input"): dict,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
@websocket_api.async_response
|
||||||
|
async def ws_start_preview(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
connection: websocket_api.ActiveConnection,
|
||||||
|
msg: dict[str, Any],
|
||||||
|
) -> None:
|
||||||
|
"""Generate a preview."""
|
||||||
|
validated = USER_SCHEMA(msg["user_input"])
|
||||||
|
|
||||||
|
# Create an EntityPlatform, needed for name translations
|
||||||
|
platform = await async_prepare_setup_platform(hass, {}, SENSOR_DOMAIN, DOMAIN)
|
||||||
|
entity_platform = EntityPlatform(
|
||||||
|
hass=hass,
|
||||||
|
logger=_LOGGER,
|
||||||
|
domain=SENSOR_DOMAIN,
|
||||||
|
platform_name=DOMAIN,
|
||||||
|
platform=platform,
|
||||||
|
scan_interval=timedelta(seconds=3600),
|
||||||
|
entity_namespace=None,
|
||||||
|
)
|
||||||
|
await entity_platform.async_load_translations()
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_preview_updated(state: str, attributes: Mapping[str, Any]) -> None:
|
||||||
|
"""Forward config entry state events to websocket."""
|
||||||
|
connection.send_message(
|
||||||
|
websocket_api.event_message(
|
||||||
|
msg["id"], {"attributes": attributes, "state": state}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
preview_entity = TimeDateSensor(validated[CONF_DISPLAY_OPTIONS])
|
||||||
|
preview_entity.hass = hass
|
||||||
|
preview_entity.platform = entity_platform
|
||||||
|
|
||||||
|
connection.send_result(msg["id"])
|
||||||
|
connection.subscriptions[msg["id"]] = preview_entity.async_start_preview(
|
||||||
|
async_preview_updated
|
||||||
|
)
|
|
@ -3,4 +3,20 @@ from __future__ import annotations
|
||||||
|
|
||||||
from typing import Final
|
from typing import Final
|
||||||
|
|
||||||
|
from homeassistant.const import Platform
|
||||||
|
|
||||||
|
CONF_DISPLAY_OPTIONS = "display_options"
|
||||||
DOMAIN: Final = "time_date"
|
DOMAIN: Final = "time_date"
|
||||||
|
PLATFORMS = [Platform.SENSOR]
|
||||||
|
TIME_STR_FORMAT = "%H:%M"
|
||||||
|
|
||||||
|
OPTION_TYPES = [
|
||||||
|
"time",
|
||||||
|
"date",
|
||||||
|
"date_time",
|
||||||
|
"date_time_utc",
|
||||||
|
"date_time_iso",
|
||||||
|
"time_date",
|
||||||
|
"beat",
|
||||||
|
"time_utc",
|
||||||
|
]
|
||||||
|
|
|
@ -2,7 +2,9 @@
|
||||||
"domain": "time_date",
|
"domain": "time_date",
|
||||||
"name": "Time & Date",
|
"name": "Time & Date",
|
||||||
"codeowners": ["@fabaff"],
|
"codeowners": ["@fabaff"],
|
||||||
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/time_date",
|
"documentation": "https://www.home-assistant.io/integrations/time_date",
|
||||||
|
"integration_type": "service",
|
||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
"quality_scale": "internal"
|
"quality_scale": "internal"
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,19 @@
|
||||||
"""Support for showing the date and the time."""
|
"""Support for showing the date and the time."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Callable, Mapping
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
import logging
|
import logging
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
|
from homeassistant.components.sensor import (
|
||||||
|
ENTITY_ID_FORMAT,
|
||||||
|
PLATFORM_SCHEMA,
|
||||||
|
SensorEntity,
|
||||||
|
)
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import CONF_DISPLAY_OPTIONS, EVENT_CORE_CONFIG_UPDATE
|
from homeassistant.const import CONF_DISPLAY_OPTIONS, EVENT_CORE_CONFIG_UPDATE
|
||||||
from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, callback
|
from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, callback
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
@ -16,22 +23,12 @@ from homeassistant.helpers.issue_registry import IssueSeverity, async_create_iss
|
||||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||||
import homeassistant.util.dt as dt_util
|
import homeassistant.util.dt as dt_util
|
||||||
|
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN, OPTION_TYPES
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
TIME_STR_FORMAT = "%H:%M"
|
TIME_STR_FORMAT = "%H:%M"
|
||||||
|
|
||||||
OPTION_TYPES = {
|
|
||||||
"time": "Time",
|
|
||||||
"date": "Date",
|
|
||||||
"date_time": "Date & Time",
|
|
||||||
"date_time_utc": "Date & Time (UTC)",
|
|
||||||
"date_time_iso": "Date & Time (ISO)",
|
|
||||||
"time_date": "Time & Date",
|
|
||||||
"beat": "Internet Time",
|
|
||||||
"time_utc": "Time (UTC)",
|
|
||||||
}
|
|
||||||
|
|
||||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||||
{
|
{
|
||||||
|
@ -71,7 +68,17 @@ async def async_setup_platform(
|
||||||
_LOGGER.warning("'beat': is deprecated and will be removed in version 2024.7")
|
_LOGGER.warning("'beat': is deprecated and will be removed in version 2024.7")
|
||||||
|
|
||||||
async_add_entities(
|
async_add_entities(
|
||||||
[TimeDateSensor(hass, variable) for variable in config[CONF_DISPLAY_OPTIONS]]
|
[TimeDateSensor(variable) for variable in config[CONF_DISPLAY_OPTIONS]]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
||||||
|
) -> None:
|
||||||
|
"""Set up the Time & Date sensor."""
|
||||||
|
|
||||||
|
async_add_entities(
|
||||||
|
[TimeDateSensor(entry.options[CONF_DISPLAY_OPTIONS], entry.entry_id)]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -79,19 +86,19 @@ class TimeDateSensor(SensorEntity):
|
||||||
"""Implementation of a Time and Date sensor."""
|
"""Implementation of a Time and Date sensor."""
|
||||||
|
|
||||||
_attr_should_poll = False
|
_attr_should_poll = False
|
||||||
|
_attr_has_entity_name = True
|
||||||
|
_state: str | None = None
|
||||||
|
unsub: CALLBACK_TYPE | None = None
|
||||||
|
|
||||||
def __init__(self, hass: HomeAssistant, option_type: str) -> None:
|
def __init__(self, option_type: str, entry_id: str | None = None) -> None:
|
||||||
"""Initialize the sensor."""
|
"""Initialize the sensor."""
|
||||||
self._name = OPTION_TYPES[option_type]
|
self._attr_translation_key = option_type
|
||||||
self.type = option_type
|
self.type = option_type
|
||||||
self._state: str | None = None
|
object_id = "internet_time" if option_type == "beat" else option_type
|
||||||
self.hass = hass
|
self.entity_id = ENTITY_ID_FORMAT.format(object_id)
|
||||||
self.unsub: CALLBACK_TYPE | None = None
|
self._attr_unique_id = option_type if entry_id else None
|
||||||
|
|
||||||
@property
|
self._update_internal_state(dt_util.utcnow())
|
||||||
def name(self) -> str:
|
|
||||||
"""Return the name of the sensor."""
|
|
||||||
return self._name
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def native_value(self) -> str | None:
|
def native_value(self) -> str | None:
|
||||||
|
@ -107,6 +114,35 @@ class TimeDateSensor(SensorEntity):
|
||||||
return "mdi:calendar"
|
return "mdi:calendar"
|
||||||
return "mdi:clock"
|
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:
|
async def async_added_to_hass(self) -> None:
|
||||||
"""Set up first update."""
|
"""Set up first update."""
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,83 @@
|
||||||
{
|
{
|
||||||
|
"title": "Time & Date",
|
||||||
|
"config": {
|
||||||
|
"abort": {
|
||||||
|
"already_configured": "The chosen Time & Date sensor has already been configured"
|
||||||
|
},
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"description": "Select from the sensor options below",
|
||||||
|
"data": {
|
||||||
|
"display_options": "Sensor type"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"timezone_not_exist": "Timezone is not set in Home Assistant configuration"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"step": {
|
||||||
|
"init": {
|
||||||
|
"data": {
|
||||||
|
"display_options": "[%key:component::time_date::config::step::user::data::display_options%]"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"selector": {
|
||||||
|
"display_options": {
|
||||||
|
"options": {
|
||||||
|
"time": "Time",
|
||||||
|
"date": "Date",
|
||||||
|
"date_time": "Date & Time",
|
||||||
|
"date_time_utc": "Date & Time (UTC)",
|
||||||
|
"date_time_iso": "Date & Time (ISO)",
|
||||||
|
"time_date": "Time & Date",
|
||||||
|
"beat": "Internet time",
|
||||||
|
"time_utc": "Time (UTC)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"entity": {
|
||||||
|
"sensor": {
|
||||||
|
"time": {
|
||||||
|
"name": "[%key:component::time_date::selector::display_options::options::time%]"
|
||||||
|
},
|
||||||
|
"date": {
|
||||||
|
"name": "[%key:component::time_date::selector::display_options::options::date%]"
|
||||||
|
},
|
||||||
|
"date_time": {
|
||||||
|
"name": "[%key:component::time_date::selector::display_options::options::date_time%]"
|
||||||
|
},
|
||||||
|
"date_time_utc": {
|
||||||
|
"name": "[%key:component::time_date::selector::display_options::options::date_time_utc%]"
|
||||||
|
},
|
||||||
|
"date_time_iso": {
|
||||||
|
"name": "[%key:component::time_date::selector::display_options::options::date_time_iso%]"
|
||||||
|
},
|
||||||
|
"time_date": {
|
||||||
|
"name": "[%key:component::time_date::selector::display_options::options::time_date%]"
|
||||||
|
},
|
||||||
|
"beat": {
|
||||||
|
"name": "[%key:component::time_date::selector::display_options::options::beat%]"
|
||||||
|
},
|
||||||
|
"time_utc": {
|
||||||
|
"name": "[%key:component::time_date::selector::display_options::options::time_utc%]"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"issues": {
|
"issues": {
|
||||||
"deprecated_beat": {
|
"deprecated_beat": {
|
||||||
"title": "The `{config_key}` Time & Date sensor is being removed",
|
"title": "The `{config_key}` Time & Date sensor is being removed",
|
||||||
"description": "Please remove the `{config_key}` key from the `{display_options}` for the {integration} entry in your configuration.yaml file and restart Home Assistant to fix this issue."
|
"fix_flow": {
|
||||||
|
"step": {
|
||||||
|
"confirm": {
|
||||||
|
"title": "[%key:component::time_date::issues::deprecated_beat::title%]",
|
||||||
|
"description": "Please remove the `{config_key}` key from the {integration} config entry options and click submit to fix this issue."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -520,6 +520,7 @@ FLOWS = {
|
||||||
"tibber",
|
"tibber",
|
||||||
"tile",
|
"tile",
|
||||||
"tilt_ble",
|
"tilt_ble",
|
||||||
|
"time_date",
|
||||||
"todoist",
|
"todoist",
|
||||||
"tolo",
|
"tolo",
|
||||||
"tomorrowio",
|
"tomorrowio",
|
||||||
|
|
|
@ -6026,9 +6026,8 @@
|
||||||
"iot_class": "local_push"
|
"iot_class": "local_push"
|
||||||
},
|
},
|
||||||
"time_date": {
|
"time_date": {
|
||||||
"name": "Time & Date",
|
"integration_type": "service",
|
||||||
"integration_type": "hub",
|
"config_flow": true,
|
||||||
"config_flow": false,
|
|
||||||
"iot_class": "local_push"
|
"iot_class": "local_push"
|
||||||
},
|
},
|
||||||
"tmb": {
|
"tmb": {
|
||||||
|
@ -7051,6 +7050,7 @@
|
||||||
"switch_as_x",
|
"switch_as_x",
|
||||||
"tag",
|
"tag",
|
||||||
"threshold",
|
"threshold",
|
||||||
|
"time_date",
|
||||||
"tod",
|
"tod",
|
||||||
"uptime",
|
"uptime",
|
||||||
"utility_meter",
|
"utility_meter",
|
||||||
|
|
|
@ -382,7 +382,7 @@ class EntityComponent(Generic[_EntityT]):
|
||||||
if scan_interval is None:
|
if scan_interval is None:
|
||||||
scan_interval = self.scan_interval
|
scan_interval = self.scan_interval
|
||||||
|
|
||||||
return EntityPlatform(
|
entity_platform = EntityPlatform(
|
||||||
hass=self.hass,
|
hass=self.hass,
|
||||||
logger=self.logger,
|
logger=self.logger,
|
||||||
domain=self.domain,
|
domain=self.domain,
|
||||||
|
@ -391,6 +391,8 @@ class EntityComponent(Generic[_EntityT]):
|
||||||
scan_interval=scan_interval,
|
scan_interval=scan_interval,
|
||||||
entity_namespace=entity_namespace,
|
entity_namespace=entity_namespace,
|
||||||
)
|
)
|
||||||
|
entity_platform.async_prepare()
|
||||||
|
return entity_platform
|
||||||
|
|
||||||
async def _async_shutdown(self, event: Event) -> None:
|
async def _async_shutdown(self, event: Event) -> None:
|
||||||
"""Call when Home Assistant is stopping."""
|
"""Call when Home Assistant is stopping."""
|
||||||
|
|
|
@ -145,10 +145,6 @@ class EntityPlatform:
|
||||||
# which powers entity_component.add_entities
|
# which powers entity_component.add_entities
|
||||||
self.parallel_updates_created = platform is None
|
self.parallel_updates_created = platform is None
|
||||||
|
|
||||||
hass.data.setdefault(DATA_ENTITY_PLATFORM, {}).setdefault(
|
|
||||||
self.platform_name, []
|
|
||||||
).append(self)
|
|
||||||
|
|
||||||
self.domain_entities: dict[str, Entity] = hass.data.setdefault(
|
self.domain_entities: dict[str, Entity] = hass.data.setdefault(
|
||||||
DATA_DOMAIN_ENTITIES, {}
|
DATA_DOMAIN_ENTITIES, {}
|
||||||
).setdefault(domain, {})
|
).setdefault(domain, {})
|
||||||
|
@ -310,44 +306,8 @@ class EntityPlatform:
|
||||||
logger = self.logger
|
logger = self.logger
|
||||||
hass = self.hass
|
hass = self.hass
|
||||||
full_name = f"{self.platform_name}.{self.domain}"
|
full_name = f"{self.platform_name}.{self.domain}"
|
||||||
object_id_language = (
|
|
||||||
hass.config.language
|
|
||||||
if hass.config.language in languages.NATIVE_ENTITY_IDS
|
|
||||||
else languages.DEFAULT_LANGUAGE
|
|
||||||
)
|
|
||||||
|
|
||||||
async def get_translations(
|
await self.async_load_translations()
|
||||||
language: str, category: str, integration: str
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
"""Get entity translations."""
|
|
||||||
try:
|
|
||||||
return await translation.async_get_translations(
|
|
||||||
hass, language, category, {integration}
|
|
||||||
)
|
|
||||||
except Exception as err: # pylint: disable=broad-exception-caught
|
|
||||||
_LOGGER.debug(
|
|
||||||
"Could not load translations for %s",
|
|
||||||
integration,
|
|
||||||
exc_info=err,
|
|
||||||
)
|
|
||||||
return {}
|
|
||||||
|
|
||||||
self.component_translations = await get_translations(
|
|
||||||
hass.config.language, "entity_component", self.domain
|
|
||||||
)
|
|
||||||
self.platform_translations = await get_translations(
|
|
||||||
hass.config.language, "entity", self.platform_name
|
|
||||||
)
|
|
||||||
if object_id_language == hass.config.language:
|
|
||||||
self.object_id_component_translations = self.component_translations
|
|
||||||
self.object_id_platform_translations = self.platform_translations
|
|
||||||
else:
|
|
||||||
self.object_id_component_translations = await get_translations(
|
|
||||||
object_id_language, "entity_component", self.domain
|
|
||||||
)
|
|
||||||
self.object_id_platform_translations = await get_translations(
|
|
||||||
object_id_language, "entity", self.platform_name
|
|
||||||
)
|
|
||||||
|
|
||||||
logger.info("Setting up %s", full_name)
|
logger.info("Setting up %s", full_name)
|
||||||
warn_task = hass.loop.call_at(
|
warn_task = hass.loop.call_at(
|
||||||
|
@ -430,6 +390,48 @@ class EntityPlatform:
|
||||||
finally:
|
finally:
|
||||||
warn_task.cancel()
|
warn_task.cancel()
|
||||||
|
|
||||||
|
async def async_load_translations(self) -> None:
|
||||||
|
"""Load translations."""
|
||||||
|
hass = self.hass
|
||||||
|
object_id_language = (
|
||||||
|
hass.config.language
|
||||||
|
if hass.config.language in languages.NATIVE_ENTITY_IDS
|
||||||
|
else languages.DEFAULT_LANGUAGE
|
||||||
|
)
|
||||||
|
|
||||||
|
async def get_translations(
|
||||||
|
language: str, category: str, integration: str
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""Get entity translations."""
|
||||||
|
try:
|
||||||
|
return await translation.async_get_translations(
|
||||||
|
hass, language, category, {integration}
|
||||||
|
)
|
||||||
|
except Exception as err: # pylint: disable=broad-exception-caught
|
||||||
|
_LOGGER.debug(
|
||||||
|
"Could not load translations for %s",
|
||||||
|
integration,
|
||||||
|
exc_info=err,
|
||||||
|
)
|
||||||
|
return {}
|
||||||
|
|
||||||
|
self.component_translations = await get_translations(
|
||||||
|
hass.config.language, "entity_component", self.domain
|
||||||
|
)
|
||||||
|
self.platform_translations = await get_translations(
|
||||||
|
hass.config.language, "entity", self.platform_name
|
||||||
|
)
|
||||||
|
if object_id_language == hass.config.language:
|
||||||
|
self.object_id_component_translations = self.component_translations
|
||||||
|
self.object_id_platform_translations = self.platform_translations
|
||||||
|
else:
|
||||||
|
self.object_id_component_translations = await get_translations(
|
||||||
|
object_id_language, "entity_component", self.domain
|
||||||
|
)
|
||||||
|
self.object_id_platform_translations = await get_translations(
|
||||||
|
object_id_language, "entity", self.platform_name
|
||||||
|
)
|
||||||
|
|
||||||
def _schedule_add_entities(
|
def _schedule_add_entities(
|
||||||
self, new_entities: Iterable[Entity], update_before_add: bool = False
|
self, new_entities: Iterable[Entity], update_before_add: bool = False
|
||||||
) -> None:
|
) -> None:
|
||||||
|
@ -783,6 +785,13 @@ class EntityPlatform:
|
||||||
self._async_unsub_polling()
|
self._async_unsub_polling()
|
||||||
self._async_unsub_polling = None
|
self._async_unsub_polling = None
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_prepare(self) -> None:
|
||||||
|
"""Register the entity platform in DATA_ENTITY_PLATFORM."""
|
||||||
|
self.hass.data.setdefault(DATA_ENTITY_PLATFORM, {}).setdefault(
|
||||||
|
self.platform_name, []
|
||||||
|
).append(self)
|
||||||
|
|
||||||
async def async_destroy(self) -> None:
|
async def async_destroy(self) -> None:
|
||||||
"""Destroy an entity platform.
|
"""Destroy an entity platform.
|
||||||
|
|
||||||
|
|
|
@ -1 +1,30 @@
|
||||||
"""Tests for the time_date component."""
|
"""Tests for the time_date component."""
|
||||||
|
|
||||||
|
from homeassistant.components.time_date.const import DOMAIN
|
||||||
|
from homeassistant.config_entries import SOURCE_USER
|
||||||
|
from homeassistant.const import CONF_DISPLAY_OPTIONS
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
|
from tests.common import MockConfigEntry
|
||||||
|
|
||||||
|
|
||||||
|
async def load_int(
|
||||||
|
hass: HomeAssistant, display_option: str | None = None
|
||||||
|
) -> MockConfigEntry:
|
||||||
|
"""Set up the Time & Date integration in Home Assistant."""
|
||||||
|
if display_option is None:
|
||||||
|
display_option = "time"
|
||||||
|
config_entry = MockConfigEntry(
|
||||||
|
domain=DOMAIN,
|
||||||
|
source=SOURCE_USER,
|
||||||
|
data={},
|
||||||
|
options={CONF_DISPLAY_OPTIONS: display_option},
|
||||||
|
entry_id=f"1234567890_{display_option}",
|
||||||
|
)
|
||||||
|
|
||||||
|
config_entry.add_to_hass(hass)
|
||||||
|
|
||||||
|
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
return config_entry
|
||||||
|
|
14
tests/components/time_date/conftest.py
Normal file
14
tests/components/time_date/conftest.py
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
"""Fixtures for Time & Date integration tests."""
|
||||||
|
from collections.abc import Generator
|
||||||
|
from unittest.mock import AsyncMock, patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_setup_entry() -> Generator[AsyncMock, None, None]:
|
||||||
|
"""Mock setting up a config entry."""
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.time_date.async_setup_entry", return_value=True
|
||||||
|
) as mock_setup:
|
||||||
|
yield mock_setup
|
138
tests/components/time_date/test_config_flow.py
Normal file
138
tests/components/time_date/test_config_flow.py
Normal file
|
@ -0,0 +1,138 @@
|
||||||
|
"""Test the Time & Date config flow."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from unittest.mock import AsyncMock
|
||||||
|
|
||||||
|
from freezegun.api import FrozenDateTimeFactory
|
||||||
|
import pytest
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant import config_entries
|
||||||
|
from homeassistant.components.time_date.const import CONF_DISPLAY_OPTIONS, DOMAIN
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.data_entry_flow import FlowResultType
|
||||||
|
|
||||||
|
from tests.common import MockConfigEntry, async_fire_time_changed
|
||||||
|
from tests.typing import WebSocketGenerator
|
||||||
|
|
||||||
|
pytestmark = pytest.mark.usefixtures("mock_setup_entry")
|
||||||
|
|
||||||
|
|
||||||
|
async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None:
|
||||||
|
"""Test we get the forms."""
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||||
|
)
|
||||||
|
assert result["type"] == FlowResultType.FORM
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
{"display_options": "time"},
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert len(mock_setup_entry.mock_calls) == 1
|
||||||
|
assert result["type"] == FlowResultType.CREATE_ENTRY
|
||||||
|
|
||||||
|
|
||||||
|
async def test_user_flow_does_not_allow_beat(
|
||||||
|
hass: HomeAssistant, mock_setup_entry: AsyncMock
|
||||||
|
) -> None:
|
||||||
|
"""Test we get the forms."""
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||||
|
)
|
||||||
|
assert result["type"] == FlowResultType.FORM
|
||||||
|
|
||||||
|
with pytest.raises(vol.Invalid):
|
||||||
|
await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
{"display_options": ["beat"]},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_single_instance(hass: HomeAssistant) -> None:
|
||||||
|
"""Test we get the forms."""
|
||||||
|
|
||||||
|
entry = MockConfigEntry(
|
||||||
|
domain=DOMAIN, data={}, options={CONF_DISPLAY_OPTIONS: "time"}
|
||||||
|
)
|
||||||
|
entry.add_to_hass(hass)
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||||
|
)
|
||||||
|
assert result["type"] == FlowResultType.FORM
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
{"display_options": "time"},
|
||||||
|
)
|
||||||
|
assert result["type"] == FlowResultType.ABORT
|
||||||
|
assert result["reason"] == "already_configured"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_timezone_not_set(hass: HomeAssistant) -> None:
|
||||||
|
"""Test time zone not set."""
|
||||||
|
hass.config.time_zone = None
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||||
|
)
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
{"display_options": "time"},
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert result["type"] == FlowResultType.FORM
|
||||||
|
assert result["errors"] == {"base": "timezone_not_exist"}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_config_flow_preview(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
hass_ws_client: WebSocketGenerator,
|
||||||
|
freezer: FrozenDateTimeFactory,
|
||||||
|
) -> None:
|
||||||
|
"""Test the config flow preview."""
|
||||||
|
client = await hass_ws_client(hass)
|
||||||
|
freezer.move_to("2024-01-02 20:14:11.672")
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||||
|
)
|
||||||
|
assert result["type"] == FlowResultType.FORM
|
||||||
|
assert result["step_id"] == "user"
|
||||||
|
assert result["errors"] is None
|
||||||
|
assert result["preview"] == "time_date"
|
||||||
|
|
||||||
|
await client.send_json_auto_id(
|
||||||
|
{
|
||||||
|
"type": "time_date/start_preview",
|
||||||
|
"flow_id": result["flow_id"],
|
||||||
|
"flow_type": "config_flow",
|
||||||
|
"user_input": {"display_options": "time"},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
msg = await client.receive_json()
|
||||||
|
assert msg["success"]
|
||||||
|
assert msg["result"] is None
|
||||||
|
|
||||||
|
msg = await client.receive_json()
|
||||||
|
assert msg["event"] == {
|
||||||
|
"attributes": {"friendly_name": "Time", "icon": "mdi:clock"},
|
||||||
|
"state": "12:14",
|
||||||
|
}
|
||||||
|
|
||||||
|
freezer.tick(60)
|
||||||
|
async_fire_time_changed(hass)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
msg = await client.receive_json()
|
||||||
|
assert msg["event"] == {
|
||||||
|
"attributes": {"friendly_name": "Time", "icon": "mdi:clock"},
|
||||||
|
"state": "12:15",
|
||||||
|
}
|
||||||
|
assert len(hass.states.async_all()) == 0
|
18
tests/components/time_date/test_init.py
Normal file
18
tests/components/time_date/test_init.py
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
"""The tests for the Time & Date component."""
|
||||||
|
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
|
from . import load_int
|
||||||
|
|
||||||
|
|
||||||
|
async def test_setup_and_remove_config_entry(hass: HomeAssistant) -> None:
|
||||||
|
"""Test setting up and removing a config entry."""
|
||||||
|
entry = await load_int(hass)
|
||||||
|
|
||||||
|
state = hass.states.get("sensor.time")
|
||||||
|
assert state is not None
|
||||||
|
|
||||||
|
assert await hass.config_entries.async_remove(entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert hass.states.get("sensor.time") is None
|
|
@ -5,17 +5,15 @@ from unittest.mock import ANY, Mock, patch
|
||||||
from freezegun.api import FrozenDateTimeFactory
|
from freezegun.api import FrozenDateTimeFactory
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from homeassistant.components.time_date.const import DOMAIN
|
from homeassistant.components.time_date.const import DOMAIN, OPTION_TYPES
|
||||||
import homeassistant.components.time_date.sensor as time_date
|
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers import event, issue_registry as ir
|
from homeassistant.helpers import event, issue_registry as ir
|
||||||
from homeassistant.setup import async_setup_component
|
from homeassistant.setup import async_setup_component
|
||||||
import homeassistant.util.dt as dt_util
|
import homeassistant.util.dt as dt_util
|
||||||
|
|
||||||
from tests.common import async_fire_time_changed
|
from . import load_int
|
||||||
|
|
||||||
ALL_DISPLAY_OPTIONS = list(time_date.OPTION_TYPES.keys())
|
from tests.common import async_fire_time_changed
|
||||||
CONFIG = {"sensor": {"platform": "time_date", "display_options": ALL_DISPLAY_OPTIONS}}
|
|
||||||
|
|
||||||
|
|
||||||
@patch("homeassistant.components.time_date.sensor.async_track_point_in_utc_time")
|
@patch("homeassistant.components.time_date.sensor.async_track_point_in_utc_time")
|
||||||
|
@ -54,12 +52,9 @@ async def test_intervals(
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test timing intervals of sensors when time zone is UTC."""
|
"""Test timing intervals of sensors when time zone is UTC."""
|
||||||
hass.config.set_time_zone("UTC")
|
hass.config.set_time_zone("UTC")
|
||||||
config = {"sensor": {"platform": "time_date", "display_options": [display_option]}}
|
|
||||||
|
|
||||||
freezer.move_to(start_time)
|
freezer.move_to(start_time)
|
||||||
|
|
||||||
await async_setup_component(hass, "sensor", config)
|
await load_int(hass, display_option)
|
||||||
await hass.async_block_till_done()
|
|
||||||
|
|
||||||
mock_track_interval.assert_called_once_with(hass, ANY, tracked_time)
|
mock_track_interval.assert_called_once_with(hass, ANY, tracked_time)
|
||||||
|
|
||||||
|
@ -70,8 +65,8 @@ async def test_states(hass: HomeAssistant, freezer: FrozenDateTimeFactory) -> No
|
||||||
now = dt_util.utc_from_timestamp(1495068856)
|
now = dt_util.utc_from_timestamp(1495068856)
|
||||||
freezer.move_to(now)
|
freezer.move_to(now)
|
||||||
|
|
||||||
await async_setup_component(hass, "sensor", CONFIG)
|
for option in OPTION_TYPES:
|
||||||
await hass.async_block_till_done()
|
await load_int(hass, option)
|
||||||
|
|
||||||
state = hass.states.get("sensor.time")
|
state = hass.states.get("sensor.time")
|
||||||
assert state.state == "00:54"
|
assert state.state == "00:54"
|
||||||
|
@ -130,8 +125,8 @@ async def test_states_non_default_timezone(
|
||||||
now = dt_util.utc_from_timestamp(1495068856)
|
now = dt_util.utc_from_timestamp(1495068856)
|
||||||
freezer.move_to(now)
|
freezer.move_to(now)
|
||||||
|
|
||||||
await async_setup_component(hass, "sensor", CONFIG)
|
for option in OPTION_TYPES:
|
||||||
await hass.async_block_till_done()
|
await load_int(hass, option)
|
||||||
|
|
||||||
state = hass.states.get("sensor.time")
|
state = hass.states.get("sensor.time")
|
||||||
assert state.state == "20:54"
|
assert state.state == "20:54"
|
||||||
|
@ -262,9 +257,7 @@ async def test_timezone_intervals(
|
||||||
hass.config.set_time_zone(time_zone)
|
hass.config.set_time_zone(time_zone)
|
||||||
freezer.move_to(start_time)
|
freezer.move_to(start_time)
|
||||||
|
|
||||||
config = {"sensor": {"platform": "time_date", "display_options": ["date"]}}
|
await load_int(hass, "date")
|
||||||
await async_setup_component(hass, "sensor", config)
|
|
||||||
await hass.async_block_till_done()
|
|
||||||
|
|
||||||
mock_track_interval.assert_called_once()
|
mock_track_interval.assert_called_once()
|
||||||
next_time = mock_track_interval.mock_calls[0][1][2]
|
next_time = mock_track_interval.mock_calls[0][1][2]
|
||||||
|
@ -274,8 +267,8 @@ async def test_timezone_intervals(
|
||||||
|
|
||||||
async def test_icons(hass: HomeAssistant) -> None:
|
async def test_icons(hass: HomeAssistant) -> None:
|
||||||
"""Test attributes of sensors."""
|
"""Test attributes of sensors."""
|
||||||
await async_setup_component(hass, "sensor", CONFIG)
|
for option in OPTION_TYPES:
|
||||||
await hass.async_block_till_done()
|
await load_int(hass, option)
|
||||||
|
|
||||||
state = hass.states.get("sensor.time")
|
state = hass.states.get("sensor.time")
|
||||||
assert state.attributes["icon"] == "mdi:clock"
|
assert state.attributes["icon"] == "mdi:clock"
|
||||||
|
@ -313,9 +306,14 @@ async def test_deprecation_warning(
|
||||||
expected_issues: list[str],
|
expected_issues: list[str],
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test deprecation warning for swatch beat."""
|
"""Test deprecation warning for swatch beat."""
|
||||||
config = {"sensor": {"platform": "time_date", "display_options": display_options}}
|
config = {
|
||||||
|
"sensor": {
|
||||||
|
"platform": "time_date",
|
||||||
|
"display_options": display_options,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await async_setup_component(hass, "sensor", config)
|
assert await async_setup_component(hass, "sensor", config)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
warnings = [record for record in caplog.records if record.levelname == "WARNING"]
|
warnings = [record for record in caplog.records if record.levelname == "WARNING"]
|
||||||
|
|
Loading…
Add table
Reference in a new issue