From a515603aaf85cba67952d6de925316f645a77793 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 5 Mar 2024 19:10:13 +0100 Subject: [PATCH] Add button to homeworks (#112269) --- .coveragerc | 1 + .../components/homeworks/__init__.py | 3 +- homeassistant/components/homeworks/button.py | 78 ++++ .../components/homeworks/config_flow.py | 210 ++++++++++- homeassistant/components/homeworks/const.py | 3 + homeassistant/components/homeworks/light.py | 5 - .../components/homeworks/strings.json | 100 +++++ tests/components/homeworks/conftest.py | 20 + .../components/homeworks/test_config_flow.py | 352 +++++++++++++++++- 9 files changed, 763 insertions(+), 9 deletions(-) create mode 100644 homeassistant/components/homeworks/button.py diff --git a/.coveragerc b/.coveragerc index d95c636e22c1a..55179b740cf6e 100644 --- a/.coveragerc +++ b/.coveragerc @@ -546,6 +546,7 @@ omit = homeassistant/components/homematic/sensor.py homeassistant/components/homematic/switch.py homeassistant/components/homeworks/__init__.py + homeassistant/components/homeworks/button.py homeassistant/components/homeworks/light.py homeassistant/components/horizon/media_player.py homeassistant/components/hp_ilo/sensor.py diff --git a/homeassistant/components/homeworks/__init__.py b/homeassistant/components/homeworks/__init__.py index 0abb22ad2e8c0..3d9de889ae71e 100644 --- a/homeassistant/components/homeworks/__init__.py +++ b/homeassistant/components/homeworks/__init__.py @@ -36,7 +36,7 @@ _LOGGER = logging.getLogger(__name__) -PLATFORMS: list[Platform] = [Platform.LIGHT] +PLATFORMS: list[Platform] = [Platform.BUTTON, Platform.LIGHT] EVENT_BUTTON_PRESS = "homeworks_button_press" EVENT_BUTTON_RELEASE = "homeworks_button_release" @@ -186,6 +186,7 @@ def __init__( self._controller_id, self._addr, self._idx ) self._controller = controller + self._attr_extra_state_attributes = {"homeworks_address": self._addr} class HomeworksKeypad: diff --git a/homeassistant/components/homeworks/button.py b/homeassistant/components/homeworks/button.py new file mode 100644 index 0000000000000..04cf3594e0e72 --- /dev/null +++ b/homeassistant/components/homeworks/button.py @@ -0,0 +1,78 @@ +"""Support for Lutron Homeworks buttons.""" +from __future__ import annotations + +from time import sleep + +from pyhomeworks.pyhomeworks import Homeworks + +from homeassistant.components.button import ButtonEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_NAME +from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import HomeworksData, HomeworksEntity +from .const import ( + CONF_ADDR, + CONF_BUTTONS, + CONF_CONTROLLER_ID, + CONF_KEYPADS, + CONF_NUMBER, + CONF_RELEASE_DELAY, + DOMAIN, +) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up Homeworks buttons.""" + data: HomeworksData = hass.data[DOMAIN][entry.entry_id] + controller = data.controller + controller_id = entry.options[CONF_CONTROLLER_ID] + devs = [] + for keypad in entry.options.get(CONF_KEYPADS, []): + for button in keypad[CONF_BUTTONS]: + dev = HomeworksButton( + controller, + controller_id, + keypad[CONF_ADDR], + keypad[CONF_NAME], + button[CONF_NAME], + button[CONF_NUMBER], + button[CONF_RELEASE_DELAY], + ) + devs.append(dev) + async_add_entities(devs, True) + + +class HomeworksButton(HomeworksEntity, ButtonEntity): + """Homeworks Button.""" + + def __init__( + self, + controller: Homeworks, + controller_id: str, + addr: str, + keypad_name: str, + button_name: str, + button_number: int, + release_delay: float, + ) -> None: + """Create device with Addr, name, and rate.""" + super().__init__(controller, controller_id, addr, button_number, button_name) + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, f"{controller_id}.{addr}")}, name=keypad_name + ) + self._release_delay = release_delay + + def press(self) -> None: + """Press the button.""" + # pylint: disable-next=protected-access + self._controller._send(f"KBP, {self._addr}, {self._idx}") + if not self._release_delay: + return + sleep(self._release_delay) + # pylint: disable-next=protected-access + self._controller._send(f"KBR, {self._addr}, {self._idx}") diff --git a/homeassistant/components/homeworks/config_flow.py b/homeassistant/components/homeworks/config_flow.py index e7095e4f57f93..269abd705355e 100644 --- a/homeassistant/components/homeworks/config_flow.py +++ b/homeassistant/components/homeworks/config_flow.py @@ -8,6 +8,7 @@ from pyhomeworks.pyhomeworks import Homeworks import voluptuous as vol +from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN 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 @@ -37,11 +38,15 @@ from . import DEFAULT_FADE_RATE, calculate_unique_id from .const import ( CONF_ADDR, + CONF_BUTTONS, CONF_CONTROLLER_ID, CONF_DIMMERS, CONF_INDEX, CONF_KEYPADS, + CONF_NUMBER, CONF_RATE, + CONF_RELEASE_DELAY, + DEFAULT_BUTTON_NAME, DEFAULT_KEYPAD_NAME, DEFAULT_LIGHT_NAME, DOMAIN, @@ -71,6 +76,18 @@ ), } +BUTTON_EDIT = { + vol.Optional(CONF_RELEASE_DELAY, default=0): selector.NumberSelector( + selector.NumberSelectorConfig( + min=0, + max=5, + step=0.01, + mode=selector.NumberSelectorMode.BOX, + unit_of_measurement="s", + ), + ), +} + validate_addr = cv.matches_regex(r"\[\d\d:\d\d:\d\d:\d\d\]") @@ -160,6 +177,31 @@ def _validate_address(handler: SchemaCommonFlowHandler, addr: str) -> None: raise SchemaFlowError("duplicated_addr") +def _validate_button_number(handler: SchemaCommonFlowHandler, number: int) -> None: + """Validate button number.""" + keypad = handler.flow_state["_idx"] + buttons: list[dict[str, Any]] = handler.options[CONF_KEYPADS][keypad][CONF_BUTTONS] + + for button in buttons: + if button[CONF_NUMBER] == number: + raise SchemaFlowError("duplicated_number") + + +async def validate_add_button( + handler: SchemaCommonFlowHandler, user_input: dict[str, Any] +) -> dict[str, Any]: + """Validate button input.""" + user_input[CONF_NUMBER] = int(user_input[CONF_NUMBER]) + _validate_button_number(handler, user_input[CONF_NUMBER]) + + # 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. + keypad = handler.flow_state["_idx"] + buttons: list[dict[str, Any]] = handler.options[CONF_KEYPADS][keypad][CONF_BUTTONS] + buttons.append(user_input) + return {} + + async def validate_add_keypad( handler: SchemaCommonFlowHandler, user_input: dict[str, Any] ) -> dict[str, Any]: @@ -169,7 +211,7 @@ async def validate_add_keypad( # 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) + items.append(user_input | {CONF_BUTTONS: []}) return {} @@ -186,6 +228,37 @@ async def validate_add_light( return {} +async def get_select_button_schema(handler: SchemaCommonFlowHandler) -> vol.Schema: + """Return schema for selecting a button.""" + keypad = handler.flow_state["_idx"] + buttons: list[dict[str, Any]] = handler.options[CONF_KEYPADS][keypad][CONF_BUTTONS] + + return vol.Schema( + { + vol.Required(CONF_INDEX): vol.In( + { + str(index): f"{config[CONF_NAME]} ({config[CONF_NUMBER]})" + for index, config in enumerate(buttons) + }, + ) + } + ) + + +async def get_select_keypad_schema(handler: SchemaCommonFlowHandler) -> vol.Schema: + """Return schema for selecting a keypad.""" + 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_KEYPADS]) + }, + ) + } + ) + + async def get_select_light_schema(handler: SchemaCommonFlowHandler) -> vol.Schema: """Return schema for selecting a light.""" return vol.Schema( @@ -200,6 +273,14 @@ async def get_select_light_schema(handler: SchemaCommonFlowHandler) -> vol.Schem ) +async def validate_select_button( + handler: SchemaCommonFlowHandler, user_input: dict[str, Any] +) -> dict[str, Any]: + """Store button index in flow state.""" + handler.flow_state["_button_idx"] = int(user_input[CONF_INDEX]) + return {} + + async def validate_select_keypad_light( handler: SchemaCommonFlowHandler, user_input: dict[str, Any] ) -> dict[str, Any]: @@ -208,6 +289,15 @@ async def validate_select_keypad_light( return {} +async def get_edit_button_suggested_values( + handler: SchemaCommonFlowHandler, +) -> dict[str, Any]: + """Return suggested values for button editing.""" + keypad_idx: int = handler.flow_state["_idx"] + button_idx: int = handler.flow_state["_button_idx"] + return dict(handler.options[CONF_KEYPADS][keypad_idx][CONF_BUTTONS][button_idx]) + + async def get_edit_light_suggested_values( handler: SchemaCommonFlowHandler, ) -> dict[str, Any]: @@ -216,6 +306,19 @@ async def get_edit_light_suggested_values( return dict(handler.options[CONF_DIMMERS][idx]) +async def validate_button_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. + keypad_idx: int = handler.flow_state["_idx"] + button_idx: int = handler.flow_state["_button_idx"] + buttons: list[dict] = handler.options[CONF_KEYPADS][keypad_idx][CONF_BUTTONS] + buttons[button_idx].update(user_input) + return {} + + async def validate_light_edit( handler: SchemaCommonFlowHandler, user_input: dict[str, Any] ) -> dict[str, Any]: @@ -227,6 +330,22 @@ async def validate_light_edit( return {} +async def get_remove_button_schema(handler: SchemaCommonFlowHandler) -> vol.Schema: + """Return schema for button removal.""" + keypad_idx: int = handler.flow_state["_idx"] + buttons: list[dict] = handler.options[CONF_KEYPADS][keypad_idx][CONF_BUTTONS] + return vol.Schema( + { + vol.Required(CONF_INDEX): cv.multi_select( + { + str(index): f"{config[CONF_NAME]} ({config[CONF_NUMBER]})" + for index, config in enumerate(buttons) + }, + ) + } + ) + + async def get_remove_keypad_light_schema( handler: SchemaCommonFlowHandler, *, key: str ) -> vol.Schema: @@ -243,6 +362,37 @@ async def get_remove_keypad_light_schema( ) +async def validate_remove_button( + handler: SchemaCommonFlowHandler, user_input: dict[str, Any] +) -> 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) + keypad_idx: int = handler.flow_state["_idx"] + keypad: dict = handler.options[CONF_KEYPADS][keypad_idx] + items: list[dict[str, Any]] = [] + item: dict[str, Any] + for index, item in enumerate(keypad[CONF_BUTTONS]): + if str(index) not in removed_indexes: + items.append(item) + button_number = keypad[CONF_BUTTONS][index][CONF_NUMBER] + if entity_id := entity_registry.async_get_entity_id( + BUTTON_DOMAIN, + DOMAIN, + calculate_unique_id( + handler.options[CONF_CONTROLLER_ID], + keypad[CONF_ADDR], + button_number, + ), + ): + entity_registry.async_remove(entity_id) + keypad[CONF_BUTTONS] = items + return {} + + async def validate_remove_keypad_light( handler: SchemaCommonFlowHandler, user_input: dict[str, Any], *, key: str ) -> dict[str, Any]: @@ -292,12 +442,28 @@ async def validate_remove_keypad_light( vol.Required(CONF_ADDR): TextSelector(), } ) +DATA_SCHEMA_ADD_BUTTON = vol.Schema( + { + vol.Optional(CONF_NAME, default=DEFAULT_BUTTON_NAME): TextSelector(), + vol.Required(CONF_NUMBER): selector.NumberSelector( + selector.NumberSelectorConfig( + min=1, + max=24, + step=1, + mode=selector.NumberSelectorMode.BOX, + ), + ), + **BUTTON_EDIT, + } +) +DATA_SCHEMA_EDIT_BUTTON = vol.Schema(BUTTON_EDIT) DATA_SCHEMA_EDIT_LIGHT = vol.Schema(LIGHT_EDIT) OPTIONS_FLOW = { "init": SchemaFlowMenuStep( [ "add_keypad", + "select_edit_keypad", "remove_keypad", "add_light", "select_edit_light", @@ -309,6 +475,40 @@ async def validate_remove_keypad_light( suggested_values=None, validate_user_input=validate_add_keypad, ), + "select_edit_keypad": SchemaFlowFormStep( + get_select_keypad_schema, + suggested_values=None, + validate_user_input=validate_select_keypad_light, + next_step="edit_keypad", + ), + "edit_keypad": SchemaFlowMenuStep( + [ + "add_button", + "select_edit_button", + "remove_button", + ] + ), + "add_button": SchemaFlowFormStep( + DATA_SCHEMA_ADD_BUTTON, + suggested_values=None, + validate_user_input=validate_add_button, + ), + "select_edit_button": SchemaFlowFormStep( + get_select_button_schema, + suggested_values=None, + validate_user_input=validate_select_button, + next_step="edit_button", + ), + "edit_button": SchemaFlowFormStep( + DATA_SCHEMA_EDIT_BUTTON, + suggested_values=get_edit_button_suggested_values, + validate_user_input=validate_button_edit, + ), + "remove_button": SchemaFlowFormStep( + get_remove_button_schema, + suggested_values=None, + validate_user_input=validate_remove_button, + ), "remove_keypad": SchemaFlowFormStep( partial(get_remove_keypad_light_schema, key=CONF_KEYPADS), suggested_values=None, @@ -359,6 +559,14 @@ async def async_step_import(self, config: dict[str, Any]) -> ConfigFlowResult: CONF_KEYPADS: [ { CONF_ADDR: keypad[CONF_ADDR], + CONF_BUTTONS: [ + { + CONF_NAME: button[CONF_NAME], + CONF_NUMBER: button[CONF_NUMBER], + CONF_RELEASE_DELAY: button[CONF_RELEASE_DELAY], + } + for button in keypad[CONF_BUTTONS] + ], CONF_NAME: keypad[CONF_NAME], } for keypad in config[CONF_KEYPADS] diff --git a/homeassistant/components/homeworks/const.py b/homeassistant/components/homeworks/const.py index df0e5294d4b2e..c0d4439ff93cc 100644 --- a/homeassistant/components/homeworks/const.py +++ b/homeassistant/components/homeworks/const.py @@ -4,11 +4,14 @@ DOMAIN = "homeworks" CONF_ADDR = "addr" +CONF_BUTTONS = "buttons" CONF_CONTROLLER_ID = "controller_id" CONF_DIMMERS = "dimmers" CONF_INDEX = "index" CONF_KEYPADS = "keypads" +CONF_NUMBER = "number" CONF_RATE = "rate" +CONF_RELEASE_DELAY = "release_delay" DEFAULT_BUTTON_NAME = "Homeworks button" DEFAULT_KEYPAD_NAME = "Homeworks keypad" diff --git a/homeassistant/components/homeworks/light.py b/homeassistant/components/homeworks/light.py index 4c5a657df361c..a9b0e2587df86 100644 --- a/homeassistant/components/homeworks/light.py +++ b/homeassistant/components/homeworks/light.py @@ -97,11 +97,6 @@ def _set_brightness(self, level: int) -> None: float((level * 100.0) / 255.0), self._rate, 0, self._addr ) - @property - def extra_state_attributes(self) -> dict[str, str]: - """Supported attributes.""" - return {"homeworks_address": self._addr} - @property def is_on(self) -> bool: """Is the light on/off.""" diff --git a/homeassistant/components/homeworks/strings.json b/homeassistant/components/homeworks/strings.json index 3154d3e145e1d..2a1ddb44b4446 100644 --- a/homeassistant/components/homeworks/strings.json +++ b/homeassistant/components/homeworks/strings.json @@ -31,5 +31,105 @@ "description": "Add a Lutron Homeworks controller" } } + }, + "options": { + "error": { + "duplicated_addr": "The specified address is already in use", + "duplicated_number": "The specified number is already in use", + "invalid_addr": "Invalid address" + }, + "step": { + "init": { + "menu_options": { + "add_keypad": "Add keypad", + "add_light": "Add light", + "remove_keypad": "Remove keypad", + "remove_light": "Remove light", + "select_edit_keypad": "Configure keypad", + "select_edit_light": "Configure light" + } + }, + "add_button": { + "data": { + "name": "[%key:common::config_flow::data::name%]", + "number": "Number", + "release_delay": "Release delay" + }, + "data_description": { + "number": "Button number in the range 1 to 24", + "release_delay": "Time between press and release, set to 0 to only press" + }, + "title": "[%key:component::homeworks::options::step::init::menu_options::add_keypad%]" + }, + "add_keypad": { + "data": { + "name": "[%key:common::config_flow::data::name%]", + "addr": "Address" + }, + "data_description": { + "addr": "Keypad address, must be formatted as `[##:##:##:##]`" + }, + "title": "[%key:component::homeworks::options::step::init::menu_options::add_keypad%]" + }, + "add_light": { + "data": { + "name": "[%key:common::config_flow::data::name%]", + "addr": "[%key:component::homeworks::options::step::add_keypad::data::addr%]", + "rate": "Fade rate" + }, + "data_description": { + "addr": "Keypad address, must be formatted as `[##:##:##:##]`", + "rate": "Time in seconds for the light to transition to a new brightness level" + }, + "title": "[%key:component::homeworks::options::step::init::menu_options::add_light%]" + }, + "edit_button": { + "data": { + "release_delay": "[%key:component::homeworks::options::step::add_button::data::release_delay%]" + }, + "data_description": { + "release_delay": "[%key:component::homeworks::options::step::add_button::data_description::release_delay%]" + }, + "title": "[%key:component::homeworks::options::step::edit_keypad::menu_options::select_edit_button%]" + }, + "edit_keypad": { + "menu_options": { + "add_button": "Add button", + "remove_button": "Remove button", + "select_edit_button": "Configure button" + } + }, + "edit_light": { + "data": { + "rate": "[%key:component::homeworks::options::step::add_light::data::rate%]" + }, + "data_description": { + "rate": "[%key:component::homeworks::options::step::add_light::data_description::rate%]" + }, + "description": "Select a light to configure", + "title": "[%key:component::homeworks::options::step::init::menu_options::select_edit_light%]" + }, + "remove_button": { + "description": "Select buttons to remove", + "title": "[%key:component::homeworks::options::step::edit_keypad::menu_options::remove_button%]" + }, + "remove_keypad": { + "description": "Select keypads to remove", + "title": "[%key:component::homeworks::options::step::init::menu_options::remove_keypad%]" + }, + "remove_light": { + "description": "Select lights to remove", + "title": "[%key:component::homeworks::options::step::init::menu_options::remove_light%]" + }, + "select_edit_button": { + "title": "[%key:component::homeworks::options::step::edit_keypad::menu_options::select_edit_button%]" + }, + "select_edit_keypad": { + "title": "[%key:component::homeworks::options::step::init::menu_options::select_edit_keypad%]" + }, + "select_edit_light": { + "title": "[%key:component::homeworks::options::step::init::menu_options::select_edit_light%]" + } + } } } diff --git a/tests/components/homeworks/conftest.py b/tests/components/homeworks/conftest.py index 32b7778109791..273b8f1ae4bb8 100644 --- a/tests/components/homeworks/conftest.py +++ b/tests/components/homeworks/conftest.py @@ -6,10 +6,13 @@ from homeassistant.components.homeworks.const import ( CONF_ADDR, + CONF_BUTTONS, CONF_CONTROLLER_ID, CONF_DIMMERS, CONF_KEYPADS, + CONF_NUMBER, CONF_RATE, + CONF_RELEASE_DELAY, DOMAIN, ) from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT @@ -39,6 +42,23 @@ def mock_config_entry() -> MockConfigEntry: { CONF_ADDR: "[02:08:02:01]", CONF_NAME: "Foyer Keypad", + CONF_BUTTONS: [ + { + CONF_NAME: "Morning", + CONF_NUMBER: 1, + CONF_RELEASE_DELAY: None, + }, + { + CONF_NAME: "Relax", + CONF_NUMBER: 2, + CONF_RELEASE_DELAY: None, + }, + { + CONF_NAME: "Dim up", + CONF_NUMBER: 3, + CONF_RELEASE_DELAY: 0.2, + }, + ], } ], }, diff --git a/tests/components/homeworks/test_config_flow.py b/tests/components/homeworks/test_config_flow.py index 0975abf1f823a..22a8127b1354b 100644 --- a/tests/components/homeworks/test_config_flow.py +++ b/tests/components/homeworks/test_config_flow.py @@ -4,12 +4,16 @@ import pytest from pytest_unordered import unordered +from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN from homeassistant.components.homeworks.const import ( CONF_ADDR, + CONF_BUTTONS, CONF_DIMMERS, CONF_INDEX, CONF_KEYPADS, + CONF_NUMBER, CONF_RATE, + CONF_RELEASE_DELAY, DOMAIN, ) from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN @@ -154,6 +158,23 @@ async def test_import_flow( { CONF_ADDR: "[02:08:02:01]", CONF_NAME: "Foyer Keypad", + CONF_BUTTONS: [ + { + CONF_NAME: "Morning", + CONF_NUMBER: 1, + CONF_RELEASE_DELAY: None, + }, + { + CONF_NAME: "Relax", + CONF_NUMBER: 2, + CONF_RELEASE_DELAY: None, + }, + { + CONF_NAME: "Dim up", + CONF_NUMBER: 3, + CONF_RELEASE_DELAY: 0.2, + }, + ], } ], }, @@ -180,6 +201,15 @@ async def test_import_flow( "keypads": [ { "addr": "[02:08:02:01]", + "buttons": [ + { + "name": "Morning", + "number": 1, + "release_delay": None, + }, + {"name": "Relax", "number": 2, "release_delay": None}, + {"name": "Dim up", "number": 3, "release_delay": 0.2}, + ], "name": "Foyer Keypad", } ], @@ -320,6 +350,15 @@ async def test_options_add_remove_light_flow( "keypads": [ { "addr": "[02:08:02:01]", + "buttons": [ + { + "name": "Morning", + "number": 1, + "release_delay": None, + }, + {"name": "Relax", "number": 2, "release_delay": None}, + {"name": "Dim up", "number": 3, "release_delay": 0.2}, + ], "name": "Foyer Keypad", } ], @@ -362,6 +401,15 @@ async def test_options_add_remove_light_flow( "keypads": [ { "addr": "[02:08:02:01]", + "buttons": [ + { + "name": "Morning", + "number": 1, + "release_delay": None, + }, + {"name": "Relax", "number": 2, "release_delay": None}, + {"name": "Dim up", "number": 3, "release_delay": 0.2}, + ], "name": "Foyer Keypad", } ], @@ -412,9 +460,18 @@ async def test_options_add_remove_keypad_flow( "keypads": [ { "addr": "[02:08:02:01]", + "buttons": [ + { + "name": "Morning", + "number": 1, + "release_delay": None, + }, + {"name": "Relax", "number": 2, "release_delay": None}, + {"name": "Dim up", "number": 3, "release_delay": 0.2}, + ], "name": "Foyer Keypad", }, - {"addr": "[02:08:03:01]", "name": "Hall Keypad"}, + {"addr": "[02:08:03:01]", "buttons": [], "name": "Hall Keypad"}, ], "port": 1234, } @@ -447,7 +504,7 @@ async def test_options_add_remove_keypad_flow( {"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"}], + "keypads": [{"addr": "[02:08:03:01]", "buttons": [], "name": "Hall Keypad"}], "port": 1234, } await hass.async_block_till_done() @@ -551,6 +608,15 @@ async def test_options_edit_light_no_lights_flow( "keypads": [ { "addr": "[02:08:02:01]", + "buttons": [ + { + "name": "Morning", + "number": 1, + "release_delay": None, + }, + {"name": "Relax", "number": 2, "release_delay": None}, + {"name": "Dim up", "number": 3, "release_delay": 0.2}, + ], "name": "Foyer Keypad", } ], @@ -586,3 +652,285 @@ async def test_options_edit_light_flow_empty( assert result["type"] == FlowResultType.FORM assert result["step_id"] == "select_edit_light" assert result["data_schema"].schema["index"].container == {} + + +async def test_options_add_button_flow( + hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_homeworks: MagicMock +) -> None: + """Test options flow to add a button.""" + 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 len(hass.states.async_entity_ids(BUTTON_DOMAIN)) == 3 + + 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_keypad"} + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "select_edit_keypad" + assert result["data_schema"].schema["index"].container == { + "0": "Foyer Keypad ([02:08:02:01])" + } + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + {"index": "0"}, + ) + assert result["type"] == FlowResultType.MENU + assert result["step_id"] == "edit_keypad" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], {"next_step_id": "add_button"} + ) + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "add_button" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + CONF_NAME: "Dim down", + CONF_NUMBER: 4, + CONF_RELEASE_DELAY: 0.2, + }, + ) + 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": 1.0}], + "host": "192.168.0.1", + "keypads": [ + { + "addr": "[02:08:02:01]", + "buttons": [ + { + "name": "Morning", + "number": 1, + "release_delay": None, + }, + {"name": "Relax", "number": 2, "release_delay": None}, + {"name": "Dim up", "number": 3, "release_delay": 0.2}, + { + "name": "Dim down", + "number": 4, + "release_delay": 0.2, + }, + ], + "name": "Foyer Keypad", + } + ], + "port": 1234, + } + + await hass.async_block_till_done() + + # Check the new entities were added + assert len(hass.states.async_entity_ids(BUTTON_DOMAIN)) == 4 + + +async def test_options_add_button_flow_duplicate( + hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_homeworks: MagicMock +) -> None: + """Test options flow to add a button.""" + 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 len(hass.states.async_entity_ids(BUTTON_DOMAIN)) == 3 + + 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_keypad"} + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "select_edit_keypad" + assert result["data_schema"].schema["index"].container == { + "0": "Foyer Keypad ([02:08:02:01])" + } + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + {"index": "0"}, + ) + assert result["type"] == FlowResultType.MENU + assert result["step_id"] == "edit_keypad" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], {"next_step_id": "add_button"} + ) + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "add_button" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + CONF_NAME: "Dim down", + CONF_NUMBER: 1, + CONF_RELEASE_DELAY: 0.2, + }, + ) + + assert result["type"] == FlowResultType.FORM + assert result["errors"] == {"base": "duplicated_number"} + + +async def test_options_edit_button_flow( + hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_homeworks: MagicMock +) -> None: + """Test options flow to add a button.""" + 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 len(hass.states.async_entity_ids(BUTTON_DOMAIN)) == 3 + + 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_keypad"} + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "select_edit_keypad" + assert result["data_schema"].schema["index"].container == { + "0": "Foyer Keypad ([02:08:02:01])" + } + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + {"index": "0"}, + ) + assert result["type"] == FlowResultType.MENU + assert result["step_id"] == "edit_keypad" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], {"next_step_id": "select_edit_button"} + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "select_edit_button" + assert result["data_schema"].schema["index"].container == { + "0": "Morning (1)", + "1": "Relax (2)", + "2": "Dim up (3)", + } + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + {"index": "0"}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "edit_button" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + CONF_RELEASE_DELAY: 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": 1.0}], + "host": "192.168.0.1", + "keypads": [ + { + "addr": "[02:08:02:01]", + "buttons": [ + { + "name": "Morning", + "number": 1, + "release_delay": 0.0, + }, + {"name": "Relax", "number": 2, "release_delay": None}, + {"name": "Dim up", "number": 3, "release_delay": 0.2}, + ], + "name": "Foyer Keypad", + } + ], + "port": 1234, + } + + await hass.async_block_till_done() + + # Check the new entities were added + assert len(hass.states.async_entity_ids(BUTTON_DOMAIN)) == 3 + + +async def test_options_remove_button_flow( + hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_homeworks: MagicMock +) -> None: + """Test options flow to remove a button.""" + 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 len(hass.states.async_entity_ids(BUTTON_DOMAIN)) == 3 + + 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_keypad"} + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "select_edit_keypad" + assert result["data_schema"].schema["index"].container == { + "0": "Foyer Keypad ([02:08:02:01])" + } + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + {"index": "0"}, + ) + assert result["type"] == FlowResultType.MENU + assert result["step_id"] == "edit_keypad" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], {"next_step_id": "remove_button"} + ) + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "remove_button" + assert result["data_schema"].schema["index"].options == { + "0": "Morning (1)", + "1": "Relax (2)", + "2": "Dim up (3)", + } + + 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:02:01]", + "buttons": [ + {"name": "Relax", "number": 2, "release_delay": None}, + {"name": "Dim up", "number": 3, "release_delay": 0.2}, + ], + "name": "Foyer Keypad", + } + ], + "port": 1234, + } + + await hass.async_block_till_done() + + # Check the entities were removed + assert len(hass.states.async_entity_ids(BUTTON_DOMAIN)) == 2