Add config flow to Workday (#72558)

* Initial commit Workday Config Flow

* Add tests

* Remove day_to_string

* new entity name, new depr. version, clean

* Use repairs for depr. warning

* Fix issue_registry moved

* tweaks

* hassfest

* Fix CI

* FlowResultType

* breaking version

* remove translation

* Fixes

* naming

* duplicates

* abort entries match

* add_suggested_values_to_schema

* various

* validate country

* abort_entries_match in option flow

* Remove country test

* remove country not exist string

* docstring exceptions

* easier

* break version

* unneeded check

* slim tests

* Fix import test

* Fix province in abort_match

* review comments

* Fix import province

* Add review fixes

* fix reviews

* Review fixes
This commit is contained in:
G Johansson 2023-04-19 11:50:11 +02:00 committed by GitHub
parent a511e7d6bc
commit f74103c57e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 1093 additions and 20 deletions

View File

@ -1 +1,28 @@
"""Sensor to indicate whether the current day is a workday."""
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 Workday from a config entry."""
entry.async_on_unload(entry.add_update_listener(async_update_listener))
hass.config_entries.async_setup_platforms(entry, PLATFORMS)
return True
async def async_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Update listener for options."""
await hass.config_entries.async_reload(entry.entry_id)
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload Workday config entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)

View File

