Skip to content

Commit 8042899

Browse files
authored
Merge pull request #302 from plugwise/mdi_CPRegistry
Improve registry discovery and SED/SCAN configuration management
2 parents 8deac72 + 413c998 commit 8042899

File tree

14 files changed

+665
-831
lines changed

14 files changed

+665
-831
lines changed

CHANGELOG.md

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

3+
## v0.44.10 - 2025-08-11
4+
5+
- PR [302](https://github.com/plugwise/python-plugwise-usb/pull/302) Improve registry discovery and SED/SCAN configuration management
6+
- Fix for [#296](https://github.com/plugwise/plugwise_usb-beta/issues/296) Improve C+ registry collection and node discovery
7+
- Improve SED and SCAN configuration handling, include dirty bool to indicate that the configuration has changed but the node configuration has not yet.
8+
39
## v0.44.9 - 2025-07-24
410

511
- Fix for [#293](https://github.com/plugwise/plugwise_usb-beta/issues/293) via PR [299](https://github.com/plugwise/python-plugwise-usb/pull/299)

plugwise_usb/api.py

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from dataclasses import dataclass
44
from datetime import datetime
5-
from enum import Enum, auto
5+
from enum import Enum, IntEnum, auto
66
import logging
77
from typing import Any, Protocol
88

@@ -19,12 +19,12 @@ class StickEvent(Enum):
1919
NETWORK_ONLINE = auto()
2020

2121

22-
class MotionSensitivity(Enum):
22+
class MotionSensitivity(IntEnum):
2323
"""Motion sensitivity levels for Scan devices."""
2424

25-
HIGH = auto()
26-
MEDIUM = auto()
27-
OFF = auto()
25+
HIGH = 20
26+
MEDIUM = 30
27+
OFF = 255
2828

2929

3030
class NodeEvent(Enum):
@@ -118,6 +118,7 @@ class BatteryConfig:
118118
clock_sync: bool | None: Indicate if the internal clock must be synced.
119119
maintenance_interval: int | None: Interval in minutes a battery powered devices is awake for maintenance purposes.
120120
sleep_duration: int | None: Interval in minutes a battery powered devices is sleeping.
121+
dirty: bool: Settings changed, device update pending
121122
122123
"""
123124

@@ -126,6 +127,7 @@ class BatteryConfig:
126127
clock_sync: bool | None = None
127128
maintenance_interval: int | None = None
128129
sleep_duration: int | None = None
130+
dirty: bool = False
129131

130132

131133
@dataclass
@@ -145,7 +147,6 @@ class NodeInfo:
145147
"""Node hardware information."""
146148

147149
mac: str
148-
zigbee_address: int
149150
is_battery_powered: bool = False
150151
features: tuple[NodeFeature, ...] = (NodeFeature.INFO,)
151152
firmware: datetime | None = None
@@ -232,13 +233,15 @@ class MotionConfig:
232233
Attributes:
233234
reset_timer: int | None: Motion reset timer in minutes before the motion detection is switched off.
234235
daylight_mode: bool | None: Motion detection when light level is below threshold.
235-
sensitivity_level: MotionSensitivity | None: Motion sensitivity level.
236+
sensitivity_level: int | None: Motion sensitivity level.
237+
dirty: bool: Settings changed, device update pending
236238
237239
"""
238240

239241
daylight_mode: bool | None = None
240242
reset_timer: int | None = None
241-
sensitivity_level: MotionSensitivity | None = None
243+
sensitivity_level: int | None = None
244+
dirty: bool = False
242245

243246

244247
@dataclass

plugwise_usb/network/__init__.py

Lines changed: 34 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ def __init__(
7777
self._unsubscribe_node_rejoin: Callable[[], None] | None = None
7878

7979
self._discover_sed_tasks: dict[str, Task[bool]] = {}
80-
self._registry_stragglers: dict[int, str] = {}
80+
self._registry_stragglers: list[str] = []
8181
self._discover_stragglers_task: Task[None] | None = None
8282
self._load_stragglers_task: Task[None] | None = None
8383

@@ -146,7 +146,7 @@ def nodes(
146146
return self._nodes
147147

148148
@property
149-
def registry(self) -> dict[int, tuple[str, NodeType | None]]:
149+
def registry(self) -> list[str]:
150150
"""Return dictionary with all registered (joined) nodes."""
151151
return self._register.registry
152152

@@ -232,10 +232,7 @@ async def node_awake_message(self, response: PlugwiseResponse) -> None:
232232
self._awake_discovery[mac] = response.timestamp
233233
return
234234

235-
if (address := self._register.network_address(mac)) is None:
236-
if self._register.scan_completed:
237-
return
238-
235+
if not self._register.node_is_registered(mac):
239236
_LOGGER.debug(
240237
"Skip node awake message for %s because network registry address is unknown",
241238
mac,
@@ -248,7 +245,7 @@ async def node_awake_message(self, response: PlugwiseResponse) -> None:
248245
or self._discover_sed_tasks[mac].done()
249246
):
250247
self._discover_sed_tasks[mac] = create_task(
251-
self._discover_battery_powered_node(address, mac)
248+
self._discover_battery_powered_node(mac)
252249
)
253250
else:
254251
_LOGGER.debug("duplicate maintenance awake discovery for %s", mac)
@@ -280,15 +277,14 @@ async def node_rejoin_message(self, response: PlugwiseResponse) -> bool:
280277
f"Invalid response message type ({response.__class__.__name__}) received, expected NodeRejoinResponse"
281278
)
282279
mac = response.mac_decoded
283-
if (address := self._register.network_address(mac)) is None:
284-
if (address := self._register.update_node_registration(mac)) is None:
285-
raise NodeError(f"Failed to obtain address for node {mac}")
286-
287-
if self._nodes.get(mac) is None:
280+
if (
281+
self._register.update_node_registration(mac)
282+
and self._nodes.get(mac) is None
283+
):
288284
task = self._discover_sed_tasks.get(mac)
289285
if task is None or task.done():
290286
self._discover_sed_tasks[mac] = create_task(
291-
self._discover_battery_powered_node(address, mac)
287+
self._discover_battery_powered_node(mac)
292288
)
293289
else:
294290
_LOGGER.debug("duplicate awake discovery for %s", mac)
@@ -335,7 +331,7 @@ async def discover_network_coordinator(self, load: bool = False) -> bool:
335331
return False
336332

337333
if await self._discover_node(
338-
-1, self._controller.mac_coordinator, None, ping_first=False
334+
self._controller.mac_coordinator, None, ping_first=False
339335
):
340336
if load:
341337
return await self._load_node(self._controller.mac_coordinator)
@@ -349,7 +345,6 @@ async def discover_network_coordinator(self, load: bool = False) -> bool:
349345
async def _create_node_object(
350346
self,
351347
mac: str,
352-
address: int,
353348
node_type: NodeType,
354349
) -> None:
355350
"""Create node object and update network registry."""
@@ -361,7 +356,6 @@ async def _create_node_object(
361356
return
362357
node = get_plugwise_node(
363358
mac,
364-
address,
365359
self._controller,
366360
self._notify_node_event_subscribers,
367361
node_type,
@@ -371,7 +365,7 @@ async def _create_node_object(
371365
return
372366
self._nodes[mac] = node
373367
_LOGGER.debug("%s node %s added", node.__class__.__name__, mac)
374-
await self._register.update_network_registration(address, mac, node_type)
368+
await self._register.update_network_nodetype(mac, node_type)
375369

376370
if self._cache_enabled:
377371
_LOGGER.debug(
@@ -385,16 +379,13 @@ async def _create_node_object(
385379

386380
async def _discover_battery_powered_node(
387381
self,
388-
address: int,
389382
mac: str,
390383
) -> bool:
391384
"""Discover a battery powered node and add it to list of nodes.
392385
393386
Return True if discovery succeeded.
394387
"""
395-
if not await self._discover_node(
396-
address, mac, node_type=None, ping_first=False
397-
):
388+
if not await self._discover_node(mac, node_type=None, ping_first=False):
398389
return False
399390
if await self._load_node(mac):
400391
await self._notify_node_event_subscribers(NodeEvent.AWAKE, mac)
@@ -403,7 +394,6 @@ async def _discover_battery_powered_node(
403394

404395
async def _discover_node(
405396
self,
406-
address: int,
407397
mac: str,
408398
node_type: NodeType | None,
409399
ping_first: bool = True,
@@ -420,16 +410,18 @@ async def _discover_node(
420410
return True
421411

422412
if node_type is not None:
423-
await self._create_node_object(mac, address, node_type)
413+
await self._create_node_object(mac, node_type)
424414
await self._notify_node_event_subscribers(NodeEvent.DISCOVERED, mac)
425415
return True
426416

427417
# Node type is unknown, so we need to discover it first
428418
_LOGGER.debug("Starting the discovery of node %s with unknown NodeType", mac)
429419
node_info, node_ping = await self._controller.get_node_details(mac, ping_first)
430420
if node_info is None:
421+
_LOGGER.debug("Node %s with unknown NodeType not responding", mac)
422+
self._registry_stragglers.append(mac)
431423
return False
432-
await self._create_node_object(mac, address, node_info.node_type)
424+
await self._create_node_object(mac, node_info.node_type)
433425

434426
# Forward received NodeInfoResponse message to node
435427
await self._nodes[mac].message_for_node(node_info)
@@ -438,41 +430,16 @@ async def _discover_node(
438430
await self._notify_node_event_subscribers(NodeEvent.DISCOVERED, mac)
439431
return True
440432

441-
async def _discover_registered_nodes(self) -> None:
442-
"""Discover nodes."""
443-
_LOGGER.debug("Start discovery of registered nodes")
444-
registered_counter = 0
445-
for address, registration in self._register.registry.items():
446-
mac, node_type = registration
447-
if mac != "":
448-
if self._nodes.get(mac) is None:
449-
if not await self._discover_node(address, mac, node_type):
450-
self._registry_stragglers[address] = mac
451-
registered_counter += 1
452-
await sleep(0)
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-
464433
async def _discover_stragglers(self) -> None:
465434
"""Repeat Discovery of Nodes with unknown NodeType."""
466435
while len(self._registry_stragglers) > 0:
467436
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
437+
for mac in self._registry_stragglers.copy():
438+
if await self._discover_node(mac, None):
439+
self._registry_stragglers.remove(mac)
473440
_LOGGER.debug(
474441
"Total %s nodes unreachable having unknown NodeType",
475-
str(len(stragglers)),
442+
str(len(self._registry_stragglers)),
476443
)
477444

478445
async def _load_node(self, mac: str) -> bool:
@@ -515,6 +482,10 @@ async def _load_discovered_nodes(self) -> bool:
515482
)
516483
result_index += 1
517484
_LOGGER.debug("_load_discovered_nodes | END")
485+
if not all(load_result) and (
486+
self._load_stragglers_task is None or self._load_stragglers_task.done()
487+
):
488+
self._load_stragglers_task = create_task(self._load_stragglers())
518489
return all(load_result)
519490

520491
async def _unload_discovered_nodes(self) -> None:
@@ -524,25 +495,26 @@ async def _unload_discovered_nodes(self) -> None:
524495
# endregion
525496

526497
# region - Network instance
527-
async def start(self) -> None:
498+
async def start(self, load: bool = True) -> None:
528499
"""Start and activate network."""
529-
self._register.quick_scan_finished(self._discover_registered_nodes)
530-
self._register.full_scan_finished(self._discover_registered_nodes)
500+
self._register.start_node_discover(self._discover_node)
501+
if load:
502+
self._register.scan_completed_callback(self._load_discovered_nodes)
531503
await self._register.start()
532504
self._subscribe_to_protocol_events()
533505
await self._subscribe_to_node_events()
534506
self._is_running = True
507+
if len(self._registry_stragglers) > 0 and (
508+
self._discover_stragglers_task is None
509+
or self._discover_stragglers_task.done()
510+
):
511+
self._discover_stragglers_task = create_task(self._discover_stragglers())
535512

536513
async def discover_nodes(self, load: bool = True) -> bool:
537514
"""Discover nodes."""
538515
await self.discover_network_coordinator(load=load)
539516
if not self._is_running:
540-
await self.start()
541-
await self._discover_registered_nodes()
542-
if load and not await self._load_discovered_nodes():
543-
self._load_stragglers_task = create_task(self._load_stragglers())
544-
return False
545-
517+
await self.start(load=load)
546518
return True
547519

548520
async def stop(self) -> None:

plugwise_usb/network/cache.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,3 +77,14 @@ async def update_nodetypes(self, mac: str, node_type: NodeType | None) -> None:
7777
def get_nodetype(self, mac: str) -> NodeType | None:
7878
"""Return NodeType from cache."""
7979
return self._nodetypes.get(mac)
80+
81+
async def prune_cache(self, registry: list[str]) -> None:
82+
"""Remove items from cache which are not found in registry scan."""
83+
new_nodetypes: dict[str, NodeType] = {}
84+
for mac in registry:
85+
if mac == "":
86+
continue
87+
if (node_type := self.get_nodetype(mac)) is not None:
88+
new_nodetypes[mac] = node_type
89+
self._nodetypes = new_nodetypes
90+
await self.save_cache()

0 commit comments

Comments
 (0)