mirror of
https://github.com/home-assistant/core.git
synced 2025-04-24 17:27:52 +00:00
Add config flow to homeworks (#112042)
This commit is contained in:
parent
b5528de807
commit
7e7f25c859
@ -545,7 +545,8 @@ omit =
|
||||
homeassistant/components/homematic/notify.py
|
||||
homeassistant/components/homematic/sensor.py
|
||||
homeassistant/components/homematic/switch.py
|
||||
homeassistant/components/homeworks/*
|
||||
homeassistant/components/homeworks/__init__.py
|
||||
homeassistant/components/homeworks/light.py
|
||||
homeassistant/components/horizon/media_player.py
|
||||
homeassistant/components/hp_ilo/sensor.py
|
||||
homeassistant/components/huawei_lte/__init__.py
|
||||
|
@ -1,9 +1,14 @@
|
||||
"""Support for Lutron Homeworks Series 4 and 8 systems."""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from pyhomeworks.pyhomeworks import HW_BUTTON_PRESSED, HW_BUTTON_RELEASED, Homeworks
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
|
||||
from homeassistant.const import (
|
||||
CONF_HOST,
|
||||
CONF_ID,
|
||||
@ -13,27 +18,31 @@ from homeassistant.const import (
|
||||
Platform,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.discovery import load_platform
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect, dispatcher_send
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.util import slugify
|
||||
|
||||
from .const import (
|
||||
CONF_ADDR,
|
||||
CONF_CONTROLLER_ID,
|
||||
CONF_DIMMERS,
|
||||
CONF_KEYPADS,
|
||||
CONF_RATE,
|
||||
DOMAIN,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = "homeworks"
|
||||
PLATFORMS: list[Platform] = [Platform.LIGHT]
|
||||
|
||||
HOMEWORKS_CONTROLLER = "homeworks"
|
||||
EVENT_BUTTON_PRESS = "homeworks_button_press"
|
||||
EVENT_BUTTON_RELEASE = "homeworks_button_release"
|
||||
|
||||
CONF_DIMMERS = "dimmers"
|
||||
CONF_KEYPADS = "keypads"
|
||||
CONF_ADDR = "addr"
|
||||
CONF_RATE = "rate"
|
||||
DEFAULT_FADE_RATE = 1.0
|
||||
|
||||
FADE_RATE = 1.0
|
||||
|
||||
CV_FADE_RATE = vol.All(vol.Coerce(float), vol.Range(min=0, max=20))
|
||||
|
||||
@ -41,7 +50,7 @@ DIMMER_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_ADDR): cv.string,
|
||||
vol.Required(CONF_NAME): cv.string,
|
||||
vol.Optional(CONF_RATE, default=FADE_RATE): CV_FADE_RATE,
|
||||
vol.Optional(CONF_RATE, default=DEFAULT_FADE_RATE): CV_FADE_RATE,
|
||||
}
|
||||
)
|
||||
|
||||
@ -66,64 +75,137 @@ CONFIG_SCHEMA = vol.Schema(
|
||||
)
|
||||
|
||||
|
||||
def setup(hass: HomeAssistant, base_config: ConfigType) -> bool:
|
||||
@dataclass
|
||||
class HomeworksData:
|
||||
"""Container for config entry data."""
|
||||
|
||||
controller: Homeworks
|
||||
controller_id: str
|
||||
keypads: dict[str, HomeworksKeypad]
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Start Homeworks controller."""
|
||||
|
||||
def hw_callback(msg_type, values):
|
||||
"""Dispatch state changes."""
|
||||
_LOGGER.debug("callback: %s, %s", msg_type, values)
|
||||
addr = values[0]
|
||||
signal = f"homeworks_entity_{addr}"
|
||||
dispatcher_send(hass, signal, msg_type, values)
|
||||
|
||||
config = base_config[DOMAIN]
|
||||
controller = Homeworks(config[CONF_HOST], config[CONF_PORT], hw_callback)
|
||||
hass.data[HOMEWORKS_CONTROLLER] = controller
|
||||
|
||||
def cleanup(event):
|
||||
controller.close()
|
||||
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, cleanup)
|
||||
|
||||
dimmers = config[CONF_DIMMERS]
|
||||
load_platform(hass, Platform.LIGHT, DOMAIN, {CONF_DIMMERS: dimmers}, base_config)
|
||||
|
||||
for key_config in config[CONF_KEYPADS]:
|
||||
addr = key_config[CONF_ADDR]
|
||||
name = key_config[CONF_NAME]
|
||||
HomeworksKeypadEvent(hass, addr, name)
|
||||
if DOMAIN in config:
|
||||
hass.async_create_task(
|
||||
hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_IMPORT}, data=config[DOMAIN]
|
||||
)
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class HomeworksDevice(Entity):
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up Homeworks from a config entry."""
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
controller_id = entry.options[CONF_CONTROLLER_ID]
|
||||
|
||||
def hw_callback(msg_type: Any, values: Any) -> None:
|
||||
"""Dispatch state changes."""
|
||||
_LOGGER.debug("callback: %s, %s", msg_type, values)
|
||||
addr = values[0]
|
||||
signal = f"homeworks_entity_{controller_id}_{addr}"
|
||||
dispatcher_send(hass, signal, msg_type, values)
|
||||
|
||||
config = entry.options
|
||||
try:
|
||||
controller = await hass.async_add_executor_job(
|
||||
Homeworks, config[CONF_HOST], config[CONF_PORT], hw_callback
|
||||
)
|
||||
except (ConnectionError, OSError) as err:
|
||||
raise ConfigEntryNotReady from err
|
||||
|
||||
def cleanup(event):
|
||||
controller.close()
|
||||
|
||||
entry.async_on_unload(hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, cleanup))
|
||||
|
||||
keypads: dict[str, HomeworksKeypad] = {}
|
||||
for key_config in config.get(CONF_KEYPADS, []):
|
||||
addr = key_config[CONF_ADDR]
|
||||
name = key_config[CONF_NAME]
|
||||
keypads[addr] = HomeworksKeypad(hass, controller, controller_id, addr, name)
|
||||
|
||||
hass.data[DOMAIN][entry.entry_id] = HomeworksData(
|
||||
controller, controller_id, keypads
|
||||
)
|
||||
|
||||
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 a config entry."""
|
||||
if not await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
||||
return False
|
||||
|
||||
data: HomeworksData = hass.data[DOMAIN].pop(entry.entry_id)
|
||||
for keypad in data.keypads.values():
|
||||
keypad.unsubscribe()
|
||||
|
||||
await hass.async_add_executor_job(data.controller.close)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
"""Handle options update."""
|
||||
await hass.config_entries.async_reload(entry.entry_id)
|
||||
|
||||
|
||||
def calculate_unique_id(controller_id, addr, idx):
|
||||
"""Calculate entity unique id."""
|
||||
return f"homeworks.{controller_id}.{addr}.{idx}"
|
||||
|
||||
|
||||
class HomeworksEntity(Entity):
|
||||
"""Base class of a Homeworks device."""
|
||||
|
||||
_attr_should_poll = False
|
||||
|
||||
def __init__(self, controller, addr, name):
|
||||
def __init__(
|
||||
self,
|
||||
controller: Homeworks,
|
||||
controller_id: str,
|
||||
addr: str,
|
||||
idx: int,
|
||||
name: str | None,
|
||||
) -> None:
|
||||
"""Initialize Homeworks device."""
|
||||
self._addr = addr
|
||||
self._idx = idx
|
||||
self._controller_id = controller_id
|
||||
self._attr_name = name
|
||||
self._attr_unique_id = f"homeworks.{self._addr}"
|
||||
self._attr_unique_id = calculate_unique_id(
|
||||
self._controller_id, self._addr, self._idx
|
||||
)
|
||||
self._controller = controller
|
||||
|
||||
|
||||
class HomeworksKeypadEvent:
|
||||
class HomeworksKeypad:
|
||||
"""When you want signals instead of entities.
|
||||
|
||||
Stateless sensors such as keypads are expected to generate an event
|
||||
instead of a sensor entity in hass.
|
||||
"""
|
||||
|
||||
def __init__(self, hass, addr, name):
|
||||
def __init__(self, hass, controller, controller_id, addr, name):
|
||||
"""Register callback that will be used for signals."""
|
||||
self._hass = hass
|
||||
self._addr = addr
|
||||
self._controller = controller
|
||||
self._hass = hass
|
||||
self._name = name
|
||||
self._id = slugify(self._name)
|
||||
signal = f"homeworks_entity_{self._addr}"
|
||||
async_dispatcher_connect(self._hass, signal, self._update_callback)
|
||||
signal = f"homeworks_entity_{controller_id}_{self._addr}"
|
||||
_LOGGER.debug("connecting %s", signal)
|
||||
self.unsubscribe = async_dispatcher_connect(
|
||||
self._hass, signal, self._update_callback
|
||||
)
|
||||
|
||||
@callback
|
||||
def _update_callback(self, msg_type, values):
|
||||
|
465
homeassistant/components/homeworks/config_flow.py
Normal file
465
homeassistant/components/homeworks/config_flow.py
Normal file
@ -0,0 +1,465 @@
|
||||
"""Lutron Homeworks Series 4 and 8 config flow."""
|
||||
from __future__ import annotations
|
||||
|
||||
from functools import partial
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from pyhomeworks.pyhomeworks import Homeworks
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN
|
||||
from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT
|
||||
from homeassistant.core import (
|
||||
DOMAIN as HOMEASSISTANT_DOMAIN,
|
||||
HomeAssistant,
|
||||
async_get_hass,
|
||||
callback,
|
||||
)
|
||||
from homeassistant.data_entry_flow import AbortFlow
|
||||
from homeassistant.helpers import (
|
||||
config_validation as cv,
|
||||
entity_registry as er,
|
||||
issue_registry as ir,
|
||||
selector,
|
||||
)
|
||||
from homeassistant.helpers.schema_config_entry_flow import (
|
||||
SchemaCommonFlowHandler,
|
||||
SchemaFlowError,
|
||||
SchemaFlowFormStep,
|
||||
SchemaFlowMenuStep,
|
||||
SchemaOptionsFlowHandler,
|
||||
)
|
||||
from homeassistant.helpers.selector import TextSelector
|
||||
from homeassistant.util import slugify
|
||||
|
||||
from . import DEFAULT_FADE_RATE, calculate_unique_id
|
||||
from .const import (
|
||||
CONF_ADDR,
|
||||
CONF_CONTROLLER_ID,
|
||||
CONF_DIMMERS,
|
||||
CONF_INDEX,
|
||||
CONF_KEYPADS,
|
||||
CONF_RATE,
|
||||
DEFAULT_KEYPAD_NAME,
|
||||
DEFAULT_LIGHT_NAME,
|
||||
DOMAIN,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONTROLLER_EDIT = {
|
||||
vol.Required(CONF_HOST): selector.TextSelector(),
|
||||
vol.Required(CONF_PORT): selector.NumberSelector(
|
||||
selector.NumberSelectorConfig(
|
||||
min=1,
|
||||
max=65535,
|
||||
mode=selector.NumberSelectorMode.BOX,
|
||||
)
|
||||
),
|
||||
}
|
||||
|
||||
LIGHT_EDIT = {
|
||||
vol.Optional(CONF_RATE, default=DEFAULT_FADE_RATE): selector.NumberSelector(
|
||||
selector.NumberSelectorConfig(
|
||||
min=0,
|
||||
max=20,
|
||||
mode=selector.NumberSelectorMode.SLIDER,
|
||||
step=0.1,
|
||||
)
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
validate_addr = cv.matches_regex(r"\[\d\d:\d\d:\d\d:\d\d\]")
|
||||
|
||||
|
||||
async def validate_add_controller(
|
||||
handler: ConfigFlow | SchemaOptionsFlowHandler, user_input: dict[str, Any]
|
||||
) -> dict[str, Any]:
|
||||
"""Validate controller setup."""
|
||||
user_input[CONF_CONTROLLER_ID] = slugify(user_input[CONF_NAME])
|
||||
user_input[CONF_PORT] = int(user_input[CONF_PORT])
|
||||
try:
|
||||
handler._async_abort_entries_match( # pylint: disable=protected-access
|
||||
{CONF_HOST: user_input[CONF_HOST], CONF_PORT: user_input[CONF_PORT]}
|
||||
)
|
||||
except AbortFlow as err:
|
||||
raise SchemaFlowError("duplicated_host_port") from err
|
||||
|
||||
try:
|
||||
handler._async_abort_entries_match( # pylint: disable=protected-access
|
||||
{CONF_CONTROLLER_ID: user_input[CONF_CONTROLLER_ID]}
|
||||
)
|
||||
except AbortFlow as err:
|
||||
raise SchemaFlowError("duplicated_controller_id") from err
|
||||
|
||||
await _try_connection(user_input)
|
||||
|
||||
return user_input
|
||||
|
||||
|
||||
async def _try_connection(user_input: dict[str, Any]) -> None:
|
||||
"""Try connecting to the controller."""
|
||||
|
||||
def _try_connect(host: str, port: int) -> None:
|
||||
"""Try connecting to the controller.
|
||||
|
||||
Raises ConnectionError if the connection fails.
|
||||
"""
|
||||
_LOGGER.debug(
|
||||
"Trying to connect to %s:%s", user_input[CONF_HOST], user_input[CONF_PORT]
|
||||
)
|
||||
controller = Homeworks(host, port, lambda msg_types, values: None)
|
||||
controller.close()
|
||||
controller.join()
|
||||
|
||||
hass = async_get_hass()
|
||||
try:
|
||||
await hass.async_add_executor_job(
|
||||
_try_connect, user_input[CONF_HOST], user_input[CONF_PORT]
|
||||
)
|
||||
except ConnectionError as err:
|
||||
raise SchemaFlowError("connection_error") from err
|
||||
except Exception as err:
|
||||
_LOGGER.exception("Caught unexpected exception")
|
||||
raise SchemaFlowError("unknown_error") from err
|
||||
|
||||
|
||||
def _create_import_issue(hass: HomeAssistant) -> None:
|
||||
"""Create a repair issue asking the user to remove YAML."""
|
||||
ir.async_create_issue(
|
||||
hass,
|
||||
HOMEASSISTANT_DOMAIN,
|
||||
f"deprecated_yaml_{DOMAIN}",
|
||||
breaks_in_ha_version="2024.6.0",
|
||||
is_fixable=False,
|
||||
issue_domain=DOMAIN,
|
||||
severity=ir.IssueSeverity.WARNING,
|
||||
translation_key="deprecated_yaml",
|
||||
translation_placeholders={
|
||||
"domain": DOMAIN,
|
||||
"integration_title": "Lutron Homeworks",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def _validate_address(handler: SchemaCommonFlowHandler, addr: str) -> None:
|
||||
"""Validate address."""
|
||||
try:
|
||||
validate_addr(addr)
|
||||
except vol.Invalid as err:
|
||||
raise SchemaFlowError("invalid_addr") from err
|
||||
|
||||
for _key in (CONF_DIMMERS, CONF_KEYPADS):
|
||||
items: list[dict[str, Any]] = handler.options[_key]
|
||||
|
||||
for item in items:
|
||||
if item[CONF_ADDR] == addr:
|
||||
raise SchemaFlowError("duplicated_addr")
|
||||
|
||||
|
||||
async def validate_add_keypad(
|
||||
handler: SchemaCommonFlowHandler, user_input: dict[str, Any]
|
||||
) -> dict[str, Any]:
|
||||
"""Validate keypad or light input."""
|
||||
_validate_address(handler, user_input[CONF_ADDR])
|
||||
|
||||
# Standard behavior is to merge the result with the options.
|
||||
# In this case, we want to add a sub-item so we update the options directly.
|
||||
items = handler.options[CONF_KEYPADS]
|
||||
items.append(user_input)
|
||||
return {}
|
||||
|
||||
|
||||
async def validate_add_light(
|
||||
handler: SchemaCommonFlowHandler, user_input: dict[str, Any]
|
||||
) -> dict[str, Any]:
|
||||
"""Validate light input."""
|
||||
_validate_address(handler, user_input[CONF_ADDR])
|
||||
|
||||
# Standard behavior is to merge the result with the options.
|
||||
# In this case, we want to add a sub-item so we update the options directly.
|
||||
items = handler.options[CONF_DIMMERS]
|
||||
items.append(user_input)
|
||||
return {}
|
||||
|
||||
|
||||
async def get_select_light_schema(handler: SchemaCommonFlowHandler) -> vol.Schema:
|
||||
"""Return schema for selecting a light."""
|
||||
return vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_INDEX): vol.In(
|
||||
{
|
||||
str(index): f"{config[CONF_NAME]} ({config[CONF_ADDR]})"
|
||||
for index, config in enumerate(handler.options[CONF_DIMMERS])
|
||||
},
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def validate_select_keypad_light(
|
||||
handler: SchemaCommonFlowHandler, user_input: dict[str, Any]
|
||||
) -> dict[str, Any]:
|
||||
"""Store keypad or light index in flow state."""
|
||||
handler.flow_state["_idx"] = int(user_input[CONF_INDEX])
|
||||
return {}
|
||||
|
||||
|
||||
async def get_edit_light_suggested_values(
|
||||
handler: SchemaCommonFlowHandler,
|
||||
) -> dict[str, Any]:
|
||||
"""Return suggested values for light editing."""
|
||||
idx: int = handler.flow_state["_idx"]
|
||||
return dict(handler.options[CONF_DIMMERS][idx])
|
||||
|
||||
|
||||
async def validate_light_edit(
|
||||
handler: SchemaCommonFlowHandler, user_input: dict[str, Any]
|
||||
) -> dict[str, Any]:
|
||||
"""Update edited keypad or light."""
|
||||
# Standard behavior is to merge the result with the options.
|
||||
# In this case, we want to add a sub-item so we update the options directly.
|
||||
idx: int = handler.flow_state["_idx"]
|
||||
handler.options[CONF_DIMMERS][idx].update(user_input)
|
||||
return {}
|
||||
|
||||
|
||||
async def get_remove_keypad_light_schema(
|
||||
handler: SchemaCommonFlowHandler, *, key: str
|
||||
) -> vol.Schema:
|
||||
"""Return schema for keypad or light removal."""
|
||||
return vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_INDEX): cv.multi_select(
|
||||
{
|
||||
str(index): f"{config[CONF_NAME]} ({config[CONF_ADDR]})"
|
||||
for index, config in enumerate(handler.options[key])
|
||||
},
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def validate_remove_keypad_light(
|
||||
handler: SchemaCommonFlowHandler, user_input: dict[str, Any], *, key: str
|
||||
) -> dict[str, Any]:
|
||||
"""Validate remove keypad or light."""
|
||||
removed_indexes: set[str] = set(user_input[CONF_INDEX])
|
||||
|
||||
# Standard behavior is to merge the result with the options.
|
||||
# In this case, we want to remove sub-items so we update the options directly.
|
||||
entity_registry = er.async_get(handler.parent_handler.hass)
|
||||
items: list[dict[str, Any]] = []
|
||||
item: dict[str, Any]
|
||||
for index, item in enumerate(handler.options[key]):
|
||||
if str(index) not in removed_indexes:
|
||||
items.append(item)
|
||||
elif key != CONF_DIMMERS:
|
||||
continue
|
||||
if entity_id := entity_registry.async_get_entity_id(
|
||||
LIGHT_DOMAIN,
|
||||
DOMAIN,
|
||||
calculate_unique_id(
|
||||
handler.options[CONF_CONTROLLER_ID], item[CONF_ADDR], 0
|
||||
),
|
||||
):
|
||||
entity_registry.async_remove(entity_id)
|
||||
handler.options[key] = items
|
||||
return {}
|
||||
|
||||
|
||||
DATA_SCHEMA_ADD_CONTROLLER = vol.Schema(
|
||||
{
|
||||
vol.Required(
|
||||
CONF_NAME, description={"suggested_value": "Lutron Homeworks"}
|
||||
): selector.TextSelector(),
|
||||
**CONTROLLER_EDIT,
|
||||
}
|
||||
)
|
||||
DATA_SCHEMA_ADD_LIGHT = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_LIGHT_NAME): TextSelector(),
|
||||
vol.Required(CONF_ADDR): TextSelector(),
|
||||
**LIGHT_EDIT,
|
||||
}
|
||||
)
|
||||
DATA_SCHEMA_ADD_KEYPAD = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_KEYPAD_NAME): TextSelector(),
|
||||
vol.Required(CONF_ADDR): TextSelector(),
|
||||
}
|
||||
)
|
||||
DATA_SCHEMA_EDIT_LIGHT = vol.Schema(LIGHT_EDIT)
|
||||
|
||||
OPTIONS_FLOW = {
|
||||
"init": SchemaFlowMenuStep(
|
||||
[
|
||||
"add_keypad",
|
||||
"remove_keypad",
|
||||
"add_light",
|
||||
"select_edit_light",
|
||||
"remove_light",
|
||||
]
|
||||
),
|
||||
"add_keypad": SchemaFlowFormStep(
|
||||
DATA_SCHEMA_ADD_KEYPAD,
|
||||
suggested_values=None,
|
||||
validate_user_input=validate_add_keypad,
|
||||
),
|
||||
"remove_keypad": SchemaFlowFormStep(
|
||||
partial(get_remove_keypad_light_schema, key=CONF_KEYPADS),
|
||||
suggested_values=None,
|
||||
validate_user_input=partial(validate_remove_keypad_light, key=CONF_KEYPADS),
|
||||
),
|
||||
"add_light": SchemaFlowFormStep(
|
||||
DATA_SCHEMA_ADD_LIGHT,
|
||||
suggested_values=None,
|
||||
validate_user_input=validate_add_light,
|
||||
),
|
||||
"select_edit_light": SchemaFlowFormStep(
|
||||
get_select_light_schema,
|
||||
suggested_values=None,
|
||||
validate_user_input=validate_select_keypad_light,
|
||||
next_step="edit_light",
|
||||
),
|
||||
"edit_light": SchemaFlowFormStep(
|
||||
DATA_SCHEMA_EDIT_LIGHT,
|
||||
suggested_values=get_edit_light_suggested_values,
|
||||
validate_user_input=validate_light_edit,
|
||||
),
|
||||
"remove_light": SchemaFlowFormStep(
|
||||
partial(get_remove_keypad_light_schema, key=CONF_DIMMERS),
|
||||
suggested_values=None,
|
||||
validate_user_input=partial(validate_remove_keypad_light, key=CONF_DIMMERS),
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
class HomeworksConfigFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
"""Config flow for Lutron Homeworks."""
|
||||
|
||||
import_config: dict[str, Any]
|
||||
|
||||
async def async_step_import(self, config: dict[str, Any]) -> ConfigFlowResult:
|
||||
"""Start importing configuration from yaml."""
|
||||
self.import_config = {
|
||||
CONF_HOST: config[CONF_HOST],
|
||||
CONF_PORT: config[CONF_PORT],
|
||||
CONF_DIMMERS: [
|
||||
{
|
||||
CONF_ADDR: light[CONF_ADDR],
|
||||
CONF_NAME: light[CONF_NAME],
|
||||
CONF_RATE: light[CONF_RATE],
|
||||
}
|
||||
for light in config[CONF_DIMMERS]
|
||||
],
|
||||
CONF_KEYPADS: [
|
||||
{
|
||||
CONF_ADDR: keypad[CONF_ADDR],
|
||||
CONF_NAME: keypad[CONF_NAME],
|
||||
}
|
||||
for keypad in config[CONF_KEYPADS]
|
||||
],
|
||||
}
|
||||
return await self.async_step_import_controller_name()
|
||||
|
||||
async def async_step_import_controller_name(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Ask user to set a name of the controller."""
|
||||
errors = {}
|
||||
try:
|
||||
self._async_abort_entries_match(
|
||||
{
|
||||
CONF_HOST: self.import_config[CONF_HOST],
|
||||
CONF_PORT: self.import_config[CONF_PORT],
|
||||
}
|
||||
)
|
||||
except AbortFlow:
|
||||
_create_import_issue(self.hass)
|
||||
raise
|
||||
|
||||
if user_input:
|
||||
try:
|
||||
user_input[CONF_CONTROLLER_ID] = slugify(user_input[CONF_NAME])
|
||||
self._async_abort_entries_match(
|
||||
{CONF_CONTROLLER_ID: user_input[CONF_CONTROLLER_ID]}
|
||||
)
|
||||
except AbortFlow:
|
||||
errors["base"] = "duplicated_controller_id"
|
||||
else:
|
||||
self.import_config |= user_input
|
||||
return await self.async_step_import_finish()
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="import_controller_name",
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Required(
|
||||
CONF_NAME, description={"suggested_value": "Lutron Homeworks"}
|
||||
): selector.TextSelector(),
|
||||
}
|
||||
),
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
async def async_step_import_finish(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Ask user to remove YAML configuration."""
|
||||
|
||||
if user_input is not None:
|
||||
entity_registry = er.async_get(self.hass)
|
||||
config = self.import_config
|
||||
for light in config[CONF_DIMMERS]:
|
||||
addr = light[CONF_ADDR]
|
||||
if entity_id := entity_registry.async_get_entity_id(
|
||||
LIGHT_DOMAIN, DOMAIN, f"homeworks.{addr}"
|
||||
):
|
||||
entity_registry.async_update_entity(
|
||||
entity_id,
|
||||
new_unique_id=calculate_unique_id(
|
||||
config[CONF_CONTROLLER_ID], addr, 0
|
||||
),
|
||||
)
|
||||
name = config.pop(CONF_NAME)
|
||||
return self.async_create_entry(
|
||||
title=name,
|
||||
data={},
|
||||
options=config,
|
||||
)
|
||||
|
||||
return self.async_show_form(step_id="import_finish", data_schema=vol.Schema({}))
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle a flow initialized by the user."""
|
||||
errors = {}
|
||||
if user_input:
|
||||
try:
|
||||
await validate_add_controller(self, user_input)
|
||||
except SchemaFlowError as err:
|
||||
errors["base"] = str(err)
|
||||
else:
|
||||
self._async_abort_entries_match(
|
||||
{CONF_HOST: user_input[CONF_HOST], CONF_PORT: user_input[CONF_PORT]}
|
||||
)
|
||||
name = user_input.pop(CONF_NAME)
|
||||
user_input |= {CONF_DIMMERS: [], CONF_KEYPADS: []}
|
||||
return self.async_create_entry(title=name, data={}, options=user_input)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=DATA_SCHEMA_ADD_CONTROLLER,
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
@callback
|
||||
def async_get_options_flow(config_entry: ConfigEntry) -> SchemaOptionsFlowHandler:
|
||||
"""Options flow handler for Lutron Homeworks."""
|
||||
return SchemaOptionsFlowHandler(config_entry, OPTIONS_FLOW)
|
15
homeassistant/components/homeworks/const.py
Normal file
15
homeassistant/components/homeworks/const.py
Normal file
@ -0,0 +1,15 @@
|
||||
"""Constants for the Lutron Homeworks integration."""
|
||||
from __future__ import annotations
|
||||
|
||||
DOMAIN = "homeworks"
|
||||
|
||||
CONF_ADDR = "addr"
|
||||
CONF_CONTROLLER_ID = "controller_id"
|
||||
CONF_DIMMERS = "dimmers"
|
||||
CONF_INDEX = "index"
|
||||
CONF_KEYPADS = "keypads"
|
||||
CONF_RATE = "rate"
|
||||
|
||||
DEFAULT_BUTTON_NAME = "Homeworks button"
|
||||
DEFAULT_KEYPAD_NAME = "Homeworks keypad"
|
||||
DEFAULT_LIGHT_NAME = "Homeworks light"
|
@ -7,53 +7,61 @@ from typing import Any
|
||||
from pyhomeworks.pyhomeworks import HW_LIGHT_CHANGED
|
||||
|
||||
from homeassistant.components.light import ATTR_BRIGHTNESS, ColorMode, LightEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_NAME
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
from . import CONF_ADDR, CONF_DIMMERS, CONF_RATE, HOMEWORKS_CONTROLLER, HomeworksDevice
|
||||
from . import HomeworksData, HomeworksEntity
|
||||
from .const import CONF_ADDR, CONF_CONTROLLER_ID, CONF_DIMMERS, CONF_RATE, DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_platform(
|
||||
hass: HomeAssistant,
|
||||
config: ConfigType,
|
||||
add_entities: AddEntitiesCallback,
|
||||
discover_info: DiscoveryInfoType | None = None,
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
||||
) -> None:
|
||||
"""Set up Homeworks lights."""
|
||||
if discover_info is None:
|
||||
return
|
||||
|
||||
controller = hass.data[HOMEWORKS_CONTROLLER]
|
||||
data: HomeworksData = hass.data[DOMAIN][entry.entry_id]
|
||||
controller = data.controller
|
||||
controller_id = entry.options[CONF_CONTROLLER_ID]
|
||||
devs = []
|
||||
for dimmer in discover_info[CONF_DIMMERS]:
|
||||
for dimmer in entry.options.get(CONF_DIMMERS, []):
|
||||
dev = HomeworksLight(
|
||||
controller, dimmer[CONF_ADDR], dimmer[CONF_NAME], dimmer[CONF_RATE]
|
||||
controller,
|
||||
controller_id,
|
||||
dimmer[CONF_ADDR],
|
||||
dimmer[CONF_NAME],
|
||||
dimmer[CONF_RATE],
|
||||
)
|
||||
devs.append(dev)
|
||||
add_entities(devs, True)
|
||||
async_add_entities(devs, True)
|
||||
|
||||
|
||||
class HomeworksLight(HomeworksDevice, LightEntity):
|
||||
class HomeworksLight(HomeworksEntity, LightEntity):
|
||||
"""Homeworks Light."""
|
||||
|
||||
_attr_color_mode = ColorMode.BRIGHTNESS
|
||||
_attr_supported_color_modes = {ColorMode.BRIGHTNESS}
|
||||
|
||||
def __init__(self, controller, addr, name, rate):
|
||||
def __init__(
|
||||
self,
|
||||
controller,
|
||||
controller_id,
|
||||
addr,
|
||||
name,
|
||||
rate,
|
||||
):
|
||||
"""Create device with Addr, name, and rate."""
|
||||
super().__init__(controller, addr, name)
|
||||
super().__init__(controller, controller_id, addr, 0, name)
|
||||
self._rate = rate
|
||||
self._level = 0
|
||||
self._prev_level = 0
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Call when entity is added to hass."""
|
||||
signal = f"homeworks_entity_{self._addr}"
|
||||
signal = f"homeworks_entity_{self._controller_id}_{self._addr}"
|
||||
_LOGGER.debug("connecting %s", signal)
|
||||
self.async_on_remove(
|
||||
async_dispatcher_connect(self.hass, signal, self._update_callback)
|
||||
|
@ -2,6 +2,7 @@
|
||||
"domain": "homeworks",
|
||||
"name": "Lutron Homeworks",
|
||||
"codeowners": [],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/homeworks",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["pyhomeworks"],
|
||||
|
35
homeassistant/components/homeworks/strings.json
Normal file
35
homeassistant/components/homeworks/strings.json
Normal file
@ -0,0 +1,35 @@
|
||||
{
|
||||
"config": {
|
||||
"error": {
|
||||
"connection_error": "Could not connect to the controller.",
|
||||
"duplicated_controller_id": "The controller name is already in use.",
|
||||
"duplicated_host_port": "The specified host and port is already configured.",
|
||||
"unknown_error": "[%key:common::config_flow::error::unknown%]"
|
||||
},
|
||||
"step": {
|
||||
"import_finish": {
|
||||
"description": "The existing YAML configuration has succesfully been imported.\n\nYou can now remove the `homeworks` configuration from your configuration.yaml file."
|
||||
},
|
||||
"import_controller_name": {
|
||||
"description": "Lutron Homeworks is no longer configured through configuration.yaml.\n\nPlease fill in the form to import the existing configuration to the UI.",
|
||||
"data": {
|
||||
"name": "[%key:component::homeworks::config::step::user::data::name%]"
|
||||
},
|
||||
"data_description": {
|
||||
"name": "[%key:component::homeworks::config::step::user::data_description::name%]"
|
||||
}
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]",
|
||||
"name": "Controller name",
|
||||
"port": "[%key:common::config_flow::data::port%]"
|
||||
},
|
||||
"data_description": {
|
||||
"name": "A unique name identifying the Lutron Homeworks controller"
|
||||
},
|
||||
"description": "Add a Lutron Homeworks controller"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -224,6 +224,7 @@ FLOWS = {
|
||||
"homekit_controller",
|
||||
"homematicip_cloud",
|
||||
"homewizard",
|
||||
"homeworks",
|
||||
"honeywell",
|
||||
"huawei_lte",
|
||||
"hue",
|
||||
|
@ -3370,7 +3370,7 @@
|
||||
},
|
||||
"homeworks": {
|
||||
"integration_type": "hub",
|
||||
"config_flow": false,
|
||||
"config_flow": true,
|
||||
"iot_class": "local_push",
|
||||
"name": "Lutron Homeworks"
|
||||
}
|
||||
|
@ -1437,6 +1437,9 @@ pyhiveapi==0.5.16
|
||||
# homeassistant.components.homematic
|
||||
pyhomematic==0.1.77
|
||||
|
||||
# homeassistant.components.homeworks
|
||||
pyhomeworks==0.0.6
|
||||
|
||||
# homeassistant.components.ialarm
|
||||
pyialarm==2.2.0
|
||||
|
||||
|
1
tests/components/homeworks/__init__.py
Normal file
1
tests/components/homeworks/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
"""Tests for the Lutron Homeworks Series 4 and 8 integration."""
|
82
tests/components/homeworks/conftest.py
Normal file
82
tests/components/homeworks/conftest.py
Normal file
@ -0,0 +1,82 @@
|
||||
"""Common fixtures for the Lutron Homeworks Series 4 and 8 tests."""
|
||||
from collections.abc import Generator
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.homeworks.const import (
|
||||
CONF_ADDR,
|
||||
CONF_CONTROLLER_ID,
|
||||
CONF_DIMMERS,
|
||||
CONF_KEYPADS,
|
||||
CONF_RATE,
|
||||
DOMAIN,
|
||||
)
|
||||
from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_config_entry() -> MockConfigEntry:
|
||||
"""Return the default mocked config entry."""
|
||||
return MockConfigEntry(
|
||||
title="Lutron Homeworks",
|
||||
domain=DOMAIN,
|
||||
data={},
|
||||
options={
|
||||
CONF_CONTROLLER_ID: "main_controller",
|
||||
CONF_HOST: "192.168.0.1",
|
||||
CONF_PORT: 1234,
|
||||
CONF_DIMMERS: [
|
||||
{
|
||||
CONF_ADDR: "[02:08:01:01]",
|
||||
CONF_NAME: "Foyer Sconces",
|
||||
CONF_RATE: 1.0,
|
||||
}
|
||||
],
|
||||
CONF_KEYPADS: [
|
||||
{
|
||||
CONF_ADDR: "[02:08:02:01]",
|
||||
CONF_NAME: "Foyer Keypad",
|
||||
}
|
||||
],
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_empty_config_entry() -> MockConfigEntry:
|
||||
"""Return a mocked config entry with no keypads or dimmers."""
|
||||
return MockConfigEntry(
|
||||
title="Lutron Homeworks",
|
||||
domain=DOMAIN,
|
||||
data={},
|
||||
options={
|
||||
CONF_CONTROLLER_ID: "main_controller",
|
||||
CONF_HOST: "192.168.0.1",
|
||||
CONF_PORT: 1234,
|
||||
CONF_DIMMERS: [],
|
||||
CONF_KEYPADS: [],
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_homeworks() -> Generator[None, MagicMock, None]:
|
||||
"""Return a mocked Homeworks client."""
|
||||
with patch(
|
||||
"homeassistant.components.homeworks.Homeworks", autospec=True
|
||||
) as homeworks_mock, patch(
|
||||
"homeassistant.components.homeworks.config_flow.Homeworks", new=homeworks_mock
|
||||
):
|
||||
yield homeworks_mock
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_setup_entry() -> Generator[AsyncMock, None, None]:
|
||||
"""Override async_setup_entry."""
|
||||
with patch(
|
||||
"homeassistant.components.homeworks.async_setup_entry", return_value=True
|
||||
) as mock_setup_entry:
|
||||
yield mock_setup_entry
|
588
tests/components/homeworks/test_config_flow.py
Normal file
588
tests/components/homeworks/test_config_flow.py
Normal file
@ -0,0 +1,588 @@
|
||||
"""Test Lutron Homeworks Series 4 and 8 config flow."""
|
||||
from unittest.mock import ANY, MagicMock
|
||||
|
||||
import pytest
|
||||
from pytest_unordered import unordered
|
||||
|
||||
from homeassistant.components.homeworks.const import (
|
||||
CONF_ADDR,
|
||||
CONF_DIMMERS,
|
||||
CONF_INDEX,
|
||||
CONF_KEYPADS,
|
||||
CONF_RATE,
|
||||
DOMAIN,
|
||||
)
|
||||
from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN
|
||||
from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER
|
||||
from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
from homeassistant.helpers import entity_registry as er, issue_registry as ir
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
async def test_user_flow(
|
||||
hass: HomeAssistant, mock_homeworks: MagicMock, mock_setup_entry
|
||||
) -> None:
|
||||
"""Test the user configuration flow."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_USER},
|
||||
)
|
||||
|
||||
mock_controller = MagicMock()
|
||||
mock_homeworks.return_value = mock_controller
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={
|
||||
CONF_HOST: "192.168.0.1",
|
||||
CONF_NAME: "Main controller",
|
||||
CONF_PORT: 1234,
|
||||
},
|
||||
)
|
||||
assert result["type"] == FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == "Main controller"
|
||||
assert result["data"] == {}
|
||||
assert result["options"] == {
|
||||
"controller_id": "main_controller",
|
||||
"dimmers": [],
|
||||
"host": "192.168.0.1",
|
||||
"keypads": [],
|
||||
"port": 1234,
|
||||
}
|
||||
mock_homeworks.assert_called_once_with("192.168.0.1", 1234, ANY)
|
||||
mock_controller.close.assert_called_once_with()
|
||||
mock_controller.join.assert_called_once_with()
|
||||
|
||||
|
||||
async def test_user_flow_already_exists(
|
||||
hass: HomeAssistant, mock_empty_config_entry: MockConfigEntry, mock_setup_entry
|
||||
) -> None:
|
||||
"""Test the user configuration flow."""
|
||||
mock_empty_config_entry.add_to_hass(hass)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_USER},
|
||||
)
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={
|
||||
CONF_HOST: "192.168.0.1",
|
||||
CONF_NAME: "Main controller",
|
||||
CONF_PORT: 1234,
|
||||
},
|
||||
)
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["step_id"] == "user"
|
||||
assert result["errors"] == {"base": "duplicated_host_port"}
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={
|
||||
CONF_HOST: "192.168.0.2",
|
||||
CONF_NAME: "Main controller",
|
||||
CONF_PORT: 1234,
|
||||
},
|
||||
)
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["step_id"] == "user"
|
||||
assert result["errors"] == {"base": "duplicated_controller_id"}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("side_effect", "error"),
|
||||
[(ConnectionError, "connection_error"), (Exception, "unknown_error")],
|
||||
)
|
||||
async def test_user_flow_cannot_connect(
|
||||
hass: HomeAssistant,
|
||||
mock_homeworks: MagicMock,
|
||||
mock_setup_entry,
|
||||
side_effect: type[Exception],
|
||||
error: str,
|
||||
) -> None:
|
||||
"""Test handling invalid connection."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_USER},
|
||||
)
|
||||
|
||||
mock_homeworks.side_effect = side_effect
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={
|
||||
CONF_HOST: "192.168.0.1",
|
||||
CONF_NAME: "Main controller",
|
||||
CONF_PORT: 1234,
|
||||
},
|
||||
)
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["errors"] == {"base": error}
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
|
||||
async def test_import_flow(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
issue_registry: ir.IssueRegistry,
|
||||
mock_homeworks: MagicMock,
|
||||
mock_setup_entry,
|
||||
) -> None:
|
||||
"""Test importing yaml config."""
|
||||
entry = entity_registry.async_get_or_create(
|
||||
LIGHT_DOMAIN, DOMAIN, "homeworks.[02:08:01:01]"
|
||||
)
|
||||
|
||||
mock_controller = MagicMock()
|
||||
mock_homeworks.return_value = mock_controller
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_IMPORT},
|
||||
data={
|
||||
CONF_HOST: "192.168.0.1",
|
||||
CONF_PORT: 1234,
|
||||
CONF_DIMMERS: [
|
||||
{
|
||||
CONF_ADDR: "[02:08:01:01]",
|
||||
CONF_NAME: "Foyer Sconces",
|
||||
CONF_RATE: 1.0,
|
||||
}
|
||||
],
|
||||
CONF_KEYPADS: [
|
||||
{
|
||||
CONF_ADDR: "[02:08:02:01]",
|
||||
CONF_NAME: "Foyer Keypad",
|
||||
}
|
||||
],
|
||||
},
|
||||
)
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["step_id"] == "import_controller_name"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input={CONF_NAME: "Main controller"}
|
||||
)
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["step_id"] == "import_finish"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input={}
|
||||
)
|
||||
assert result["type"] == FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == "Main controller"
|
||||
assert result["data"] == {}
|
||||
assert result["options"] == {
|
||||
"controller_id": "main_controller",
|
||||
"dimmers": [{"addr": "[02:08:01:01]", "name": "Foyer Sconces", "rate": 1.0}],
|
||||
"host": "192.168.0.1",
|
||||
"keypads": [
|
||||
{
|
||||
"addr": "[02:08:02:01]",
|
||||
"name": "Foyer Keypad",
|
||||
}
|
||||
],
|
||||
"port": 1234,
|
||||
}
|
||||
assert len(issue_registry.issues) == 0
|
||||
|
||||
# Check unique ID is updated in entity registry
|
||||
entry = entity_registry.async_get(entry.id)
|
||||
assert entry.unique_id == "homeworks.main_controller.[02:08:01:01].0"
|
||||
|
||||
|
||||
async def test_import_flow_already_exists(
|
||||
hass: HomeAssistant,
|
||||
issue_registry: ir.IssueRegistry,
|
||||
mock_empty_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test importing yaml config where entry already exists."""
|
||||
mock_empty_config_entry.add_to_hass(hass)
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_IMPORT},
|
||||
data={"host": "192.168.0.1", "port": 1234, CONF_DIMMERS: [], CONF_KEYPADS: []},
|
||||
)
|
||||
assert result["type"] == FlowResultType.ABORT
|
||||
assert result["reason"] == "already_configured"
|
||||
assert len(issue_registry.issues) == 1
|
||||
|
||||
|
||||
async def test_import_flow_controller_id_exists(
|
||||
hass: HomeAssistant, mock_empty_config_entry: MockConfigEntry
|
||||
) -> None:
|
||||
"""Test importing yaml config where entry already exists."""
|
||||
mock_empty_config_entry.add_to_hass(hass)
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_IMPORT},
|
||||
data={"host": "192.168.0.2", "port": 1234, CONF_DIMMERS: [], CONF_KEYPADS: []},
|
||||
)
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["step_id"] == "import_controller_name"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input={CONF_NAME: "Main controller"}
|
||||
)
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["step_id"] == "import_controller_name"
|
||||
assert result["errors"] == {"base": "duplicated_controller_id"}
|
||||
|
||||
|
||||
async def test_options_add_light_flow(
|
||||
hass: HomeAssistant,
|
||||
mock_empty_config_entry: MockConfigEntry,
|
||||
mock_homeworks: MagicMock,
|
||||
) -> None:
|
||||
"""Test options flow to add a light."""
|
||||
mock_empty_config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(mock_empty_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
assert hass.states.async_entity_ids("light") == unordered([])
|
||||
|
||||
result = await hass.config_entries.options.async_init(
|
||||
mock_empty_config_entry.entry_id
|
||||
)
|
||||
assert result["type"] == FlowResultType.MENU
|
||||
assert result["step_id"] == "init"
|
||||
|
||||
result = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"],
|
||||
{"next_step_id": "add_light"},
|
||||
)
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["step_id"] == "add_light"
|
||||
|
||||
result = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={
|
||||
CONF_ADDR: "[02:08:01:02]",
|
||||
CONF_NAME: "Foyer Downlights",
|
||||
CONF_RATE: 2.0,
|
||||
},
|
||||
)
|
||||
assert result["type"] == FlowResultType.CREATE_ENTRY
|
||||
assert result["data"] == {
|
||||
"controller_id": "main_controller",
|
||||
"dimmers": [
|
||||
{"addr": "[02:08:01:02]", "name": "Foyer Downlights", "rate": 2.0},
|
||||
],
|
||||
"host": "192.168.0.1",
|
||||
"keypads": [],
|
||||
"port": 1234,
|
||||
}
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Check the entry was updated with the new entity
|
||||
assert hass.states.async_entity_ids("light") == unordered(
|
||||
["light.foyer_downlights"]
|
||||
)
|
||||
|
||||
|
||||
async def test_options_add_remove_light_flow(
|
||||
hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_homeworks: MagicMock
|
||||
) -> None:
|
||||
"""Test options flow to add and remove a light."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
assert hass.states.async_entity_ids("light") == unordered(["light.foyer_sconces"])
|
||||
|
||||
result = await hass.config_entries.options.async_init(mock_config_entry.entry_id)
|
||||
assert result["type"] == FlowResultType.MENU
|
||||
assert result["step_id"] == "init"
|
||||
|
||||
result = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"],
|
||||
{"next_step_id": "add_light"},
|
||||
)
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["step_id"] == "add_light"
|
||||
|
||||
result = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={
|
||||
CONF_ADDR: "[02:08:01:02]",
|
||||
CONF_NAME: "Foyer Downlights",
|
||||
CONF_RATE: 2.0,
|
||||
},
|
||||
)
|
||||
assert result["type"] == FlowResultType.CREATE_ENTRY
|
||||
assert result["data"] == {
|
||||
"controller_id": "main_controller",
|
||||
"dimmers": [
|
||||
{"addr": "[02:08:01:01]", "name": "Foyer Sconces", "rate": 1.0},
|
||||
{"addr": "[02:08:01:02]", "name": "Foyer Downlights", "rate": 2.0},
|
||||
],
|
||||
"host": "192.168.0.1",
|
||||
"keypads": [
|
||||
{
|
||||
"addr": "[02:08:02:01]",
|
||||
"name": "Foyer Keypad",
|
||||
}
|
||||
],
|
||||
"port": 1234,
|
||||
}
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Check the entry was updated with the new entity
|
||||
assert hass.states.async_entity_ids("light") == unordered(
|
||||
["light.foyer_sconces", "light.foyer_downlights"]
|
||||
)
|
||||
|
||||
# Now remove the original light
|
||||
result = await hass.config_entries.options.async_init(mock_config_entry.entry_id)
|
||||
assert result["type"] == FlowResultType.MENU
|
||||
assert result["step_id"] == "init"
|
||||
|
||||
result = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"],
|
||||
{"next_step_id": "remove_light"},
|
||||
)
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["step_id"] == "remove_light"
|
||||
assert result["data_schema"].schema["index"].options == {
|
||||
"0": "Foyer Sconces ([02:08:01:01])",
|
||||
"1": "Foyer Downlights ([02:08:01:02])",
|
||||
}
|
||||
|
||||
result = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"], user_input={CONF_INDEX: ["0"]}
|
||||
)
|
||||
assert result["type"] == FlowResultType.CREATE_ENTRY
|
||||
assert result["data"] == {
|
||||
"controller_id": "main_controller",
|
||||
"dimmers": [
|
||||
{"addr": "[02:08:01:02]", "name": "Foyer Downlights", "rate": 2.0},
|
||||
],
|
||||
"host": "192.168.0.1",
|
||||
"keypads": [
|
||||
{
|
||||
"addr": "[02:08:02:01]",
|
||||
"name": "Foyer Keypad",
|
||||
}
|
||||
],
|
||||
"port": 1234,
|
||||
}
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Check the original entity was removed, with only the new entity left
|
||||
assert hass.states.async_entity_ids("light") == unordered(
|
||||
["light.foyer_downlights"]
|
||||
)
|
||||
|
||||
|
||||
async def test_options_add_remove_keypad_flow(
|
||||
hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_homeworks: MagicMock
|
||||
) -> None:
|
||||
"""Test options flow to add and remove a keypad."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
result = await hass.config_entries.options.async_init(mock_config_entry.entry_id)
|
||||
assert result["type"] == FlowResultType.MENU
|
||||
assert result["step_id"] == "init"
|
||||
|
||||
result = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"],
|
||||
{"next_step_id": "add_keypad"},
|
||||
)
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["step_id"] == "add_keypad"
|
||||
|
||||
result = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={
|
||||
CONF_ADDR: "[02:08:03:01]",
|
||||
CONF_NAME: "Hall Keypad",
|
||||
},
|
||||
)
|
||||
assert result["type"] == FlowResultType.CREATE_ENTRY
|
||||
assert result["data"] == {
|
||||
"controller_id": "main_controller",
|
||||
"dimmers": [
|
||||
{"addr": "[02:08:01:01]", "name": "Foyer Sconces", "rate": 1.0},
|
||||
],
|
||||
"host": "192.168.0.1",
|
||||
"keypads": [
|
||||
{
|
||||
"addr": "[02:08:02:01]",
|
||||
"name": "Foyer Keypad",
|
||||
},
|
||||
{"addr": "[02:08:03:01]", "name": "Hall Keypad"},
|
||||
],
|
||||
"port": 1234,
|
||||
}
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Now remove the original keypad
|
||||
result = await hass.config_entries.options.async_init(mock_config_entry.entry_id)
|
||||
assert result["type"] == FlowResultType.MENU
|
||||
assert result["step_id"] == "init"
|
||||
|
||||
result = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"],
|
||||
{"next_step_id": "remove_keypad"},
|
||||
)
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["step_id"] == "remove_keypad"
|
||||
assert result["data_schema"].schema["index"].options == {
|
||||
"0": "Foyer Keypad ([02:08:02:01])",
|
||||
"1": "Hall Keypad ([02:08:03:01])",
|
||||
}
|
||||
|
||||
result = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"], user_input={CONF_INDEX: ["0"]}
|
||||
)
|
||||
assert result["type"] == FlowResultType.CREATE_ENTRY
|
||||
assert result["data"] == {
|
||||
"controller_id": "main_controller",
|
||||
"dimmers": [
|
||||
{"addr": "[02:08:01:01]", "name": "Foyer Sconces", "rate": 1.0},
|
||||
],
|
||||
"host": "192.168.0.1",
|
||||
"keypads": [{"addr": "[02:08:03:01]", "name": "Hall Keypad"}],
|
||||
"port": 1234,
|
||||
}
|
||||
await hass.async_block_till_done()
|
||||
|
||||
|
||||
async def test_options_add_keypad_with_error(
|
||||
hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_homeworks: MagicMock
|
||||
) -> None:
|
||||
"""Test options flow to add and remove a keypad."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
result = await hass.config_entries.options.async_init(mock_config_entry.entry_id)
|
||||
assert result["type"] == FlowResultType.MENU
|
||||
assert result["step_id"] == "init"
|
||||
|
||||
result = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"],
|
||||
{"next_step_id": "add_keypad"},
|
||||
)
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["step_id"] == "add_keypad"
|
||||
|
||||
# Try an invalid address
|
||||
result = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={
|
||||
CONF_ADDR: "[02:08:03:01",
|
||||
CONF_NAME: "Hall Keypad",
|
||||
},
|
||||
)
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["step_id"] == "add_keypad"
|
||||
assert result["errors"] == {"base": "invalid_addr"}
|
||||
|
||||
# Try an address claimed by another keypad
|
||||
result = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={
|
||||
CONF_ADDR: "[02:08:02:01]",
|
||||
CONF_NAME: "Hall Keypad",
|
||||
},
|
||||
)
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["step_id"] == "add_keypad"
|
||||
assert result["errors"] == {"base": "duplicated_addr"}
|
||||
|
||||
# Try an address claimed by a light
|
||||
result = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={
|
||||
CONF_ADDR: "[02:08:01:01]",
|
||||
CONF_NAME: "Hall Keypad",
|
||||
},
|
||||
)
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["step_id"] == "add_keypad"
|
||||
assert result["errors"] == {"base": "duplicated_addr"}
|
||||
|
||||
|
||||
async def test_options_edit_light_no_lights_flow(
|
||||
hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_homeworks: MagicMock
|
||||
) -> None:
|
||||
"""Test options flow to edit a light."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
assert hass.states.async_entity_ids("light") == unordered(["light.foyer_sconces"])
|
||||
|
||||
result = await hass.config_entries.options.async_init(mock_config_entry.entry_id)
|
||||
assert result["type"] == FlowResultType.MENU
|
||||
assert result["step_id"] == "init"
|
||||
|
||||
result = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"], {"next_step_id": "select_edit_light"}
|
||||
)
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["step_id"] == "select_edit_light"
|
||||
assert result["data_schema"].schema["index"].container == {
|
||||
"0": "Foyer Sconces ([02:08:01:01])"
|
||||
}
|
||||
|
||||
result = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"],
|
||||
{"index": "0"},
|
||||
)
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["step_id"] == "edit_light"
|
||||
|
||||
result = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"], user_input={CONF_RATE: 3.0}
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] == FlowResultType.CREATE_ENTRY
|
||||
assert result["data"] == {
|
||||
"controller_id": "main_controller",
|
||||
"dimmers": [{"addr": "[02:08:01:01]", "name": "Foyer Sconces", "rate": 3.0}],
|
||||
"host": "192.168.0.1",
|
||||
"keypads": [
|
||||
{
|
||||
"addr": "[02:08:02:01]",
|
||||
"name": "Foyer Keypad",
|
||||
}
|
||||
],
|
||||
"port": 1234,
|
||||
}
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Check the entity was updated
|
||||
assert len(hass.states.async_entity_ids("light")) == 1
|
||||
|
||||
|
||||
async def test_options_edit_light_flow_empty(
|
||||
hass: HomeAssistant,
|
||||
mock_empty_config_entry: MockConfigEntry,
|
||||
mock_homeworks: MagicMock,
|
||||
) -> None:
|
||||
"""Test options flow to edit a light."""
|
||||
mock_empty_config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(mock_empty_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
assert hass.states.async_entity_ids("light") == unordered([])
|
||||
|
||||
result = await hass.config_entries.options.async_init(
|
||||
mock_empty_config_entry.entry_id
|
||||
)
|
||||
assert result["type"] == FlowResultType.MENU
|
||||
assert result["step_id"] == "init"
|
||||
|
||||
result = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"], {"next_step_id": "select_edit_light"}
|
||||
)
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["step_id"] == "select_edit_light"
|
||||
assert result["data_schema"].schema["index"].container == {}
|
Loading…
x
Reference in New Issue
Block a user