Skip to content

Commit

Permalink
Remove clients for not available interfaces (#1845)
Browse files Browse the repository at this point in the history
* Remove clients for not available interfaces

* Remove clients for not available interfaces #2

* Remove clients for not available interfaces #3

* Fix log
  • Loading branch information
SukramJ authored Nov 16, 2024
1 parent fac0ad3 commit e0f31b9
Show file tree
Hide file tree
Showing 11 changed files with 142 additions and 55 deletions.
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/charliermarsh/ruff-pre-commit
rev: v0.7.3
rev: v0.7.4
hooks:
- id: ruff
args:
Expand Down
1 change: 1 addition & 0 deletions changelog.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Version 2024.11.2 (2024-11-15)

- Ignore unknown interfaces
- Remove clients for not available interfaces

# Version 2024.11.1 (2024-11-14)

Expand Down
150 changes: 105 additions & 45 deletions hahomematic/central/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
IGNORE_FOR_UN_IGNORE_PARAMETERS,
IP_ANY_V4,
PORT_ANY,
PRIMARY_CLIENT_CANDIDATE_INTERFACES,
UN_IGNORE_WILDCARD,
BackendSystemEvent,
DataPointCategory,
Expand Down Expand Up @@ -350,9 +351,9 @@ async def start(self) -> None:
if self._started:
_LOGGER.debug("START: Central %s already started", self.name)
return
if self._config.interface_configs and (
if self._config.enabled_interface_configs and (
ip_addr := await self._identify_ip_addr(
port=tuple(self._config.interface_configs)[0].port
port=tuple(self._config.enabled_interface_configs)[0].port
)
):
self._callback_ip_addr = ip_addr
Expand Down Expand Up @@ -512,60 +513,94 @@ async def _create_clients(self) -> bool:
self.name,
)
return False
if len(self._config.interface_configs) == 0:
if len(self._config.enabled_interface_configs) == 0:
_LOGGER.warning(
"CREATE_CLIENTS failed: No Interfaces for %s defined",
self.name,
)
return False

for interface_config in self._config.interface_configs:
try:
if client := await hmcl.create_client(
central=self,
interface_config=interface_config,
# create primary clients
for interface_config in self._config.enabled_interface_configs:
if interface_config.interface in PRIMARY_CLIENT_CANDIDATE_INTERFACES:
await self._create_client(interface_config=interface_config)

# create secondary clients
for interface_config in self._config.enabled_interface_configs:
if interface_config.interface not in PRIMARY_CLIENT_CANDIDATE_INTERFACES:
if (
self.primary_client is not None
and interface_config.interface
not in self.primary_client.system_information.available_interfaces
):
if (
available_interfaces := client.system_information.available_interfaces
) and (interface_config.interface not in available_interfaces):
_LOGGER.debug(
"CREATE_CLIENTS failed: Interface: %s is not available for backend",
interface_config.interface,
)
continue
_LOGGER.debug(
"CREATE_CLIENTS: Adding client %s to %s",
client.interface_id,
_LOGGER.warning(
"CREATE_CLIENTS failed: Interface: %s is not available for backend %s",
interface_config.interface,
self.name,
)
self._clients[client.interface_id] = client
except BaseHomematicException as ex:
self.fire_interface_event(
interface_id=interface_config.interface_id,
interface_event_type=InterfaceEventType.PROXY,
data={EventKey.AVAILABLE: False},
)
_LOGGER.warning(
"CREATE_CLIENTS failed: No connection to interface %s [%s]",
interface_config.interface_id,
reduce_args(args=ex.args),
)
interface_config.disable()
continue
await self._create_client(interface_config=interface_config)

if self.has_clients:
if self.has_all_enabled_clients:
_LOGGER.debug(
"CREATE_CLIENTS: All clients successfully created for %s",
self.name,
)
return True

if self.primary_client is not None:
_LOGGER.warning(
"CREATE_CLIENTS: Created %i of %i clients",
len(self._clients),
len(self._config.enabled_interface_configs),
)
return True

_LOGGER.debug("CREATE_CLIENTS failed for %s", self.name)
return False

async def _create_client(self, interface_config: hmcl.InterfaceConfig) -> None:
"""Create a client."""
try:
if client := await hmcl.create_client(
central=self,
interface_config=interface_config,
):
_LOGGER.debug(
"CREATE_CLIENT: Adding client %s to %s",
client.interface_id,
self.name,
)
self._clients[client.interface_id] = client
except BaseHomematicException as ex:
self.fire_interface_event(
interface_id=interface_config.interface_id,
interface_event_type=InterfaceEventType.PROXY,
data={EventKey.AVAILABLE: False},
)

_LOGGER.warning(
"CREATE_CLIENT failed: No connection to interface %s [%s]",
interface_config.interface_id,
reduce_args(args=ex.args),
)

async def _init_clients(self) -> None:
"""Init clients of control unit, and start connection checker."""
for client in self._clients.values():
if client.interface not in self.system_information.available_interfaces:
_LOGGER.debug(
"INIT_CLIENTS failed: Interface: %s is not available for backend %s",
client.interface,
self.name,
)
del self._clients[client.interface_id]
continue
if await client.proxy_init() == ProxyInitState.INIT_SUCCESS:
_LOGGER.debug("INIT_CLIENTS: client for %s initialized", client.interface_id)
_LOGGER.debug(
"INIT_CLIENTS: client %s initialized for %s", client.interface_id, self.name
)

async def _de_init_clients(self) -> None:
"""De-init clients."""
Expand Down Expand Up @@ -634,13 +669,24 @@ def _stop_connection_checker(self) -> None:

async def validate_config_and_get_system_information(self) -> SystemInformation:
"""Validate the central configuration."""
if len(self._config.interface_configs) == 0:
if len(self._config.enabled_interface_configs) == 0:
raise NoClientsException("validate_config: No clients defined.")

system_information = SystemInformation()
for interface_config in self._config.interface_configs:
client = await hmcl.create_client(central=self, interface_config=interface_config)
if not system_information.serial:
for interface_config in self._config.enabled_interface_configs:
try:
client = await hmcl.create_client(central=self, interface_config=interface_config)
except BaseHomematicException as ex:
_LOGGER.error(
"VALIDATE_CONFIG_AND_GET_SYSTEM_INFORMATION failed for client %s: %s",
interface_config.interface,
reduce_args(args=ex.args),
)
raise
if (
client.interface in PRIMARY_CLIENT_CANDIDATE_INTERFACES
and not system_information.serial
):
system_information = client.system_information
return system_information

Expand Down Expand Up @@ -701,7 +747,7 @@ def _get_primary_client(self) -> hmcl.Client | None:
"""Return the client by interface_id or the first with a virtual remote."""
client: hmcl.Client | None = None
for client in self._clients.values():
if client.interface in (Interface.HMIP_RF, Interface.BIDCOS_RF) and client.available:
if client.interface in PRIMARY_CLIENT_CANDIDATE_INTERFACES and client.available:
return client
return client

Expand Down Expand Up @@ -741,11 +787,15 @@ def has_client(self, interface_id: str) -> bool:
return interface_id in self._clients

@property
def has_clients(self) -> bool:
def has_all_enabled_clients(self) -> bool:
"""Check if all configured clients exists in central."""
count_client = len(self._clients)
count_client_defined = len(self._config.interface_configs)
return count_client > 0 and count_client == count_client_defined
return count_client > 0 and count_client == len(self._config.enabled_interface_configs)

@property
def has_clients(self) -> bool:
"""Check if clients exists in central."""
return len(self._clients) > 0

async def _load_caches(self) -> None:
"""Load files to caches."""
Expand All @@ -754,8 +804,12 @@ async def _load_caches(self) -> None:
await self._paramset_descriptions.load()
await self._device_details.load()
await self._data_cache.load()
except orjson.JSONDecodeError: # pragma: no cover
_LOGGER.warning("LOAD_CACHES failed: Unable to load caches for %s", self.name)
except orjson.JSONDecodeError as ex: # pragma: no cover
_LOGGER.warning(
"LOAD_CACHES failed: Unable to load caches for %s: %s",
self.name,
reduce_args(args=ex.args),
)
await self.clear_caches()

async def _create_devices(self, new_device_addresses: dict[str, set[str]]) -> None:
Expand Down Expand Up @@ -1412,7 +1466,7 @@ async def _check_connection(self) -> None:
self._central.name,
)
try:
if not self._central.has_clients:
if not self._central.has_all_enabled_clients:
_LOGGER.warning(
"CHECK_CONNECTION failed: No clients exist. "
"Trying to create clients for server %s",
Expand Down Expand Up @@ -1487,7 +1541,7 @@ def __init__(
self.host: Final = host
self.include_internal_programs: Final = include_internal_programs
self.include_internal_sysvars: Final = include_internal_sysvars
self.interface_configs: Final = interface_configs
self._interface_configs: Final = interface_configs
self.json_port: Final = json_port
self.listen_ip_addr: Final = listen_ip_addr
self.listen_port: Final = listen_port
Expand Down Expand Up @@ -1524,6 +1578,11 @@ def load_un_ignore(self) -> bool:
"""Return if un_ignore should be loaded."""
return self.start_direct is False

@property
def enabled_interface_configs(self) -> tuple[hmcl.InterfaceConfig, ...]:
"""Return the interface configs."""
return tuple(ic for ic in self._interface_configs if ic.enabled is True)

@property
def use_caches(self) -> bool:
"""Return if caches should be used."""
Expand Down Expand Up @@ -1555,6 +1614,7 @@ def check_config(self) -> None:
callback_host=self.callback_host,
callback_port=self.callback_port,
json_port=self.json_port,
interface_configs=self._interface_configs,
):
failures = ", ".join(config_failures)
raise HaHomematicConfigException(failures)
Expand Down
13 changes: 11 additions & 2 deletions hahomematic/client/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -186,9 +186,8 @@ async def proxy_init(self) -> ProxyInitState:
"""Init the proxy has to tell the CCU / Homegear where to send the events."""

if not self.supports_xml_rpc:
device_descriptions = await self.list_devices()
await self.central.add_new_devices(
interface_id=self.interface_id, device_descriptions=device_descriptions
interface_id=self.interface_id, device_descriptions=await self.list_devices()
)
return ProxyInitState.INIT_SUCCESS
try:
Expand Down Expand Up @@ -1647,6 +1646,7 @@ def __init__(
self.port: Final = port
self.remote_path: Final = remote_path
self._init_validate()
self._enabled: bool = True

def _init_validate(self) -> None:
"""Validate the client_config."""
Expand All @@ -1657,6 +1657,15 @@ def _init_validate(self) -> None:
", ".join(list(Interface)),
)

@property
def enabled(self) -> bool:
"""Return if the interface config is enabled."""
return self._enabled

def disable(self) -> None:
"""Disable the interface config."""
self._enabled = False


async def create_client(
central: hmcu.CentralUnit,
Expand Down
2 changes: 1 addition & 1 deletion hahomematic/client/xml_rpc.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ async def __async_request(self, *args, **kwargs): # type: ignore[no-untyped-def
if not self._connection_state.has_issue(issuer=self, iid=self.interface_id):
if per.errmsg == "Unauthorized":
raise AuthFailure(per) from per
raise NoConnectionException(per) from per
raise NoConnectionException(per.errmsg) from per
except Exception as ex:
raise ClientException(ex) from ex

Expand Down
2 changes: 2 additions & 0 deletions hahomematic/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -526,6 +526,8 @@ class ParameterType(StrEnum):
DataPointCategory.UPDATE,
)

PRIMARY_CLIENT_CANDIDATE_INTERFACES: Final = (Interface.HMIP_RF, Interface.BIDCOS_RF)

RELEVANT_INIT_PARAMETERS: Final[tuple[Parameter, ...]] = (
Parameter.CONFIG_PENDING,
Parameter.STICKY_UN_REACH,
Expand Down
17 changes: 16 additions & 1 deletion hahomematic/support.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from __future__ import annotations

import base64
from collections.abc import Collection
from collections.abc import Collection, Set as AbstractSet
import contextlib
from dataclasses import dataclass
from datetime import datetime
Expand All @@ -18,6 +18,7 @@
import sys
from typing import Any, Final

from hahomematic import client as hmcl
from hahomematic.config import TIMEOUT
from hahomematic.const import (
ALLOWED_HOSTNAME_PATTERN,
Expand All @@ -32,6 +33,7 @@
INIT_DATETIME,
MAX_CACHE_AGE,
NO_CACHE_ENTRY,
PRIMARY_CLIENT_CANDIDATE_INTERFACES,
CommandRxMode,
ParamsetKey,
RxMode,
Expand Down Expand Up @@ -83,6 +85,7 @@ def check_config(
callback_host: str | None,
callback_port: int | None,
json_port: int | None,
interface_configs: AbstractSet[hmcl.InterfaceConfig] | None = None,
) -> list[str]:
"""Check config. Throws BaseHomematicException on failure."""
config_failures: list[str] = []
Expand All @@ -109,10 +112,22 @@ def check_config(
config_failures.append("Invalid callback port")
if json_port and not is_port(port=json_port):
config_failures.append("Invalid json port")
if interface_configs and not has_primary_client(interface_configs=interface_configs):
config_failures.append(
f"No primary interface ({", ".join(PRIMARY_CLIENT_CANDIDATE_INTERFACES)}) defined"
)

return config_failures


def has_primary_client(interface_configs: AbstractSet[hmcl.InterfaceConfig]) -> bool:
"""Check if all configured clients exists in central."""
for interface_config in interface_configs:
if interface_config.interface in PRIMARY_CLIENT_CANDIDATE_INTERFACES:
return True
return False


def delete_file(folder: str, file_name: str) -> None:
"""Delete the file."""
file_path = os.path.join(folder, file_name)
Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
aiohttp>=3.10.10
aiohttp>=3.11.2
orjson>=3.10.11
python-slugify>=8.0.4
voluptuous>=0.15.2
2 changes: 1 addition & 1 deletion requirements_test.txt
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
-r requirements.txt
-r requirements_test_pre_commit.txt

coverage==7.6.5
coverage==7.6.7
freezegun==1.5.1
mypy-dev==1.14.0a2
pip==24.3.1
Expand Down
Loading

0 comments on commit e0f31b9

Please sign in to comment.