Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add switch entities for LCN key-locks and regulator-locks #127731

Merged
merged 10 commits into from
Oct 29, 2024
37 changes: 37 additions & 0 deletions homeassistant/components/lcn/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,17 @@

import pypck

from homeassistant.components.automation import automations_with_entity
from homeassistant.components.binary_sensor import (
DOMAIN as DOMAIN_BINARY_SENSOR,
BinarySensorEntity,
)
from homeassistant.components.script import scripts_with_entity
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_DOMAIN, CONF_ENTITIES, CONF_SOURCE
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
from homeassistant.helpers.typing import ConfigType

from .const import (
Expand Down Expand Up @@ -83,11 +86,28 @@ def __init__(self, config: ConfigType, config_entry: ConfigEntry) -> 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(
self.setpoint_variable
)

entity_automations = automations_with_entity(self.hass, self.entity_id)
entity_scripts = scripts_with_entity(self.hass, self.entity_id)
if entity_automations + entity_scripts:
async_create_issue(
self.hass,
DOMAIN,
f"deprecated_binary_sensor_{self.entity_id}",
breaks_in_ha_version="2025.5.0",
is_fixable=False,
severity=IssueSeverity.WARNING,
translation_key="deprecated_regulatorlock_sensor",
translation_placeholders={
"entity": f"{DOMAIN_BINARY_SENSOR}.{self.name.lower().replace(' ', '_')}",
},
)

async def async_will_remove_from_hass(self) -> None:
"""Run when entity will be removed from hass."""
await super().async_will_remove_from_hass()
Expand Down Expand Up @@ -156,9 +176,26 @@ def __init__(self, config: ConfigType, config_entry: ConfigEntry) -> 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(self.source)

entity_automations = automations_with_entity(self.hass, self.entity_id)
entity_scripts = scripts_with_entity(self.hass, self.entity_id)
if entity_automations + entity_scripts:
async_create_issue(
self.hass,
DOMAIN,
f"deprecated_binary_sensor_{self.entity_id}",
breaks_in_ha_version="2025.5.0",
is_fixable=False,
severity=IssueSeverity.WARNING,
translation_key="deprecated_keylock_sensor",
translation_placeholders={
"entity": f"{DOMAIN_BINARY_SENSOR}.{self.name.lower().replace(' ', '_')}",
},
)

async def async_will_remove_from_hass(self) -> None:
"""Run when entity will be removed from hass."""
await super().async_will_remove_from_hass()
Expand Down
1 change: 1 addition & 0 deletions homeassistant/components/lcn/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
CONF_KEYS = "keys"
CONF_TIME = "time"
CONF_TIME_UNIT = "time_unit"
CONF_LOCK_TIME = "lock_time"
CONF_TABLE = "table"
CONF_ROW = "row"
CONF_TEXT = "text"
Expand Down
6 changes: 5 additions & 1 deletion homeassistant/components/lcn/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,9 +125,13 @@


DOMAIN_DATA_SWITCH: VolDictType = {
vol.Required(CONF_OUTPUT): vol.All(vol.Upper, vol.In(OUTPUT_PORTS + RELAY_PORTS)),
vol.Required(CONF_OUTPUT): vol.All(
vol.Upper,
vol.In(OUTPUT_PORTS + RELAY_PORTS + SETPOINTS + KEYS),
),
}


#
# Configuration
#
Expand Down
6 changes: 5 additions & 1 deletion homeassistant/components/lcn/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,11 @@ def input_received(self, input_obj: InputType) -> None:
):
return

self._attr_native_value = input_obj.get_value().to_var_unit(self.unit)
is_regulator = self.variable.name in SETPOINTS
self._attr_native_value = input_obj.get_value().to_var_unit(
self.unit, is_regulator
)

self.async_write_ha_state()


Expand Down
8 changes: 8 additions & 0 deletions homeassistant/components/lcn/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,14 @@
"connection_refused": {
"title": "Unable to connect to PCHK.",
"description": "Configuring LCN using YAML is being removed but there was an error importing your YAML configuration.\n\nEnsure the connection (IP and port) to the LCN bus coupler is correct.\n\nConsider removing the LCN YAML configuration from your configuration.yaml file and continue to [set up the integration]({url}) manually."
},
"deprecated_regulatorlock_sensor": {
"title": "Deprecated LCN regulator lock binary sensor entity found in {info}",
"description": "Your LCN regulator lock binary sensor entity `{entity}` is beeing used in automations or scripts. A regulator lock switch entity is available and should be used going forward.\n\nPlease adjust your automations or scripts to fix this issue."
},
"deprecated_keylock_sensor": {
"title": "Deprecated LCN key lock binary sensor entity found in {info}",
"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."
}
},
"services": {
Expand Down
127 changes: 125 additions & 2 deletions homeassistant/components/lcn/switch.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
CONF_OUTPUT,
DOMAIN,
OUTPUT_PORTS,
RELAY_PORTS,
SETPOINTS,
)
from .entity import LcnEntity
from .helpers import InputType
Expand All @@ -32,12 +34,18 @@ def add_lcn_switch_entities(
entity_configs: Iterable[ConfigType],
) -> None:
"""Add entities for this domain."""
entities: list[LcnOutputSwitch | LcnRelaySwitch] = []
entities: list[
LcnOutputSwitch | LcnRelaySwitch | LcnRegulatorLockSwitch | LcnKeyLockSwitch
] = []
for entity_config in entity_configs:
if entity_config[CONF_DOMAIN_DATA][CONF_OUTPUT] in OUTPUT_PORTS:
entities.append(LcnOutputSwitch(entity_config, config_entry))
else: # in RELAY_PORTS
elif entity_config[CONF_DOMAIN_DATA][CONF_OUTPUT] in RELAY_PORTS:
entities.append(LcnRelaySwitch(entity_config, config_entry))
elif entity_config[CONF_DOMAIN_DATA][CONF_OUTPUT] in SETPOINTS:
entities.append(LcnRegulatorLockSwitch(entity_config, config_entry))
else: # in KEYS
entities.append(LcnKeyLockSwitch(entity_config, config_entry))

