Skip to content

Commit 4478f09

Browse files
committed
Spread the logging around more.
1 parent b60a344 commit 4478f09

7 files changed

Lines changed: 31 additions & 14 deletions

File tree

‎src/powersensor_local/async_event_emitter.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,4 +40,4 @@ async def emit(self, event_name: str, *args: Any) -> None:
4040
await callback(event_name, *args)
4141
except Exception as e:
4242
if self._logger is not None:
43-
self._logger.exception(f"Logic error: exception escaped from callback: {e}")
43+
self._logger.exception("Logic error: exception escaped from callback: %s", e)

‎src/powersensor_local/plug_api.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,9 @@ def __init__(self, mac: str, ip: str, port: int = 49476, proto: str = 'udp', log
4646
self._mac: str = mac
4747
self._listener: PlugListenerUdp | PlugListenerTcp
4848
if proto == 'udp':
49-
self._listener = PlugListenerUdp(ip, port)
49+
self._listener = PlugListenerUdp(ip, port, logger)
5050
elif proto == 'tcp':
51-
self._listener = PlugListenerTcp(ip, port)
51+
self._listener = PlugListenerTcp(ip, port, logger)
5252
else:
5353
raise ValueError(f'Unsupported proto: {proto}')
5454
self._listener.subscribe('message', self._on_message)

‎src/powersensor_local/plug_listener_tcp.py‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import sys
55

66
from asyncio import StreamReader, StreamWriter
7+
from logging import Logger
78

89
from powersensor_local.async_event_emitter import AsyncEventEmitter
910

@@ -23,7 +24,7 @@ class PlugListenerTcp(AsyncEventEmitter):
2324
The event handlers must be async.
2425
"""
2526

26-
def __init__(self, ip: str, port: int = 49476):
27+
def __init__(self, ip: str, port: int = 49476, logger: Logger|None = None):
2728
"""
2829
Create a :class:`PlugListenerTcp` bound to the given IP address.
2930
@@ -34,7 +35,7 @@ def __init__(self, ip: str, port: int = 49476):
3435
port : int, optional
3536
TCP port used by the plug (default ``49476``).
3637
"""
37-
super().__init__()
38+
super().__init__(logger = logger)
3839
self._ip: str = ip
3940
self._port: int = port
4041
self._task: asyncio.Task[None] | None = None

‎src/powersensor_local/plug_listener_udp.py‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import sys
66

77
from asyncio import TimerHandle
8+
from logging import Logger
89
from typing import Any, Coroutine
910

1011
from powersensor_local.async_event_emitter import AsyncEventEmitter
@@ -27,7 +28,7 @@ class PlugListenerUdp(AsyncEventEmitter, asyncio.DatagramProtocol):
2728
The event handlers must be async.
2829
"""
2930

30-
def __init__(self, ip: str, port: int = 49476):
31+
def __init__(self, ip: str, port: int = 49476, logger: Logger|None = None):
3132
"""
3233
Create a :class:`PlugListenerUdp` bound to the given IP address.
3334
@@ -38,7 +39,7 @@ def __init__(self, ip: str, port: int = 49476):
3839
port : int, optional
3940
UDP port used by the plug (default ``49476``).
4041
"""
41-
super().__init__()
42+
super().__init__(logger = logger)
4243
self._ip: str = ip
4344
self._port: int = port
4445
self._backoff: int = 0 # exponential backoff

‎src/powersensor_local/virtual_household.py‎

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

33
import sys
44
from dataclasses import dataclass
5+
from logging import Logger
56
from typing import Optional
67

78
from .async_event_emitter import AsyncEventEmitter
@@ -138,9 +139,9 @@ class VirtualHousehold(AsyncEventEmitter):
138139
field to take note of summation resets.
139140
"""
140141

141-
def __init__(self, with_solar: bool):
142+
def __init__(self, with_solar: bool, logger: Logger|None = None):
142143
"""Constructor.
143-
with_solar True if it's already known that solar exists. Will be
144+
with_solar: True if it's already known that solar exists. Will be
144145
automatically enabled upon encountering a solar event during
145146
processing, but until such a time may generate incorrect values
146147
for home usage. Similarly, if this is set to True but no solar
@@ -156,8 +157,10 @@ def __init__(self, with_solar: bool):
156157
would be generating incorrect data until such a time the solar
157158
sensor is recharged. It is vastly preferable to have the system
158159
show no data than show incorrect data.
160+
logger: An optional logger to capture leaked exceptions from event
161+
callbacks.
159162
"""
160-
super().__init__()
163+
super().__init__(logger)
161164
self._expect_solar = with_solar
162165
self._summation = self.SummationInfo(0, 0, 0, 0)
163166
self._counters = self.Counters(0, 0, 0, 0, 0)

‎src/powersensor_local/zeroconf_devices.py‎

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@
3737
import asyncio
3838
import logging
3939
import sys
40+
41+
from asyncio import Task
42+
from logging import Logger
4043
from typing import Any, Callable, Coroutine
4144

4245
from .devices import _AsyncCallback, _LogLevel, _PowersensorDevicesBase
@@ -92,7 +95,7 @@ def __init__(
9295
service_type: str = _SERVICE_TYPE_UDP,
9396
debounce_timeout: float = _DEBOUNCE_DEFAULT_S,
9497
relay_now_relaying_for: bool = False,
95-
logger: 'logging.Logger | None' = None,
98+
logger: Logger|None = None,
9699
) -> None:
97100
"""Initialise.
98101
@@ -118,6 +121,7 @@ def __init__(
118121
self._zc_owned = zeroconf_instance is None # True → we close it in stop()
119122
self._service_type = service_type
120123
self._debounce_seconds = debounce_timeout
124+
self._cb_logger = logger
121125
self._browser: Any = None
122126
self._listener: _Listener | None = None
123127
self._pending_removals: dict[str, asyncio.TimerHandle] = {}
@@ -223,8 +227,15 @@ async def _on_zc_remove(self, mac: str) -> None:
223227
def _internal_callback(self, coro: _InternalCallback) -> None:
224228
"""Helper to prevent gc collection of short-lived callback tasks."""
225229
task = asyncio.create_task(coro)
230+
231+
def cleanup(task: Task[None]) -> None:
232+
self._internal_callbacks.discard(task)
233+
e = task.exception()
234+
if e is not None and not task.cancelled():
235+
self._maybe_log(_LogLevel.ERROR, 'Exception escaped callback: %s', e)
236+
226237
self._internal_callbacks.add(task)
227-
task.add_done_callback(self._internal_callbacks.discard)
238+
task.add_done_callback(cleanup)
228239

229240

230241
class _Listener(_zc.ServiceListener):

‎tests/test_async_event_emitter.py‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,8 @@ async def test_exception_unhandled() -> None:
6868
emitter = AsyncEventEmitter(logger)
6969
mock = AsyncMock()
7070
emitter.subscribe('e', mock)
71-
mock.side_effect = KeyError('oops')
71+
e = KeyError('oops')
72+
mock.side_effect = e
7273
await emitter.emit('e')
7374
mock.assert_called_once()
74-
logger.exception.assert_called_once_with("Logic error: exception escaped from callback: 'oops'")
75+
logger.exception.assert_called_once_with("Logic error: exception escaped from callback: %s", e)

0 commit comments

Comments
 (0)