@ -12,10 +12,14 @@ from homeassistant.components.binary_sensor import (
PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA,
BinarySensorEntity,
)
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.const import CONF_NAME
from homeassistant.core import HomeAssistant
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.device_registry import DeviceEntryType
from homeassistant.helpers.entity import DeviceInfo
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.util import dt
@ -32,6 +36,7 @@ from .const import (
DEFAULT_NAME,
DEFAULT_OFFSET,
DEFAULT_WORKDAYS,
DOMAIN,
LOGGER,
)
@ -76,21 +81,44 @@ PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend(
)
def setup_platform(
async def async_setup_platform(
hass: HomeAssistant,
config: ConfigType,
add_entities: AddEntitiesCallback,
async_add_entities: AddEntitiesCallback,
discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""Set up the Workday sensor."""
add_holidays: list[DateLike] = config[CONF_ADD_HOLIDAYS]
remove_holidays: list[str] = config[CONF_REMOVE_HOLIDAYS]
country: str = config[CONF_COUNTRY]
days_offset: int = config[CONF_OFFSET]
excludes: list[str] = config[CONF_EXCLUDES]
province: str | None = config.get(CONF_PROVINCE)
sensor_name: str = config[CONF_NAME]
workdays: list[str] = config[CONF_WORKDAYS]
async_create_issue(
hass,
DOMAIN,
"deprecated_yaml",
breaks_in_ha_version="2023.7.0",
is_fixable=False,
severity=IssueSeverity.WARNING,
translation_key="deprecated_yaml",
)
hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_IMPORT},
data=config,
)
)
async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""Set up the Workday sensor."""
add_holidays: list[DateLike] = entry.options[CONF_ADD_HOLIDAYS]
remove_holidays: list[str] = entry.options[CONF_REMOVE_HOLIDAYS]
country: str = entry.options[CONF_COUNTRY]
days_offset: int = int(entry.options[CONF_OFFSET])
excludes: list[str] = entry.options[CONF_EXCLUDES]
province: str | None = entry.options.get(CONF_PROVINCE)
sensor_name: str = entry.options[CONF_NAME]
workdays: list[str] = entry.options[CONF_WORKDAYS]
year: int = (dt.now() + timedelta(days=days_offset)).year
obj_holidays: HolidayBase = getattr(holidays, country)(years=year)
@ -131,8 +159,17 @@ def setup_platform(
_holiday_string = holiday_date.strftime("%Y-%m-%d")
LOGGER.debug("%s %s", _holiday_string, name)
add_entities(
[IsWorkdaySensor(obj_holidays, workdays, excludes, days_offset, sensor_name)],
async_add_entities(
[
IsWorkdaySensor(
obj_holidays,
workdays,
excludes,
days_offset,
sensor_name,
entry.entry_id,
)
],
True,
)
@ -140,6 +177,8 @@ def setup_platform(
class IsWorkdaySensor(BinarySensorEntity):
"""Implementation of a Workday sensor."""
_attr_has_entity_name = True
def __init__(
self,
obj_holidays: HolidayBase,
@ -147,9 +186,9 @@ class IsWorkdaySensor(BinarySensorEntity):
excludes: list[str],
days_offset: int,
name: str,
entry_id: str,
) -> None:
"""Initialize the Workday sensor."""
self._attr_name = name
self._obj_holidays = obj_holidays
self._workdays = workdays
self._excludes = excludes
@ -159,6 +198,14 @@ class IsWorkdaySensor(BinarySensorEntity):
CONF_EXCLUDES: excludes,
CONF_OFFSET: days_offset,
}
self._attr_unique_id = entry_id
self._attr_device_info = DeviceInfo(
entry_type=DeviceEntryType.SERVICE,
identifiers={(DOMAIN, entry_id)},
manufacturer="python-holidays",
model=holidays.__version__,
name=name,
)
def is_include(self, day: str, now: date) -> bool:
"""Check if given day is in the includes list."""

View File

@ -0,0 +1,308 @@
"""Adds config flow for Workday integration."""
from __future__ import annotations
from typing import Any
import holidays
from holidays import HolidayBase
import voluptuous as vol
from homeassistant.config_entries import (
ConfigEntry,
ConfigFlow,
OptionsFlowWithConfigEntry,
)
from homeassistant.const import CONF_NAME
from homeassistant.core import callback
from homeassistant.data_entry_flow import AbortFlow, FlowResult
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.selector import (
NumberSelector,
NumberSelectorConfig,
NumberSelectorMode,
SelectSelector,
SelectSelectorConfig,
SelectSelectorMode,
TextSelector,
)
from homeassistant.util import dt
from .const import (
ALLOWED_DAYS,
CONF_ADD_HOLIDAYS,
CONF_COUNTRY,
CONF_EXCLUDES,
CONF_OFFSET,
CONF_PROVINCE,
CONF_REMOVE_HOLIDAYS,
CONF_WORKDAYS,
DEFAULT_EXCLUDES,
DEFAULT_NAME,
DEFAULT_OFFSET,
DEFAULT_WORKDAYS,
DOMAIN,
)
NONE_SENTINEL = "none"
def add_province_to_schema(
schema: vol.Schema,
options: dict[str, Any],
) -> vol.Schema:
"""Update schema with province from country."""
year: int = dt.now().year
obj_holidays: HolidayBase = getattr(holidays, options[CONF_COUNTRY])(years=year)
if not obj_holidays.subdivisions:
return schema
province_list = [NONE_SENTINEL, *obj_holidays.subdivisions]
add_schema = {
vol.Optional(CONF_PROVINCE, default=NONE_SENTINEL): SelectSelector(
SelectSelectorConfig(
options=province_list,
mode=SelectSelectorMode.DROPDOWN,
translation_key=CONF_PROVINCE,
)
),
}
return vol.Schema({**DATA_SCHEMA_OPT.schema, **add_schema})
def validate_custom_dates(user_input: dict[str, Any]) -> None:
"""Validate custom dates for add/remove holidays."""
for add_date in user_input[CONF_ADD_HOLIDAYS]:
if dt.parse_date(add_date) is None:
raise AddDatesError("Incorrect date")
year: int = dt.now().year
obj_holidays: HolidayBase = getattr(holidays, user_input[CONF_COUNTRY])(years=year)
if user_input.get(CONF_PROVINCE):
obj_holidays = getattr(holidays, user_input[CONF_COUNTRY])(
subdiv=user_input[CONF_PROVINCE], years=year
)
for remove_date in user_input[CONF_REMOVE_HOLIDAYS]:
if dt.parse_date(remove_date) is None:
if obj_holidays.get_named(remove_date) == []:
raise RemoveDatesError("Incorrect date or name")
DATA_SCHEMA_SETUP = vol.Schema(
{
vol.Required(CONF_NAME, default=DEFAULT_NAME): TextSelector(),
vol.Required(CONF_COUNTRY): SelectSelector(
SelectSelectorConfig(
options=list(holidays.list_supported_countries()),
mode=SelectSelectorMode.DROPDOWN,
)
),
}
)
DATA_SCHEMA_OPT = vol.Schema(
{
vol.Optional(CONF_EXCLUDES, default=DEFAULT_EXCLUDES): SelectSelector(
SelectSelectorConfig(
options=ALLOWED_DAYS,
multiple=True,
mode=SelectSelectorMode.DROPDOWN,
translation_key="days",
)
),
vol.Optional(CONF_OFFSET, default=DEFAULT_OFFSET): NumberSelector(
NumberSelectorConfig(min=-10, max=10, step=1, mode=NumberSelectorMode.BOX)
),
vol.Optional(CONF_WORKDAYS, default=DEFAULT_WORKDAYS): SelectSelector(
SelectSelectorConfig(
options=ALLOWED_DAYS,
multiple=True,
mode=SelectSelectorMode.DROPDOWN,
translation_key="days",
)
),
vol.Optional(CONF_ADD_HOLIDAYS, default=[]): SelectSelector(
SelectSelectorConfig(
options=[],
multiple=True,
custom_value=True,
mode=SelectSelectorMode.DROPDOWN,
)
),
vol.Optional(CONF_REMOVE_HOLIDAYS, default=[]): SelectSelector(
SelectSelectorConfig(
options=[],
multiple=True,
custom_value=True,
mode=SelectSelectorMode.DROPDOWN,
)
),
}
)
class WorkdayConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Workday integration."""
VERSION = 1
data: dict[str, Any] = {}
@staticmethod
@callback
def async_get_options_flow(
config_entry: ConfigEntry,
) -> WorkdayOptionsFlowHandler:
"""Get the options flow for this handler."""
return WorkdayOptionsFlowHandler(config_entry)
async def async_step_import(self, config: dict[str, Any]) -> FlowResult:
"""Import a configuration from config.yaml."""
abort_match = {
CONF_COUNTRY: config[CONF_COUNTRY],
CONF_EXCLUDES: config[CONF_EXCLUDES],
CONF_OFFSET: config[CONF_OFFSET],
CONF_WORKDAYS: config[CONF_WORKDAYS],
CONF_ADD_HOLIDAYS: config[CONF_ADD_HOLIDAYS],
CONF_REMOVE_HOLIDAYS: config[CONF_REMOVE_HOLIDAYS],
CONF_PROVINCE: config.get(CONF_PROVINCE),
}
new_config = config.copy()
new_config[CONF_PROVINCE] = config.get(CONF_PROVINCE)
self._async_abort_entries_match(abort_match)
return await self.async_step_options(user_input=new_config)
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle the user initial step."""
errors: dict[str, str] = {}
if user_input is not None:
self.data = user_input
return await self.async_step_options()
return self.async_show_form(
step_id="user",
data_schema=DATA_SCHEMA_SETUP,
errors=errors,
)
async def async_step_options(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle remaining flow."""
errors: dict[str, str] = {}
if user_input is not None:
combined_input: dict[str, Any] = {**self.data, **user_input}
if combined_input.get(CONF_PROVINCE, NONE_SENTINEL) == NONE_SENTINEL:
combined_input[CONF_PROVINCE] = None
try:
await self.hass.async_add_executor_job(
validate_custom_dates, combined_input
)
except AddDatesError:
errors["add_holidays"] = "add_holiday_error"
except RemoveDatesError:
errors["remove_holidays"] = "remove_holiday_error"
except NotImplementedError:
self.async_abort(reason="incorrect_province")
abort_match = {
CONF_COUNTRY: combined_input[CONF_COUNTRY],
CONF_EXCLUDES: combined_input[CONF_EXCLUDES],
CONF_OFFSET: combined_input[CONF_OFFSET],
CONF_WORKDAYS: combined_input[CONF_WORKDAYS],
CONF_ADD_HOLIDAYS: combined_input[CONF_ADD_HOLIDAYS],
CONF_REMOVE_HOLIDAYS: combined_input[CONF_REMOVE_HOLIDAYS],
CONF_PROVINCE: combined_input[CONF_PROVINCE],
}
self._async_abort_entries_match(abort_match)
if not errors:
return self.async_create_entry(
title=combined_input[CONF_NAME],
data={},
options=combined_input,
)
schema = await self.hass.async_add_executor_job(
add_province_to_schema, DATA_SCHEMA_OPT, self.data
)
new_schema = self.add_suggested_values_to_schema(schema, user_input)
return self.async_show_form(
step_id="options",
data_schema=new_schema,
errors=errors,
)
class WorkdayOptionsFlowHandler(OptionsFlowWithConfigEntry):
"""Handle Workday options."""
async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Manage Workday options."""
errors: dict[str, str] = {}
if user_input is not None:
combined_input: dict[str, Any] = {**self.options, **user_input}
if combined_input.get(CONF_PROVINCE, NONE_SENTINEL) == NONE_SENTINEL:
combined_input[CONF_PROVINCE] = None
try:
await self.hass.async_add_executor_job(
validate_custom_dates, combined_input
)
except AddDatesError:
errors["add_holidays"] = "add_holiday_error"
except RemoveDatesError:
errors["remove_holidays"] = "remove_holiday_error"
else:
try:
self._async_abort_entries_match(
{
CONF_COUNTRY: self._config_entry.options[CONF_COUNTRY],
CONF_EXCLUDES: combined_input[CONF_EXCLUDES],
CONF_OFFSET: combined_input[CONF_OFFSET],
CONF_WORKDAYS: combined_input[CONF_WORKDAYS],
CONF_ADD_HOLIDAYS: combined_input[CONF_ADD_HOLIDAYS],
CONF_REMOVE_HOLIDAYS: combined_input[CONF_REMOVE_HOLIDAYS],
CONF_PROVINCE: combined_input[CONF_PROVINCE],
}
)
except AbortFlow as err:
errors = {"base": err.reason}
else:
return self.async_create_entry(data=combined_input)
saved_options = self.options.copy()
if saved_options[CONF_PROVINCE] is None:
saved_options[CONF_PROVINCE] = NONE_SENTINEL
schema: vol.Schema = await self.hass.async_add_executor_job(
add_province_to_schema, DATA_SCHEMA_OPT, self.options
)
new_schema = self.add_suggested_values_to_schema(schema, user_input)
return self.async_show_form(
step_id="init",
data_schema=new_schema,
errors=errors,
)
class AddDatesError(HomeAssistantError):
"""Exception for error adding dates."""
class RemoveDatesError(HomeAssistantError):
"""Exception for error removing dates."""
class CountryNotExist(HomeAssistantError):
"""Exception country does not exist error."""

View File

@ -3,12 +3,15 @@ from __future__ import annotations
import logging
from homeassistant.const import WEEKDAYS
from homeassistant.const import WEEKDAYS, Platform
LOGGER = logging.getLogger(__package__)
ALLOWED_DAYS = WEEKDAYS + ["holiday"]
DOMAIN = "workday"
PLATFORMS = [Platform.BINARY_SENSOR]
CONF_COUNTRY = "country"
CONF_PROVINCE = "province"
CONF_WORKDAYS = "workdays"

View File

@ -2,6 +2,7 @@
"domain": "workday",
"name": "Workday",
"codeowners": ["@fabaff", "@gjohansson-ST"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/workday",
"iot_class": "local_polling",
"loggers": [

View File

@ -0,0 +1,90 @@
{
"config": {
"abort": {
"incorrect_province": "Incorrect subdivision from yaml import"
},
"step": {
"user": {
"data": {
"name": "[%key:common::config_flow::data::name%]",
"country": "Country"
}
},
"options": {
"data": {
"excludes": "Excludes",
"days_offset": "Offset",
"workdays": "Workdays",
"add_holidays": "Add holidays",
"remove_holidays": "Remove Holidays",
"province": "Subdivision of country"
},
"data_description": {
"excludes": "List of workdays to exclude",
"days_offset": "Days offset",
"workdays": "List of workdays",
"add_holidays": "Add custom holidays as YYYY-MM-DD",
"remove_holidays": "Remove holidays as YYYY-MM-DD or by using partial of name",
"province": "State, Terroritory, Province, Region of Country"
}
}
},
"error": {
"add_holiday_error": "Incorrect format on date (YYYY-MM-DD)",
"remove_holiday_error": "Incorrect format on date (YYYY-MM-DD) or holiday name not found",
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]"
}
},
"options": {
"step": {
"init": {
"data": {
"excludes": "[%key:component::workday::config::step::options::data::excludes%]",
"days_offset": "[%key:component::workday::config::step::options::data::days_offset%]",
"workdays": "[%key:component::workday::config::step::options::data::workdays%]",
"add_holidays": "[%key:component::workday::config::step::options::data::add_holidays%]",
"remove_holidays": "[%key:component::workday::config::step::options::data::remove_holidays%]",
"province": "[%key:component::workday::config::step::options::data::province%]"
},
"data_description": {
"excludes": "[%key:component::workday::config::step::options::data_description::excludes%]",
"days_offset": "[%key:component::workday::config::step::options::data_description::days_offset%]",
"workdays": "[%key:component::workday::config::step::options::data_description::workdays%]",
"add_holidays": "[%key:component::workday::config::step::options::data_description::add_holidays%]",
"remove_holidays": "[%key:component::workday::config::step::options::data_description::remove_holidays%]",
"province": "[%key:component::workday::config::step::options::data_description::province%]"
}
}
},
"error": {
"add_holiday_error": "Incorrect format on date (YYYY-MM-DD)",
"remove_holiday_error": "Incorrect format on date (YYYY-MM-DD) or holiday name not found",
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]"
}
},
"issues": {
"deprecated_yaml": {
"title": "The Workday YAML configuration is being removed",
"description": "Configuring Workday using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the Workday YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue."
}
},
"selector": {
"province": {
"options": {
"none": "No subdivision"
}
},
"days": {
"options": {
"mon": "Monday",
"tue": "Tuesday",
"wed": "Wednesday",
"thu": "Thursday",
"fri": "Friday",
"sat": "Saturday",
"sun": "Sunday",
"holiday": "Holidays"
}
}
}
}

View File

@ -500,6 +500,7 @@ FLOWS = {
"wiz",
"wled",
"wolflink",
"workday",
"ws66i",
"xbox",
"xiaomi_aqara",

View File

@ -6224,7 +6224,7 @@
"workday": {
"name": "Workday",
"integration_type": "hub",
"config_flow": false,
"config_flow": true,
"iot_class": "local_polling"
},
"worldclock": {

View File

@ -8,22 +8,37 @@ from homeassistant.components.workday.const import (
DEFAULT_NAME,
DEFAULT_OFFSET,
DEFAULT_WORKDAYS,
DOMAIN,
)
from homeassistant.config_entries import SOURCE_USER
from homeassistant.core import HomeAssistant
from homeassistant.setup import async_setup_component
from tests.common import MockConfigEntry
async def init_integration(
hass: HomeAssistant,
config: dict[str, Any],
) -> None:
"""Set up the Workday integration in Home Assistant."""
entry_id: str = "1",
source: str = SOURCE_USER,
) -> MockConfigEntry:
"""Set up the Scrape integration in Home Assistant."""
await async_setup_component(
hass, "binary_sensor", {"binary_sensor": {"platform": "workday", **config}}
config_entry = MockConfigEntry(
domain=DOMAIN,
source=source,
data={},
options=config,
entry_id=entry_id,
)
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
TEST_CONFIG_WITH_PROVINCE = {
"name": DEFAULT_NAME,

View File

@ -0,0 +1,14 @@
"""Fixtures for Workday 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.workday.async_setup_entry", return_value=True
) as mock_setup:
yield mock_setup

View File

@ -79,6 +79,34 @@ async def test_setup(
}
async def test_setup_from_import(
hass: HomeAssistant,
freezer: FrozenDateTimeFactory,
) -> None:
"""Test setup from various configs."""
freezer.move_to(datetime(2022, 4, 15, 12, tzinfo=UTC)) # Monday
await async_setup_component(
hass,
"binary_sensor",
{
"binary_sensor": {
"platform": "workday",
"country": "DE",
}
},
)
await hass.async_block_till_done()
state = hass.states.get("binary_sensor.workday_sensor")
assert state.state == "off"
assert state.attributes == {
"friendly_name": "Workday Sensor",
"workdays": ["mon", "tue", "wed", "thu", "fri"],
"excludes": ["sat", "sun", "holiday"],
"days_offset": 0,
}
async def test_setup_with_invalid_province_from_yaml(hass: HomeAssistant) -> None:
"""Test setup invalid province with import."""

View File

@ -0,0 +1,488 @@
"""Test the Workday config flow."""
from __future__ import annotations
import pytest
from homeassistant import config_entries
from homeassistant.components.workday.const import (
CONF_ADD_HOLIDAYS,
CONF_COUNTRY,
CONF_EXCLUDES,
CONF_OFFSET,
CONF_PROVINCE,
CONF_REMOVE_HOLIDAYS,
CONF_WORKDAYS,
DEFAULT_EXCLUDES,
DEFAULT_NAME,
DEFAULT_OFFSET,
DEFAULT_WORKDAYS,
DOMAIN,
)
from homeassistant.const import CONF_NAME
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType
from . import init_integration
from tests.common import MockConfigEntry
pytestmark = pytest.mark.usefixtures("mock_setup_entry")
async def test_form(hass: HomeAssistant) -> 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
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
CONF_NAME: "Workday Sensor",
CONF_COUNTRY: "DE",
},
)
await hass.async_block_till_done()
result3 = await hass.config_entries.flow.async_configure(
result2["flow_id"],
{
CONF_EXCLUDES: DEFAULT_EXCLUDES,
CONF_OFFSET: DEFAULT_OFFSET,
CONF_WORKDAYS: DEFAULT_WORKDAYS,
CONF_ADD_HOLIDAYS: [],
CONF_REMOVE_HOLIDAYS: [],
CONF_PROVINCE: "none",
},
)
await hass.async_block_till_done()
assert result3["type"] == FlowResultType.CREATE_ENTRY
assert result3["title"] == "Workday Sensor"
assert result3["options"] == {
"name": "Workday Sensor",
"country": "DE",
"excludes": ["sat", "sun", "holiday"],
"days_offset": 0,
"workdays": ["mon", "tue", "wed", "thu", "fri"],
"add_holidays": [],
"remove_holidays": [],
"province": None,
}
async def test_form_no_subdivision(hass: HomeAssistant) -> None:
"""Test we get the forms correctly without subdivision."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == FlowResultType.FORM
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
CONF_NAME: "Workday Sensor",
CONF_COUNTRY: "SE",
},
)
await hass.async_block_till_done()
result3 = await hass.config_entries.flow.async_configure(
result2["flow_id"],
{
CONF_EXCLUDES: DEFAULT_EXCLUDES,
CONF_OFFSET: DEFAULT_OFFSET,
CONF_WORKDAYS: DEFAULT_WORKDAYS,
CONF_ADD_HOLIDAYS: [],
CONF_REMOVE_HOLIDAYS: [],
},
)
await hass.async_block_till_done()
assert result3["type"] == FlowResultType.CREATE_ENTRY
assert result3["title"] == "Workday Sensor"
assert result3["options"] == {
"name": "Workday Sensor",
"country": "SE",
"excludes": ["sat", "sun", "holiday"],
"days_offset": 0,
"workdays": ["mon", "tue", "wed", "thu", "fri"],
"add_holidays": [],
"remove_holidays": [],
"province": None,
}
async def test_import_flow_success(hass: HomeAssistant) -> None:
"""Test a successful import of yaml."""
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_IMPORT},
data={
CONF_NAME: DEFAULT_NAME,
CONF_COUNTRY: "DE",
CONF_EXCLUDES: DEFAULT_EXCLUDES,
CONF_OFFSET: DEFAULT_OFFSET,
CONF_WORKDAYS: DEFAULT_WORKDAYS,
CONF_ADD_HOLIDAYS: [],
CONF_REMOVE_HOLIDAYS: [],
},
)
await hass.async_block_till_done()
assert result["type"] == FlowResultType.CREATE_ENTRY
assert result["title"] == "Workday Sensor"
assert result["options"] == {
"name": "Workday Sensor",
"country": "DE",
"excludes": ["sat", "sun", "holiday"],
"days_offset": 0,
"workdays": ["mon", "tue", "wed", "thu", "fri"],
"add_holidays": [],
"remove_holidays": [],
"province": None,
}
result2 = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_IMPORT},
data={
CONF_NAME: "Workday Sensor 2",
CONF_COUNTRY: "DE",
CONF_PROVINCE: "BW",
CONF_EXCLUDES: DEFAULT_EXCLUDES,
CONF_OFFSET: DEFAULT_OFFSET,
CONF_WORKDAYS: DEFAULT_WORKDAYS,
CONF_ADD_HOLIDAYS: [],
CONF_REMOVE_HOLIDAYS: [],
},
)
await hass.async_block_till_done()
assert result2["type"] == FlowResultType.CREATE_ENTRY
assert result2["title"] == "Workday Sensor 2"
assert result2["options"] == {
"name": "Workday Sensor 2",
"country": "DE",
"province": "BW",
"excludes": ["sat", "sun", "holiday"],
"days_offset": 0,
"workdays": ["mon", "tue", "wed", "thu", "fri"],
"add_holidays": [],
"remove_holidays": [],
}
async def test_import_flow_already_exist(hass: HomeAssistant) -> None:
"""Test import of yaml already exist."""
entry = MockConfigEntry(
domain=DOMAIN,
data={},
options={
"name": "Workday Sensor",
"country": "DE",
"excludes": ["sat", "sun", "holiday"],
"days_offset": 0,
"workdays": ["mon", "tue", "wed", "thu", "fri"],
"add_holidays": [],
"remove_holidays": [],
"province": None,
},
)
entry.add_to_hass(hass)
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_IMPORT},
data={
CONF_NAME: "Workday sensor 2",
CONF_COUNTRY: "DE",
CONF_EXCLUDES: ["sat", "sun", "holiday"],
CONF_OFFSET: 0,
CONF_WORKDAYS: ["mon", "tue", "wed", "thu", "fri"],
CONF_ADD_HOLIDAYS: [],
CONF_REMOVE_HOLIDAYS: [],
},
)
await hass.async_block_till_done()
assert result["type"] == FlowResultType.ABORT
assert result["reason"] == "already_configured"
async def test_import_flow_province_no_conflict(hass: HomeAssistant) -> None:
"""Test import of yaml with province."""
entry = MockConfigEntry(
domain=DOMAIN,
data={},
options={
"name": "Workday Sensor",
"country": "DE",
"excludes": ["sat", "sun", "holiday"],
"days_offset": 0,
"workdays": ["mon", "tue", "wed", "thu", "fri"],
"add_holidays": [],
"remove_holidays": [],
},
)
entry.add_to_hass(hass)
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_IMPORT},
data={
CONF_NAME: "Workday sensor 2",
CONF_COUNTRY: "DE",
CONF_PROVINCE: "BW",
CONF_EXCLUDES: ["sat", "sun", "holiday"],
CONF_OFFSET: 0,
CONF_WORKDAYS: ["mon", "tue", "wed", "thu", "fri"],
CONF_ADD_HOLIDAYS: [],
CONF_REMOVE_HOLIDAYS: [],
},
)
await hass.async_block_till_done()
assert result["type"] == FlowResultType.CREATE_ENTRY
async def test_options_form(hass: HomeAssistant) -> None:
"""Test we get the form in options."""
entry = await init_integration(
hass,
{
"name": "Workday Sensor",
"country": "DE",
"excludes": ["sat", "sun", "holiday"],
"days_offset": 0,
"workdays": ["mon", "tue", "wed", "thu", "fri"],
"add_holidays": [],
"remove_holidays": [],
"province": None,
},
)
result = await hass.config_entries.options.async_init(entry.entry_id)
result2 = await hass.config_entries.options.async_configure(
result["flow_id"],
user_input={
"excludes": ["sat", "sun", "holiday"],
"days_offset": 0,
"workdays": ["mon", "tue", "wed", "thu", "fri"],
"add_holidays": [],
"remove_holidays": [],
"province": "BW",
},
)
assert result2["type"] == FlowResultType.CREATE_ENTRY
assert result2["data"] == {
"name": "Workday Sensor",
"country": "DE",
"excludes": ["sat", "sun", "holiday"],
"days_offset": 0,
"workdays": ["mon", "tue", "wed", "thu", "fri"],
"add_holidays": [],
"remove_holidays": [],
"province": "BW",
}
async def test_form_incorrect_dates(hass: HomeAssistant) -> None:
"""Test errors in setup entry."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == FlowResultType.FORM
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
CONF_NAME: "Workday Sensor",
CONF_COUNTRY: "DE",
},
)
await hass.async_block_till_done()
result3 = await hass.config_entries.flow.async_configure(
result2["flow_id"],
{
CONF_EXCLUDES: DEFAULT_EXCLUDES,
CONF_OFFSET: DEFAULT_OFFSET,
CONF_WORKDAYS: DEFAULT_WORKDAYS,
CONF_ADD_HOLIDAYS: ["2022-xx-12"],
CONF_REMOVE_HOLIDAYS: [],
CONF_PROVINCE: "none",
},
)
await hass.async_block_till_done()
assert result3["errors"] == {"add_holidays": "add_holiday_error"}
result3 = await hass.config_entries.flow.async_configure(
result2["flow_id"],
{
CONF_EXCLUDES: DEFAULT_EXCLUDES,
CONF_OFFSET: DEFAULT_OFFSET,
CONF_WORKDAYS: DEFAULT_WORKDAYS,
CONF_ADD_HOLIDAYS: ["2022-12-12"],
CONF_REMOVE_HOLIDAYS: ["Does not exist"],
CONF_PROVINCE: "none",
},
)
await hass.async_block_till_done()
assert result3["errors"] == {"remove_holidays": "remove_holiday_error"}
result3 = await hass.config_entries.flow.async_configure(
result2["flow_id"],
{
CONF_EXCLUDES: DEFAULT_EXCLUDES,
CONF_OFFSET: DEFAULT_OFFSET,
CONF_WORKDAYS: DEFAULT_WORKDAYS,
CONF_ADD_HOLIDAYS: ["2022-12-12"],
CONF_REMOVE_HOLIDAYS: ["Weihnachtstag"],
CONF_PROVINCE: "none",
},
)
await hass.async_block_till_done()
assert result3["type"] == FlowResultType.CREATE_ENTRY
assert result3["title"] == "Workday Sensor"
assert result3["options"] == {
"name": "Workday Sensor",
"country": "DE",
"excludes": ["sat", "sun", "holiday"],
"days_offset": 0,
"workdays": ["mon", "tue", "wed", "thu", "fri"],
"add_holidays": ["2022-12-12"],
"remove_holidays": ["Weihnachtstag"],
"province": None,
}
async def test_options_form_incorrect_dates(hass: HomeAssistant) -> None:
"""Test errors in options."""
entry = await init_integration(
hass,
{
"name": "Workday Sensor",
"country": "DE",
"excludes": ["sat", "sun", "holiday"],
"days_offset": 0,
"workdays": ["mon", "tue", "wed", "thu", "fri"],
"add_holidays": [],
"remove_holidays": [],
"province": None,
},
)
result = await hass.config_entries.options.async_init(entry.entry_id)
result2 = await hass.config_entries.options.async_configure(
result["flow_id"],
user_input={
"excludes": ["sat", "sun", "holiday"],
"days_offset": 0,
"workdays": ["mon", "tue", "wed", "thu", "fri"],
"add_holidays": ["2022-xx-12"],
"remove_holidays": [],
"province": "BW",
},
)
assert result2["errors"] == {"add_holidays": "add_holiday_error"}
result2 = await hass.config_entries.options.async_configure(
result["flow_id"],
user_input={
"excludes": ["sat", "sun", "holiday"],
"days_offset": 0,
"workdays": ["mon", "tue", "wed", "thu", "fri"],
"add_holidays": ["2022-12-12"],
"remove_holidays": ["Does not exist"],
"province": "BW",
},
)
assert result2["errors"] == {"remove_holidays": "remove_holiday_error"}
result2 = await hass.config_entries.options.async_configure(
result["flow_id"],
user_input={
"excludes": ["sat", "sun", "holiday"],
"days_offset": 0,
"workdays": ["mon", "tue", "wed", "thu", "fri"],
"add_holidays": ["2022-12-12"],
"remove_holidays": ["Weihnachtstag"],
"province": "BW",
},
)
assert result2["type"] == FlowResultType.CREATE_ENTRY
assert result2["data"] == {
"name": "Workday Sensor",
"country": "DE",
"excludes": ["sat", "sun", "holiday"],
"days_offset": 0,
"workdays": ["mon", "tue", "wed", "thu", "fri"],
"add_holidays": ["2022-12-12"],
"remove_holidays": ["Weihnachtstag"],
"province": "BW",
}
async def test_options_form_abort_duplicate(hass: HomeAssistant) -> None:
"""Test errors in options for duplicates."""
await init_integration(
hass,
{
"name": "Workday Sensor",
"country": "DE",
"excludes": ["sat", "sun", "holiday"],
"days_offset": 0,
"workdays": ["mon", "tue", "wed", "thu", "fri"],
"add_holidays": [],
"remove_holidays": [],
"province": None,
},
entry_id="1",
)
entry2 = await init_integration(
hass,
{
"name": "Workday Sensor2",
"country": "DE",
"excludes": ["sat", "sun", "holiday"],
"days_offset": 0,
"workdays": ["mon", "tue", "wed", "thu", "fri"],
"add_holidays": ["2023-03-28"],
"remove_holidays": [],
"province": None,
},
entry_id="2",
)
result = await hass.config_entries.options.async_init(entry2.entry_id)
result2 = await hass.config_entries.options.async_configure(
result["flow_id"],
user_input={
"excludes": ["sat", "sun", "holiday"],
"days_offset": 0.0,
"workdays": ["mon", "tue", "wed", "thu", "fri"],
"add_holidays": [],
"remove_holidays": [],
"province": "none",
},
)
assert result2["type"] == FlowResultType.FORM
assert result2["errors"] == {"base": "already_configured"}

View File

@ -0,0 +1,51 @@
"""Test Workday component setup process."""
from __future__ import annotations
from datetime import datetime
from freezegun.api import FrozenDateTimeFactory
from homeassistant import config_entries
from homeassistant.core import HomeAssistant
from homeassistant.util.dt import UTC
from . import TEST_CONFIG_EXAMPLE_1, TEST_CONFIG_WITH_PROVINCE, init_integration
async def test_load_unload_entry(hass: HomeAssistant) -> None:
"""Test load and unload entry."""
entry = await init_integration(hass, TEST_CONFIG_EXAMPLE_1)
state = hass.states.get("binary_sensor.workday_sensor")
assert state
await hass.config_entries.async_remove(entry.entry_id)
await hass.async_block_till_done()
state = hass.states.get("binary_sensor.workday_sensor")
assert not state
async def test_update_options(
hass: HomeAssistant,
freezer: FrozenDateTimeFactory,
) -> None:
"""Test options update and config entry is reloaded."""
freezer.move_to(datetime(2023, 4, 12, 12, tzinfo=UTC)) # Monday
entry = await init_integration(hass, TEST_CONFIG_WITH_PROVINCE)
assert entry.state == config_entries.ConfigEntryState.LOADED
assert entry.update_listeners is not None
state = hass.states.get("binary_sensor.workday_sensor")
assert state.state == "on"
new_options = TEST_CONFIG_WITH_PROVINCE.copy()
new_options["add_holidays"] = ["2023-04-12"]
hass.config_entries.async_update_entry(entry, options=new_options)
await hass.async_block_till_done()
entry_check = hass.config_entries.async_get_entry("1")
assert entry_check.state == config_entries.ConfigEntryState.LOADED
state = hass.states.get("binary_sensor.workday_sensor")
assert state.state == "off"