Skip to content

Commit 65df530

Browse files
committed
fixes + optional logging structure
1 parent b3c8a08 commit 65df530

2 files changed

Lines changed: 93 additions & 44 deletions

File tree

src/powersensor_local/devices.py

Lines changed: 51 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""Abstraction interface for unified event stream from Powersensor devices."""
22
import asyncio
3+
import logging
34
import sys
45

56
from datetime import datetime, timezone
@@ -35,9 +36,40 @@ class _PowersensorDevicesBase:
3536
callback. Subclasses are responsible for discovery — they call
3637
_plug_discovered(mac, ip, port) when a plug appears and
3738
_plug_lost(mac) when one disappears.
39+
40+
Known events
41+
------------
42+
All subclasses emit the following lifecycle events:
43+
44+
**device_found**
45+
A device has been discovered or re-discovered.
46+
``{ event: "device_found", device_type: "plug"|"sensor", mac: "..." }``
47+
48+
**device_lost**
49+
A device has definitively disappeared (plug) or been inactive long
50+
enough to expire (sensor).
51+
``{ event: "device_lost", mac: "..." }``
52+
53+
Additionally, all events from ``xlatemsg.translate_raw_message`` may be
54+
issued for subscribed devices, with the event name inserted into the
55+
``event`` field. Known measurement events include:
56+
57+
``average_flow``, ``average_power``, ``average_power_components``,
58+
``battery_level``, ``exception``, ``now_relaying_for``,
59+
``radio_signal_quality``, ``summation_energy``, ``summation_volume``.
60+
61+
When ``relay_now_relaying_for=True`` the raw ``now_relaying_for`` wire
62+
message is forwarded to the callback immediately after the synthesised
63+
``device_found`` for the same sensor MAC.
64+
65+
Note: ``scan_complete`` is only emitted by PowersensorLegacyDevices.
3866
"""
3967

40-
def __init__(self, relay_now_relaying_for: bool = False) -> None:
68+
def __init__(
69+
self,
70+
relay_now_relaying_for: bool = False,
71+
logger: 'logging.Logger | None' = None,
72+
) -> None:
4173
"""Initialise the base.
4274
4375
Parameters
@@ -50,12 +82,17 @@ def __init__(self, relay_now_relaying_for: bool = False) -> None:
5082
caller's callback unchanged, in addition to any ``device_found``
5183
synthesis. Set this to True when the caller wants to inspect relay
5284
metadata directly (e.g. the HA dispatcher).
85+
logger:
86+
Optional :class:`logging.Logger` instance. When provided, the
87+
library emits debug/warning/error messages via this logger. When
88+
None (default) the library is completely silent.
5389
"""
5490
self._event_cb = None
5591
self._devices: dict[str, '_PowersensorDevicesBase._Device'] = {}
5692
self._plug_apis: dict[str, PlugApi] = {}
5793
self._timer: '_PowersensorDevicesBase._Timer | None' = None
5894
self._relay_now_relaying_for = relay_now_relaying_for
95+
self._logger = logger
5996

6097
# ------------------------------------------------------------------
6198
# Public subscription API
@@ -105,8 +142,8 @@ async def _plug_discovered(self, mac: str, ip: str, port: int) -> None:
105142
if api.ip_address == ip and api.port == port:
106143
return # no change
107144
# Address changed — disconnect stale connection and reconnect.
108-
await api.disconnect()
109-
self._plug_apis.pop(mac)
145+
await self._plug_apis.pop(mac).disconnect()
146+
await self._remove_device(mac)
110147

111148
await self._add_device(mac, 'plug')
112149
api = PlugApi(mac, ip, port)
@@ -136,9 +173,8 @@ async def _emit_if_subscribed(self, ev: str, mac: str, obj: dict) -> None:
136173
async def _reemit(self, ev: str, obj: dict[str, str]) -> None:
137174
mac: str|None = obj.get('mac')
138175
if mac is None:
139-
# we don't log anything in this library, but if we did perhaps
140-
# _LOGGER.warning("Received event '%s' with no MAC address -- ignoring", ev) might be appropriate
141-
# for now...silence
176+
if self._logger:
177+
self._logger.warning("Received event '%s' with no MAC address — ignoring", ev)
142178
return
143179
device = self._devices.get(mac)
144180
if device is not None:
@@ -230,9 +266,10 @@ def __init__(
230266
self,
231267
bcast_addr: str = '<broadcast>',
232268
relay_now_relaying_for: bool = False,
269+
logger: 'logging.Logger | None' = None,
233270
) -> None:
234271
"""Create a fresh instance, without scanning for devices."""
235-
super().__init__(relay_now_relaying_for=relay_now_relaying_for)
272+
super().__init__(relay_now_relaying_for=relay_now_relaying_for, logger=logger)
236273
self._discovery = LegacyDiscovery(bcast_addr)
237274

