Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/powersensor_local/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@
'Message',
'CommonEventsDocs',
]
__version__ = "2.4.1rc2"
__version__ = "2.4.1rc3"
from .devices import (
AsyncEventCallback,
PowersensorDevices,
Expand Down
17 changes: 16 additions & 1 deletion src/powersensor_local/devices.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ def unsubscribe(self, mac: str) -> None:
async def stop(self) -> None:
"""Stop event streaming and disconnect from all devices.

To restart, call start() (legacy) or add_plug() (zeroconf) again.
To restart, call start() again.
"""
for plug in list(self._plug_apis.values()):
await plug.disconnect()
Expand Down Expand Up @@ -219,6 +219,21 @@ async def _add_device(self, mac: str, typ: str) -> None:
async def _remove_device(self, mac: str) -> None:
if mac in self._devices:
self._devices.pop(mac)
# A sensor is announced only by now_relaying_for, which a PlugApi
# synthesizes for just the first message it relays for that sensor,
# and remembers having done so for as long as the PlugApi exists.
# A sensor that expires while its plug's PlugApi lives on, because
# its battery went flat, it was out of range of every plug, or its
# plug was off the network without its mDNS record being removed,
# would therefore never be announced again. Its data would resume
# through the same plug, but with no now_relaying_for to re-add it,
# _emit_if_subscribed would drop every event, and the caller would
# never hear from it again until it restarted the library. So make
# every plug forget it: whichever plug next relays for the sensor
# synthesizes now_relaying_for again, and it is announced as if
# seen for the first time, role included.
for api in self._plug_apis.values():
api.forget_relayed(mac)
if self._event_cb is not None:
await self._event_cb({
'event': 'device_lost',
Expand Down
8 changes: 8 additions & 0 deletions src/powersensor_local/plug_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,14 @@ async def disconnect(self) -> None:
"""Disconnects from the plug and stops further connection attempts."""
await self._listener.disconnect()

def forget_relayed(self, mac: str) -> None:
"""Forget having relayed messages for the given device.

The next message relayed for it synthesizes 'now_relaying_for' again,
as if this plug had never relayed for it before.
"""
self._seen.discard(mac)

async def _on_message(self, _: str, message: Message) -> None:
"""Translates the raw message and emits the resulting messages, if any.

Expand Down
39 changes: 38 additions & 1 deletion tests/test_devices.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,26 @@

from powersensor_local import devices as devices_module
from powersensor_local.plug_api import PlugApi
from powersensor_local.xlatemsg import Event
from powersensor_local.xlatemsg import Event, Message
from powersensor_local.zeroconf_devices import PowersensorZeroconfDevices

PLUG = 'aabbccddeeff'
SENSOR = '112233445566'

SENSOR_MESSAGE: Message = {
'starttime': 1772164705, 'raw_rssi': -91, 'device': 'sensor', 'rssi': -89.0397534995969,
'type': 'instant_power', 'summation_start': 1769972064, 'batteryMicrovolt': 3803104,
'mac': SENSOR, 'duration': 30, 'role': 'house-net', 'power': 992, 'unit': 'w',
'summation': -281230401,
}
# The events a plug relaying SENSOR_MESSAGE emits for it.
SENSOR_EVENTS = [
('average_power', SENSOR),
('summation_energy', SENSOR),
('battery_level', SENSOR),
('radio_signal_quality', SENSOR),
]


async def with_plug(events: list[Event]) -> PowersensorZeroconfDevices:
"""Return devices with one plug discovered, recording every event.
Expand Down Expand Up @@ -53,6 +67,29 @@ async def test_expired_plug_is_announced_again_when_its_data_resumes() -> None:
]


@pytest.mark.asyncio
async def test_expired_sensor_is_announced_again_when_its_data_resumes() -> None:
events: list[Event] = []
with patch.object(PlugApi, 'connect'):
devices = await with_plug(events)
plug_api = devices._plug_apis[PLUG]
await plug_api._on_message('message', SENSOR_MESSAGE)
await expire_all(devices)
# The sensor's data resumes through the same PlugApi, which synthesized
# now_relaying_for for it before it expired and must do so again.
await plug_api._on_message('message', SENSOR_MESSAGE)
await devices.stop()
assert [(e['event'], e['mac']) for e in events] == [
('device_found', PLUG),
('device_found', SENSOR),
*SENSOR_EVENTS,
('device_lost', PLUG),
('device_lost', SENSOR),
('device_found', SENSOR),
*SENSOR_EVENTS,
]


@pytest.mark.asyncio
async def test_data_for_an_unknown_device_is_not_announced() -> None:
events: list[Event] = []
Expand Down
Loading