Add config flow to homeworks (#112042)

This commit is contained in:
Erik Montnemery 2024-03-04 19:09:39 +01:00 committed by GitHub
parent b5528de807
commit 7e7f25c859
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 1343 additions and 61 deletions

View File

@ -545,7 +545,8 @@ omit =
homeassistant/components/homematic/notify.py homeassistant/components/homematic/notify.py
homeassistant/components/homematic/sensor.py homeassistant/components/homematic/sensor.py
homeassistant/components/homematic/switch.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/horizon/media_player.py
homeassistant/components/hp_ilo/sensor.py homeassistant/components/hp_ilo/sensor.py
homeassistant/components/huawei_lte/__init__.py homeassistant/components/huawei_lte/__init__.py

View File

@ -1,9 +1,14 @@
"""Support for Lutron Homeworks Series 4 and 8 systems.""" """Support for Lutron Homeworks Series 4 and 8 systems."""
from __future__ import annotations
from dataclasses import dataclass
import logging import logging
from typing import Any
from pyhomeworks.pyhomeworks import HW_BUTTON_PRESSED, HW_BUTTON_RELEASED, Homeworks from pyhomeworks.pyhomeworks import HW_BUTTON_PRESSED, HW_BUTTON_RELEASED, Homeworks
import voluptuous as vol import voluptuous as vol
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
CONF_HOST, CONF_HOST,
CONF_ID, CONF_ID,
@ -13,27 +18,31 @@ from homeassistant.const import (
Platform, Platform,
) )
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryNotReady
import homeassistant.helpers.config_validation as cv 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.dispatcher import async_dispatcher_connect, dispatcher_send
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.typing import ConfigType
from homeassistant.util import slugify from homeassistant.util import slugify
from .const import (
CONF_ADDR,
CONF_CONTROLLER_ID,
CONF_DIMMERS,
CONF_KEYPADS,
CONF_RATE,
DOMAIN,
)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DOMAIN = "homeworks" PLATFORMS: list[Platform] = [Platform.LIGHT]
HOMEWORKS_CONTROLLER = "homeworks"
EVENT_BUTTON_PRESS = "homeworks_button_press" EVENT_BUTTON_PRESS = "homeworks_button_press"
EVENT_BUTTON_RELEASE = "homeworks_button_release" EVENT_BUTTON_RELEASE = "homeworks_button_release"
CONF_DIMMERS = "dimmers" DEFAULT_FADE_RATE = 1.0
CONF_KEYPADS = "keypads"
CONF_ADDR = "addr"
CONF_RATE = "rate"
FADE_RATE = 1.0
CV_FADE_RATE = vol.All(vol.Coerce(float), vol.Range(min=0, max=20)) 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_ADDR): cv.string,
vol.Required(CONF_NAME): 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.""" """Start Homeworks controller."""
def hw_callback(msg_type, values): if DOMAIN in config:
"""Dispatch state changes.""" hass.async_create_task(
_LOGGER.debug("callback: %s, %s", msg_type, values) hass.config_entries.flow.async_init(
addr = values[0] DOMAIN, context={"source": SOURCE_IMPORT}, data=config[DOMAIN]
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)
return True 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.""" """Base class of a Homeworks device."""
_attr_should_poll = False _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.""" """Initialize Homeworks device."""
self._addr = addr self._addr = addr
self._idx = idx
self._controller_id = controller_id
self._attr_name = name 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 self._controller = controller
class HomeworksKeypadEvent: class HomeworksKeypad:
"""When you want signals instead of entities. """When you want signals instead of entities.
Stateless sensors such as keypads are expected to generate an event Stateless sensors such as keypads are expected to generate an event
instead of a sensor entity in hass. 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.""" """Register callback that will be used for signals."""
self._hass = hass
self._addr = addr self._addr = addr
self._controller = controller
self._hass = hass
self._name = name self._name = name
self._id = slugify(self._name) self._id = slugify(self._name)
signal = f"homeworks_entity_{self._addr}" signal = f"homeworks_entity_{controller_id}_{self._addr}"
async_dispatcher_connect(self._hass, signal, self._update_callback) _LOGGER.debug("connecting %s", signal)
self.unsubscribe = async_dispatcher_connect(
self._hass, signal, self._update_callback
)
@callback @callback
def _update_callback(self, msg_type, values): def _update_callback(self, msg_type, values):