async_add_entities(entities)

Expand Down Expand Up @@ -164,3 +172,118 @@ def input_received(self, input_obj: InputType) -> None:

self._attr_is_on = input_obj.get_state(self.output.value)
self.async_write_ha_state()


class LcnRegulatorLockSwitch(LcnEntity, SwitchEntity):
"""Representation of a LCN switch for regulator locks."""

_attr_is_on = False

def __init__(self, config: ConfigType, config_entry: ConfigEntry) -> None:
"""Initialize the LCN switch."""
super().__init__(config, config_entry)

self.setpoint_variable = pypck.lcn_defs.Var[
config[CONF_DOMAIN_DATA][CONF_OUTPUT]
]
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):
return
self._attr_is_on = True
self.async_write_ha_state()

async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the entity off."""
if not await self.device_connection.lock_regulator(self.reg_id, False):
return
self._attr_is_on = False
self.async_write_ha_state()

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.ModStatusVar)
or input_obj.get_var() != self.setpoint_variable
):
return

self._attr_is_on = input_obj.get_value().is_locked_regulator()
self.async_write_ha_state()


class LcnKeyLockSwitch(LcnEntity, SwitchEntity):
"""Representation of a LCN switch for key locks."""

_attr_is_on = False

def __init__(self, config: ConfigType, config_entry: ConfigEntry) -> None:
"""Initialize the LCN switch."""
super().__init__(config, config_entry)

self.key = pypck.lcn_defs.Key[config[CONF_DOMAIN_DATA][CONF_OUTPUT]]
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
states[self.key_id] = pypck.lcn_defs.KeyLockStateModifier.ON

if not await self.device_connection.lock_keys(self.table_id, states):
return

self._attr_is_on = True
self.async_write_ha_state()

async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the entity off."""
states = [pypck.lcn_defs.KeyLockStateModifier.NOCHANGE] * 8
states[self.key_id] = pypck.lcn_defs.KeyLockStateModifier.OFF

if not await self.device_connection.lock_keys(self.table_id, states):
return

self._attr_is_on = False
self.async_write_ha_state()

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.ModStatusKeyLocks)
or self.key not in pypck.lcn_defs.Key
):
return

self._attr_is_on = input_obj.get_state(self.table_id, self.key_id)
self.async_write_ha_state()
18 changes: 18 additions & 0 deletions tests/components/lcn/fixtures/config_entry_pchk.json
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,24 @@
"output": "RELAY2"
}
},
{
"address": [0, 7, false],
"name": "Switch_Regulator1",
"resource": "r1varsetpoint",
"domain": "switch",
"domain_data": {
"output": "R1VARSETPOINT"
}
},
{
"address": [0, 7, false],
"name": "Switch_KeyLock1",
"resource": "a1",
"domain": "switch",
"domain_data": {
"output": "A1"
}
},
{
"address": [0, 5, true],
"name": "Switch_Group5",
Expand Down
18 changes: 18 additions & 0 deletions tests/components/lcn/fixtures/config_entry_pchk_v1_1.json
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,24 @@
"output": "RELAY2"
}
},
{
"address": [0, 7, false],
"name": "Switch_Regulator1",
"resource": "r1varsetpoint",
"domain": "switch",
"domain_data": {
"output": "R1VARSETPOINT"
}
},
{
"address": [0, 7, false],
"name": "Switch_KeyLock1",
"resource": "a1",
"domain": "switch",
"domain_data": {
"output": "A1"
}
},
{
"address": [0, 5, true],
"name": "Switch_Group5",
Expand Down
18 changes: 18 additions & 0 deletions tests/components/lcn/fixtures/config_entry_pchk_v1_2.json
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,24 @@
"output": "RELAY2"
}
},
{
"address": [0, 7, false],
"name": "Switch_Regulator1",
"resource": "r1varsetpoint",
"domain": "switch",
"domain_data": {
"output": "R1VARSETPOINT"
}
},
{
"address": [0, 7, false],
"name": "Switch_KeyLock1",
"resource": "a1",
"domain": "switch",
"domain_data": {
"output": "A1"
}
},
{
"address": [0, 5, true],
"name": "Switch_Group5",
Expand Down
Loading