Add config flow to worldclock (#121775)

This commit is contained in:
G Johansson 2024-07-19 10:08:14 +02:00 committed by GitHub
parent 339b5117c5
commit 362c772d67
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 460 additions and 32 deletions

View File

@ -1 +1,25 @@
"""The worldclock component.""" """The worldclock component."""
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 Worldclock from a config entry."""
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
entry.async_on_unload(entry.add_update_listener(update_listener))
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload World clock config entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Handle options update."""
await hass.config_entries.async_reload(entry.entry_id)

View File

@ -0,0 +1,107 @@
"""Config flow for World clock."""
from __future__ import annotations
from collections.abc import Mapping
from typing import Any, cast
import zoneinfo
import voluptuous as vol
from homeassistant.const import CONF_NAME, CONF_TIME_ZONE
from homeassistant.helpers.schema_config_entry_flow import (
SchemaCommonFlowHandler,
SchemaConfigFlowHandler,
SchemaFlowFormStep,
)
from homeassistant.helpers.selector import (
SelectOptionDict,
SelectSelector,
SelectSelectorConfig,
SelectSelectorMode,
TextSelector,
)
from .const import CONF_TIME_FORMAT, DEFAULT_NAME, DEFAULT_TIME_STR_FORMAT, DOMAIN
TIME_STR_OPTIONS = [
SelectOptionDict(
value=DEFAULT_TIME_STR_FORMAT, label=f"14:05 ({DEFAULT_TIME_STR_FORMAT})"
),
SelectOptionDict(value="%I:%M %p", label="11:05 am (%I:%M %p)"),
SelectOptionDict(value="%Y-%m-%d %H:%M", label="2024-01-01 14:05 (%Y-%m-%d %H:%M)"),
SelectOptionDict(
value="%a, %b %d, %Y %I:%M %p",
label="Monday, Jan 01, 2024 11:05 am (%a, %b %d, %Y %I:%M %p)",
),
]
async def validate_duplicate(
handler: SchemaCommonFlowHandler, user_input: dict[str, Any]
) -> dict[str, Any]:
"""Validate already existing entry."""
handler.parent_handler._async_abort_entries_match({**handler.options, **user_input}) # noqa: SLF001
return user_input
async def get_schema(handler: SchemaCommonFlowHandler) -> vol.Schema:
"""Get available timezones."""
get_timezones: list[str] = list(
await handler.parent_handler.hass.async_add_executor_job(
zoneinfo.available_timezones
)
)
return vol.Schema(
{
vol.Required(CONF_NAME, default=DEFAULT_NAME): TextSelector(),
vol.Required(CONF_TIME_ZONE): SelectSelector(
SelectSelectorConfig(
options=get_timezones, mode=SelectSelectorMode.DROPDOWN, sort=True
)
),
}
).extend(DATA_SCHEMA_OPTIONS.schema)
DATA_SCHEMA_OPTIONS = vol.Schema(
{
vol.Optional(CONF_TIME_FORMAT, default=DEFAULT_TIME_STR_FORMAT): SelectSelector(
SelectSelectorConfig(
options=TIME_STR_OPTIONS,
custom_value=True,
mode=SelectSelectorMode.DROPDOWN,
)
)
}
)
CONFIG_FLOW = {
"user": SchemaFlowFormStep(
schema=get_schema,
validate_user_input=validate_duplicate,
),
"import": SchemaFlowFormStep(
schema=get_schema,
validate_user_input=validate_duplicate,
),
}
OPTIONS_FLOW = {
"init": SchemaFlowFormStep(
DATA_SCHEMA_OPTIONS,
validate_user_input=validate_duplicate,
)
}
class WorldclockConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN):
"""Handle a config flow for Worldclock."""
config_flow = CONFIG_FLOW
options_flow = OPTIONS_FLOW
def async_config_entry_title(self, options: Mapping[str, Any]) -> str:
"""Return config entry title."""
return cast(str, options[CONF_NAME])

View File

@ -0,0 +1,11 @@
"""Constants for world clock component."""
from homeassistant.const import Platform
DOMAIN = "worldclock"
PLATFORMS = [Platform.SENSOR]
CONF_TIME_FORMAT = "time_format"
DEFAULT_NAME = "Worldclock Sensor"
DEFAULT_TIME_STR_FORMAT = "%H:%M"

