2
2
import threading
3
3
from xml .etree import ElementTree
4
4
import time
5
+ import itertools
5
6
from emu_power import response_entities
6
7
7
8
@@ -10,8 +11,9 @@ class Emu:
10
11
# Construct a new Emu object. Set synchronous to true to to attempt to
11
12
# return results synchronously if possible. Timeout is the time period
12
13
# in seconds until a request is considered failed. Poll factor indicates
13
- # the fraction of a second to check for a response.
14
- def __init__ (self , debug = False , synchronous = False , timeout = 10 , poll_factor = 2 ):
14
+ # the fraction of a second to check for a response. Set fresh_only to True
15
+ # to only return fresh responses from get_data. Only useful in asynchronous mode.
16
+ def __init__ (self , debug = False , fresh_only = False , synchronous = False , timeout = 10 , poll_factor = 2 ):
15
17
16
18
# Internal communication
17
19
self ._channel_open = False
@@ -21,6 +23,8 @@ def __init__(self, debug=False, synchronous=False, timeout=10, poll_factor=2):
21
23
22
24
self .debug = debug
23
25
26
+ self .fresh_only = fresh_only
27
+
24
28
self .synchronous = synchronous
25
29
self .timeout = timeout
26
30
self .poll_factor = poll_factor
@@ -35,11 +39,15 @@ def __init__(self, debug=False, synchronous=False, timeout=10, poll_factor=2):
35
39
# Get the most recent fresh response that has come in. This
36
40
# should be used in asynchronous mode.
37
41
def get_data (self , klass ):
42
+
38
43
res = self ._data .get (klass .tag_name ())
39
- if res is not None :
40
- res .fresh = False
41
- if not res .fresh :
44
+ if not self .fresh_only :
45
+ return res
46
+
47
+ if res is None or not res .fresh :
42
48
return None
49
+
50
+ res .fresh = False
43
51
return res
44
52
45
53
# Open communication channel
@@ -49,7 +57,7 @@ def start_serial(self, port_name):
49
57
return True
50
58
51
59
try :
52
- self ._serial_port = self . _create_serial (port_name )
60
+ self ._serial_port = serial . Serial (port_name , 115200 , timeout = 1 )
53
61
except serial .serialutil .SerialException :
54
62
return False
55
63
@@ -71,12 +79,6 @@ def stop_serial(self):
71
79
self ._serial_port = None
72
80
return True
73
81
74
- # Internal helper for opening serial channel to device
75
- def _create_serial (self , port ):
76
- baud_rate = 115200
77
- timeout = 1
78
- return serial .Serial (port , baud_rate , timeout = timeout )
79
-
80
82
# Main communication thread - handles all asynchronous messaging
81
83
def _communication_thread (self ):
82
84
while True :
@@ -90,25 +92,28 @@ def _communication_thread(self):
90
92
91
93
if len (bin_lines ) > 0 :
92
94
95
+ # A response can have multiple fragments, so we wrap them in a pseudo root for parsing
93
96
try :
94
- # TODO: Handle multiple fragments correctly (get_schedule )
95
- tree = ElementTree .fromstringlist (bin_lines )
97
+ wrapped = itertools . chain ( '<Root>' , bin_lines , '</Root>' )
98
+ root = ElementTree .fromstringlist (wrapped )
96
99
except ElementTree .ParseError :
97
100
if self .debug :
98
101
print ("Malformed XML " + b'' .join (bin_lines ).decode ('ASCII' ))
99
102
continue
100
103
101
- if self .debug :
102
- ElementTree .dump (tree )
104
+ for tree in root :
103
105
104
- response_type = tree .tag
105
- klass = response_entities .Entity .tag_to_class (response_type )
106
- if klass is None :
107
106
if self .debug :
108
- print ("Unsupported tag " + response_type )
109
- continue
110
- else :
111
- self ._data [response_type ] = klass (tree )
107
+ ElementTree .dump (tree )
108
+
109
+ response_type = tree .tag
110
+ klass = response_entities .Entity .tag_to_class (response_type )
111
+ if klass is None :
112
+ if self .debug :
113
+ print ("Unsupported tag " + response_type )
114
+ continue
115
+ else :
116
+ self ._data [response_type ] = klass (tree )
112
117
113
118
# Issue a command to the device. Pass the command name as the first
114
119
# argument, and any additional params as a dict. Will return immediately
@@ -171,6 +176,15 @@ def _format_yn(self, value):
171
176
def _format_hex (self , num , digits = 8 ):
172
177
return "0x{:0{digits}x}" .format (num , digits = digits )
173
178
179
+ # Check if an event is a valid value
180
+ def _check_valid_event (self , event , allow_none = True ):
181
+ enum = ['time' , 'summation' , 'billing_period' , 'block_period' ,
182
+ 'message' , 'price' , 'scheduled_prices' , 'demand' ]
183
+ if allow_none :
184
+ enum .append (None )
185
+ if event not in enum :
186
+ raise ValueError ('Invalid event specified' )
187
+
174
188
# The following are convenience methods for sending commands. Commands
175
189
# can also be sent manually using the generic issue_command method.
176
190
@@ -192,18 +206,12 @@ def get_device_info(self):
192
206
return self .issue_command ('get_device_info' , return_class = response_entities .DeviceInfo )
193
207
194
208
def get_schedule (self , mac = None , event = None ):
195
-
196
- if event not in ['time' , 'price' , 'demand' , 'summation' , 'message' ]:
197
- raise ValueError ('Valid events are time, price, demand, summation, or message' )
198
-
209
+ self ._check_valid_event (event )
199
210
opts = {'MeterMacId' : mac , 'Event' : event }
200
211
return self .issue_command ('get_schedule' , opts , return_class = response_entities .ScheduleInfo )
201
212
202
213
def set_schedule (self , mac = None , event = None , frequency = 10 , enabled = True ):
203
-
204
- if event not in ['time' , 'price' , 'demand' , 'summation' , 'message' ]:
205
- raise ValueError ('Valid events are time, price, demand, summation, or message' )
206
-
214
+ self ._check_valid_event (event , allow_none = False )
207
215
opts = {
208
216
'MeterMacId' : mac ,
209
217
'Event' : event ,
@@ -213,10 +221,7 @@ def set_schedule(self, mac=None, event=None, frequency=10, enabled=True):
213
221
return self .issue_command ('set_schedule' , opts )
214
222
215
223
def set_schedule_default (self , mac = None , event = None ):
216
-
217
- if event not in ['time' , 'price' , 'demand' , 'summation' , 'message' ]:
218
- raise ValueError ('Valid events are time, price, demand, summation, or message' )
219
-
224
+ self ._check_valid_event (event )
220
225
opts = {'MeterMacId' : mac , 'Event' : event }
221
226
return self .issue_command ('set_schedule_default' , opts )
222
227
0 commit comments