Skip to content

Commit cd4dfa7

Browse files
committed
Merge remote-tracking branch 'origin/main' into formatting-changes
# Conflicts: # src/powersensor_local/__init__.py # src/powersensor_local/plug_api.py # src/powersensor_local/plug_listener.py
2 parents d0d4be2 + 338ed98 commit cd4dfa7

4 files changed

Lines changed: 57 additions & 30 deletions

File tree

src/powersensor_local/__init__.py

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,37 @@
11
"""Direct (non-cloud) interface to Powersensor devices
22
3-
This package contains two abstraction layers:
3+
This package contains various abstractions for interacting with Powersensor
4+
devices on the local network.
45
5-
• PowersensorDevices is the main API layer
6-
• PowersensorListener provides a lower-level abstraction
6+
The recommended approach is to use mDNS to discover plugs via their service
7+
"_powersensor._tcp.local", and then instantiate a PlugApi to obtain the event
8+
stream from each plug.
79
8-
These are both available within this namespace, or specifically as
9-
devices.PowersensorDevices and listener.PowersensorListener
10+
A legacy abstraction is also provided via PowersensorDevices, which uses
11+
an older way of discovering plugs.
1012
11-
The 'events' and 'rawfirehose' modules are helper utilities provided as
12-
debug aids, which get installed under the names ps-events and ps-rawfirehose
13-
respectively.
13+
Lower-level interfaces are available in the PlugListener and
14+
PowersensorListener classed, though they are not recommended for general use.
15+
16+
Additionally a convience abstraction for translating some of the events into
17+
a household view is available in VirtualHousehold.
18+
19+
Quick overview:
20+
• PlugApi is the recommended API layer
21+
• PlugListener is the lower-level abstraction used by PlugApi
22+
• PowersensorDevices is the legacy main API layer
23+
• PowersensorListener provides a (legacy) lower-level abstraction
24+
• VirtualHousehold can be used to translate events into a household view
25+
26+
The 'plugevents' and 'rawplug' modules are helper utilities provided as
27+
debug aids, which get installed under the names ps-plugevents and ps-rawplug
28+
respectively. There are also the legacy 'events' and 'rawfirehose' debug aids
29+
which get installed under the names ps-events and ps-rawfirehose respectively.
1430
"""
15-
__all__ = [ 'devices', 'listener', 'plug_api', 'plug_listener' ]
31+
__all__ = [ 'devices', 'listener', 'plug_api', 'plug_listener', 'virtual_household', 'PlugApi' ]
1632
__version__ = "2.0.0"
1733
from .devices import PowersensorDevices
1834
from .listener import PowersensorListener
1935
from .plug_api import PlugApi
2036
from .plug_listener import PlugListener
37+
from .virtual_household import VirtualHousehold

src/powersensor_local/plug_api.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import sys
22
from pathlib import Path
3-
43
project_root = str(Path(__file__).parents[1])
54
if project_root not in sys.path:
65
sys.path.append(project_root)
@@ -53,20 +52,21 @@ async def disconnect(self):
5352
async def _on_message(self, _, message):
5453
"""Translates the raw message and emits the resulting messages, if any.
5554
56-
Also synthesizes 'now_relaying_for' messages as needed.
55+
Also synthesises 'now_relaying_for' messages as needed.
5756
"""
57+
evs = None
5858
try:
5959
evs = translate_raw_message(message, self._mac)
6060
except KeyError:
6161
# Ignore malformed messages
6262
return
6363

64-
message_mac = message.get('mac')
65-
if message_mac != self._mac and message_mac not in self._seen:
66-
self._seen.add(message_mac)
64+
msgmac = message.get('mac')
65+
if msgmac != self._mac and msgmac not in self._seen:
66+
self._seen.add(msgmac)
6767
# We want to emit this prior to events with data
6868
ev = {
69-
'mac': message_mac,
69+
'mac': msgmac,
7070
'device_type': message.get('device'),
7171
'role': message.get('role'),
7272
}

src/powersensor_local/plug_listener.py

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,16 @@
11
import asyncio
22
import json
3+
import socket
4+
import time
35

46
import sys
57
from pathlib import Path
6-
78
project_root = str(Path(__file__).parents[1])
89
if project_root not in sys.path:
910
sys.path.append(project_root)
1011

1112
from powersensor_local.async_event_emitter import AsyncEventEmitter
1213

13-
14-
async def _send_subscribe(writer):
15-
writer.write(b'subscribe(60)\n')
16-
await writer.drain()
17-
18-
1914
class PlugListener(AsyncEventEmitter):
2015
"""An interface class for accessing the event stream from a single plug.
2116
The following events may be emitted:
@@ -24,8 +19,11 @@ class PlugListener(AsyncEventEmitter):
2419
- ("disconnected") When a connection is dropped, be it intentional or not.
2520
- ("message",{...}) For each event message received from the plug. The
2621
plug's JSON message is decoded into a dict which is passed as the second
27-
argument to the registered event handler(s). The event handlers must be
28-
async.
22+
argument to the registered event handler(s).
23+
- ("malformed",line) If JSON decoding of a message fails. The raw line
24+
is included (as a byte string).
25+
26+
The event handlers must be async.
2927
"""
3028

3129
def __init__(self, ip, port=49476):
@@ -72,14 +70,16 @@ async def _close_connection(self):
7270
await self.emit('disconnected')
7371

7472
async def _do_connection(self, backoff = 0):
73+
if self._disconnecting:
74+
return
7575
if backoff < 9:
7676
backoff += 1
7777
try:
7878
await self.emit('connecting')
7979
reader, writer = await asyncio.open_connection(self._ip, self._port)
8080
self._connection = (reader, writer)
8181

82-
await _send_subscribe(writer)
82+
await self._send_subscribe(writer)
8383
backoff = 1
8484

8585
await self.emit('connected')
@@ -91,7 +91,7 @@ async def _do_connection(self, backoff = 0):
9191
# Handle disconnection and retry with exponential backoff
9292
await self._close_connection()
9393
if self._disconnecting:
94-
return None
94+
return
9595
await asyncio.sleep(min(5 * 60, 2**backoff * 1))
9696
return await self._do_connection(backoff)
9797

@@ -105,10 +105,14 @@ async def _process_line(self, reader, writer):
105105
typ = message['type']
106106
if typ == 'subscription':
107107
if message['subtype'] == 'warning':
108-
await _send_subscribe(writer)
108+
await self._send_subscribe(writer)
109109
elif typ == 'discovery':
110110
pass
111111
else:
112112
await self.emit('message', message)
113-
except json.decoder.JSONDecodeError as ex:
114-
print(f"JSON error {ex} from {data}")
113+
except (json.decoder.JSONDecodeError) as ex:
114+
await self.emit('malformed', data)
115+
116+
async def _send_subscribe(self, writer):
117+
writer.write(b'subscribe(60)\n')
118+
await writer.drain()

src/powersensor_local/virtual_household.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
"""Abstraction for producing a household view."""
22

3-
from async_event_emitter import AsyncEventEmitter
3+
import sys
4+
from pathlib import Path
5+
project_root = str(Path(__file__).parents[1])
6+
if project_root not in sys.path:
7+
sys.path.append(project_root)
8+
9+
from powersensor_local.async_event_emitter import AsyncEventEmitter
410
from dataclasses import dataclass
511
from typing import Optional
612

0 commit comments

Comments
 (0)