View 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)

View 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"

View File

@ -7,53 +7,61 @@ from typing import Any
from pyhomeworks.pyhomeworks import HW_LIGHT_CHANGED from pyhomeworks.pyhomeworks import HW_LIGHT_CHANGED
from homeassistant.components.light import ATTR_BRIGHTNESS, ColorMode, LightEntity from homeassistant.components.light import ATTR_BRIGHTNESS, ColorMode, LightEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_NAME from homeassistant.const import CONF_NAME
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddEntitiesCallback 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__) _LOGGER = logging.getLogger(__name__)
def setup_platform( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
config: ConfigType,
add_entities: AddEntitiesCallback,
discover_info: DiscoveryInfoType | None = None,
) -> None: ) -> None:
"""Set up Homeworks lights.""" """Set up Homeworks lights."""
if discover_info is None: data: HomeworksData = hass.data[DOMAIN][entry.entry_id]
return controller = data.controller
controller_id = entry.options[CONF_CONTROLLER_ID]
controller = hass.data[HOMEWORKS_CONTROLLER]
devs = [] devs = []
for dimmer in discover_info[CONF_DIMMERS]: for dimmer in entry.options.get(CONF_DIMMERS, []):
dev = HomeworksLight( 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) devs.append(dev)
add_entities(devs, True) async_add_entities(devs, True)
class HomeworksLight(HomeworksDevice, LightEntity): class HomeworksLight(HomeworksEntity, LightEntity):
"""Homeworks Light.""" """Homeworks Light."""
_attr_color_mode = ColorMode.BRIGHTNESS _attr_color_mode = ColorMode.BRIGHTNESS
_attr_supported_color_modes = {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.""" """Create device with Addr, name, and rate."""
super().__init__(controller, addr, name) super().__init__(controller, controller_id, addr, 0, name)
self._rate = rate self._rate = rate
self._level = 0 self._level = 0
self._prev_level = 0 self._prev_level = 0
async def async_added_to_hass(self) -> None: async def async_added_to_hass(self) -> None:
"""Call when entity is added to hass.""" """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) _LOGGER.debug("connecting %s", signal)
self.async_on_remove( self.async_on_remove(
async_dispatcher_connect(self.hass, signal, self._update_callback) async_dispatcher_connect(self.hass, signal, self._update_callback)

View File

@ -2,6 +2,7 @@
"domain": "homeworks", "domain": "homeworks",
"name": "Lutron Homeworks", "name": "Lutron Homeworks",
"codeowners": [], "codeowners": [],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/homeworks", "documentation": "https://www.home-assistant.io/integrations/homeworks",
"iot_class": "local_push", "iot_class": "local_push",
"loggers": ["pyhomeworks"], "loggers": ["pyhomeworks"],

View 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"
}
}
}
}

View File

@ -224,6 +224,7 @@ FLOWS = {
"homekit_controller", "homekit_controller",
"homematicip_cloud", "homematicip_cloud",
"homewizard", "homewizard",
"homeworks",
"honeywell", "honeywell",
"huawei_lte", "huawei_lte",
"hue", "hue",

View File

@ -3370,7 +3370,7 @@
}, },
"homeworks": { "homeworks": {
"integration_type": "hub", "integration_type": "hub",
"config_flow": false, "config_flow": true,
"iot_class": "local_push", "iot_class": "local_push",
"name": "Lutron Homeworks" "name": "Lutron Homeworks"
} }

View File

@ -1437,6 +1437,9 @@ pyhiveapi==0.5.16
# homeassistant.components.homematic # homeassistant.components.homematic
pyhomematic==0.1.77 pyhomematic==0.1.77
# homeassistant.components.homeworks
pyhomeworks==0.0.6
# homeassistant.components.ialarm # homeassistant.components.ialarm
pyialarm==2.2.0 pyialarm==2.2.0

View File

@ -0,0 +1 @@
"""Tests for the Lutron Homeworks Series 4 and 8 integration."""

View 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

View 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 == {}