From 10c12623bfc0b3a06ffaa88bf986f61818cfb8be Mon Sep 17 00:00:00 2001 From: Andre Lengwenus Date: Mon, 17 Nov 2025 15:50:26 +0100 Subject: [PATCH] Switch LCN integration to local polling (#152601) --- homeassistant/components/lcn/binary_sensor.py | 22 ++---- homeassistant/components/lcn/climate.py | 28 +++---- homeassistant/components/lcn/cover.py | 66 +++++++---------- homeassistant/components/lcn/entity.py | 18 +++-- homeassistant/components/lcn/helpers.py | 14 +++- homeassistant/components/lcn/light.py | 36 +++------ homeassistant/components/lcn/manifest.json | 4 +- homeassistant/components/lcn/sensor.py | 35 ++++----- homeassistant/components/lcn/services.py | 3 - homeassistant/components/lcn/switch.py | 74 ++++++------------- homeassistant/generated/integrations.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/lcn/conftest.py | 32 +++++--- .../components/lcn/snapshots/test_cover.ambr | 8 +- tests/components/lcn/snapshots/test_init.ambr | 18 ++--- tests/components/lcn/test_climate.py | 11 ++- tests/components/lcn/test_cover.py | 41 +++++++--- tests/components/lcn/test_light.py | 8 +- tests/components/lcn/test_switch.py | 32 ++++++-- 20 files changed, 226 insertions(+), 230 deletions(-) diff --git a/homeassistant/components/lcn/binary_sensor.py b/homeassistant/components/lcn/binary_sensor.py index a9f194fe1b8..9c7b2058a4f 100644 --- a/homeassistant/components/lcn/binary_sensor.py +++ b/homeassistant/components/lcn/binary_sensor.py @@ -1,6 +1,7 @@ """Support for LCN binary sensors.""" from collections.abc import Iterable +from datetime import timedelta from functools import partial import pypck @@ -19,6 +20,7 @@ from .entity import LcnEntity from .helpers import InputType, LcnConfigEntry PARALLEL_UPDATES = 0 +SCAN_INTERVAL = timedelta(minutes=1) def add_lcn_entities( @@ -69,21 +71,11 @@ class LcnBinarySensor(LcnEntity, BinarySensorEntity): config[CONF_DOMAIN_DATA][CONF_SOURCE] ] - async def async_added_to_hass(self) -> None: - """Run when entity about to be added to hass.""" - await super().async_added_to_hass() - if not self.device_connection.is_group: - await self.device_connection.activate_status_request_handler( - self.bin_sensor_port - ) - - async def async_will_remove_from_hass(self) -> None: - """Run when entity will be removed from hass.""" - await super().async_will_remove_from_hass() - if not self.device_connection.is_group: - await self.device_connection.cancel_status_request_handler( - self.bin_sensor_port - ) + async def async_update(self) -> None: + """Update the state of the entity.""" + await self.device_connection.request_status_binary_sensors( + SCAN_INTERVAL.seconds + ) def input_received(self, input_obj: InputType) -> None: """Set sensor value when LCN input object (command) is received.""" diff --git a/homeassistant/components/lcn/climate.py b/homeassistant/components/lcn/climate.py index 5dc1419cecc..53eb86e0127 100644 --- a/homeassistant/components/lcn/climate.py +++ b/homeassistant/components/lcn/climate.py @@ -1,6 +1,8 @@ """Support for LCN climate control.""" +import asyncio from collections.abc import Iterable +from datetime import timedelta from functools import partial from typing import Any, cast @@ -36,6 +38,7 @@ from .entity import LcnEntity from .helpers import InputType, LcnConfigEntry PARALLEL_UPDATES = 0 +SCAN_INTERVAL = timedelta(minutes=1) def add_lcn_entities( @@ -110,20 +113,6 @@ class LcnClimate(LcnEntity, ClimateEntity): ClimateEntityFeature.TURN_OFF | ClimateEntityFeature.TURN_ON ) - async def async_added_to_hass(self) -> None: - """Run when entity about to be added to hass.""" - await super().async_added_to_hass() - if not self.device_connection.is_group: - await self.device_connection.activate_status_request_handler(self.variable) - await self.device_connection.activate_status_request_handler(self.setpoint) - - async def async_will_remove_from_hass(self) -> None: - """Run when entity will be removed from hass.""" - await super().async_will_remove_from_hass() - if not self.device_connection.is_group: - await self.device_connection.cancel_status_request_handler(self.variable) - await self.device_connection.cancel_status_request_handler(self.setpoint) - @property def temperature_unit(self) -> str: """Return the unit of measurement.""" @@ -192,6 +181,17 @@ class LcnClimate(LcnEntity, ClimateEntity): self._target_temperature = temperature self.async_write_ha_state() + async def async_update(self) -> None: + """Update the state of the entity.""" + await asyncio.gather( + self.device_connection.request_status_variable( + self.variable, SCAN_INTERVAL.seconds + ), + self.device_connection.request_status_variable( + self.setpoint, SCAN_INTERVAL.seconds + ), + ) + def input_received(self, input_obj: InputType) -> None: """Set temperature value when LCN input object is received.""" if not isinstance(input_obj, pypck.inputs.ModStatusVar): diff --git a/homeassistant/components/lcn/cover.py b/homeassistant/components/lcn/cover.py index cb292f7cadf..951bb353bfc 100644 --- a/homeassistant/components/lcn/cover.py +++ b/homeassistant/components/lcn/cover.py @@ -1,6 +1,8 @@ """Support for LCN covers.""" +import asyncio from collections.abc import Iterable +from datetime import timedelta from functools import partial from typing import Any @@ -27,6 +29,7 @@ from .entity import LcnEntity from .helpers import InputType, LcnConfigEntry PARALLEL_UPDATES = 0 +SCAN_INTERVAL = timedelta(minutes=1) def add_lcn_entities( @@ -73,7 +76,7 @@ async def async_setup_entry( class LcnOutputsCover(LcnEntity, CoverEntity): """Representation of a LCN cover connected to output ports.""" - _attr_is_closed = False + _attr_is_closed = True _attr_is_closing = False _attr_is_opening = False _attr_assumed_state = True @@ -93,28 +96,6 @@ class LcnOutputsCover(LcnEntity, CoverEntity): else: self.reverse_time = None - async def async_added_to_hass(self) -> None: - """Run when entity about to be added to hass.""" - await super().async_added_to_hass() - if not self.device_connection.is_group: - await self.device_connection.activate_status_request_handler( - pypck.lcn_defs.OutputPort["OUTPUTUP"] - ) - await self.device_connection.activate_status_request_handler( - pypck.lcn_defs.OutputPort["OUTPUTDOWN"] - ) - - async def async_will_remove_from_hass(self) -> None: - """Run when entity will be removed from hass.""" - await super().async_will_remove_from_hass() - if not self.device_connection.is_group: - await self.device_connection.cancel_status_request_handler( - pypck.lcn_defs.OutputPort["OUTPUTUP"] - ) - await self.device_connection.cancel_status_request_handler( - pypck.lcn_defs.OutputPort["OUTPUTDOWN"] - ) - async def async_close_cover(self, **kwargs: Any) -> None: """Close the cover.""" state = pypck.lcn_defs.MotorStateModifier.DOWN @@ -147,6 +128,18 @@ class LcnOutputsCover(LcnEntity, CoverEntity): self._attr_is_opening = False self.async_write_ha_state() + async def async_update(self) -> None: + """Update the state of the entity.""" + if not self.device_connection.is_group: + await asyncio.gather( + self.device_connection.request_status_output( + pypck.lcn_defs.OutputPort["OUTPUTUP"], SCAN_INTERVAL.seconds + ), + self.device_connection.request_status_output( + pypck.lcn_defs.OutputPort["OUTPUTDOWN"], SCAN_INTERVAL.seconds + ), + ) + def input_received(self, input_obj: InputType) -> None: """Set cover states when LCN input object (command) is received.""" if ( @@ -175,7 +168,7 @@ class LcnOutputsCover(LcnEntity, CoverEntity): class LcnRelayCover(LcnEntity, CoverEntity): """Representation of a LCN cover connected to relays.""" - _attr_is_closed = False + _attr_is_closed = True _attr_is_closing = False _attr_is_opening = False _attr_assumed_state = True @@ -206,20 +199,6 @@ class LcnRelayCover(LcnEntity, CoverEntity): self._is_closing = False self._is_opening = False - async def async_added_to_hass(self) -> None: - """Run when entity about to be added to hass.""" - await super().async_added_to_hass() - if not self.device_connection.is_group: - await self.device_connection.activate_status_request_handler( - self.motor, self.positioning_mode - ) - - async def async_will_remove_from_hass(self) -> None: - """Run when entity will be removed from hass.""" - await super().async_will_remove_from_hass() - if not self.device_connection.is_group: - await self.device_connection.cancel_status_request_handler(self.motor) - async def async_close_cover(self, **kwargs: Any) -> None: """Close the cover.""" if not await self.device_connection.control_motor_relays( @@ -274,6 +253,17 @@ class LcnRelayCover(LcnEntity, CoverEntity): self.async_write_ha_state() + async def async_update(self) -> None: + """Update the state of the entity.""" + coros = [self.device_connection.request_status_relays(SCAN_INTERVAL.seconds)] + if self.positioning_mode == pypck.lcn_defs.MotorPositioningMode.BS4: + coros.append( + self.device_connection.request_status_motor_position( + self.motor, self.positioning_mode, SCAN_INTERVAL.seconds + ) + ) + await asyncio.gather(*coros) + def input_received(self, input_obj: InputType) -> None: """Set cover states when LCN input object (command) is received.""" if isinstance(input_obj, pypck.inputs.ModStatusRelays): diff --git a/homeassistant/components/lcn/entity.py b/homeassistant/components/lcn/entity.py index f94251983b4..aab9ad7ca88 100644 --- a/homeassistant/components/lcn/entity.py +++ b/homeassistant/components/lcn/entity.py @@ -22,7 +22,6 @@ from .helpers import ( class LcnEntity(Entity): """Parent class for all entities associated with the LCN component.""" - _attr_should_poll = False _attr_has_entity_name = True device_connection: DeviceConnectionType @@ -57,15 +56,24 @@ class LcnEntity(Entity): ).lower(), ) + @property + def should_poll(self) -> bool: + """Groups may not poll for a status.""" + return not self.device_connection.is_group + async def async_added_to_hass(self) -> None: """Run when entity about to be added to hass.""" self.device_connection = get_device_connection( self.hass, self.config[CONF_ADDRESS], self.config_entry ) - if not self.device_connection.is_group: - self._unregister_for_inputs = self.device_connection.register_for_inputs( - self.input_received - ) + if self.device_connection.is_group: + return + + self._unregister_for_inputs = self.device_connection.register_for_inputs( + self.input_received + ) + + self.schedule_update_ha_state(force_refresh=True) async def async_will_remove_from_hass(self) -> None: """Run when entity will be removed from hass.""" diff --git a/homeassistant/components/lcn/helpers.py b/homeassistant/components/lcn/helpers.py index 4937b5dbca7..feeaec6268a 100644 --- a/homeassistant/components/lcn/helpers.py +++ b/homeassistant/components/lcn/helpers.py @@ -251,13 +251,19 @@ async def async_update_device_config( """Fill missing values in device_config with infos from LCN bus.""" # fetch serial info if device is module if not (is_group := device_config[CONF_ADDRESS][2]): # is module - await device_connection.serial_known + await device_connection.serials_known() if device_config[CONF_HARDWARE_SERIAL] == -1: - device_config[CONF_HARDWARE_SERIAL] = device_connection.hardware_serial + device_config[CONF_HARDWARE_SERIAL] = ( + device_connection.serials.hardware_serial + ) if device_config[CONF_SOFTWARE_SERIAL] == -1: - device_config[CONF_SOFTWARE_SERIAL] = device_connection.software_serial + device_config[CONF_SOFTWARE_SERIAL] = ( + device_connection.serials.software_serial + ) if device_config[CONF_HARDWARE_TYPE] == -1: - device_config[CONF_HARDWARE_TYPE] = device_connection.hardware_type.value + device_config[CONF_HARDWARE_TYPE] = ( + device_connection.serials.hardware_type.value + ) # fetch name if device is module if device_config[CONF_NAME] != "": diff --git a/homeassistant/components/lcn/light.py b/homeassistant/components/lcn/light.py index b9dad0aeb19..e690bb420d1 100644 --- a/homeassistant/components/lcn/light.py +++ b/homeassistant/components/lcn/light.py @@ -1,6 +1,7 @@ """Support for LCN lights.""" from collections.abc import Iterable +from datetime import timedelta from functools import partial from typing import Any @@ -33,6 +34,7 @@ from .helpers import InputType, LcnConfigEntry BRIGHTNESS_SCALE = (1, 100) PARALLEL_UPDATES = 0 +SCAN_INTERVAL = timedelta(minutes=1) def add_lcn_entities( @@ -100,18 +102,6 @@ class LcnOutputLight(LcnEntity, LightEntity): self._attr_color_mode = ColorMode.ONOFF self._attr_supported_color_modes = {self._attr_color_mode} - async def async_added_to_hass(self) -> None: - """Run when entity about to be added to hass.""" - await super().async_added_to_hass() - if not self.device_connection.is_group: - await self.device_connection.activate_status_request_handler(self.output) - - async def async_will_remove_from_hass(self) -> None: - """Run when entity will be removed from hass.""" - await super().async_will_remove_from_hass() - if not self.device_connection.is_group: - await self.device_connection.cancel_status_request_handler(self.output) - async def async_turn_on(self, **kwargs: Any) -> None: """Turn the entity on.""" if ATTR_TRANSITION in kwargs: @@ -157,6 +147,12 @@ class LcnOutputLight(LcnEntity, LightEntity): self._attr_is_on = False self.async_write_ha_state() + async def async_update(self) -> None: + """Update the state of the entity.""" + await self.device_connection.request_status_output( + self.output, SCAN_INTERVAL.seconds + ) + def input_received(self, input_obj: InputType) -> None: """Set light state when LCN input object (command) is received.""" if ( @@ -184,18 +180,6 @@ class LcnRelayLight(LcnEntity, LightEntity): self.output = pypck.lcn_defs.RelayPort[config[CONF_DOMAIN_DATA][CONF_OUTPUT]] - async def async_added_to_hass(self) -> None: - """Run when entity about to be added to hass.""" - await super().async_added_to_hass() - if not self.device_connection.is_group: - await self.device_connection.activate_status_request_handler(self.output) - - async def async_will_remove_from_hass(self) -> None: - """Run when entity will be removed from hass.""" - await super().async_will_remove_from_hass() - if not self.device_connection.is_group: - await self.device_connection.cancel_status_request_handler(self.output) - async def async_turn_on(self, **kwargs: Any) -> None: """Turn the entity on.""" states = [pypck.lcn_defs.RelayStateModifier.NOCHANGE] * 8 @@ -214,6 +198,10 @@ class LcnRelayLight(LcnEntity, LightEntity): self._attr_is_on = False self.async_write_ha_state() + async def async_update(self) -> None: + """Update the state of the entity.""" + await self.device_connection.request_status_relays(SCAN_INTERVAL.seconds) + def input_received(self, input_obj: InputType) -> None: """Set light state when LCN input object (command) is received.""" if not isinstance(input_obj, pypck.inputs.ModStatusRelays): diff --git a/homeassistant/components/lcn/manifest.json b/homeassistant/components/lcn/manifest.json index a08ee0d8880..8c5da184b52 100644 --- a/homeassistant/components/lcn/manifest.json +++ b/homeassistant/components/lcn/manifest.json @@ -6,8 +6,8 @@ "config_flow": true, "dependencies": ["http", "websocket_api"], "documentation": "https://www.home-assistant.io/integrations/lcn", - "iot_class": "local_push", + "iot_class": "local_polling", "loggers": ["pypck"], "quality_scale": "bronze", - "requirements": ["pypck==0.8.12", "lcn-frontend==0.2.7"] + "requirements": ["pypck==0.9.2", "lcn-frontend==0.2.7"] } diff --git a/homeassistant/components/lcn/sensor.py b/homeassistant/components/lcn/sensor.py index 3d3f946b3aa..d9d76cd1792 100644 --- a/homeassistant/components/lcn/sensor.py +++ b/homeassistant/components/lcn/sensor.py @@ -1,6 +1,7 @@ """Support for LCN sensors.""" from collections.abc import Iterable +from datetime import timedelta from functools import partial from itertools import chain @@ -40,6 +41,8 @@ from .entity import LcnEntity from .helpers import InputType, LcnConfigEntry PARALLEL_UPDATES = 0 +SCAN_INTERVAL = timedelta(minutes=1) + DEVICE_CLASS_MAPPING = { pypck.lcn_defs.VarUnit.CELSIUS: SensorDeviceClass.TEMPERATURE, @@ -128,17 +131,11 @@ class LcnVariableSensor(LcnEntity, SensorEntity): ) self._attr_device_class = DEVICE_CLASS_MAPPING.get(self.unit) - async def async_added_to_hass(self) -> None: - """Run when entity about to be added to hass.""" - await super().async_added_to_hass() - if not self.device_connection.is_group: - await self.device_connection.activate_status_request_handler(self.variable) - - async def async_will_remove_from_hass(self) -> None: - """Run when entity will be removed from hass.""" - await super().async_will_remove_from_hass() - if not self.device_connection.is_group: - await self.device_connection.cancel_status_request_handler(self.variable) + async def async_update(self) -> None: + """Update the state of the entity.""" + await self.device_connection.request_status_variable( + self.variable, SCAN_INTERVAL.seconds + ) def input_received(self, input_obj: InputType) -> None: """Set sensor value when LCN input object (command) is received.""" @@ -170,17 +167,11 @@ class LcnLedLogicSensor(LcnEntity, SensorEntity): config[CONF_DOMAIN_DATA][CONF_SOURCE] ] - async def async_added_to_hass(self) -> None: - """Run when entity about to be added to hass.""" - await super().async_added_to_hass() - if not self.device_connection.is_group: - await self.device_connection.activate_status_request_handler(self.source) - - async def async_will_remove_from_hass(self) -> None: - """Run when entity will be removed from hass.""" - await super().async_will_remove_from_hass() - if not self.device_connection.is_group: - await self.device_connection.cancel_status_request_handler(self.source) + async def async_update(self) -> None: + """Update the state of the entity.""" + await self.device_connection.request_status_led_and_logic_ops( + SCAN_INTERVAL.seconds + ) def input_received(self, input_obj: InputType) -> None: """Set sensor value when LCN input object (command) is received.""" diff --git a/homeassistant/components/lcn/services.py b/homeassistant/components/lcn/services.py index 8a172ccac2e..738397e8cb5 100644 --- a/homeassistant/components/lcn/services.py +++ b/homeassistant/components/lcn/services.py @@ -380,9 +380,6 @@ class LockKeys(LcnServiceCall): else: await device_connection.lock_keys(table_id, states) - handler = device_connection.status_requests_handler - await handler.request_status_locked_keys_timeout() - class DynText(LcnServiceCall): """Send dynamic text to LCN-GTxD displays.""" diff --git a/homeassistant/components/lcn/switch.py b/homeassistant/components/lcn/switch.py index f0bb432fef9..0b4156550b8 100644 --- a/homeassistant/components/lcn/switch.py +++ b/homeassistant/components/lcn/switch.py @@ -1,6 +1,7 @@ """Support for LCN switches.""" from collections.abc import Iterable +from datetime import timedelta from functools import partial from typing import Any @@ -17,6 +18,7 @@ from .entity import LcnEntity from .helpers import InputType, LcnConfigEntry PARALLEL_UPDATES = 0 +SCAN_INTERVAL = timedelta(minutes=1) def add_lcn_switch_entities( @@ -77,18 +79,6 @@ class LcnOutputSwitch(LcnEntity, SwitchEntity): self.output = pypck.lcn_defs.OutputPort[config[CONF_DOMAIN_DATA][CONF_OUTPUT]] - async def async_added_to_hass(self) -> None: - """Run when entity about to be added to hass.""" - await super().async_added_to_hass() - if not self.device_connection.is_group: - await self.device_connection.activate_status_request_handler(self.output) - - async def async_will_remove_from_hass(self) -> None: - """Run when entity will be removed from hass.""" - await super().async_will_remove_from_hass() - if not self.device_connection.is_group: - await self.device_connection.cancel_status_request_handler(self.output) - async def async_turn_on(self, **kwargs: Any) -> None: """Turn the entity on.""" if not await self.device_connection.dim_output(self.output.value, 100, 0): @@ -103,6 +93,12 @@ class LcnOutputSwitch(LcnEntity, SwitchEntity): self._attr_is_on = False self.async_write_ha_state() + async def async_update(self) -> None: + """Update the state of the entity.""" + await self.device_connection.request_status_output( + self.output, SCAN_INTERVAL.seconds + ) + def input_received(self, input_obj: InputType) -> None: """Set switch state when LCN input object (command) is received.""" if ( @@ -126,18 +122,6 @@ class LcnRelaySwitch(LcnEntity, SwitchEntity): self.output = pypck.lcn_defs.RelayPort[config[CONF_DOMAIN_DATA][CONF_OUTPUT]] - async def async_added_to_hass(self) -> None: - """Run when entity about to be added to hass.""" - await super().async_added_to_hass() - if not self.device_connection.is_group: - await self.device_connection.activate_status_request_handler(self.output) - - async def async_will_remove_from_hass(self) -> None: - """Run when entity will be removed from hass.""" - await super().async_will_remove_from_hass() - if not self.device_connection.is_group: - await self.device_connection.cancel_status_request_handler(self.output) - async def async_turn_on(self, **kwargs: Any) -> None: """Turn the entity on.""" states = [pypck.lcn_defs.RelayStateModifier.NOCHANGE] * 8 @@ -156,6 +140,10 @@ class LcnRelaySwitch(LcnEntity, SwitchEntity): self._attr_is_on = False self.async_write_ha_state() + async def async_update(self) -> None: + """Update the state of the entity.""" + await self.device_connection.request_status_relays(SCAN_INTERVAL.seconds) + def input_received(self, input_obj: InputType) -> None: """Set switch state when LCN input object (command) is received.""" if not isinstance(input_obj, pypck.inputs.ModStatusRelays): @@ -179,22 +167,6 @@ class LcnRegulatorLockSwitch(LcnEntity, SwitchEntity): ] self.reg_id = pypck.lcn_defs.Var.to_set_point_id(self.setpoint_variable) - async def async_added_to_hass(self) -> None: - """Run when entity about to be added to hass.""" - await super().async_added_to_hass() - if not self.device_connection.is_group: - await self.device_connection.activate_status_request_handler( - self.setpoint_variable - ) - - async def async_will_remove_from_hass(self) -> None: - """Run when entity will be removed from hass.""" - await super().async_will_remove_from_hass() - if not self.device_connection.is_group: - await self.device_connection.cancel_status_request_handler( - self.setpoint_variable - ) - async def async_turn_on(self, **kwargs: Any) -> None: """Turn the entity on.""" if not await self.device_connection.lock_regulator(self.reg_id, True): @@ -209,6 +181,12 @@ class LcnRegulatorLockSwitch(LcnEntity, SwitchEntity): self._attr_is_on = False self.async_write_ha_state() + async def async_update(self) -> None: + """Update the state of the entity.""" + await self.device_connection.request_status_variable( + self.setpoint_variable, SCAN_INTERVAL.seconds + ) + def input_received(self, input_obj: InputType) -> None: """Set switch state when LCN input object (command) is received.""" if ( @@ -234,18 +212,6 @@ class LcnKeyLockSwitch(LcnEntity, SwitchEntity): self.table_id = ord(self.key.name[0]) - 65 self.key_id = int(self.key.name[1]) - 1 - async def async_added_to_hass(self) -> None: - """Run when entity about to be added to hass.""" - await super().async_added_to_hass() - if not self.device_connection.is_group: - await self.device_connection.activate_status_request_handler(self.key) - - async def async_will_remove_from_hass(self) -> None: - """Run when entity will be removed from hass.""" - await super().async_will_remove_from_hass() - if not self.device_connection.is_group: - await self.device_connection.cancel_status_request_handler(self.key) - async def async_turn_on(self, **kwargs: Any) -> None: """Turn the entity on.""" states = [pypck.lcn_defs.KeyLockStateModifier.NOCHANGE] * 8 @@ -268,6 +234,10 @@ class LcnKeyLockSwitch(LcnEntity, SwitchEntity): self._attr_is_on = False self.async_write_ha_state() + async def async_update(self) -> None: + """Update the state of the entity.""" + await self.device_connection.request_status_locked_keys(SCAN_INTERVAL.seconds) + def input_received(self, input_obj: InputType) -> None: """Set switch state when LCN input object (command) is received.""" if ( diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index ddff8ad4232..e22efdde2ad 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -3418,7 +3418,7 @@ "name": "LCN", "integration_type": "hub", "config_flow": true, - "iot_class": "local_push" + "iot_class": "local_polling" }, "ld2410_ble": { "name": "LD2410 BLE", diff --git a/requirements_all.txt b/requirements_all.txt index 75ea9f3b08b..abd74e3c7fe 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2269,7 +2269,7 @@ pypaperless==4.1.1 pypca==0.0.7 # homeassistant.components.lcn -pypck==0.8.12 +pypck==0.9.2 # homeassistant.components.pglab pypglab==0.0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index dd926ad4464..0dde3eb6b7d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1892,7 +1892,7 @@ pypalazzetti==0.1.20 pypaperless==4.1.1 # homeassistant.components.lcn -pypck==0.8.12 +pypck==0.9.2 # homeassistant.components.pglab pypglab==0.0.5 diff --git a/tests/components/lcn/conftest.py b/tests/components/lcn/conftest.py index e588cc7b952..0282e970fab 100644 --- a/tests/components/lcn/conftest.py +++ b/tests/components/lcn/conftest.py @@ -5,8 +5,8 @@ from typing import Any from unittest.mock import AsyncMock, Mock, patch import pypck -import pypck.module -from pypck.module import GroupConnection, ModuleConnection +from pypck import lcn_defs +from pypck.module import GroupConnection, ModuleConnection, Serials import pytest from homeassistant.components.lcn import PchkConnectionManager @@ -25,16 +25,28 @@ LATEST_CONFIG_ENTRY_VERSION = (LcnFlowHandler.VERSION, LcnFlowHandler.MINOR_VERS class MockModuleConnection(ModuleConnection): """Fake a LCN module connection.""" - status_request_handler = AsyncMock() - activate_status_request_handler = AsyncMock() - cancel_status_request_handler = AsyncMock() request_name = AsyncMock(return_value="TestModule") + request_serials = AsyncMock( + return_value=Serials( + hardware_serial=0x1A20A1234, + manu=0x01, + software_serial=0x190B11, + hardware_type=lcn_defs.HardwareType.UPP, + ) + ) send_command = AsyncMock(return_value=True) + request_status_output = AsyncMock() + request_status_relays = AsyncMock() + request_status_motor_position = AsyncMock() + request_status_binary_sensors = AsyncMock() + request_status_variable = AsyncMock() + request_status_led_and_logic_ops = AsyncMock() + request_status_locked_keys = AsyncMock() def __init__(self, *args: Any, **kwargs: Any) -> None: """Construct ModuleConnection instance.""" super().__init__(*args, **kwargs) - self.serials_request_handler.serial_known.set() + self._serials_known.set() class MockGroupConnection(GroupConnection): @@ -55,14 +67,10 @@ class MockPchkConnectionManager(PchkConnectionManager): async def async_close(self) -> None: """Mock closing a connection to PCHK.""" - def get_address_conn(self, addr, request_serials=False): - """Get LCN address connection.""" - return super().get_address_conn(addr, request_serials) - @patch.object(pypck.connection, "ModuleConnection", MockModuleConnection) - def get_module_conn(self, addr, request_serials=False): + def get_module_conn(self, addr): """Get LCN module connection.""" - return super().get_module_conn(addr, request_serials) + return super().get_module_conn(addr) @patch.object(pypck.connection, "GroupConnection", MockGroupConnection) def get_group_conn(self, addr): diff --git a/tests/components/lcn/snapshots/test_cover.ambr b/tests/components/lcn/snapshots/test_cover.ambr index b5d02b8b43b..f59393a8e91 100644 --- a/tests/components/lcn/snapshots/test_cover.ambr +++ b/tests/components/lcn/snapshots/test_cover.ambr @@ -46,7 +46,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'open', + 'state': 'closed', }) # --- # name: test_setup_lcn_cover[cover.testmodule_cover_relays-entry] @@ -96,7 +96,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'open', + 'state': 'closed', }) # --- # name: test_setup_lcn_cover[cover.testmodule_cover_relays_bs4-entry] @@ -146,7 +146,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'open', + 'state': 'closed', }) # --- # name: test_setup_lcn_cover[cover.testmodule_cover_relays_module-entry] @@ -196,6 +196,6 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'open', + 'state': 'closed', }) # --- diff --git a/tests/components/lcn/snapshots/test_init.ambr b/tests/components/lcn/snapshots/test_init.ambr index 8d7a858cf16..f60044be6c1 100644 --- a/tests/components/lcn/snapshots/test_init.ambr +++ b/tests/components/lcn/snapshots/test_init.ambr @@ -9,10 +9,10 @@ 7, False, ), - 'hardware_serial': -1, - 'hardware_type': -1, + 'hardware_serial': 7013536308, + 'hardware_type': 11, 'name': 'TestModule', - 'software_serial': -1, + 'software_serial': 1641233, }), ]), 'dim_mode': 'STEPS200', @@ -50,10 +50,10 @@ 7, False, ), - 'hardware_serial': -1, - 'hardware_type': -1, + 'hardware_serial': 7013536308, + 'hardware_type': 11, 'name': 'TestModule', - 'software_serial': -1, + 'software_serial': 1641233, }), ]), 'dim_mode': 'STEPS200', @@ -143,10 +143,10 @@ 7, False, ), - 'hardware_serial': -1, - 'hardware_type': -1, + 'hardware_serial': 7013536308, + 'hardware_type': 11, 'name': 'TestModule', - 'software_serial': -1, + 'software_serial': 1641233, }), ]), 'dim_mode': 'STEPS200', diff --git a/tests/components/lcn/test_climate.py b/tests/components/lcn/test_climate.py index ceb6f9524d1..e44a620e33c 100644 --- a/tests/components/lcn/test_climate.py +++ b/tests/components/lcn/test_climate.py @@ -52,8 +52,15 @@ async def test_set_hvac_mode_heat(hass: HomeAssistant, entry: MockConfigEntry) - await init_integration(hass, entry) with patch.object(MockModuleConnection, "lock_regulator") as lock_regulator: - state = hass.states.get("climate.testmodule_climate1") - state.state = HVACMode.OFF + await hass.services.async_call( + DOMAIN_CLIMATE, + SERVICE_SET_HVAC_MODE, + { + ATTR_ENTITY_ID: "climate.testmodule_climate1", + ATTR_HVAC_MODE: HVACMode.OFF, + }, + blocking=True, + ) # command failed lock_regulator.return_value = False diff --git a/tests/components/lcn/test_cover.py b/tests/components/lcn/test_cover.py index 1ac4ea6f664..6c8ed622ad0 100644 --- a/tests/components/lcn/test_cover.py +++ b/tests/components/lcn/test_cover.py @@ -63,7 +63,8 @@ async def test_outputs_open(hass: HomeAssistant, entry: MockConfigEntry) -> None MockModuleConnection, "control_motor_outputs" ) as control_motor_outputs: state = hass.states.get(COVER_OUTPUTS) - state.state = CoverState.CLOSED + assert state is not None + assert state.state == CoverState.CLOSED # command failed control_motor_outputs.return_value = False @@ -110,8 +111,12 @@ async def test_outputs_close(hass: HomeAssistant, entry: MockConfigEntry) -> Non with patch.object( MockModuleConnection, "control_motor_outputs" ) as control_motor_outputs: - state = hass.states.get(COVER_OUTPUTS) - state.state = CoverState.OPEN + await hass.services.async_call( + DOMAIN_COVER, + SERVICE_OPEN_COVER, + {ATTR_ENTITY_ID: COVER_OUTPUTS}, + blocking=True, + ) # command failed control_motor_outputs.return_value = False @@ -158,8 +163,12 @@ async def test_outputs_stop(hass: HomeAssistant, entry: MockConfigEntry) -> None with patch.object( MockModuleConnection, "control_motor_outputs" ) as control_motor_outputs: - state = hass.states.get(COVER_OUTPUTS) - state.state = CoverState.CLOSING + await hass.services.async_call( + DOMAIN_COVER, + SERVICE_CLOSE_COVER, + {ATTR_ENTITY_ID: COVER_OUTPUTS}, + blocking=True, + ) # command failed control_motor_outputs.return_value = False @@ -203,7 +212,8 @@ async def test_relays_open(hass: HomeAssistant, entry: MockConfigEntry) -> None: MockModuleConnection, "control_motor_relays" ) as control_motor_relays: state = hass.states.get(COVER_RELAYS) - state.state = CoverState.CLOSED + assert state is not None + assert state.state == CoverState.CLOSED # command failed control_motor_relays.return_value = False @@ -250,8 +260,12 @@ async def test_relays_close(hass: HomeAssistant, entry: MockConfigEntry) -> None with patch.object( MockModuleConnection, "control_motor_relays" ) as control_motor_relays: - state = hass.states.get(COVER_RELAYS) - state.state = CoverState.OPEN + await hass.services.async_call( + DOMAIN_COVER, + SERVICE_OPEN_COVER, + {ATTR_ENTITY_ID: COVER_RELAYS}, + blocking=True, + ) # command failed control_motor_relays.return_value = False @@ -298,8 +312,12 @@ async def test_relays_stop(hass: HomeAssistant, entry: MockConfigEntry) -> None: with patch.object( MockModuleConnection, "control_motor_relays" ) as control_motor_relays: - state = hass.states.get(COVER_RELAYS) - state.state = CoverState.CLOSING + await hass.services.async_call( + DOMAIN_COVER, + SERVICE_CLOSE_COVER, + {ATTR_ENTITY_ID: COVER_RELAYS}, + blocking=True, + ) # command failed control_motor_relays.return_value = False @@ -360,7 +378,8 @@ async def test_relays_set_position( MockModuleConnection, "control_motor_relays_position" ) as control_motor_relays_position: state = hass.states.get(entity_id) - state.state = CoverState.CLOSED + assert state is not None + assert state.state == CoverState.CLOSED # command failed control_motor_relays_position.return_value = False diff --git a/tests/components/lcn/test_light.py b/tests/components/lcn/test_light.py index b13e18bbbd1..0d1bf2619bb 100644 --- a/tests/components/lcn/test_light.py +++ b/tests/components/lcn/test_light.py @@ -209,8 +209,12 @@ async def test_relay_turn_off(hass: HomeAssistant, entry: MockConfigEntry) -> No states = [RelayStateModifier.NOCHANGE] * 8 states[0] = RelayStateModifier.OFF - state = hass.states.get(LIGHT_RELAY1) - state.state = STATE_ON + await hass.services.async_call( + DOMAIN_LIGHT, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: LIGHT_RELAY1}, + blocking=True, + ) # command failed control_relays.return_value = False diff --git a/tests/components/lcn/test_switch.py b/tests/components/lcn/test_switch.py index 0c0067c8875..9f314efe6c4 100644 --- a/tests/components/lcn/test_switch.py +++ b/tests/components/lcn/test_switch.py @@ -93,8 +93,12 @@ async def test_output_turn_off(hass: HomeAssistant, entry: MockConfigEntry) -> N await init_integration(hass, entry) with patch.object(MockModuleConnection, "dim_output") as dim_output: - state = hass.states.get(SWITCH_OUTPUT1) - state.state = STATE_ON + await hass.services.async_call( + DOMAIN_SWITCH, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: SWITCH_OUTPUT1}, + blocking=True, + ) # command failed dim_output.return_value = False @@ -176,8 +180,12 @@ async def test_relay_turn_off(hass: HomeAssistant, entry: MockConfigEntry) -> No states = [RelayStateModifier.NOCHANGE] * 8 states[0] = RelayStateModifier.OFF - state = hass.states.get(SWITCH_RELAY1) - state.state = STATE_ON + await hass.services.async_call( + DOMAIN_SWITCH, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: SWITCH_RELAY1}, + blocking=True, + ) # command failed control_relays.return_value = False @@ -257,8 +265,12 @@ async def test_regulatorlock_turn_off( await init_integration(hass, entry) with patch.object(MockModuleConnection, "lock_regulator") as lock_regulator: - state = hass.states.get(SWITCH_REGULATOR1) - state.state = STATE_ON + await hass.services.async_call( + DOMAIN_SWITCH, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: SWITCH_REGULATOR1}, + blocking=True, + ) # command failed lock_regulator.return_value = False @@ -340,8 +352,12 @@ async def test_keylock_turn_off(hass: HomeAssistant, entry: MockConfigEntry) -> states = [KeyLockStateModifier.NOCHANGE] * 8 states[0] = KeyLockStateModifier.OFF - state = hass.states.get(SWITCH_KEYLOCKK1) - state.state = STATE_ON + await hass.services.async_call( + DOMAIN_SWITCH, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: SWITCH_KEYLOCKK1}, + blocking=True, + ) # command failed lock_keys.return_value = False