View File

@ -2,6 +2,7 @@
"domain": "worldclock", "domain": "worldclock",
"name": "Worldclock", "name": "Worldclock",
"codeowners": ["@fabaff"], "codeowners": ["@fabaff"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/worldclock", "documentation": "https://www.home-assistant.io/integrations/worldclock",
"iot_class": "local_push", "iot_class": "local_push",
"quality_scale": "internal" "quality_scale": "internal"

View File

@ -10,17 +10,16 @@ from homeassistant.components.sensor import (
PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA,
SensorEntity, SensorEntity,
) )
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.const import CONF_NAME, CONF_TIME_ZONE from homeassistant.const import CONF_NAME, CONF_TIME_ZONE
from homeassistant.core import HomeAssistant from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
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
CONF_TIME_FORMAT = "time_format" from .const import CONF_TIME_FORMAT, DEFAULT_NAME, DEFAULT_TIME_STR_FORMAT, DOMAIN
DEFAULT_NAME = "Worldclock Sensor"
DEFAULT_TIME_STR_FORMAT = "%H:%M"
PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend(
{ {
@ -38,13 +37,44 @@ async def async_setup_platform(
discovery_info: DiscoveryInfoType | None = None, discovery_info: DiscoveryInfoType | None = None,
) -> None: ) -> None:
"""Set up the World clock sensor.""" """Set up the World clock sensor."""
time_zone = dt_util.get_time_zone(config[CONF_TIME_ZONE]) hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_IMPORT},
data=config,
)
)
async_create_issue(
hass,
HOMEASSISTANT_DOMAIN,
f"deprecated_yaml_{DOMAIN}",
breaks_in_ha_version="2025.2.0",
is_fixable=False,
issue_domain=DOMAIN,
severity=IssueSeverity.WARNING,
translation_key="deprecated_yaml",
translation_placeholders={
"domain": DOMAIN,
"integration_title": "Worldclock",
},
)
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the World clock sensor entry."""
time_zone = await dt_util.async_get_time_zone(entry.options[CONF_TIME_ZONE])
async_add_entities( async_add_entities(
[ [
WorldClockSensor( WorldClockSensor(
time_zone, time_zone,
config[CONF_NAME], entry.options[CONF_NAME],
config[CONF_TIME_FORMAT], entry.options[CONF_TIME_FORMAT],
entry.entry_id,
) )
], ],
True, True,
@ -56,11 +86,14 @@ class WorldClockSensor(SensorEntity):
_attr_icon = "mdi:clock" _attr_icon = "mdi:clock"
def __init__(self, time_zone: tzinfo | None, name: str, time_format: str) -> None: def __init__(
self, time_zone: tzinfo | None, name: str, time_format: str, unique_id: str
) -> None:
"""Initialize the sensor.""" """Initialize the sensor."""
self._attr_name = name self._attr_name = name
self._time_zone = time_zone self._time_zone = time_zone
self._time_format = time_format self._time_format = time_format
self._attr_unique_id = unique_id
async def async_update(self) -> None: async def async_update(self) -> None:
"""Get the time and updates the states.""" """Get the time and updates the states."""

View File

@ -0,0 +1,35 @@
{
"config": {
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]"
},
"step": {
"user": {
"data": {
"name": "[%key:common::config_flow::data::name%]",
"time_zone": "Timezone",
"time_format": "Time format"
},
"data_description": {
"time_zone": "Select timezone from list",
"time_format": "Select a pre-defined format from the list or define your own format."
}
}
}
},
"options": {
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]"
},
"step": {
"init": {
"data": {
"time_format": "[%key:component::worldclock::config::step::user::data::time_format%]"
},
"data_description": {
"time_format": "[%key:component::worldclock::config::step::user::data_description::time_format%]"
}
}
}
}
}

View File

@ -643,6 +643,7 @@ FLOWS = {
"wled", "wled",
"wolflink", "wolflink",
"workday", "workday",
"worldclock",
"ws66i", "ws66i",
"wyoming", "wyoming",
"xbox", "xbox",

View File

@ -6870,7 +6870,7 @@
"worldclock": { "worldclock": {
"name": "Worldclock", "name": "Worldclock",
"integration_type": "hub", "integration_type": "hub",
"config_flow": false, "config_flow": true,
"iot_class": "local_push" "iot_class": "local_push"
}, },
"worldtidesinfo": { "worldtidesinfo": {

View File

@ -0,0 +1,66 @@
"""Fixtures for the Worldclock integration."""
from __future__ import annotations
from collections.abc import Generator
from typing import Any
from unittest.mock import AsyncMock, patch
import pytest
from homeassistant.components.worldclock.const import (
CONF_TIME_FORMAT,
DEFAULT_NAME,
DEFAULT_TIME_STR_FORMAT,
DOMAIN,
)
from homeassistant.config_entries import SOURCE_USER
from homeassistant.const import CONF_NAME, CONF_TIME_ZONE
from homeassistant.core import HomeAssistant
from tests.common import MockConfigEntry
@pytest.fixture
def mock_setup_entry() -> Generator[AsyncMock]:
"""Automatically patch setup."""
with patch(
"homeassistant.components.worldclock.async_setup_entry",
return_value=True,
) as mock_setup_entry:
yield mock_setup_entry
@pytest.fixture(name="get_config")
async def get_config_to_integration_load() -> dict[str, Any]:
"""Return configuration.
To override the config, tests can be marked with:
@pytest.mark.parametrize("get_config", [{...}])
"""
return {
CONF_NAME: DEFAULT_NAME,
CONF_TIME_ZONE: "America/New_York",
CONF_TIME_FORMAT: DEFAULT_TIME_STR_FORMAT,
}
@pytest.fixture(name="loaded_entry")
async def load_integration(
hass: HomeAssistant, get_config: dict[str, Any]
) -> MockConfigEntry:
"""Set up the Worldclock integration in Home Assistant."""
config_entry = MockConfigEntry(
domain=DOMAIN,
title=DEFAULT_NAME,
source=SOURCE_USER,
options=get_config,
entry_id="1",
)
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

View File

@ -0,0 +1,104 @@
"""Test the Worldclock config flow."""
from __future__ import annotations
from unittest.mock import AsyncMock
from homeassistant import config_entries
from homeassistant.components.worldclock.const import (
CONF_TIME_FORMAT,
DEFAULT_NAME,
DEFAULT_TIME_STR_FORMAT,
DOMAIN,
)
from homeassistant.const import CONF_NAME, CONF_TIME_ZONE
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType
from tests.common import MockConfigEntry
async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None:
"""Test we get the form."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["step_id"] == "user"
assert result["type"] is FlowResultType.FORM
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
CONF_NAME: DEFAULT_NAME,
CONF_TIME_ZONE: "America/New_York",
},
)
await hass.async_block_till_done()
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["version"] == 1
assert result["options"] == {
CONF_NAME: DEFAULT_NAME,
CONF_TIME_ZONE: "America/New_York",
CONF_TIME_FORMAT: DEFAULT_TIME_STR_FORMAT,
}
assert len(mock_setup_entry.mock_calls) == 1
async def test_options_flow(hass: HomeAssistant, loaded_entry: MockConfigEntry) -> None:
"""Test options flow."""
result = await hass.config_entries.options.async_init(loaded_entry.entry_id)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "init"
result = await hass.config_entries.options.async_configure(
result["flow_id"],
user_input={
CONF_TIME_FORMAT: "%a, %b %d, %Y %I:%M %p",
},
)
await hass.async_block_till_done()
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["data"] == {
CONF_NAME: DEFAULT_NAME,
CONF_TIME_ZONE: "America/New_York",
CONF_TIME_FORMAT: "%a, %b %d, %Y %I:%M %p",
}
await hass.async_block_till_done()
# Check the entity was updated, no new entity was created
assert len(hass.states.async_all()) == 1
state = hass.states.get("sensor.worldclock_sensor")
assert state is not None
async def test_entry_already_exist(
hass: HomeAssistant, loaded_entry: MockConfigEntry
) -> None:
"""Test abort when entry already exist."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["step_id"] == "user"
assert result["type"] is FlowResultType.FORM
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
CONF_NAME: DEFAULT_NAME,
CONF_TIME_ZONE: "America/New_York",
CONF_TIME_FORMAT: DEFAULT_TIME_STR_FORMAT,
},
)
await hass.async_block_till_done()
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "already_configured"

View File

@ -0,0 +1,17 @@
"""Test Worldclock component setup process."""
from __future__ import annotations
from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant
from tests.common import MockConfigEntry
async def test_unload_entry(hass: HomeAssistant, loaded_entry: MockConfigEntry) -> None:
"""Test unload an entry."""
assert loaded_entry.state is ConfigEntryState.LOADED
assert await hass.config_entries.async_unload(loaded_entry.entry_id)
await hass.async_block_till_done()
assert loaded_entry.state is ConfigEntryState.NOT_LOADED

View File

@ -1,19 +1,32 @@
"""The test for the World clock sensor platform.""" """The test for the World clock sensor platform."""
from datetime import tzinfo
import pytest import pytest
from homeassistant.core import HomeAssistant from homeassistant.components.worldclock.const import (
CONF_TIME_FORMAT,
DEFAULT_NAME,
DOMAIN,
)
from homeassistant.const import CONF_NAME, CONF_TIME_ZONE
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
from homeassistant.helpers import 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 MockConfigEntry
@pytest.fixture @pytest.fixture
def time_zone(): async def time_zone() -> tzinfo | None:
"""Fixture for time zone.""" """Fixture for time zone."""
return dt_util.get_time_zone("America/New_York") return await dt_util.async_get_time_zone("America/New_York")
async def test_time(hass: HomeAssistant, time_zone) -> None: async def test_time_imported_from_yaml(
hass: HomeAssistant, time_zone: tzinfo | None, issue_registry: ir.IssueRegistry
) -> None:
"""Test the time at a different location.""" """Test the time at a different location."""
config = {"sensor": {"platform": "worldclock", "time_zone": "America/New_York"}} config = {"sensor": {"platform": "worldclock", "time_zone": "America/New_York"}}
@ -29,26 +42,42 @@ async def test_time(hass: HomeAssistant, time_zone) -> None:
assert state.state == dt_util.now(time_zone=time_zone).strftime("%H:%M") assert state.state == dt_util.now(time_zone=time_zone).strftime("%H:%M")
issue = issue_registry.async_get_issue(
async def test_time_format(hass: HomeAssistant, time_zone) -> None: HOMEASSISTANT_DOMAIN, f"deprecated_yaml_{DOMAIN}"
"""Test time_format setting."""
time_format = "%a, %b %d, %Y %I:%M %p"
config = {
"sensor": {
"platform": "worldclock",
"time_zone": "America/New_York",
"time_format": time_format,
}
}
assert await async_setup_component(
hass,
"sensor",
config,
) )
await hass.async_block_till_done() assert issue
assert issue.issue_domain == DOMAIN
async def test_time_from_config_entry(
hass: HomeAssistant, time_zone: tzinfo | None, loaded_entry: MockConfigEntry
) -> None:
"""Test the time at a different location."""
state = hass.states.get("sensor.worldclock_sensor") state = hass.states.get("sensor.worldclock_sensor")
assert state is not None assert state is not None
assert state.state == dt_util.now(time_zone=time_zone).strftime(time_format) assert state.state == dt_util.now(time_zone=time_zone).strftime("%H:%M")
@pytest.mark.parametrize(
"get_config",
[
{
CONF_NAME: DEFAULT_NAME,
CONF_TIME_ZONE: "America/New_York",
CONF_TIME_FORMAT: "%a, %b %d, %Y %I:%M %p",
}
],
)
async def test_time_format(
hass: HomeAssistant, time_zone: tzinfo | None, loaded_entry: MockConfigEntry
) -> None:
"""Test time_format setting."""
state = hass.states.get("sensor.worldclock_sensor")
assert state is not None
assert state.state == dt_util.now(time_zone=time_zone).strftime(
"%a, %b %d, %Y %I:%M %p"
)