11import asyncio
2- from datetime import datetime , timezone
3-
2+ import json
43import sys
5- from pathlib import Path
64
5+ from datetime import datetime , timezone
6+ from pathlib import Path
77project_root = str (Path (__file__ ).parents [1 ])
88if project_root not in sys .path :
99 sys .path .append (project_root )
1010
11-
12- from powersensor_local .listener import PowersensorListener
11+ from powersensor_local . legacy_discovery import LegacyDiscovery
12+ from powersensor_local .plug_api import PlugApi
1313from powersensor_local .xlatemsg import translate_raw_message
14+
1415EXPIRY_CHECK_INTERVAL_S = 30
1516EXPIRY_TIMEOUT_S = 5 * 60
1617
17-
18- def _make_events (obj , relayer ):
19- evs = []
20- kvs = translate_raw_message (obj , relayer )
21- for key , ev in kvs .items ():
22- ev ['event' ] = key
23- evs .append (ev )
24-
25- return evs
26-
27-
2818class PowersensorDevices :
2919 """Abstraction interface for the unified event stream from all Powersensor
3020 devices on the local network.
3121 """
3222
33- def __init__ (self , broadcast_address = '<broadcast>' ):
23+ def __init__ (self , bcast_addr = '<broadcast>' ):
3424 """Creates a fresh instance, without scanning for devices."""
35-
3625 self ._event_cb = None
37- self ._ps = PowersensorListener ( broadcast_address )
26+ self ._discovery = LegacyDiscovery ( bcast_addr )
3827 self ._devices = dict ()
3928 self ._timer = None
29+ self ._plug_apis = dict ()
4030
41- async def start (self , async_event_callback ):
31+ async def start (self , async_event_cb ):
4232 """Registers the async event callback function and starts the scan
4333 of the local network to discover present devices. The callback is
4434 of the form
4535
46- Parameters:
47- -----------
48- async_event_callback : Callable
49-
50- A callable asynchronous method for handling json messages. Example::
36+ async def yourcallback(event: dict)
5137
52- async def your_callback(event: dict):
53- pass
38+ Known events:
5439
40+ scan_complete:
41+ Indicates the discovery of Powersensor devices has completed.
42+ Emitted in response to start() and rescan() calls.
43+ The number of found gateways (plugs) is reported.
5544
56- Known Events:
57- -------------
58- * scan_complete
45+ { event: "scan_complete", gateway_count: N }
5946
60- Indicates the discovery of Powersensor devices has completed.
61- Emitted in response to start() and rescan() calls .
62- The number of found gateways (plugs) is reported.::
47+ device_found:
48+ A new device found on the network .
49+ The order found devices are announced is not fixed.
6350
64- { event: "scan_complete", gateway_count: N }
65-
66- * device_found
67-
68- A new device found on the network.
69- The order found devices are announced is not fixed.::
70-
71- { event: "device_found",
72- device_type: "plug" or "sensor",
73- mac: "...",
74- }
51+ { event: "device_found",
52+ device_type: "plug" or "sensor",
53+ mac: "...",
54+ }
7555
76- An optional field named "via" is present for sensor devices, and
77- shows the MAC address of the gateway the sensor is communicating
78- via.
56+ device_lost:
57+ A device appears to no longer be present on the network.
7958
80- * device_lost
81- A device appears to no longer be present on the network.::
59+ { event: "device_lost", mac: "..." }
8260
83- { event: "device_lost", mac: "..." }
8461
8562 Additionally, all events described in xlatemsg.translate_raw_message
8663 may be issued. The event name is inserted into the field 'event'.
@@ -91,22 +68,22 @@ async def your_callback(event: dict):
9168 on the network, but are instead detected when they relay data through
9269 a plug via long-range radio.
9370 """
94-
95- self ._event_cb = async_event_callback
96- await self ._on_scanned (await self ._ps .scan ())
71+ self ._event_cb = async_event_cb
72+ await self ._on_scanned (await self ._discovery .scan ())
9773 self ._timer = self ._Timer (EXPIRY_CHECK_INTERVAL_S , self ._on_timer )
98- return len (self ._ips )
74+ return len (self ._plug_apis )
9975
10076 async def rescan (self ):
10177 """Performs a fresh scan of the network to discover added devices,
10278 or devices which have changed their IP address for some reason."""
103- await self ._on_scanned (await self ._ps .scan ())
79+ await self ._on_scanned (await self ._discovery .scan ())
10480
10581 async def stop (self ):
10682 """Stops the event streaming and disconnects from the devices.
10783 To restart the event streaming, call start() again."""
108- await self ._ps .unsubscribe ()
109- await self ._ps .stop ()
84+ for plug in self ._plug_apis .values ():
85+ await plug .disconnect ()
86+ self ._plug_apis = dict ()
11087 self ._event_cb = None
11188 if self ._timer :
11289 self ._timer .terminate ()
@@ -124,69 +101,78 @@ def unsubscribe(self, mac):
124101 if device :
125102 device .subscribed = False
126103
127- async def _on_scanned (self , ips ):
128- self ._ips = ips
129- if self ._event_cb :
130- ev = {
131- 'event' : 'scan_complete' ,
132- 'gateway_count' : len (ips ),
133- }
134- await self ._event_cb (ev )
135-
136- await self ._ps .subscribe (self ._on_msg )
104+ async def _emit_if_subscribed (self , ev , obj ):
105+ if self ._event_cb is None :
106+ return
107+ device = self ._devices .get (obj .get ('mac' ))
108+ if device is not None and device .subscribed :
109+ obj ['event' ] = ev
110+ await self ._event_cb (obj )
137111
138- async def _on_msg (self , obj ):
139- mac = obj .get ('mac' )
140- if mac and not self ._devices .get (mac ):
141- typ = obj .get ('device' )
142- via = obj .get ('via' )
143- await self ._add_device (mac , typ , via )
144-
145- device = self ._devices [mac ]
146- device .mark_active ()
147-
148- if self ._event_cb and device .subscribed :
149- relayer = obj .get ('via' ) or mac
150- evs = _make_events (obj , relayer )
151- if len (evs ) > 0 :
152- for ev in evs :
153- await self ._event_cb (ev )
112+ async def _reemit (self , ev , obj ):
113+ mac = obj ['mac' ]
114+ device = self ._devices .get (mac )
115+ if device is not None :
116+ device .mark_active ()
117+
118+ if ev == 'now_relaying_for' :
119+ await self ._add_device (mac , 'sensor' )
120+ else :
121+ await self ._emit_if_subscribed (ev , obj )
122+
123+ async def _on_scanned (self , found ):
124+ for device in found :
125+ mac = device ['id' ]
126+ ip = device ['ip' ]
127+ if not mac in self ._devices :
128+ await self ._add_device (mac , 'plug' )
129+ api = PlugApi (mac , ip )
130+ self ._plug_apis [mac ] = api
131+ api .subscribe ('average_flow' , self ._reemit )
132+ api .subscribe ('average_power' , self ._reemit )
133+ api .subscribe ('average_power_components' , self ._reemit )
134+ api .subscribe ('battery_level' , self ._reemit )
135+ api .subscribe ('exception' , self ._reemit )
136+ api .subscribe ('now_relaying_for' , self ._reemit )
137+ api .subscribe ('radio_signal_quality' , self ._reemit )
138+ api .subscribe ('summation_energy' , self ._reemit )
139+ api .subscribe ('summation_volume' , self ._reemit )
140+ api .connect ()
141+
142+ await self ._event_cb ({
143+ 'event' : 'scan_complete' ,
144+ 'gateway_count' : len (found ),
145+ })
154146
155147 async def _on_timer (self ):
156148 devices = list (self ._devices .values ())
157149 for device in devices :
158150 if device .has_expired ():
159151 await self ._remove_device (device .mac )
160152
161- async def _add_device (self , mac , typ , via ):
162- self ._devices [mac ] = self ._Device (mac , typ , via )
163- if self ._event_cb :
164- ev = {
165- 'event' : 'device_found' ,
166- 'device_type' : typ ,
167- 'mac' : mac ,
168- }
169- if via :
170- ev ['via' ] = via
171- await self ._event_cb (ev )
153+ async def _add_device (self , mac , typ ):
154+ if mac in self ._devices :
155+ return
156+ self ._devices [mac ] = self ._Device (mac )
157+ await self ._event_cb ({
158+ 'event' : 'device_found' ,
159+ 'mac' : mac ,
160+ 'device_type:' : typ ,
161+ })
172162
173163 async def _remove_device (self , mac ):
174- if self ._devices . get ( mac ) :
164+ if mac in self ._devices :
175165 self ._devices .pop (mac )
176- if self ._event_cb :
177- ev = {
178- 'event' : 'device_lost' ,
179- 'mac' : mac
180- }
181- await self ._event_cb (ev )
166+ await self ._event_cb ({
167+ 'event' : 'device_lost' ,
168+ 'mac' : mac ,
169+ })
182170
183171 ### Supporting classes ###
184172
185173 class _Device :
186- def __init__ (self , mac , typ , via ):
174+ def __init__ (self , mac ):
187175 self .mac = mac
188- self .type = typ
189- self .via = via
190176 self .subscribed = False
191177 self ._last_active = datetime .now (timezone .utc )
192178
0 commit comments