238275
async def start(self, async_event_cb) -> int:
@@ -242,25 +279,20 @@ async def start(self, async_event_cb) -> int:
242279
243280
async def yourcallback(event: dict) -> None
244281
245-
Known lifecycle events emitted:
282+
See _PowersensorDevicesBase for the full list of known events.
283+
284+
Additionally emits:
246285
247286
**scan_complete**
248287
Indicates discovery has completed.
249288
``{ event: "scan_complete", gateway_count: N }``
250289
251-
**device_found**
252-
A new device found on the network.
253-
``{ event: "device_found", device_type: "plug"|"sensor", mac: "..." }``
254-
255-
**device_lost**
256-
A device appears to no longer be present.
257-
``{ event: "device_lost", mac: "..." }``
258-
259-
Additionally all events from xlatemsg.translate_raw_message may be
260-
issued, with the event name inserted into the ``event`` field.
261-
262290
Returns the number of gateway plugs found.
263291
"""
292+
if self._timer is not None:
293+
if self._logger:
294+
self._logger.warning("start() called while already running — ignoring")
295+
return len(self._plug_apis)
264296
self._event_cb = async_event_cb
265297
await self._on_scanned(await self._discovery.scan())
266298
self._start_expiry_timer()

src/powersensor_local/zeroconf_devices.py

Lines changed: 42 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,6 @@
6666

6767
from powersensor_local.devices import _PowersensorDevicesBase
6868

69-
_LOGGER = logging.getLogger(__name__)
70-
7169
_SERVICE_TYPE_UDP = '_powersensor._udp.local.'
7270
_SERVICE_TYPE_TCP = '_powersensor._tcp.local.'
7371

@@ -114,6 +112,7 @@ def __init__(
114112
service_type: str = _SERVICE_TYPE_UDP,
115113
debounce_timeout: float = _DEBOUNCE_DEFAULT_S,
116114
relay_now_relaying_for: bool = False,
115+
logger: 'logging.Logger | None' = None,
117116
) -> None:
118117
"""Initialise.
119118
@@ -131,8 +130,10 @@ def __init__(
131130
the plug as truly gone. Defaults to 60 s.
132131
relay_now_relaying_for:
133132
See _PowersensorDevicesBase for documentation.
133+
logger:
134+
See _PowersensorDevicesBase for documentation.
134135
"""
135-
super().__init__(relay_now_relaying_for=relay_now_relaying_for)
136+
super().__init__(relay_now_relaying_for=relay_now_relaying_for, logger=logger)
136137
self._zc_instance = zeroconf_instance
137138
self._zc_owned = zeroconf_instance is None # True → we close it in stop()
138139
self._service_type = service_type
@@ -152,6 +153,11 @@ async def start(self, async_event_cb) -> None:
152153
Plugs already present on the network will trigger add_service
153154
callbacks shortly after the browser starts.
154155
"""
156+
if self._browser is not None:
157+
if self._logger:
158+
self._logger.warning("start() called while already running — ignoring")
159+
return
160+
155161
self._event_cb = async_event_cb
156162
self._start_expiry_timer()
157163

@@ -224,19 +230,22 @@ def _schedule_removal(self, mac: str) -> None:
224230
mac,
225231
)
226232
self._pending_removals[mac] = handle
227-
_LOGGER.debug("Scheduled removal for %s in %.0f s", mac, self._debounce_seconds)
233+
if self._logger:
234+
self._logger.debug("Scheduled removal for %s in %.0f s", mac, self._debounce_seconds)
228235

229236
def _on_debounce_expired(self, mac: str) -> None:
230237
"""Called by the event loop when the debounce timer fires."""
231238
self._pending_removals.pop(mac, None)
232-
_LOGGER.info("Plug %s still absent after debounce — removing", mac)
239+
if self._logger:
240+
self._logger.info("Plug %s still absent after debounce — removing", mac)
233241
asyncio.get_running_loop().create_task(self._plug_lost(mac))
234242

