Skip to content

Commit

Permalink
Improve connection error handling (#2029)
Browse files Browse the repository at this point in the history
  • Loading branch information
SukramJ authored Jan 30, 2025
1 parent e175f34 commit 87fdb64
Show file tree
Hide file tree
Showing 5 changed files with 55 additions and 34 deletions.
4 changes: 4 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
# Version 2025.1.21 (2025-01-30)

- Improve connection error handling

# Version 2025.1.20 (2025-01-30)

- Fix index issue with WEEK_PROGRAM_POINTER
Expand Down
76 changes: 46 additions & 30 deletions hahomematic/central/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ class CentralUnit(PayloadMixin):
def __init__(self, central_config: CentralConfig) -> None:
"""Init the central unit."""
self._started: bool = False
self._clients_started: bool = False
self._device_add_semaphore: Final = asyncio.Semaphore()
self._connection_state: Final = CentralConnectionState()
self._tasks: Final[set[asyncio.Future[Any]]] = set()
Expand Down Expand Up @@ -425,7 +426,7 @@ async def start(self) -> None:
for client in self._clients.values():
await self._refresh_device_descriptions(client=client)
else:
await self._start_clients()
self._clients_started = await self._start_clients()
if self._config.enable_server:
self._start_scheduler()

Expand Down Expand Up @@ -468,7 +469,8 @@ async def stop(self) -> None:
async def restart_clients(self) -> None:
"""Restart clients."""
await self._stop_clients()
await self._start_clients()
if await self._start_clients():
_LOGGER.info("RESTART_CLIENTS: Central %s restarted clients", self.name)

@inspector(re_raise=False)
async def refresh_firmware_data(self, device_address: str | None = None) -> None:
Expand Down Expand Up @@ -510,14 +512,16 @@ async def _refresh_device_descriptions(self, client: hmcl.Client, device_address
device_descriptions=device_descriptions,
)

async def _start_clients(self) -> None:
async def _start_clients(self) -> bool:
"""Start clients ."""
if await self._create_clients():
await self._load_caches()
if new_device_addresses := self._check_for_new_device_addresses():
await self._create_devices(new_device_addresses=new_device_addresses)
await self._init_hub()
await self._init_clients()
if not await self._create_clients():
return False
await self._load_caches()
if new_device_addresses := self._check_for_new_device_addresses():
await self._create_devices(new_device_addresses=new_device_addresses)
await self._init_hub()
await self._init_clients()
return True

async def _stop_clients(self) -> None:
"""Stop clients."""
Expand All @@ -527,6 +531,7 @@ async def _stop_clients(self) -> None:
await client.stop()
_LOGGER.debug("STOP_CLIENTS: Clearing existing clients.")
self._clients.clear()
self._clients_started = False

async def _create_clients(self) -> bool:
"""Create clients for the central unit. Start connection checker afterwards."""
Expand Down Expand Up @@ -564,25 +569,22 @@ async def _create_clients(self) -> bool:
continue
await self._create_client(interface_config=interface_config)

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:
if not self.all_clients_active:
_LOGGER.warning(
"CREATE_CLIENTS: Created %i of %i clients",
"CREATE_CLIENTS failed: Created %i of %i clients",
len(self._clients),
len(self._config.enabled_interface_configs),
)
return False

if self.primary_client is None:
_LOGGER.warning("CREATE_CLIENTS failed: No primary client identified for %s", self.name)
return True

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

async def _create_client(self, interface_config: hmcl.InterfaceConfig) -> None:
async def _create_client(self, interface_config: hmcl.InterfaceConfig) -> bool:
"""Create a client."""
try:
if client := await hmcl.create_client(
Expand All @@ -595,6 +597,7 @@ async def _create_client(self, interface_config: hmcl.InterfaceConfig) -> None:
self.name,
)
self._clients[client.interface_id] = client
return True
except BaseHomematicException as ex:
self.fire_interface_event(
interface_id=interface_config.interface_id,
Expand All @@ -607,6 +610,7 @@ async def _create_client(self, interface_config: hmcl.InterfaceConfig) -> None:
interface_config.interface_id,
reduce_args(args=ex.args),
)
return False

async def _init_clients(self) -> None:
"""Init clients of control unit, and start connection checker."""
Expand Down Expand Up @@ -802,7 +806,7 @@ def has_client(self, interface_id: str) -> bool:
return interface_id in self._clients

@property
def has_all_enabled_clients(self) -> bool:
def all_clients_active(self) -> bool:
"""Check if all configured clients exists in central."""
count_client = len(self._clients)
return count_client > 0 and count_client == len(self._config.enabled_interface_configs)
Expand Down Expand Up @@ -1526,7 +1530,7 @@ def stop(self) -> None:
async def _run_scheduler_tasks(self) -> None:
"""Run all tasks."""
while self._active:
if not self._central.started or not self._devices_created:
if not self._central.started:
_LOGGER.debug("SCHEDULER: Waiting till central %s is started", self._central.name)
await asyncio.sleep(10)
continue
Expand All @@ -1542,7 +1546,7 @@ async def _check_connection(self) -> None:
"""Check connection to backend."""
_LOGGER.debug("CHECK_CONNECTION: Checking connection to server %s", self._central.name)
try:
if not self._central.has_all_enabled_clients:
if not self._central.all_clients_active:
_LOGGER.warning(
"CHECK_CONNECTION failed: No clients exist. Trying to create clients for server %s",
self._central.name,
Expand Down Expand Up @@ -1580,15 +1584,15 @@ async def _refresh_client_data(self) -> None:
return

if (poll_clients := self._central.poll_clients) is not None and len(poll_clients) > 0:
_LOGGER.debug("REFRESH_CLIENT_DATA: Checking connection to server %s", self._central.name)
_LOGGER.debug("REFRESH_CLIENT_DATA: Loading data for %s", self._central.name)
for client in poll_clients:
await self._central.load_and_refresh_data_point_data(interface=client.interface)
self._central.set_last_event_dt(interface_id=client.interface_id)

@inspector(re_raise=False)
async def _refresh_sysvar_data(self) -> None:
"""Refresh system variables."""
if not self._central.config.enable_sysvar_scan or not self._central.available:
if not self._central.config.enable_sysvar_scan or not self._central.available or not self._devices_created:
return

_LOGGER.debug("REFRESH_SYSVAR_DATA: For %s", self._central.name)
Expand All @@ -1597,7 +1601,7 @@ async def _refresh_sysvar_data(self) -> None:
@inspector(re_raise=False)
async def _refresh_program_data(self) -> None:
"""Refresh system program_data."""
if not self._central.config.enable_program_scan or not self._central.available:
if not self._central.config.enable_program_scan or not self._central.available or not self._devices_created:
return

_LOGGER.debug("REFRESH_PROGRAM_DATA: For %s", self._central.name)
Expand All @@ -1606,7 +1610,11 @@ async def _refresh_program_data(self) -> None:
@inspector(re_raise=False)
async def _fetch_device_firmware_update_data(self) -> None:
"""Periodically fetch device firmware update data from backend."""
if not self._central.config.enable_device_firmware_check or not self._central.available:
if (
not self._central.config.enable_device_firmware_check
or not self._central.available
or not self._devices_created
):
return

_LOGGER.debug(
Expand All @@ -1618,7 +1626,11 @@ async def _fetch_device_firmware_update_data(self) -> None:
@inspector(re_raise=False)
async def _fetch_device_firmware_update_data_in_delivery(self) -> None:
"""Periodically fetch device firmware update data from backend."""
if not self._central.config.enable_device_firmware_check or not self._central.available:
if (
not self._central.config.enable_device_firmware_check
or not self._central.available
or not self._devices_created
):
return

_LOGGER.debug(
Expand All @@ -1635,7 +1647,11 @@ async def _fetch_device_firmware_update_data_in_delivery(self) -> None:
@inspector(re_raise=False)
async def _fetch_device_firmware_update_data_in_update(self) -> None:
"""Periodically fetch device firmware update data from backend."""
if not self._central.config.enable_device_firmware_check or not self._central.available:
if (
not self._central.config.enable_device_firmware_check
or not self._central.available
or not self._devices_created
):
return

_LOGGER.debug(
Expand Down
3 changes: 2 additions & 1 deletion hahomematic/client/xml_rpc.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,10 @@ class _XmlRpcMethod(StrEnum):

_OS_ERROR_CODES: Final[dict[int, str]] = {
errno.ECONNREFUSED: "Connection refused",
errno.EHOSTUNREACH: "No route to host",
errno.ENETUNREACH: "Network is unreachable",
errno.ENOEXEC: "Exec",
errno.ETIMEDOUT: "Operation timed out",
errno.EHOSTUNREACH: "No route to host",
}


Expand Down
2 changes: 1 addition & 1 deletion hahomematic/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import re
from typing import Any, Final, NamedTuple, Required, TypedDict

VERSION: Final = "2025.1.20"
VERSION: Final = "2025.1.21"

# default
DEFAULT_CUSTOM_ID: Final = "custom_id"
Expand Down
4 changes: 2 additions & 2 deletions tests/test_central.py
Original file line number Diff line number Diff line change
Expand Up @@ -838,7 +838,7 @@ async def test_central_without_interface_config(factory: helper.Factory) -> None
"""Test central other methods."""
central = await factory.get_raw_central(interface_config=None)
try:
assert central.has_all_enabled_clients is False
assert central.all_clients_active is False

with pytest.raises(NoClientsException):
await central.validate_config_and_get_system_information()
Expand All @@ -847,7 +847,7 @@ async def test_central_without_interface_config(factory: helper.Factory) -> None
central.get_client("NOT_A_VALID_INTERFACE_ID")

await central.start()
assert central.has_all_enabled_clients is False
assert central.all_clients_active is False

assert central.available is True
assert central.system_information.serial is None
Expand Down

0 comments on commit 87fdb64

Please sign in to comment.