mirror of
https://github.com/home-assistant/core.git
synced 2025-04-23 08:47:57 +00:00
Add config flow for times of the day binary sensor (#68246)
Co-authored-by: Franck Nijhof <git@frenck.dev>
This commit is contained in:
parent
f562f4264f
commit
9f079a22d5
@ -1 +1,27 @@
|
||||
"""The tod component."""
|
||||
"""The Times of the Day integration."""
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up Times of the Day from a config entry."""
|
||||
hass.config_entries.async_setup_platforms(entry, (Platform.BINARY_SENSOR,))
|
||||
|
||||
entry.async_on_unload(entry.add_update_listener(config_entry_update_listener))
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def config_entry_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
"""Update listener, called when the config entry options are changed."""
|
||||
await hass.config_entries.async_reload(entry.entry_id)
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
return await hass.config_entries.async_unload_platforms(
|
||||
entry, (Platform.BINARY_SENSOR,)
|
||||
)
|
||||
|
@ -8,6 +8,7 @@ import logging
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
CONF_AFTER,
|
||||
CONF_BEFORE,
|
||||
@ -22,15 +23,19 @@ from homeassistant.helpers.sun import get_astral_event_date, get_astral_event_ne
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .const import (
|
||||
CONF_AFTER_OFFSET,
|
||||
CONF_AFTER_TIME,
|
||||
CONF_BEFORE_OFFSET,
|
||||
CONF_BEFORE_TIME,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTR_AFTER = "after"
|
||||
ATTR_BEFORE = "before"
|
||||
ATTR_NEXT_UPDATE = "next_update"
|
||||
|
||||
CONF_AFTER_OFFSET = "after_offset"
|
||||
CONF_BEFORE_OFFSET = "before_offset"
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||
{
|
||||
vol.Required(CONF_AFTER): vol.Any(cv.time, vol.All(vol.Lower, cv.sun_event)),
|
||||
@ -42,6 +47,28 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Initialize Times of the Day config entry."""
|
||||
if hass.config.time_zone is None:
|
||||
_LOGGER.error("Timezone is not set in Home Assistant configuration")
|
||||
return
|
||||
|
||||
after = cv.time(config_entry.options[CONF_AFTER_TIME])
|
||||
after_offset = timedelta(0)
|
||||
before = cv.time(config_entry.options[CONF_BEFORE_TIME])
|
||||
before_offset = timedelta(0)
|
||||
name = config_entry.title
|
||||
unique_id = config_entry.entry_id
|
||||
|
||||
async_add_entities(
|
||||
[TodSensor(name, after, after_offset, before, before_offset, unique_id)]
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_platform(
|
||||
hass: HomeAssistant,
|
||||
config: ConfigType,
|
||||
@ -58,12 +85,12 @@ async def async_setup_platform(
|
||||
before = config[CONF_BEFORE]
|
||||
before_offset = config[CONF_BEFORE_OFFSET]
|
||||
name = config[CONF_NAME]
|
||||
sensor = TodSensor(name, after, after_offset, before, before_offset)
|
||||
sensor = TodSensor(name, after, after_offset, before, before_offset, None)
|
||||
|
||||
async_add_entities([sensor])
|
||||
|
||||
|
||||
def is_sun_event(sun_event):
|
||||
def _is_sun_event(sun_event):
|
||||
"""Return true if event is sun event not time."""
|
||||
return sun_event in (SUN_EVENT_SUNRISE, SUN_EVENT_SUNSET)
|
||||
|
||||
@ -71,8 +98,9 @@ def is_sun_event(sun_event):
|
||||
class TodSensor(BinarySensorEntity):
|
||||
"""Time of the Day Sensor."""
|
||||
|
||||
def __init__(self, name, after, after_offset, before, before_offset):
|
||||
def __init__(self, name, after, after_offset, before, before_offset, unique_id):
|
||||
"""Init the ToD Sensor..."""
|
||||
self._attr_unique_id = unique_id
|
||||
self._name = name
|
||||
self._time_before = self._time_after = self._next_update = None
|
||||
self._after_offset = after_offset
|
||||
@ -119,11 +147,11 @@ class TodSensor(BinarySensorEntity):
|
||||
# calculate utc datetime corresponding to local time
|
||||
return dt_util.as_utc(datetime.combine(current_local_date, naive_time))
|
||||
|
||||
def _calculate_boudary_time(self):
|
||||
def _calculate_boundary_time(self):
|
||||
"""Calculate internal absolute time boundaries."""
|
||||
nowutc = dt_util.utcnow()
|
||||
# If after value is a sun event instead of absolute time
|
||||
if is_sun_event(self._after):
|
||||
if _is_sun_event(self._after):
|
||||
# Calculate the today's event utc time or
|
||||
# if not available take next
|
||||
after_event_date = get_astral_event_date(
|
||||
@ -139,7 +167,7 @@ class TodSensor(BinarySensorEntity):
|
||||
self._time_after = after_event_date
|
||||
|
||||
# If before value is a sun event instead of absolute time
|
||||
if is_sun_event(self._before):
|
||||
if _is_sun_event(self._before):
|
||||
# Calculate the today's event utc time or if not available take
|
||||
# next
|
||||
before_event_date = get_astral_event_date(
|
||||
@ -168,7 +196,7 @@ class TodSensor(BinarySensorEntity):
|
||||
# _time_after is set to 23:00 today
|
||||
# nowutc is set to 10:00 today
|
||||
if (
|
||||
not is_sun_event(self._after)
|
||||
not _is_sun_event(self._after)
|
||||
and self._time_after > nowutc
|
||||
and self._time_before > nowutc + timedelta(days=1)
|
||||
):
|
||||
@ -182,7 +210,7 @@ class TodSensor(BinarySensorEntity):
|
||||
|
||||
def _turn_to_next_day(self):
|
||||
"""Turn to to the next day."""
|
||||
if is_sun_event(self._after):
|
||||
if _is_sun_event(self._after):
|
||||
self._time_after = get_astral_event_next(
|
||||
self.hass, self._after, self._time_after - self._after_offset
|
||||
)
|
||||
@ -191,7 +219,7 @@ class TodSensor(BinarySensorEntity):
|
||||
# Offset is already there
|
||||
self._time_after += timedelta(days=1)
|
||||
|
||||
if is_sun_event(self._before):
|
||||
if _is_sun_event(self._before):
|
||||
self._time_before = get_astral_event_next(
|
||||
self.hass, self._before, self._time_before - self._before_offset
|
||||
)
|
||||
@ -202,7 +230,7 @@ class TodSensor(BinarySensorEntity):
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Call when entity about to be added to Home Assistant."""
|
||||
self._calculate_boudary_time()
|
||||
self._calculate_boundary_time()
|
||||
self._calculate_next_update()
|
||||
|
||||
@callback
|
||||
|
49
homeassistant/components/tod/config_flow.py
Normal file
49
homeassistant/components/tod/config_flow.py
Normal file
@ -0,0 +1,49 @@
|
||||
"""Config flow for Times of the Day integration."""
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
from typing import Any, cast
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import CONF_NAME
|
||||
from homeassistant.helpers import selector
|
||||
from homeassistant.helpers.helper_config_entry_flow import (
|
||||
HelperConfigFlowHandler,
|
||||
HelperFlowFormStep,
|
||||
HelperFlowMenuStep,
|
||||
)
|
||||
|
||||
from .const import CONF_AFTER_TIME, CONF_BEFORE_TIME, DOMAIN
|
||||
|
||||
OPTIONS_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_AFTER_TIME): selector.selector({"time": {}}),
|
||||
vol.Optional(CONF_BEFORE_TIME): selector.selector({"time": {}}),
|
||||
}
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_NAME): selector.selector({"text": {}}),
|
||||
}
|
||||
).extend(OPTIONS_SCHEMA.schema)
|
||||
|
||||
CONFIG_FLOW: dict[str, HelperFlowFormStep | HelperFlowMenuStep] = {
|
||||
"user": HelperFlowFormStep(CONFIG_SCHEMA)
|
||||
}
|
||||
|
||||
OPTIONS_FLOW: dict[str, HelperFlowFormStep | HelperFlowMenuStep] = {
|
||||
"init": HelperFlowFormStep(OPTIONS_SCHEMA)
|
||||
}
|
||||
|
||||
|
||||
class ConfigFlowHandler(HelperConfigFlowHandler, domain=DOMAIN):
|
||||
"""Handle a config or options flow for Times of the Day."""
|
||||
|
||||
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["name"])
|
8
homeassistant/components/tod/const.py
Normal file
8
homeassistant/components/tod/const.py
Normal file
@ -0,0 +1,8 @@
|
||||
"""Constants for the Times of the Day integration."""
|
||||
|
||||
DOMAIN = "tod"
|
||||
|
||||
CONF_AFTER_TIME = "after_time"
|
||||
CONF_AFTER_OFFSET = "after_offset"
|
||||
CONF_BEFORE_TIME = "before_time"
|
||||
CONF_BEFORE_OFFSET = "before_offset"
|
@ -1,8 +1,10 @@
|
||||
{
|
||||
"domain": "tod",
|
||||
"integration_type": "helper",
|
||||
"name": "Times of the Day",
|
||||
"documentation": "https://www.home-assistant.io/integrations/tod",
|
||||
"codeowners": [],
|
||||
"quality_scale": "internal",
|
||||
"iot_class": "local_push"
|
||||
"iot_class": "local_push",
|
||||
"config_flow": true
|
||||
}
|
||||
|
30
homeassistant/components/tod/strings.json
Normal file
30
homeassistant/components/tod/strings.json
Normal file
@ -0,0 +1,30 @@
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "New Times of the Day Sensor",
|
||||
"description": "Configure when the sensor should turn on and off.",
|
||||
"data": {
|
||||
"after": "On after",
|
||||
"after_time": "On time",
|
||||
"before": "Off after",
|
||||
"before_time": "Off time",
|
||||
"name": "Name"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"description": "[%key:component::tod::config::step::user::description%]",
|
||||
"data": {
|
||||
"after": "[%key:component::tod::config::step::user::data::after%]",
|
||||
"after_time": "[%key:component::tod::config::step::user::data::after_time%]",
|
||||
"before": "[%key:component::tod::config::step::user::data::before%]",
|
||||
"before_time": "[%key:component::tod::config::step::user::data::before_time%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
30
homeassistant/components/tod/translations/en.json
Normal file
30
homeassistant/components/tod/translations/en.json
Normal file
@ -0,0 +1,30 @@
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"after": "On after",
|
||||
"after_time": "On time",
|
||||
"before": "Off after",
|
||||
"before_time": "Off time",
|
||||
"name": "Name"
|
||||
},
|
||||
"description": "Configure when the sensor should turn on and off.",
|
||||
"title": "New Times of the Day Sensor"
|
||||
}
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"data": {
|
||||
"after": "On after",
|
||||
"after_time": "On time",
|
||||
"before": "Off after",
|
||||
"before_time": "Off time"
|
||||
},
|
||||
"description": "Configure when the sensor should turn on and off."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -400,6 +400,7 @@ FLOWS = {
|
||||
"zwave_me"
|
||||
],
|
||||
"helper": [
|
||||
"derivative"
|
||||
"derivative",
|
||||
"tod"
|
||||
]
|
||||
}
|
||||
|
125
tests/components/tod/test_config_flow.py
Normal file
125
tests/components/tod/test_config_flow.py
Normal file
@ -0,0 +1,125 @@
|
||||
"""Test the Times of the Day config flow."""
|
||||
from unittest.mock import patch
|
||||
|
||||
from freezegun import freeze_time
|
||||
import pytest
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components.tod.const import DOMAIN
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
@pytest.mark.parametrize("platform", ("sensor",))
|
||||
async def test_config_flow(hass: HomeAssistant, platform) -> None:
|
||||
"""Test the config flow."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
assert result["type"] == RESULT_TYPE_FORM
|
||||
assert result["errors"] is None
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.tod.async_setup_entry",
|
||||
return_value=True,
|
||||
) as mock_setup_entry:
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
"after_time": "10:00",
|
||||
"before_time": "18:00",
|
||||
"name": "My tod",
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] == RESULT_TYPE_CREATE_ENTRY
|
||||
assert result["title"] == "My tod"
|
||||
assert result["data"] == {}
|
||||
assert result["options"] == {
|
||||
"after_time": "10:00",
|
||||
"before_time": "18:00",
|
||||
"name": "My tod",
|
||||
}
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
config_entry = hass.config_entries.async_entries(DOMAIN)[0]
|
||||
assert config_entry.data == {}
|
||||
assert config_entry.options == {
|
||||
"after_time": "10:00",
|
||||
"before_time": "18:00",
|
||||
"name": "My tod",
|
||||
}
|
||||
assert config_entry.title == "My tod"
|
||||
|
||||
|
||||
def get_suggested(schema, key):
|
||||
"""Get suggested value for key in voluptuous schema."""
|
||||
for k in schema.keys():
|
||||
if k == key:
|
||||
if k.description is None or "suggested_value" not in k.description:
|
||||
return None
|
||||
return k.description["suggested_value"]
|
||||
# Wanted key absent from schema
|
||||
raise Exception
|
||||
|
||||
|
||||
@freeze_time("2022-03-16 17:37:00", tz_offset=-7)
|
||||
async def test_options(hass: HomeAssistant) -> None:
|
||||
"""Test reconfiguring."""
|
||||
# Setup the config entry
|
||||
config_entry = MockConfigEntry(
|
||||
data={},
|
||||
domain=DOMAIN,
|
||||
options={
|
||||
"after_time": "10:00",
|
||||
"before_time": "18:05",
|
||||
"name": "My tod",
|
||||
},
|
||||
title="My tod",
|
||||
)
|
||||
config_entry.add_to_hass(hass)
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
result = await hass.config_entries.options.async_init(config_entry.entry_id)
|
||||
assert result["type"] == RESULT_TYPE_FORM
|
||||
assert result["step_id"] == "init"
|
||||
schema = result["data_schema"].schema
|
||||
assert get_suggested(schema, "after_time") == "10:00"
|
||||
assert get_suggested(schema, "before_time") == "18:05"
|
||||
|
||||
result = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={
|
||||
"after_time": "10:00",
|
||||
"before_time": "17:05",
|
||||
},
|
||||
)
|
||||
assert result["type"] == RESULT_TYPE_CREATE_ENTRY
|
||||
assert result["data"] == {
|
||||
"after_time": "10:00",
|
||||
"before_time": "17:05",
|
||||
"name": "My tod",
|
||||
}
|
||||
assert config_entry.data == {}
|
||||
assert config_entry.options == {
|
||||
"after_time": "10:00",
|
||||
"before_time": "17:05",
|
||||
"name": "My tod",
|
||||
}
|
||||
assert config_entry.title == "My tod"
|
||||
|
||||
# Check config entry is reloaded with new options
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Check the entity was updated, no new entity was created
|
||||
assert len(hass.states.async_all()) == 1
|
||||
|
||||
# Check the state of the entity has changed as expected
|
||||
state = hass.states.get("binary_sensor.my_tod")
|
||||
assert state.state == "off"
|
||||
assert state.attributes["after"] == "2022-03-16T10:00:00-07:00"
|
||||
assert state.attributes["before"] == "2022-03-16T17:05:00-07:00"
|
49
tests/components/tod/test_init.py
Normal file
49
tests/components/tod/test_init.py
Normal file
@ -0,0 +1,49 @@
|
||||
"""Test the Times of the Day integration."""
|
||||
from freezegun import freeze_time
|
||||
|
||||
from homeassistant.components.tod.const import DOMAIN
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
@freeze_time("2022-03-16 17:37:00", tz_offset=-7)
|
||||
async def test_setup_and_remove_config_entry(hass: HomeAssistant) -> None:
|
||||
"""Test setting up and removing a config entry."""
|
||||
registry = er.async_get(hass)
|
||||
tod_entity_id = "binary_sensor.my_tod"
|
||||
|
||||
# Setup the config entry
|
||||
config_entry = MockConfigEntry(
|
||||
data={},
|
||||
domain=DOMAIN,
|
||||
options={
|
||||
"after_time": "10:00:00",
|
||||
"before_time": "18:05:00",
|
||||
"name": "My tod",
|
||||
},
|
||||
title="My tod",
|
||||
)
|
||||
config_entry.add_to_hass(hass)
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Check the entity is registered in the entity registry
|
||||
assert registry.async_get(tod_entity_id) is not None
|
||||
|
||||
# Check the platform is setup correctly
|
||||
state = hass.states.get(tod_entity_id)
|
||||
# Check the state of the entity is as expected
|
||||
state = hass.states.get("binary_sensor.my_tod")
|
||||
assert state.state == "off"
|
||||
assert state.attributes["after"] == "2022-03-16T10:00:00-07:00"
|
||||
assert state.attributes["before"] == "2022-03-16T18:05:00-07:00"
|
||||
|
||||
# Remove the config entry
|
||||
assert await hass.config_entries.async_remove(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Check the state and entity registry entry are removed
|
||||
assert hass.states.get(tod_entity_id) is None
|
||||
assert registry.async_get(tod_entity_id) is None
|
Loading…
x
Reference in New Issue
Block a user