From a46a0ad2b4692b3a8dd6f0df07df721bc518f20d Mon Sep 17 00:00:00 2001 From: Andre Lengwenus Date: Wed, 18 Dec 2024 14:35:02 +0100 Subject: [PATCH] Add device_id parameter to LCN actions (service calls) (#129590) --- homeassistant/components/lcn/__init__.py | 2 + homeassistant/components/lcn/const.py | 1 + homeassistant/components/lcn/helpers.py | 11 +- homeassistant/components/lcn/services.py | 53 +++++- homeassistant/components/lcn/services.yaml | 118 +++++++++++-- homeassistant/components/lcn/strings.json | 89 ++++++++++ tests/components/lcn/test_services.py | 193 +++++++++++++++------ 7 files changed, 398 insertions(+), 69 deletions(-) diff --git a/homeassistant/components/lcn/__init__.py b/homeassistant/components/lcn/__init__.py index eb26ef48e4e..6dc6fb1ecc4 100644 --- a/homeassistant/components/lcn/__init__.py +++ b/homeassistant/components/lcn/__init__.py @@ -31,6 +31,7 @@ from .const import ( CONF_SK_NUM_TRIES, CONF_TRANSITION, CONNECTION, + DEVICE_CONNECTIONS, DOMAIN, PLATFORMS, ) @@ -102,6 +103,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b _LOGGER.debug('LCN connected to "%s"', config_entry.title) hass.data[DOMAIN][config_entry.entry_id] = { CONNECTION: lcn_connection, + DEVICE_CONNECTIONS: {}, ADD_ENTITIES_CALLBACKS: {}, } # Update config_entry with LCN device serials diff --git a/homeassistant/components/lcn/const.py b/homeassistant/components/lcn/const.py index 97aeeecd8b5..cee9da9be43 100644 --- a/homeassistant/components/lcn/const.py +++ b/homeassistant/components/lcn/const.py @@ -20,6 +20,7 @@ DEFAULT_NAME = "pchk" ADD_ENTITIES_CALLBACKS = "add_entities_callbacks" CONNECTION = "connection" +DEVICE_CONNECTIONS = "device_connections" CONF_HARDWARE_SERIAL = "hardware_serial" CONF_SOFTWARE_SERIAL = "software_serial" CONF_HARDWARE_TYPE = "hardware_type" diff --git a/homeassistant/components/lcn/helpers.py b/homeassistant/components/lcn/helpers.py index 6a9c63ea212..348305c775e 100644 --- a/homeassistant/components/lcn/helpers.py +++ b/homeassistant/components/lcn/helpers.py @@ -38,6 +38,7 @@ from .const import ( CONF_SCENES, CONF_SOFTWARE_SERIAL, CONNECTION, + DEVICE_CONNECTIONS, DOMAIN, LED_PORTS, LOGICOP_PORTS, @@ -237,7 +238,7 @@ def register_lcn_address_devices( identifiers = {(DOMAIN, generate_unique_id(config_entry.entry_id, address))} if device_config[CONF_ADDRESS][2]: # is group - device_model = f"LCN group (g{address[0]:03d}{address[1]:03d})" + device_model = "LCN group" sw_version = None else: # is module hardware_type = device_config[CONF_HARDWARE_TYPE] @@ -245,10 +246,10 @@ def register_lcn_address_devices( hardware_name = pypck.lcn_defs.HARDWARE_DESCRIPTIONS[hardware_type] else: hardware_name = pypck.lcn_defs.HARDWARE_DESCRIPTIONS[-1] - device_model = f"{hardware_name} (m{address[0]:03d}{address[1]:03d})" + device_model = f"{hardware_name}" sw_version = f"{device_config[CONF_SOFTWARE_SERIAL]:06X}" - device_registry.async_get_or_create( + device_entry = device_registry.async_get_or_create( config_entry_id=config_entry.entry_id, identifiers=identifiers, via_device=host_identifiers, @@ -258,6 +259,10 @@ def register_lcn_address_devices( model=device_model, ) + hass.data[DOMAIN][config_entry.entry_id][DEVICE_CONNECTIONS][ + device_entry.id + ] = get_device_connection(hass, address, config_entry) + async def async_update_device_config( device_connection: DeviceConnectionType, device_config: ConfigType diff --git a/homeassistant/components/lcn/services.py b/homeassistant/components/lcn/services.py index 92f5863c47e..a6c42de0487 100644 --- a/homeassistant/components/lcn/services.py +++ b/homeassistant/components/lcn/services.py @@ -8,12 +8,21 @@ import voluptuous as vol from homeassistant.const import ( CONF_ADDRESS, CONF_BRIGHTNESS, + CONF_DEVICE_ID, CONF_HOST, CONF_STATE, CONF_UNIT_OF_MEASUREMENT, ) -from homeassistant.core import HomeAssistant, ServiceCall +from homeassistant.core import ( + HomeAssistant, + ServiceCall, + ServiceResponse, + SupportsResponse, +) +from homeassistant.exceptions import ServiceValidationError +from homeassistant.helpers import device_registry as dr import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from .const import ( CONF_KEYS, @@ -30,6 +39,7 @@ from .const import ( CONF_TRANSITION, CONF_VALUE, CONF_VARIABLE, + DEVICE_CONNECTIONS, DOMAIN, LED_PORTS, LED_STATUS, @@ -53,7 +63,13 @@ from .helpers import ( class LcnServiceCall: """Parent class for all LCN service calls.""" - schema = vol.Schema({vol.Required(CONF_ADDRESS): is_address}) + schema = vol.Schema( + { + vol.Optional(CONF_DEVICE_ID): cv.string, + vol.Optional(CONF_ADDRESS): is_address, + } + ) + supports_response = SupportsResponse.NONE def __init__(self, hass: HomeAssistant) -> None: """Initialize service call.""" @@ -61,8 +77,37 @@ class LcnServiceCall: def get_device_connection(self, service: ServiceCall) -> DeviceConnectionType: """Get address connection object.""" - address, host_name = service.data[CONF_ADDRESS] + if CONF_DEVICE_ID not in service.data and CONF_ADDRESS not in service.data: + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="no_device_identifier", + ) + if CONF_DEVICE_ID in service.data: + device_id = service.data[CONF_DEVICE_ID] + device_registry = dr.async_get(self.hass) + if not (device := device_registry.async_get(device_id)): + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="invalid_device_id", + translation_placeholders={"device_id": device_id}, + ) + + return self.hass.data[DOMAIN][device.primary_config_entry][ + DEVICE_CONNECTIONS + ][device_id] + + async_create_issue( + self.hass, + DOMAIN, + "deprecated_address_parameter", + breaks_in_ha_version="2025.6.0", + is_fixable=False, + severity=IssueSeverity.WARNING, + translation_key="deprecated_address_parameter", + ) + + address, host_name = service.data[CONF_ADDRESS] for config_entry in self.hass.config_entries.async_entries(DOMAIN): if config_entry.data[CONF_HOST] == host_name: device_connection = get_device_connection( @@ -73,7 +118,7 @@ class LcnServiceCall: return device_connection raise ValueError("Invalid host name.") - async def async_call_service(self, service: ServiceCall) -> None: + async def async_call_service(self, service: ServiceCall) -> ServiceResponse: """Execute service call.""" raise NotImplementedError diff --git a/homeassistant/components/lcn/services.yaml b/homeassistant/components/lcn/services.yaml index d62a1e72d45..f58e79b9f40 100644 --- a/homeassistant/components/lcn/services.yaml +++ b/homeassistant/components/lcn/services.yaml @@ -2,8 +2,76 @@ output_abs: fields: + device_id: + example: "91aa039a2fb6e0b9f9ec7eb219a6b7d2" + selector: &device_selector + device: + filter: + - integration: lcn + model: LCN group + - integration: lcn + model: UnknownModuleType + - integration: lcn + model: LCN-SW1.0 + - integration: lcn + model: LCN-SW1.1 + - integration: lcn + model: LCN-UP1.0 + - integration: lcn + model: LCN-UP2 + - integration: lcn + model: LCN-SW2 + - integration: lcn + model: LCN-UP-Profi1-Plus + - integration: lcn + model: LCN-DI12 + - integration: lcn + model: LCN-HU + - integration: lcn + model: LCN-SH + - integration: lcn + model: LCN-UP2 + - integration: lcn + model: LCN-UPP + - integration: lcn + model: LCN-SK + - integration: lcn + model: LCN-LD + - integration: lcn + model: LCN-SH-Plus + - integration: lcn + model: LCN-UPS + - integration: lcn + model: LCN_UPS24V + - integration: lcn + model: LCN-GTM + - integration: lcn + model: LCN-SHS + - integration: lcn + model: LCN-ESD + - integration: lcn + model: LCN-EB2 + - integration: lcn + model: LCN-MRS + - integration: lcn + model: LCN-EB11 + - integration: lcn + model: LCN-UMR + - integration: lcn + model: LCN-UPU + - integration: lcn + model: LCN-UMR24V + - integration: lcn + model: LCN-SHD + - integration: lcn + model: LCN-SHU + - integration: lcn + model: LCN-SR6 + - integration: lcn + model: LCN-UMF + - integration: lcn + model: LCN-WBH address: - required: true example: "myhome.s0.m7" selector: text: @@ -34,8 +102,10 @@ output_abs: output_rel: fields: + device_id: + example: "91aa039a2fb6e0b9f9ec7eb219a6b7d2" + selector: *device_selector address: - required: true example: "myhome.s0.m7" selector: text: @@ -58,8 +128,10 @@ output_rel: output_toggle: fields: + device_id: + example: "91aa039a2fb6e0b9f9ec7eb219a6b7d2" + selector: *device_selector address: - required: true example: "myhome.s0.m7" selector: text: @@ -83,8 +155,10 @@ output_toggle: relays: fields: + device_id: + example: "91aa039a2fb6e0b9f9ec7eb219a6b7d2" + selector: *device_selector address: - required: true example: "myhome.s0.m7" selector: text: @@ -96,8 +170,10 @@ relays: led: fields: + device_id: + example: "91aa039a2fb6e0b9f9ec7eb219a6b7d2" + selector: *device_selector address: - required: true example: "myhome.s0.m7" selector: text: @@ -130,8 +206,10 @@ led: var_abs: fields: + device_id: + example: "91aa039a2fb6e0b9f9ec7eb219a6b7d2" + selector: *device_selector address: - required: true example: "myhome.s0.m7" selector: text: @@ -197,8 +275,10 @@ var_abs: var_reset: fields: + device_id: + example: "91aa039a2fb6e0b9f9ec7eb219a6b7d2" + selector: *device_selector address: - required: true example: "myhome.s0.m7" selector: text: @@ -230,8 +310,10 @@ var_reset: var_rel: fields: + device_id: + example: "91aa039a2fb6e0b9f9ec7eb219a6b7d2" + selector: *device_selector address: - required: true example: "myhome.s0.m7" selector: text: @@ -321,8 +403,10 @@ var_rel: lock_regulator: fields: + device_id: + example: "91aa039a2fb6e0b9f9ec7eb219a6b7d2" + selector: *device_selector address: - required: true example: "myhome.s0.m7" selector: text: @@ -355,8 +439,10 @@ lock_regulator: send_keys: fields: + device_id: + example: "91aa039a2fb6e0b9f9ec7eb219a6b7d2" + selector: *device_selector address: - required: true example: "myhome.s0.m7" selector: text: @@ -402,8 +488,10 @@ send_keys: lock_keys: fields: + device_id: + example: "91aa039a2fb6e0b9f9ec7eb219a6b7d2" + selector: *device_selector address: - required: true example: "myhome.s0.m7" selector: text: @@ -445,8 +533,10 @@ lock_keys: dyn_text: fields: + device_id: + example: "91aa039a2fb6e0b9f9ec7eb219a6b7d2" + selector: *device_selector address: - required: true example: "myhome.s0.m7" selector: text: @@ -464,8 +554,10 @@ dyn_text: pck: fields: + device_id: + example: "91aa039a2fb6e0b9f9ec7eb219a6b7d2" + selector: *device_selector address: - required: true example: "myhome.s0.m7" selector: text: diff --git a/homeassistant/components/lcn/strings.json b/homeassistant/components/lcn/strings.json index 088a3654500..988c2a637fb 100644 --- a/homeassistant/components/lcn/strings.json +++ b/homeassistant/components/lcn/strings.json @@ -70,6 +70,10 @@ "deprecated_keylock_sensor": { "title": "Deprecated LCN key lock binary sensor", "description": "Your LCN key lock binary sensor entity `{entity}` is beeing used in automations or scripts. A key lock switch entity is available and should be used going forward.\n\nPlease adjust your automations or scripts to fix this issue." + }, + "deprecated_address_parameter": { + "title": "Deprecated 'address' parameter", + "description": "The 'address' parameter in the LCN service calls is deprecated. The 'devide_id' parameter should be used going forward.\n\nPlease adjust your automations or scripts to fix this issue." } }, "services": { @@ -77,6 +81,10 @@ "name": "Output absolute brightness", "description": "Sets absolute brightness of output port in percent.", "fields": { + "device_id": { + "name": "[%key:common::config_flow::data::device%]", + "description": "The device_id of the LCN module or group." + }, "address": { "name": "Address", "description": "Module address." @@ -99,6 +107,10 @@ "name": "Output relative brightness", "description": "Sets relative brightness of output port in percent.", "fields": { + "device_id": { + "name": "[%key:common::config_flow::data::device%]", + "description": "[%key:component::lcn::services::output_abs::fields::device_id::description%]" + }, "address": { "name": "Address", "description": "[%key:component::lcn::services::output_abs::fields::address::description%]" @@ -117,6 +129,10 @@ "name": "Toggle output", "description": "Toggles output port.", "fields": { + "device_id": { + "name": "[%key:common::config_flow::data::device%]", + "description": "[%key:component::lcn::services::output_abs::fields::device_id::description%]" + }, "address": { "name": "Address", "description": "[%key:component::lcn::services::output_abs::fields::address::description%]" @@ -135,6 +151,10 @@ "name": "Relays", "description": "Sets the relays status.", "fields": { + "device_id": { + "name": "[%key:common::config_flow::data::device%]", + "description": "[%key:component::lcn::services::output_abs::fields::device_id::description%]" + }, "address": { "name": "Address", "description": "[%key:component::lcn::services::output_abs::fields::address::description%]" @@ -149,6 +169,10 @@ "name": "LED", "description": "Sets the led state.", "fields": { + "device_id": { + "name": "[%key:common::config_flow::data::device%]", + "description": "[%key:component::lcn::services::output_abs::fields::device_id::description%]" + }, "address": { "name": "Address", "description": "[%key:component::lcn::services::output_abs::fields::address::description%]" @@ -167,6 +191,10 @@ "name": "Set absolute variable", "description": "Sets absolute value of a variable or setpoint.", "fields": { + "device_id": { + "name": "[%key:common::config_flow::data::device%]", + "description": "[%key:component::lcn::services::output_abs::fields::device_id::description%]" + }, "address": { "name": "Address", "description": "[%key:component::lcn::services::output_abs::fields::address::description%]" @@ -189,6 +217,10 @@ "name": "Reset variable", "description": "Resets value of variable or setpoint.", "fields": { + "device_id": { + "name": "[%key:common::config_flow::data::device%]", + "description": "[%key:component::lcn::services::output_abs::fields::device_id::description%]" + }, "address": { "name": "Address", "description": "[%key:component::lcn::services::output_abs::fields::address::description%]" @@ -203,6 +235,10 @@ "name": "Shift variable", "description": "Shift value of a variable, setpoint or threshold.", "fields": { + "device_id": { + "name": "[%key:common::config_flow::data::device%]", + "description": "[%key:component::lcn::services::output_abs::fields::device_id::description%]" + }, "address": { "name": "Address", "description": "[%key:component::lcn::services::output_abs::fields::address::description%]" @@ -229,6 +265,10 @@ "name": "Lock regulator", "description": "Locks a regulator setpoint.", "fields": { + "device_id": { + "name": "[%key:common::config_flow::data::device%]", + "description": "[%key:component::lcn::services::output_abs::fields::device_id::description%]" + }, "address": { "name": "Address", "description": "[%key:component::lcn::services::output_abs::fields::address::description%]" @@ -247,6 +287,10 @@ "name": "Send keys", "description": "Sends keys (which executes bound commands).", "fields": { + "device_id": { + "name": "[%key:common::config_flow::data::device%]", + "description": "[%key:component::lcn::services::output_abs::fields::device_id::description%]" + }, "address": { "name": "Address", "description": "[%key:component::lcn::services::output_abs::fields::address::description%]" @@ -273,6 +317,10 @@ "name": "Lock keys", "description": "Locks keys.", "fields": { + "device_id": { + "name": "[%key:common::config_flow::data::device%]", + "description": "[%key:component::lcn::services::output_abs::fields::device_id::description%]" + }, "address": { "name": "Address", "description": "[%key:component::lcn::services::output_abs::fields::address::description%]" @@ -299,6 +347,10 @@ "name": "Dynamic text", "description": "Sends dynamic text to LCN-GTxD displays.", "fields": { + "device_id": { + "name": "[%key:common::config_flow::data::device%]", + "description": "[%key:component::lcn::services::output_abs::fields::device_id::description%]" + }, "address": { "name": "Address", "description": "[%key:component::lcn::services::output_abs::fields::address::description%]" @@ -317,6 +369,10 @@ "name": "PCK", "description": "Sends arbitrary PCK command.", "fields": { + "device_id": { + "name": "[%key:common::config_flow::data::device%]", + "description": "[%key:component::lcn::services::output_abs::fields::device_id::description%]" + }, "address": { "name": "Address", "description": "[%key:component::lcn::services::output_abs::fields::address::description%]" @@ -326,6 +382,39 @@ "description": "PCK command (without address header)." } } + }, + "address_to_device_id": { + "name": "Address to device id", + "description": "Convert LCN address to device id.", + "fields": { + "id": { + "name": "Module or group id", + "description": "Target module or group id." + }, + "segment_id": { + "name": "Segment id", + "description": "Target segment id." + }, + "type": { + "name": "Type", + "description": "Target type." + }, + "host": { + "name": "Host name", + "description": "Host name as given in the integration panel." + } + } + } + }, + "exceptions": { + "no_device_identifier": { + "message": "No device identifier provided. Please provide the device id." + }, + "invalid_address": { + "message": "LCN device for given address has not been configured." + }, + "invalid_device_id": { + "message": "LCN device for given device id has not been configured." } } } diff --git a/tests/components/lcn/test_services.py b/tests/components/lcn/test_services.py index a4ea559cd72..cd97e3484e3 100644 --- a/tests/components/lcn/test_services.py +++ b/tests/components/lcn/test_services.py @@ -26,22 +26,37 @@ from homeassistant.components.lcn.services import LcnService from homeassistant.const import ( CONF_ADDRESS, CONF_BRIGHTNESS, + CONF_DEVICE_ID, CONF_STATE, CONF_UNIT_OF_MEASUREMENT, ) from homeassistant.core import HomeAssistant +import homeassistant.helpers.issue_registry as ir from homeassistant.setup import async_setup_component from .conftest import ( MockConfigEntry, MockModuleConnection, - MockPchkConnectionManager, + get_device, init_integration, ) -@patch("homeassistant.components.lcn.PchkConnectionManager", MockPchkConnectionManager) -async def test_service_output_abs(hass: HomeAssistant, entry: MockConfigEntry) -> None: +def device_config( + hass: HomeAssistant, entry: MockConfigEntry, config_type: str +) -> dict[str, str]: + """Return test device config depending on type.""" + if config_type == CONF_ADDRESS: + return {CONF_ADDRESS: "pchk.s0.m7"} + return {CONF_DEVICE_ID: get_device(hass, entry, (0, 7, False)).id} + + +@pytest.mark.parametrize("config_type", [CONF_ADDRESS, CONF_DEVICE_ID]) +async def test_service_output_abs( + hass: HomeAssistant, + entry: MockConfigEntry, + config_type: str, +) -> None: """Test output_abs service.""" await async_setup_component(hass, "persistent_notification", {}) await init_integration(hass, entry) @@ -51,7 +66,7 @@ async def test_service_output_abs(hass: HomeAssistant, entry: MockConfigEntry) - DOMAIN, LcnService.OUTPUT_ABS, { - CONF_ADDRESS: "pchk.s0.m7", + **device_config(hass, entry, config_type), CONF_OUTPUT: "output1", CONF_BRIGHTNESS: 100, CONF_TRANSITION: 5, @@ -62,8 +77,12 @@ async def test_service_output_abs(hass: HomeAssistant, entry: MockConfigEntry) - dim_output.assert_awaited_with(0, 100, 9) -@patch("homeassistant.components.lcn.PchkConnectionManager", MockPchkConnectionManager) -async def test_service_output_rel(hass: HomeAssistant, entry: MockConfigEntry) -> None: +@pytest.mark.parametrize("config_type", [CONF_ADDRESS, CONF_DEVICE_ID]) +async def test_service_output_rel( + hass: HomeAssistant, + entry: MockConfigEntry, + config_type: str, +) -> None: """Test output_rel service.""" await async_setup_component(hass, "persistent_notification", {}) await init_integration(hass, entry) @@ -73,7 +92,7 @@ async def test_service_output_rel(hass: HomeAssistant, entry: MockConfigEntry) - DOMAIN, LcnService.OUTPUT_REL, { - CONF_ADDRESS: "pchk.s0.m7", + **device_config(hass, entry, config_type), CONF_OUTPUT: "output1", CONF_BRIGHTNESS: 25, }, @@ -83,9 +102,11 @@ async def test_service_output_rel(hass: HomeAssistant, entry: MockConfigEntry) - rel_output.assert_awaited_with(0, 25) -@patch("homeassistant.components.lcn.PchkConnectionManager", MockPchkConnectionManager) +@pytest.mark.parametrize("config_type", [CONF_ADDRESS, CONF_DEVICE_ID]) async def test_service_output_toggle( - hass: HomeAssistant, entry: MockConfigEntry + hass: HomeAssistant, + entry: MockConfigEntry, + config_type: str, ) -> None: """Test output_toggle service.""" await async_setup_component(hass, "persistent_notification", {}) @@ -96,7 +117,7 @@ async def test_service_output_toggle( DOMAIN, LcnService.OUTPUT_TOGGLE, { - CONF_ADDRESS: "pchk.s0.m7", + **device_config(hass, entry, config_type), CONF_OUTPUT: "output1", CONF_TRANSITION: 5, }, @@ -106,8 +127,12 @@ async def test_service_output_toggle( toggle_output.assert_awaited_with(0, 9) -@patch("homeassistant.components.lcn.PchkConnectionManager", MockPchkConnectionManager) -async def test_service_relays(hass: HomeAssistant, entry: MockConfigEntry) -> None: +@pytest.mark.parametrize("config_type", [CONF_ADDRESS, CONF_DEVICE_ID]) +async def test_service_relays( + hass: HomeAssistant, + entry: MockConfigEntry, + config_type: str, +) -> None: """Test relays service.""" await async_setup_component(hass, "persistent_notification", {}) await init_integration(hass, entry) @@ -116,7 +141,7 @@ async def test_service_relays(hass: HomeAssistant, entry: MockConfigEntry) -> No await hass.services.async_call( DOMAIN, LcnService.RELAYS, - {CONF_ADDRESS: "pchk.s0.m7", CONF_STATE: "0011TT--"}, + {**device_config(hass, entry, config_type), CONF_STATE: "0011TT--"}, blocking=True, ) @@ -126,8 +151,12 @@ async def test_service_relays(hass: HomeAssistant, entry: MockConfigEntry) -> No control_relays.assert_awaited_with(relay_states) -@patch("homeassistant.components.lcn.PchkConnectionManager", MockPchkConnectionManager) -async def test_service_led(hass: HomeAssistant, entry: MockConfigEntry) -> None: +@pytest.mark.parametrize("config_type", [CONF_ADDRESS, CONF_DEVICE_ID]) +async def test_service_led( + hass: HomeAssistant, + entry: MockConfigEntry, + config_type: str, +) -> None: """Test led service.""" await async_setup_component(hass, "persistent_notification", {}) await init_integration(hass, entry) @@ -136,7 +165,11 @@ async def test_service_led(hass: HomeAssistant, entry: MockConfigEntry) -> None: await hass.services.async_call( DOMAIN, LcnService.LED, - {CONF_ADDRESS: "pchk.s0.m7", CONF_LED: "led6", CONF_STATE: "blink"}, + { + **device_config(hass, entry, config_type), + CONF_LED: "led6", + CONF_STATE: "blink", + }, blocking=True, ) @@ -146,8 +179,12 @@ async def test_service_led(hass: HomeAssistant, entry: MockConfigEntry) -> None: control_led.assert_awaited_with(led, led_state) -@patch("homeassistant.components.lcn.PchkConnectionManager", MockPchkConnectionManager) -async def test_service_var_abs(hass: HomeAssistant, entry: MockConfigEntry) -> None: +@pytest.mark.parametrize("config_type", [CONF_ADDRESS, CONF_DEVICE_ID]) +async def test_service_var_abs( + hass: HomeAssistant, + entry: MockConfigEntry, + config_type: str, +) -> None: """Test var_abs service.""" await async_setup_component(hass, "persistent_notification", {}) await init_integration(hass, entry) @@ -157,7 +194,7 @@ async def test_service_var_abs(hass: HomeAssistant, entry: MockConfigEntry) -> N DOMAIN, LcnService.VAR_ABS, { - CONF_ADDRESS: "pchk.s0.m7", + **device_config(hass, entry, config_type), CONF_VARIABLE: "var1", CONF_VALUE: 75, CONF_UNIT_OF_MEASUREMENT: "%", @@ -170,8 +207,12 @@ async def test_service_var_abs(hass: HomeAssistant, entry: MockConfigEntry) -> N ) -@patch("homeassistant.components.lcn.PchkConnectionManager", MockPchkConnectionManager) -async def test_service_var_rel(hass: HomeAssistant, entry: MockConfigEntry) -> None: +@pytest.mark.parametrize("config_type", [CONF_ADDRESS, CONF_DEVICE_ID]) +async def test_service_var_rel( + hass: HomeAssistant, + entry: MockConfigEntry, + config_type: str, +) -> None: """Test var_rel service.""" await async_setup_component(hass, "persistent_notification", {}) await init_integration(hass, entry) @@ -181,7 +222,7 @@ async def test_service_var_rel(hass: HomeAssistant, entry: MockConfigEntry) -> N DOMAIN, LcnService.VAR_REL, { - CONF_ADDRESS: "pchk.s0.m7", + **device_config(hass, entry, config_type), CONF_VARIABLE: "var1", CONF_VALUE: 10, CONF_UNIT_OF_MEASUREMENT: "%", @@ -198,8 +239,12 @@ async def test_service_var_rel(hass: HomeAssistant, entry: MockConfigEntry) -> N ) -@patch("homeassistant.components.lcn.PchkConnectionManager", MockPchkConnectionManager) -async def test_service_var_reset(hass: HomeAssistant, entry: MockConfigEntry) -> None: +@pytest.mark.parametrize("config_type", [CONF_ADDRESS, CONF_DEVICE_ID]) +async def test_service_var_reset( + hass: HomeAssistant, + entry: MockConfigEntry, + config_type: str, +) -> None: """Test var_reset service.""" await async_setup_component(hass, "persistent_notification", {}) await init_integration(hass, entry) @@ -208,16 +253,18 @@ async def test_service_var_reset(hass: HomeAssistant, entry: MockConfigEntry) -> await hass.services.async_call( DOMAIN, LcnService.VAR_RESET, - {CONF_ADDRESS: "pchk.s0.m7", CONF_VARIABLE: "var1"}, + {**device_config(hass, entry, config_type), CONF_VARIABLE: "var1"}, blocking=True, ) var_reset.assert_awaited_with(pypck.lcn_defs.Var["VAR1"]) -@patch("homeassistant.components.lcn.PchkConnectionManager", MockPchkConnectionManager) +@pytest.mark.parametrize("config_type", [CONF_ADDRESS, CONF_DEVICE_ID]) async def test_service_lock_regulator( - hass: HomeAssistant, entry: MockConfigEntry + hass: HomeAssistant, + entry: MockConfigEntry, + config_type: str, ) -> None: """Test lock_regulator service.""" await async_setup_component(hass, "persistent_notification", {}) @@ -228,7 +275,7 @@ async def test_service_lock_regulator( DOMAIN, LcnService.LOCK_REGULATOR, { - CONF_ADDRESS: "pchk.s0.m7", + **device_config(hass, entry, config_type), CONF_SETPOINT: "r1varsetpoint", CONF_STATE: True, }, @@ -238,8 +285,12 @@ async def test_service_lock_regulator( lock_regulator.assert_awaited_with(0, True) -@patch("homeassistant.components.lcn.PchkConnectionManager", MockPchkConnectionManager) -async def test_service_send_keys(hass: HomeAssistant, entry: MockConfigEntry) -> None: +@pytest.mark.parametrize("config_type", [CONF_ADDRESS, CONF_DEVICE_ID]) +async def test_service_send_keys( + hass: HomeAssistant, + entry: MockConfigEntry, + config_type: str, +) -> None: """Test send_keys service.""" await async_setup_component(hass, "persistent_notification", {}) await init_integration(hass, entry) @@ -248,7 +299,11 @@ async def test_service_send_keys(hass: HomeAssistant, entry: MockConfigEntry) -> await hass.services.async_call( DOMAIN, LcnService.SEND_KEYS, - {CONF_ADDRESS: "pchk.s0.m7", CONF_KEYS: "a1a5d8", CONF_STATE: "hit"}, + { + **device_config(hass, entry, config_type), + CONF_KEYS: "a1a5d8", + CONF_STATE: "hit", + }, blocking=True, ) @@ -260,9 +315,11 @@ async def test_service_send_keys(hass: HomeAssistant, entry: MockConfigEntry) -> send_keys.assert_awaited_with(keys, pypck.lcn_defs.SendKeyCommand["HIT"]) -@patch("homeassistant.components.lcn.PchkConnectionManager", MockPchkConnectionManager) +@pytest.mark.parametrize("config_type", [CONF_ADDRESS, CONF_DEVICE_ID]) async def test_service_send_keys_hit_deferred( - hass: HomeAssistant, entry: MockConfigEntry + hass: HomeAssistant, + entry: MockConfigEntry, + config_type: str, ) -> None: """Test send_keys (hit_deferred) service.""" await async_setup_component(hass, "persistent_notification", {}) @@ -281,7 +338,7 @@ async def test_service_send_keys_hit_deferred( DOMAIN, LcnService.SEND_KEYS, { - CONF_ADDRESS: "pchk.s0.m7", + **device_config(hass, entry, config_type), CONF_KEYS: "a1a5d8", CONF_TIME: 5, CONF_TIME_UNIT: "s", @@ -304,7 +361,7 @@ async def test_service_send_keys_hit_deferred( DOMAIN, LcnService.SEND_KEYS, { - CONF_ADDRESS: "pchk.s0.m7", + **device_config(hass, entry, config_type), CONF_KEYS: "a1a5d8", CONF_STATE: "make", CONF_TIME: 5, @@ -314,8 +371,12 @@ async def test_service_send_keys_hit_deferred( ) -@patch("homeassistant.components.lcn.PchkConnectionManager", MockPchkConnectionManager) -async def test_service_lock_keys(hass: HomeAssistant, entry: MockConfigEntry) -> None: +@pytest.mark.parametrize("config_type", [CONF_ADDRESS, CONF_DEVICE_ID]) +async def test_service_lock_keys( + hass: HomeAssistant, + entry: MockConfigEntry, + config_type: str, +) -> None: """Test lock_keys service.""" await async_setup_component(hass, "persistent_notification", {}) await init_integration(hass, entry) @@ -324,7 +385,11 @@ async def test_service_lock_keys(hass: HomeAssistant, entry: MockConfigEntry) -> await hass.services.async_call( DOMAIN, LcnService.LOCK_KEYS, - {CONF_ADDRESS: "pchk.s0.m7", CONF_TABLE: "a", CONF_STATE: "0011TT--"}, + { + **device_config(hass, entry, config_type), + CONF_TABLE: "a", + CONF_STATE: "0011TT--", + }, blocking=True, ) @@ -334,9 +399,11 @@ async def test_service_lock_keys(hass: HomeAssistant, entry: MockConfigEntry) -> lock_keys.assert_awaited_with(0, lock_states) -@patch("homeassistant.components.lcn.PchkConnectionManager", MockPchkConnectionManager) +@pytest.mark.parametrize("config_type", [CONF_ADDRESS, CONF_DEVICE_ID]) async def test_service_lock_keys_tab_a_temporary( - hass: HomeAssistant, entry: MockConfigEntry + hass: HomeAssistant, + entry: MockConfigEntry, + config_type: str, ) -> None: """Test lock_keys (tab_a_temporary) service.""" await async_setup_component(hass, "persistent_notification", {}) @@ -350,7 +417,7 @@ async def test_service_lock_keys_tab_a_temporary( DOMAIN, LcnService.LOCK_KEYS, { - CONF_ADDRESS: "pchk.s0.m7", + **device_config(hass, entry, config_type), CONF_STATE: "0011TT--", CONF_TIME: 10, CONF_TIME_UNIT: "s", @@ -376,7 +443,7 @@ async def test_service_lock_keys_tab_a_temporary( DOMAIN, LcnService.LOCK_KEYS, { - CONF_ADDRESS: "pchk.s0.m7", + **device_config(hass, entry, config_type), CONF_TABLE: "b", CONF_STATE: "0011TT--", CONF_TIME: 10, @@ -386,8 +453,12 @@ async def test_service_lock_keys_tab_a_temporary( ) -@patch("homeassistant.components.lcn.PchkConnectionManager", MockPchkConnectionManager) -async def test_service_dyn_text(hass: HomeAssistant, entry: MockConfigEntry) -> None: +@pytest.mark.parametrize("config_type", [CONF_ADDRESS, CONF_DEVICE_ID]) +async def test_service_dyn_text( + hass: HomeAssistant, + entry: MockConfigEntry, + config_type: str, +) -> None: """Test dyn_text service.""" await async_setup_component(hass, "persistent_notification", {}) await init_integration(hass, entry) @@ -396,15 +467,23 @@ async def test_service_dyn_text(hass: HomeAssistant, entry: MockConfigEntry) -> await hass.services.async_call( DOMAIN, LcnService.DYN_TEXT, - {CONF_ADDRESS: "pchk.s0.m7", CONF_ROW: 1, CONF_TEXT: "text in row 1"}, + { + **device_config(hass, entry, config_type), + CONF_ROW: 1, + CONF_TEXT: "text in row 1", + }, blocking=True, ) dyn_text.assert_awaited_with(0, "text in row 1") -@patch("homeassistant.components.lcn.PchkConnectionManager", MockPchkConnectionManager) -async def test_service_pck(hass: HomeAssistant, entry: MockConfigEntry) -> None: +@pytest.mark.parametrize("config_type", [CONF_ADDRESS, CONF_DEVICE_ID]) +async def test_service_pck( + hass: HomeAssistant, + entry: MockConfigEntry, + config_type: str, +) -> None: """Test pck service.""" await async_setup_component(hass, "persistent_notification", {}) await init_integration(hass, entry) @@ -413,14 +492,13 @@ async def test_service_pck(hass: HomeAssistant, entry: MockConfigEntry) -> None: await hass.services.async_call( DOMAIN, LcnService.PCK, - {CONF_ADDRESS: "pchk.s0.m7", CONF_PCK: "PIN4"}, + {**device_config(hass, entry, config_type), CONF_PCK: "PIN4"}, blocking=True, ) pck.assert_awaited_with("PIN4") -@patch("homeassistant.components.lcn.PchkConnectionManager", MockPchkConnectionManager) async def test_service_called_with_invalid_host_id( hass: HomeAssistant, entry: MockConfigEntry ) -> None: @@ -437,3 +515,20 @@ async def test_service_called_with_invalid_host_id( ) pck.assert_not_awaited() + + +async def test_service_with_deprecated_address_parameter( + hass: HomeAssistant, entry: MockConfigEntry, issue_registry: ir.IssueRegistry +) -> None: + """Test service puts issue in registry if called with address parameter.""" + await async_setup_component(hass, "persistent_notification", {}) + await init_integration(hass, entry) + + await hass.services.async_call( + DOMAIN, + LcnService.PCK, + {CONF_ADDRESS: "pchk.s0.m7", CONF_PCK: "PIN4"}, + blocking=True, + ) + + assert issue_registry.async_get_issue(DOMAIN, "deprecated_address_parameter")