Skip to content

Commit

Permalink
Add LCN panel using lcn-frontend module
Browse files Browse the repository at this point in the history
  • Loading branch information
alengwenus committed Mar 5, 2024
1 parent 4d82ea5 commit dd7d04b
Show file tree
Hide file tree
Showing 23 changed files with 895 additions and 72 deletions.
1 change: 1 addition & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -679,6 +679,7 @@ omit =
homeassistant/components/lcn/climate.py
homeassistant/components/lcn/helpers.py
homeassistant/components/lcn/services.py
homeassistant/components/lcn/websocket.py
homeassistant/components/ld2410_ble/__init__.py
homeassistant/components/ld2410_ble/binary_sensor.py
homeassistant/components/ld2410_ble/coordinator.py
Expand Down
29 changes: 29 additions & 0 deletions homeassistant/components/lcn/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import pypck

from homeassistant import config_entries
from homeassistant.components.homeassistant import DOMAIN as HOMEASSISTANT_DOMAIN
from homeassistant.const import (
CONF_ADDRESS,
CONF_DEVICE_ID,
Expand All @@ -23,9 +24,11 @@
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
from homeassistant.helpers.typing import ConfigType

from .const import (
ADD_ENTITIES_CALLBACKS,
CONF_DIM_MODE,
CONF_DOMAIN_DATA,
CONF_SK_NUM_TRIES,
Expand All @@ -46,6 +49,7 @@
)
from .schemas import CONFIG_SCHEMA # noqa: F401
from .services import SERVICES
from .websocket import register_panel

_LOGGER = logging.getLogger(__name__)

Expand All @@ -55,6 +59,28 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
if DOMAIN not in config:
return True

_LOGGER.warning(
"Configuration of LCN integration in YAML is deprecated and "
"will be removed in a future release; Your existing configuration "
"has been imported into the UI automatically "
"and can be safely removed from your configuration.yaml file"
)
async_create_issue(
hass,
HOMEASSISTANT_DOMAIN,
f"deprecated_yaml_{DOMAIN}",
breaks_in_ha_version="2024.7.0",
is_fixable=False,
is_persistent=False,
issue_domain=DOMAIN,
severity=IssueSeverity.WARNING,
translation_key="deprecated_yaml",
translation_placeholders={
"domain": DOMAIN,
"integration_title": "LCN",
},
)

# initialize a config_flow for all LCN configurations read from
# configuration.yaml
config_entries_data = import_lcn_config(config[DOMAIN])
Expand Down Expand Up @@ -114,6 +140,7 @@ async def async_setup_entry(
_LOGGER.debug('LCN connected to "%s"', config_entry.title)
hass.data[DOMAIN][config_entry.entry_id] = {
CONNECTION: lcn_connection,
ADD_ENTITIES_CALLBACKS: {},
}
# Update config_entry with LCN device serials
await async_update_config_entry(hass, config_entry)
Expand All @@ -139,6 +166,8 @@ async def async_setup_entry(
DOMAIN, service_name, service(hass).async_call_service, service.schema
)

await register_panel(hass)

return True


Expand Down
12 changes: 10 additions & 2 deletions homeassistant/components/lcn/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,13 @@
from homeassistant.helpers.typing import ConfigType

from . import LcnEntity
from .const import BINSENSOR_PORTS, CONF_DOMAIN_DATA, SETPOINTS
from .const import (
ADD_ENTITIES_CALLBACKS,
BINSENSOR_PORTS,
CONF_DOMAIN_DATA,
DOMAIN,
SETPOINTS,
)
from .helpers import DeviceConnectionType, InputType, get_device_connection


Expand Down Expand Up @@ -42,8 +48,10 @@ async def async_setup_entry(
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up LCN switch entities from a config entry."""
hass.data[DOMAIN][config_entry.entry_id][ADD_ENTITIES_CALLBACKS].update(
{DOMAIN_BINARY_SENSOR: (async_add_entities, create_lcn_binary_sensor_entity)}
)
entities = []

for entity_config in config_entry.data[CONF_ENTITIES]:
if entity_config[CONF_DOMAIN] == DOMAIN_BINARY_SENSOR:
entities.append(
Expand Down
5 changes: 5 additions & 0 deletions homeassistant/components/lcn/climate.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,13 @@

from . import LcnEntity
from .const import (
ADD_ENTITIES_CALLBACKS,
CONF_DOMAIN_DATA,
CONF_LOCKABLE,
CONF_MAX_TEMP,
CONF_MIN_TEMP,
CONF_SETPOINT,
DOMAIN,
)
from .helpers import DeviceConnectionType, InputType, get_device_connection

Expand All @@ -55,6 +57,9 @@ async def async_setup_entry(
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up LCN switch entities from a config entry."""
hass.data[DOMAIN][config_entry.entry_id][ADD_ENTITIES_CALLBACKS].update(
{DOMAIN_CLIMATE: (async_add_entities, create_lcn_climate_entity)}
)
entities = []

for entity_config in config_entry.data[CONF_ENTITIES]:
Expand Down
170 changes: 135 additions & 35 deletions homeassistant/components/lcn/config_flow.py
Original file line number Diff line number Diff line change
@@ -1,33 +1,51 @@
"""Config flow to configure the LCN integration."""
from __future__ import annotations

import asyncio
import logging
from typing import Any

import pypck
import voluptuous as vol

from homeassistant.config_entries import (
SOURCE_IMPORT,
ConfigEntry,
ConfigFlow,
ConfigFlowResult,
)
from homeassistant import config_entries
from homeassistant.const import (
CONF_BASE,
CONF_DEVICES,
CONF_ENTITIES,
CONF_HOST,
CONF_IP_ADDRESS,
CONF_PASSWORD,
CONF_PORT,
CONF_USERNAME,
)
from homeassistant.core import HomeAssistant
from homeassistant.core import HomeAssistant, callback
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.typing import ConfigType

from .const import CONF_DIM_MODE, CONF_SK_NUM_TRIES, DOMAIN
from .const import CONF_DIM_MODE, CONF_SK_NUM_TRIES, DIM_MODES, DOMAIN
from .helpers import purge_device_registry, purge_entity_registry

_LOGGER = logging.getLogger(__name__)

OPTIONS_DATA = {
vol.Required(CONF_IP_ADDRESS, default=""): str,
vol.Required(CONF_PORT, default=4114): cv.positive_int,
vol.Required(CONF_USERNAME, default=""): str,
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),
}

USER_DATA = {vol.Required(CONF_HOST, default="pchk"): str, **OPTIONS_DATA}

OPTIONS_SCHEMA = vol.Schema(OPTIONS_DATA)
USER_SCHEMA = vol.Schema(USER_DATA)

def get_config_entry(hass: HomeAssistant, data: ConfigType) -> ConfigEntry | None:

def get_config_entry(
hass: HomeAssistant, data: ConfigType
) -> config_entries.ConfigEntry | None:
"""Check config entries for already configured entries based on the ip address/port."""
return next(
(
Expand All @@ -40,8 +58,10 @@ def get_config_entry(hass: HomeAssistant, data: ConfigType) -> ConfigEntry | Non
)


async def validate_connection(host_name: str, data: ConfigType) -> ConfigType:
async def validate_connection(data: ConfigType) -> str | None:
"""Validate if a connection to LCN can be established."""
error = None
host_name = data[CONF_HOST]
host = data[CONF_IP_ADDRESS]
port = data[CONF_PORT]
username = data[CONF_USERNAME]
Expand All @@ -60,43 +80,51 @@ async def validate_connection(host_name: str, data: ConfigType) -> ConfigType:
host, port, username, password, settings=settings
)

await connection.async_connect(timeout=5)
try:
await connection.async_connect(timeout=5)
_LOGGER.debug("LCN connection validated")
except pypck.connection.PchkAuthenticationError:
_LOGGER.warning('Authentication on PCHK "%s" failed', host_name)
error = "authentication_error"
except pypck.connection.PchkLicenseError:
_LOGGER.warning(
'Maximum number of connections on PCHK "%s" was '
"reached. An additional license key is required",
host_name,
)
error = "license_error"
except (TimeoutError, ConnectionRefusedError):
_LOGGER.warning('Connection to PCHK "%s" failed', host_name)
error = "connection_refused"

_LOGGER.debug("LCN connection validated")
await connection.async_close()
return data
return error


class LcnFlowHandler(ConfigFlow, domain=DOMAIN):
class LcnFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a LCN config flow."""

VERSION = 1

async def async_step_import(self, data: ConfigType) -> ConfigFlowResult:
@staticmethod
@callback
def async_get_options_flow(
config_entry: config_entries.ConfigEntry,
) -> config_entries.OptionsFlow:
"""Get options flow for this handler."""
return LcnOptionsFlowHandler(config_entry)

async def async_step_import(
self, data: ConfigType
) -> config_entries.ConfigFlowResult:
"""Import existing configuration from LCN."""
host_name = data[CONF_HOST]
# validate the imported connection parameters
try:
await validate_connection(host_name, data)
except pypck.connection.PchkAuthenticationError:
_LOGGER.warning('Authentication on PCHK "%s" failed', host_name)
return self.async_abort(reason="authentication_error")
except pypck.connection.PchkLicenseError:
_LOGGER.warning(
(
'Maximum number of connections on PCHK "%s" was '
"reached. An additional license key is required"
),
host_name,
)
return self.async_abort(reason="license_error")
except TimeoutError:
_LOGGER.warning('Connection to PCHK "%s" failed', host_name)
return self.async_abort(reason="connection_timeout")
if (error := await validate_connection(data)) is not None:
return self.async_abort(reason=error)

# check if we already have a host with the same address configured
if entry := get_config_entry(self.hass, data):
entry.source = SOURCE_IMPORT
entry.source = config_entries.SOURCE_IMPORT
# Cleanup entity and device registry, if we imported from configuration.yaml to
# remove orphans when entities were removed from configuration
purge_entity_registry(self.hass, entry.entry_id, data)
Expand All @@ -105,4 +133,76 @@ async def async_step_import(self, data: ConfigType) -> ConfigFlowResult:
self.hass.config_entries.async_update_entry(entry, data=data)
return self.async_abort(reason="existing_configuration_updated")

return self.async_create_entry(title=f"{host_name}", data=data)
return self.async_create_entry(title=f"{data[CONF_HOST]}", data=data)

async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> config_entries.ConfigFlowResult:
"""Handle a flow initiated by the user."""
if user_input is None:
return self.async_show_form(step_id="user", data_schema=USER_SCHEMA)

errors = None
if get_config_entry(self.hass, user_input):
errors = {CONF_BASE: "already_configured"}
elif (error := await validate_connection(user_input)) is not None:
errors = {CONF_BASE: error}

if errors is not None:
return self.async_show_form(
step_id="user",
data_schema=self.add_suggested_values_to_schema(
USER_SCHEMA, user_input
),
errors=errors,
)

data: dict = {
**user_input,
CONF_DEVICES: [],
CONF_ENTITIES: [],
}

return self.async_create_entry(title=data[CONF_HOST], data=data)


class LcnOptionsFlowHandler(config_entries.OptionsFlow):
"""Handle LCN options."""

def __init__(self, config_entry: config_entries.ConfigEntry) -> None:
"""Initialize LCN options flow."""
self.config_entry = config_entry

async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> config_entries.ConfigFlowResult:
"""Manage the LCN options."""
errors = None
if user_input is not None:
user_input[CONF_HOST] = self.config_entry.data[CONF_HOST]

await self.hass.config_entries.async_unload(self.config_entry.entry_id)
if (error := await validate_connection(user_input)) is not None:
errors = {CONF_BASE: error}

# brief delay to allow host free up used license for validation
await asyncio.sleep(0.5)

if errors is None:
data = self.config_entry.data.copy()
data.update(user_input)
self.hass.config_entries.async_update_entry(
self.config_entry, data=data
)
await self.hass.config_entries.async_setup(self.config_entry.entry_id)
return self.async_create_entry(title="", data={})

await self.hass.config_entries.async_setup(self.config_entry.entry_id)

return self.async_show_form(
step_id="init",
data_schema=self.add_suggested_values_to_schema(
OPTIONS_SCHEMA, self.config_entry.data
),
errors=errors or {},
)
1 change: 1 addition & 0 deletions homeassistant/components/lcn/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
DATA_LCN = "lcn"
DEFAULT_NAME = "pchk"

ADD_ENTITIES_CALLBACKS = "add_entities_callbacks"
CONNECTION = "connection"
CONF_HARDWARE_SERIAL = "hardware_serial"
CONF_SOFTWARE_SERIAL = "software_serial"
Expand Down
11 changes: 10 additions & 1 deletion homeassistant/components/lcn/cover.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,13 @@
from homeassistant.helpers.typing import ConfigType

from . import LcnEntity
from .const import CONF_DOMAIN_DATA, CONF_MOTOR, CONF_REVERSE_TIME
from .const import (
ADD_ENTITIES_CALLBACKS,
CONF_DOMAIN_DATA,
CONF_MOTOR,
CONF_REVERSE_TIME,
DOMAIN,
)
from .helpers import DeviceConnectionType, InputType, get_device_connection

PARALLEL_UPDATES = 0
Expand All @@ -39,6 +45,9 @@ async def async_setup_entry(
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up LCN cover entities from a config entry."""
hass.data[DOMAIN][config_entry.entry_id][ADD_ENTITIES_CALLBACKS].update(
{DOMAIN_COVER: (async_add_entities, create_lcn_cover_entity)}
)
entities = []

for entity_config in config_entry.data[CONF_ENTITIES]:
Expand Down
Loading

0 comments on commit dd7d04b

Please sign in to comment.