235243
def _cancel_pending_removal(self, mac: str, source: str) -> None:
236244
handle = self._pending_removals.pop(mac, None)
237245
if handle:
238246
handle.cancel()
239-
_LOGGER.debug("Cancelled pending removal for %s (%s)", mac, source)
247+
if self._logger:
248+
self._logger.debug("Cancelled pending removal for %s (%s)", mac, source)
240249

241250
# ------------------------------------------------------------------
242251
# Called from _Listener (zeroconf thread → event loop via stored loop ref)
@@ -300,40 +309,46 @@ def _extract(self, zc: Any, type_: str, name: str) -> tuple[str, str, int] | Non
300309
"""
301310
info = _zc.ServiceInfo(type_, name)
302311
if not info.load_from_cache(zc):
303-
_LOGGER.warning(
304-
"No cache entry for %s — device will appear on next mDNS announcement",
305-
name,
306-
)
312+
if self._owner._logger:
313+
self._owner._logger.warning(
314+
"No cache entry for %s — device will appear on next mDNS announcement",
315+
name,
316+
)
307317
return None
308318

309319
addresses = info.parsed_addresses()
310320
if not addresses:
311-
_LOGGER.warning("No addresses in zeroconf cache record for %s", name)
321+
if self._owner._logger:
322+
self._owner._logger.warning("No addresses in zeroconf cache record for %s", name)
312323
return None
313324

314325
try:
315326
raw_id = info.properties[b'id']
316327
except KeyError:
317-
_LOGGER.error("Missing 'id' property in zeroconf record for %s", name)
328+
if self._owner._logger:
329+
self._owner._logger.error("Missing 'id' property in zeroconf record for %s", name)
318330
return None
319331

320332
if raw_id is None:
321-
_LOGGER.error("'id' property in zeroconf record for %s has no value", name)
333+
if self._owner._logger:
334+
self._owner._logger.error("'id' property in zeroconf record for %s has no value", name)
322335
return None
323336

324337
if info.port is None:
325-
_LOGGER.error("No port in zeroconf record for %s", name)
338+
if self._owner._logger:
339+
self._owner._logger.error("No port in zeroconf record for %s", name)
326340
return None
327341

328342
return raw_id.decode('utf-8'), addresses[0], info.port
329343

330344
def add_service(self, zc: Any, type_: str, name: str) -> None:
331345
result = self._extract(zc, type_, name)
332346
if result is None:
333-
_LOGGER.warning(
334-
"add_service: no info available for %s — will retry on next announcement",
335-
name,
336-
)
347+
if self._owner._logger:
348+
self._owner._logger.warning(
349+
"add_service: no info available for %s — will retry on next announcement",
350+
name,
351+
)
337352
return
338353
mac, ip, port = result
339354
self._name_to_mac[name] = mac
@@ -342,10 +357,11 @@ def add_service(self, zc: Any, type_: str, name: str) -> None:
342357
def update_service(self, zc: Any, type_: str, name: str) -> None:
343358
result = self._extract(zc, type_, name)
344359
if result is None:
345-
_LOGGER.warning(
346-
"update_service: no info available for %s — will retry on next announcement",
347-
name,
348-
)
360+
if self._owner._logger:
361+
self._owner._logger.warning(
362+
"update_service: no info available for %s — will retry on next announcement",
363+
name,
364+
)
349365
return
350366
mac, ip, port = result
351367
self._name_to_mac[name] = mac
@@ -354,9 +370,10 @@ def update_service(self, zc: Any, type_: str, name: str) -> None:
354370
def remove_service(self, zc: Any, type_: str, name: str) -> None:
355371
mac = self._name_to_mac.pop(name, None)
356372
if mac is None:
357-
_LOGGER.warning(
358-
"remove_service for %s: MAC not in cache — removal ignored", name
359-
)
373+
if self._owner._logger:
374+
self._owner._logger.warning(
375+
"remove_service for %s: MAC not in cache — removal ignored", name
376+
)
360377
return
361378
self._owner._on_zc_remove(mac, self._loop)
362379

0 commit comments

Comments
 (0)