Make acknowledge requests from LCN modules optional (#125765)

* Add acknowledge flag to config_entry

* Add acknowledge option to lcn configuration

* Fix tests

* Bump pypck to 0.7.23

* Add entry fixture for config_entry version 1.1 to test migration

* Add data_description to strings.json

* Create versioned config_entry in tests
This commit is contained in:
Andre Lengwenus 2024-09-14 09:21:15 +02:00 committed by GitHub
parent d121e4c9b5
commit 5685ba7f55
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 317 additions and 9 deletions

View File

@ -22,6 +22,7 @@ from homeassistant.helpers.typing import ConfigType
from .const import (
ADD_ENTITIES_CALLBACKS,
CONF_ACKNOWLEDGE,
CONF_DIM_MODE,
CONF_SK_NUM_TRIES,
CONNECTION,
@ -73,6 +74,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
settings = {
"SK_NUM_TRIES": config_entry.data[CONF_SK_NUM_TRIES],
"DIM_MODE": pypck.lcn_defs.OutputPortDimMode[config_entry.data[CONF_DIM_MODE]],
"ACKNOWLEDGE": config_entry.data[CONF_ACKNOWLEDGE],
}
# connect to PCHK
@ -137,6 +139,32 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
return True
async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
"""Migrate old entry."""
_LOGGER.debug(
"Migrating configuration from version %s.%s",
config_entry.version,
config_entry.minor_version,
)
if config_entry.version == 1:
new_data = {**config_entry.data}
if config_entry.minor_version < 2:
new_data[CONF_ACKNOWLEDGE] = False
hass.config_entries.async_update_entry(
config_entry, data=new_data, minor_version=2, version=1
)
_LOGGER.debug(
"Migration to configuration version %s.%s successful",
config_entry.version,
config_entry.minor_version,
)
return True
async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
"""Close connection to PCHK host represented by config_entry."""
# forward unloading to platforms

View File

@ -26,7 +26,7 @@ from homeassistant.helpers.issue_registry import IssueSeverity, async_create_iss
from homeassistant.helpers.typing import ConfigType
from . import PchkConnectionManager
from .const import CONF_DIM_MODE, CONF_SK_NUM_TRIES, DIM_MODES, DOMAIN
from .const import CONF_ACKNOWLEDGE, CONF_DIM_MODE, CONF_SK_NUM_TRIES, DIM_MODES, DOMAIN
from .helpers import purge_device_registry, purge_entity_registry
_LOGGER = logging.getLogger(__name__)
@ -38,6 +38,7 @@ CONFIG_DATA = {
vol.Required(CONF_PASSWORD, default=""): str,
vol.Required(CONF_SK_NUM_TRIES, default=0): cv.positive_int,
vol.Required(CONF_DIM_MODE, default="STEPS200"): vol.In(DIM_MODES),
vol.Required(CONF_ACKNOWLEDGE, default=False): cv.boolean,
}
USER_DATA = {vol.Required(CONF_HOST, default="pchk"): str, **CONFIG_DATA}
@ -71,10 +72,12 @@ async def validate_connection(data: ConfigType) -> str | None:
password = data[CONF_PASSWORD]
sk_num_tries = data[CONF_SK_NUM_TRIES]
dim_mode = data[CONF_DIM_MODE]
acknowledge = data[CONF_ACKNOWLEDGE]
settings = {
"SK_NUM_TRIES": sk_num_tries,
"DIM_MODE": pypck.lcn_defs.OutputPortDimMode[dim_mode],
"ACKNOWLEDGE": acknowledge,
}
_LOGGER.debug("Validating connection parameters to PCHK host '%s'", host_name)
@ -108,6 +111,7 @@ class LcnFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a LCN config flow."""
VERSION = 1
MINOR_VERSION = 2
async def async_step_import(self, import_data: dict[str, Any]) -> ConfigFlowResult:
"""Import existing configuration from LCN."""

View File

@ -25,6 +25,7 @@ CONF_SOFTWARE_SERIAL = "software_serial"
CONF_HARDWARE_TYPE = "hardware_type"
CONF_DOMAIN_DATA = "domain_data"
CONF_ACKNOWLEDGE = "acknowledge"
CONF_CONNECTIONS = "connections"
CONF_SK_NUM_TRIES = "sk_num_tries"
CONF_OUTPUT = "output"

View File

@ -37,6 +37,7 @@ from homeassistant.helpers.typing import ConfigType
from .const import (
BINSENSOR_PORTS,
CONF_ACKNOWLEDGE,
CONF_CLIMATES,
CONF_CONNECTIONS,
CONF_DIM_MODE,
@ -158,6 +159,7 @@ def import_lcn_config(lcn_config: ConfigType) -> list[ConfigType]:
"password": "lcn,
"sk_num_tries: 0,
"dim_mode: "STEPS200",
"acknowledge": False,
"devices": [
{
"address": (0, 7, False)
@ -192,6 +194,7 @@ def import_lcn_config(lcn_config: ConfigType) -> list[ConfigType]:
CONF_PASSWORD: connection[CONF_PASSWORD],
CONF_SK_NUM_TRIES: connection[CONF_SK_NUM_TRIES],
CONF_DIM_MODE: connection[CONF_DIM_MODE],
CONF_ACKNOWLEDGE: False,
CONF_DEVICES: [],
CONF_ENTITIES: [],
}

View File

@ -8,5 +8,5 @@
"documentation": "https://www.home-assistant.io/integrations/lcn",
"iot_class": "local_push",
"loggers": ["pypck"],
"requirements": ["pypck==0.7.22", "lcn-frontend==0.1.6"]
"requirements": ["pypck==0.7.23", "lcn-frontend==0.1.6"]
}

View File

@ -26,7 +26,12 @@
"username": "[%key:common::config_flow::data::username%]",
"password": "[%key:common::config_flow::data::password%]",
"sk_num_tries": "Segment coupler scan attempts",
"dim_mode": "Dimming mode"
"dim_mode": "Dimming mode",
"acknowledge": "Request acknowledgement from modules"
},
"data_description": {
"dim_mode": "The number of steps used for dimming outputs.",
"acknowledge": "Retry sendig commands if no response is received (increases bus traffic)."
}
},
"reconfigure": {
@ -37,8 +42,13 @@
"port": "[%key:common::config_flow::data::port%]",
"username": "[%key:common::config_flow::data::username%]",
"password": "[%key:common::config_flow::data::password%]",
"sk_num_tries": "Segment coupler scan attempts",
"dim_mode": "Dimming mode"
"sk_num_tries": "[%key:component::lcn::config::step::user::data::sk_num_tries%]",
"dim_mode": "[%key:component::lcn::config::step::user::data::dim_mode%]",
"acknowledge": "[%key:component::lcn::config::step::user::data::acknowledge%]"
},
"data_description": {
"dim_mode": "[%key:component::lcn::config::step::user::data_description::dim_mode%]",
"acknowledge": "[%key:component::lcn::config::step::user::data_description::acknowledge%]"
}
}
},

View File

@ -2130,7 +2130,7 @@ pyownet==0.10.0.post1
pypca==0.0.7
# homeassistant.components.lcn
pypck==0.7.22
pypck==0.7.23
# homeassistant.components.pjlink
pypjlink2==1.2.1

View File

@ -1711,7 +1711,7 @@ pyoverkiz==1.13.14
pyownet==0.10.0.post1
# homeassistant.components.lcn
pypck==0.7.22
pypck==0.7.23
# homeassistant.components.pjlink
pypjlink2==1.2.1

View File

@ -10,6 +10,7 @@ from pypck.module import GroupConnection, ModuleConnection
import pytest
from homeassistant.components.lcn import PchkConnectionManager
from homeassistant.components.lcn.config_flow import LcnFlowHandler
from homeassistant.components.lcn.const import DOMAIN
from homeassistant.components.lcn.helpers import AddressType, generate_unique_id
from homeassistant.const import CONF_ADDRESS, CONF_DEVICES, CONF_ENTITIES, CONF_HOST
@ -19,6 +20,8 @@ from homeassistant.setup import async_setup_component
from tests.common import MockConfigEntry, load_fixture
LATEST_CONFIG_ENTRY_VERSION = (LcnFlowHandler.VERSION, LcnFlowHandler.MINOR_VERSION)
class MockModuleConnection(ModuleConnection):
"""Fake a LCN module connection."""
@ -71,7 +74,9 @@ class MockPchkConnectionManager(PchkConnectionManager):
send_command = AsyncMock()
def create_config_entry(name: str) -> MockConfigEntry:
def create_config_entry(
name: str, version: tuple[int, int] = LATEST_CONFIG_ENTRY_VERSION
) -> MockConfigEntry:
"""Set up config entries with configuration data."""
fixture_filename = f"lcn/config_entry_{name}.json"
entry_data = json.loads(load_fixture(fixture_filename))
@ -89,6 +94,8 @@ def create_config_entry(name: str) -> MockConfigEntry:
title=title,
data=entry_data,
options=options,
version=version[0],
minor_version=version[1],
)

View File

@ -6,6 +6,7 @@
"password": "lcn",
"sk_num_tries": 0,
"dim_mode": "STEPS200",
"acknowledge": false,
"devices": [],
"entities": [
{

View File

@ -6,6 +6,7 @@
"password": "lcn",
"sk_num_tries": 0,
"dim_mode": "STEPS200",
"acknowledge": false,
"devices": [
{
"address": [0, 7, false],

View File

@ -0,0 +1,230 @@
{
"host": "pchk",
"ip_address": "192.168.2.41",
"port": 4114,
"username": "lcn",
"password": "lcn",
"sk_num_tries": 0,
"dim_mode": "STEPS200",
"devices": [
{
"address": [0, 7, false],
"name": "TestModule",
"hardware_serial": -1,
"software_serial": -1,
"hardware_type": -1
},
{
"address": [0, 5, true],
"name": "TestGroup",
"hardware_serial": -1,
"software_serial": -1,
"hardware_type": -1
}
],
"entities": [
{
"address": [0, 7, false],
"name": "Light_Output1",
"resource": "output1",
"domain": "light",
"domain_data": {
"output": "OUTPUT1",
"dimmable": true,
"transition": 5000.0
}
},
{
"address": [0, 7, false],
"name": "Light_Output2",
"resource": "output2",
"domain": "light",
"domain_data": {
"output": "OUTPUT2",
"dimmable": false,
"transition": 0
}
},
{
"address": [0, 7, false],
"name": "Light_Relay1",
"resource": "relay1",
"domain": "light",
"domain_data": {
"output": "RELAY1",
"dimmable": false,
"transition": 0.0
}
},
{
"address": [0, 7, false],
"name": "Switch_Output1",
"resource": "output1",
"domain": "switch",
"domain_data": {
"output": "OUTPUT1"
}
},
{
"address": [0, 7, false],
"name": "Switch_Output2",
"resource": "output2",
"domain": "switch",
"domain_data": {
"output": "OUTPUT2"
}
},
{
"address": [0, 7, false],
"name": "Switch_Relay1",
"resource": "relay1",
"domain": "switch",
"domain_data": {
"output": "RELAY1"
}
},
{
"address": [0, 7, false],
"name": "Switch_Relay2",
"resource": "relay2",
"domain": "switch",
"domain_data": {
"output": "RELAY2"
}
},
{
"address": [0, 5, true],
"name": "Switch_Group5",
"resource": "relay1",
"domain": "switch",
"domain_data": {
"output": "RELAY1"
}
},
{
"address": [0, 7, false],
"name": "Cover_Outputs",
"resource": "outputs",
"domain": "cover",
"domain_data": {
"motor": "OUTPUTS",
"reverse_time": "RT1200"
}
},
{
"address": [0, 7, false],
"name": "Cover_Relays",
"resource": "motor1",
"domain": "cover",
"domain_data": {
"motor": "MOTOR1",
"reverse_time": "RT1200"
}
},
{
"address": [0, 7, false],
"name": "Climate1",
"resource": "var1.r1varsetpoint",
"domain": "climate",
"domain_data": {
"source": "VAR1",
"setpoint": "R1VARSETPOINT",
"lockable": true,
"min_temp": 0.0,
"max_temp": 40.0,
"unit_of_measurement": "°C"
}
},
{
"address": [0, 7, false],
"name": "Romantic",
"resource": "0.0",
"domain": "scene",
"domain_data": {
"register": 0,
"scene": 0,
"outputs": ["OUTPUT1", "OUTPUT2", "RELAY1"],
"transition": null
}
},
{
"address": [0, 7, false],
"name": "Romantic Transition",
"resource": "0.1",
"domain": "scene",
"domain_data": {
"register": 0,
"scene": 1,
"outputs": ["OUTPUT1", "OUTPUT2", "RELAY1"],
"transition": 10
}
},
{
"address": [0, 7, false],
"name": "Sensor_LockRegulator1",
"resource": "r1varsetpoint",
"domain": "binary_sensor",
"domain_data": {
"source": "R1VARSETPOINT"
}
},
{
"address": [0, 7, false],
"name": "Binary_Sensor1",
"resource": "binsensor1",
"domain": "binary_sensor",
"domain_data": {
"source": "BINSENSOR1"
}
},
{
"address": [0, 7, false],
"name": "Sensor_KeyLock",
"resource": "a5",
"domain": "binary_sensor",
"domain_data": {
"source": "A5"
}
},
{
"address": [0, 7, false],
"name": "Sensor_Var1",
"resource": "var1",
"domain": "sensor",
"domain_data": {
"source": "VAR1",
"unit_of_measurement": "°C"
}
},
{
"address": [0, 7, false],
"name": "Sensor_Setpoint1",
"resource": "r1varsetpoint",
"domain": "sensor",
"domain_data": {
"source": "R1VARSETPOINT",
"unit_of_measurement": "°C"
}
},
{
"address": [0, 7, false],
"name": "Sensor_Led6",
"resource": "led6",
"domain": "sensor",
"domain_data": {
"source": "LED6",
"unit_of_measurement": "NATIVE"
}
},
{
"address": [0, 7, false],
"name": "Sensor_LogicOp1",
"resource": "logicop1",
"domain": "sensor",
"domain_data": {
"source": "LOGICOP1",
"unit_of_measurement": "NATIVE"
}
}
]
}

View File

@ -7,7 +7,12 @@ import pytest
from homeassistant import config_entries, data_entry_flow
from homeassistant.components.lcn.config_flow import LcnFlowHandler, validate_connection
from homeassistant.components.lcn.const import CONF_DIM_MODE, CONF_SK_NUM_TRIES, DOMAIN
from homeassistant.components.lcn.const import (
CONF_ACKNOWLEDGE,
CONF_DIM_MODE,
CONF_SK_NUM_TRIES,
DOMAIN,
)
from homeassistant.const import (
CONF_BASE,
CONF_DEVICES,
@ -31,6 +36,7 @@ CONFIG_DATA = {
CONF_PASSWORD: "lcn",
CONF_SK_NUM_TRIES: 0,
CONF_DIM_MODE: "STEPS200",
CONF_ACKNOWLEDGE: False,
}
CONNECTION_DATA = {CONF_HOST: "pchk", **CONFIG_DATA}

View File

@ -14,6 +14,7 @@ from homeassistant.helpers import device_registry as dr, entity_registry as er
from .conftest import (
MockConfigEntry,
MockPchkConnectionManager,
create_config_entry,
init_integration,
setup_component,
)
@ -125,3 +126,19 @@ async def test_async_setup_from_configuration_yaml(hass: HomeAssistant) -> None:
await setup_component(hass)
assert async_setup_entry.await_count == 2
@patch("homeassistant.components.lcn.PchkConnectionManager", MockPchkConnectionManager)
async def test_migrate_1_1(hass: HomeAssistant, entry) -> None:
"""Test migration config entry."""
entry_v1_1 = create_config_entry("pchk_v1_1", version=(1, 1))
entry_v1_1.add_to_hass(hass)
await hass.config_entries.async_setup(entry_v1_1.entry_id)
await hass.async_block_till_done()
entry_migrated = hass.config_entries.async_get_entry(entry_v1_1.entry_id)
assert entry_migrated.state is ConfigEntryState.LOADED
assert entry_migrated.version == 1
assert entry_migrated.minor_version == 2
assert entry_migrated.data == entry.data