Skip to content

Commit 895142e

Browse files
authored
Merge pull request #279 from plugwise/mdi
keep repeating discover_nodes until all registered nodes are online
2 parents 4b35a5c + 6fec426 commit 895142e

File tree

11 files changed

+199
-203
lines changed

11 files changed

+199
-203
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,4 @@ fixtures/*
1919
tmp
2020
.cache
2121
appdata_folder
22+
mock_folder_that_exists/*

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Changelog
22

3+
## v0.44.6 - 2025-07-06
4+
5+
- PR [279](https://github.com/plugwise/python-plugwise-usb/pull/279): Improve registry cache and node load behaviour
6+
37
## v0.44.5 - 2025-06-22
48

59
- PR [274](https://github.com/plugwise/python-plugwise-usb/pull/274): Make the energy-reset function available to Plus devices

plugwise_usb/constants.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@
4040
# In bigger networks a response from a Node could take up a while, so lets use 15 seconds.
4141
NODE_TIME_OUT: Final = 15
4242

43+
# Retry delay discover nodes
44+
NODE_RETRY_DISCOVER_INTERVAL = 60
45+
NODE_RETRY_LOAD_INTERVAL = 60
46+
4347
MAX_RETRIES: Final = 3
4448
SUPPRESS_INITIALIZATION_WARNINGS: Final = 10 # Minutes to suppress (expected) communication warning messages after initialization
4549

plugwise_usb/network/__init__.py

Lines changed: 67 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,12 @@
1212

1313
from ..api import NodeEvent, NodeType, PlugwiseNode, StickEvent
1414
from ..connection import StickController
15-
from ..constants import ENERGY_NODE_TYPES, UTF8
15+
from ..constants import (
16+
ENERGY_NODE_TYPES,
17+
NODE_RETRY_DISCOVER_INTERVAL,
18+
NODE_RETRY_LOAD_INTERVAL,
19+
UTF8,
20+
)
1621
from ..exceptions import CacheError, MessageError, NodeError, StickError, StickTimeout
1722
from ..helpers.util import validate_mac
1823
from ..messages.requests import CircleMeasureIntervalRequest, NodePingRequest
@@ -72,6 +77,9 @@ def __init__(
7277
self._unsubscribe_node_rejoin: Callable[[], None] | None = None
7378

7479
self._discover_sed_tasks: dict[str, Task[bool]] = {}
80+
self._registry_stragglers: dict[int, str] = {}
81+
self._discover_stragglers_task: Task[None] | None = None
82+
self._load_stragglers_task: Task[None] | None = None
7583

7684
# region - Properties
7785

@@ -338,7 +346,7 @@ async def discover_network_coordinator(self, load: bool = False) -> bool:
338346
# endregion
339347

340348
# region - Nodes
341-
def _create_node_object(
349+
async def _create_node_object(
342350
self,
343351
mac: str,
344352
address: int,
@@ -363,7 +371,7 @@ def _create_node_object(
363371
return
364372
self._nodes[mac] = node
365373
_LOGGER.debug("%s node %s added", node.__class__.__name__, mac)
366-
self._register.update_network_registration(address, mac, node_type)
374+
await self._register.update_network_registration(address, mac, node_type)
367375

368376
if self._cache_enabled:
369377
_LOGGER.debug(
@@ -404,22 +412,24 @@ async def _discover_node(
404412
405413
Return True if discovery succeeded.
406414
"""
407-
_LOGGER.debug("Start discovery of node %s ", mac)
415+
_LOGGER.debug(
416+
"Start discovery of node %s with NodeType %s", mac, str(node_type)
417+
)
408418
if self._nodes.get(mac) is not None:
409419
_LOGGER.debug("Skip discovery of already known node %s ", mac)
410420
return True
411421

412422
if node_type is not None:
413-
self._create_node_object(mac, address, node_type)
423+
await self._create_node_object(mac, address, node_type)
414424
await self._notify_node_event_subscribers(NodeEvent.DISCOVERED, mac)
415425
return True
416426

417427
# Node type is unknown, so we need to discover it first
418-
_LOGGER.debug("Starting the discovery of node %s", mac)
428+
_LOGGER.debug("Starting the discovery of node %s with unknown NodeType", mac)
419429
node_info, node_ping = await self._controller.get_node_details(mac, ping_first)
420430
if node_info is None:
421431
return False
422-
self._create_node_object(mac, address, node_info.node_type)
432+
await self._create_node_object(mac, address, node_info.node_type)
423433

424434
# Forward received NodeInfoResponse message to node
425435
await self._nodes[mac].message_for_node(node_info)
@@ -431,15 +441,39 @@ async def _discover_node(
431441
async def _discover_registered_nodes(self) -> None:
432442
"""Discover nodes."""
433443
_LOGGER.debug("Start discovery of registered nodes")
434-
counter = 0
444+
registered_counter = 0
435445
for address, registration in self._register.registry.items():
436446
mac, node_type = registration
437447
if mac != "":
438448
if self._nodes.get(mac) is None:
439-
await self._discover_node(address, mac, node_type)
440-
counter += 1
449+
if not await self._discover_node(address, mac, node_type):
450+
self._registry_stragglers[address] = mac
451+
registered_counter += 1
441452
await sleep(0)
442-
_LOGGER.debug("Total %s registered node(s)", str(counter))
453+
if len(self._registry_stragglers) > 0 and (
454+
self._discover_stragglers_task is None
455+
or self._discover_stragglers_task.done()
456+
):
457+
self._discover_stragglers_task = create_task(self._discover_stragglers())
458+
_LOGGER.debug(
459+
"Total %s online of %s registered node(s)",
460+
str(len(self._nodes)),
461+
str(registered_counter),
462+
)
463+
464+
async def _discover_stragglers(self) -> None:
465+
"""Repeat Discovery of Nodes with unknown NodeType."""
466+
while len(self._registry_stragglers) > 0:
467+
await sleep(NODE_RETRY_DISCOVER_INTERVAL)
468+
stragglers: dict[int, str] = {}
469+
for address, mac in self._registry_stragglers.items():
470+
if not await self._discover_node(address, mac, None):
471+
stragglers[address] = mac
472+
self._registry_stragglers = stragglers
473+
_LOGGER.debug(
474+
"Total %s nodes unreachable having unknown NodeType",
475+
str(len(stragglers)),
476+
)
443477

444478
async def _load_node(self, mac: str) -> bool:
445479
"""Load node."""
@@ -452,6 +486,12 @@ async def _load_node(self, mac: str) -> bool:
452486
return True
453487
return False
454488

489+
async def _load_stragglers(self) -> None:
490+
"""Retry failed load operation."""
491+
await sleep(NODE_RETRY_LOAD_INTERVAL)
492+
while not self._load_discovered_nodes():
493+
await sleep(NODE_RETRY_LOAD_INTERVAL)
494+
455495
async def _load_discovered_nodes(self) -> bool:
456496
"""Load all nodes currently discovered."""
457497
_LOGGER.debug("_load_discovered_nodes | START | %s", len(self._nodes))
@@ -499,10 +539,10 @@ async def discover_nodes(self, load: bool = True) -> bool:
499539
await self.discover_network_coordinator(load=load)
500540
if not self._is_running:
501541
await self.start()
502-
503542
await self._discover_registered_nodes()
504-
if load:
505-
return await self._load_discovered_nodes()
543+
if load and not await self._load_discovered_nodes():
544+
self._load_stragglers_task = create_task(self._load_stragglers())
545+
return False
506546

507547
return True
508548

@@ -512,10 +552,22 @@ async def stop(self) -> None:
512552
for task in self._discover_sed_tasks.values():
513553
if not task.done():
514554
task.cancel()
555+
if (
556+
hasattr(self, "_load_stragglers_task")
557+
and self._load_stragglers_task
558+
and not self._load_stragglers_task.done()
559+
):
560+
self._load_stragglers_task.cancel()
561+
if (
562+
hasattr(self, "_discover_stragglers_task")
563+
and self._discover_stragglers_task
564+
and not self._discover_stragglers_task.done()
565+
):
566+
self._discover_stragglers_task.cancel()
515567
self._is_running = False
516568
self._unsubscribe_to_protocol_events()
517569
await self._unload_discovered_nodes()
518-
await self._register.stop()
570+
self._register.stop()
519571
_LOGGER.debug("Stopping finished")
520572

521573
# endregion

plugwise_usb/network/cache.py

Lines changed: 42 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,10 @@
55
import logging
66

77
from ..api import NodeType
8-
from ..constants import CACHE_DATA_SEPARATOR
98
from ..helpers.cache import PlugwiseCache
109

1110
_LOGGER = logging.getLogger(__name__)
12-
_NETWORK_CACHE_FILE_NAME = "nodes.cache"
11+
_NETWORK_CACHE_FILE_NAME = "nodetype.cache"
1312

1413

1514
class NetworkRegistrationCache(PlugwiseCache):
@@ -18,68 +17,63 @@ class NetworkRegistrationCache(PlugwiseCache):
1817
def __init__(self, cache_root_dir: str = "") -> None:
1918
"""Initialize NetworkCache class."""
2019
super().__init__(_NETWORK_CACHE_FILE_NAME, cache_root_dir)
21-
self._registrations: dict[int, tuple[str, NodeType | None]] = {}
20+
self._nodetypes: dict[str, NodeType] = {}
2221

2322
@property
24-
def registrations(self) -> dict[int, tuple[str, NodeType | None]]:
23+
def nodetypes(self) -> dict[str, NodeType]:
2524
"""Cached network information."""
26-
return self._registrations
25+
return self._nodetypes
2726

2827
async def save_cache(self) -> None:
2928
"""Save the node information to file."""
3029
cache_data_to_save: dict[str, str] = {}
31-
for address in range(-1, 64, 1):
32-
mac, node_type = self._registrations.get(address, ("", None))
33-
if node_type is None:
34-
node_value = ""
35-
else:
36-
node_value = str(node_type)
37-
cache_data_to_save[str(address)] = (
38-
f"{mac}{CACHE_DATA_SEPARATOR}{node_value}"
39-
)
30+
for mac, node_type in self._nodetypes.items():
31+
node_value = str(node_type)
32+
cache_data_to_save[mac] = node_value
33+
_LOGGER.debug("Save NodeTypes %s", str(len(cache_data_to_save)))
4034
await self.write_cache(cache_data_to_save)
4135

4236
async def clear_cache(self) -> None:
4337
"""Clear current cache."""
44-
self._registrations = {}
38+
self._nodetypes = {}
4539
await self.delete_cache()
4640

4741
async def restore_cache(self) -> None:
4842
"""Load the previously stored information."""
4943
data: dict[str, str] = await self.read_cache()
50-
self._registrations = {}
51-
for _key, _data in data.items():
52-
address = int(_key)
53-
try:
54-
if CACHE_DATA_SEPARATOR in _data:
55-
values = _data.split(CACHE_DATA_SEPARATOR)
56-
else:
57-
# legacy data separator can by remove at next version
58-
values = _data.split(";")
59-
mac = values[0]
60-
node_type: NodeType | None = None
61-
if values[1] != "":
62-
node_type = NodeType[values[1][9:]]
63-
self._registrations[address] = (mac, node_type)
64-
_LOGGER.debug(
65-
"Restore registry address %s with mac %s with node type %s",
66-
address,
67-
mac if mac != "" else "<empty>",
68-
str(node_type),
69-
)
70-
except (KeyError, IndexError):
71-
_LOGGER.warning(
72-
"Skip invalid data '%s' in cache file '%s'",
73-
_data,
74-
self._cache_file,
75-
)
44+
self._nodetypes = {}
45+
for mac, node_value in data.items():
46+
node_type: NodeType | None = None
47+
if len(node_value) >= 10:
48+
try:
49+
node_type = NodeType[node_value[9:]]
50+
except KeyError:
51+
node_type = None
52+
if node_type is None:
53+
_LOGGER.warning("Invalid NodeType in cache: %s", node_value)
54+
continue
55+
self._nodetypes[mac] = node_type
56+
_LOGGER.debug(
57+
"Restore NodeType for mac %s with node type %s",
58+
mac,
59+
str(node_type),
60+
)
7661

77-
def update_registration(
78-
self, address: int, mac: str, node_type: NodeType | None
79-
) -> None:
62+
async def update_nodetypes(self, mac: str, node_type: NodeType | None) -> None:
8063
"""Save node information in cache."""
81-
if self._registrations.get(address) is not None:
82-
_, current_node_type = self._registrations[address]
83-
if current_node_type is not None and node_type is None:
64+
if node_type is None:
65+
return
66+
if (current_node_type := self._nodetypes.get(mac)) is not None:
67+
if current_node_type == node_type:
8468
return
85-
self._registrations[address] = (mac, node_type)
69+
_LOGGER.warning(
70+
"Cache contained mismatched NodeType %s replacing with %s",
71+
str(current_node_type),
72+
str(node_type),
73+
)
74+
self._nodetypes[mac] = node_type
75+
await self.save_cache()
76+
77+
def get_nodetype(self, mac: str) -> NodeType | None:
78+
"""Return NodeType from cache."""
79+
return self._nodetypes.get(mac)

0 commit comments

Comments
 (0)