Skip to content

Commit fb8b257

Browse files
committed
Some improvements to asynchronous mode
Update readme with better examples Bump version
1 parent 162d2ee commit fb8b257

File tree

3 files changed

+47
-36
lines changed

3 files changed

+47
-36
lines changed

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,12 @@ api.get_instantaneous_usage()
4747
time.sleep(5)
4848
response = api.get_data(InstantaneousUsage)
4949
```
50+
Note: In real programs using asynchronous mode, it would probably make sense to make
51+
use of the schedule function of the EMU-2. This sets the frequency that certain events
52+
are sent, and allows for data to be received without constant polling. The schedule may
53+
be set using the set_schedule(...) method of the API. After the schedule is set up, the
54+
consuming program may periodically call the get_data method to access the most recent
55+
received data.
5056

5157
### Contributing
5258
Contributions are welcome! Not all commands have been thoroughly tested yet, since I

emu_power/__init__.py

Lines changed: 40 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import threading
33
from xml.etree import ElementTree
44
import time
5+
import itertools
56
from emu_power import response_entities
67

78

@@ -10,8 +11,9 @@ class Emu:
1011
# Construct a new Emu object. Set synchronous to true to to attempt to
1112
# return results synchronously if possible. Timeout is the time period
1213
# 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):
1517

1618
# Internal communication
1719
self._channel_open = False
@@ -21,6 +23,8 @@ def __init__(self, debug=False, synchronous=False, timeout=10, poll_factor=2):
2123

2224
self.debug = debug
2325

26+
self.fresh_only = fresh_only
27+
2428
self.synchronous = synchronous
2529
self.timeout = timeout
2630
self.poll_factor = poll_factor
@@ -35,11 +39,15 @@ def __init__(self, debug=False, synchronous=False, timeout=10, poll_factor=2):
3539
# Get the most recent fresh response that has come in. This
3640
# should be used in asynchronous mode.
3741
def get_data(self, klass):
42+
3843
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:
4248
return None
49+
50+
res.fresh = False
4351
return res
4452

4553
# Open communication channel
@@ -49,7 +57,7 @@ def start_serial(self, port_name):
4957
return True
5058

5159
try:
52-
self._serial_port = self._create_serial(port_name)
60+
self._serial_port = serial.Serial(port_name, 115200, timeout=1)
5361
except serial.serialutil.SerialException:
5462
return False
5563

@@ -71,12 +79,6 @@ def stop_serial(self):
7179
self._serial_port = None
7280
return True
7381

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-
8082
# Main communication thread - handles all asynchronous messaging
8183
def _communication_thread(self):
8284
while True:
@@ -90,25 +92,28 @@ def _communication_thread(self):
9092

9193
if len(bin_lines) > 0:
9294

95+
# A response can have multiple fragments, so we wrap them in a pseudo root for parsing
9396
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)
9699
except ElementTree.ParseError:
97100
if self.debug:
98101
print("Malformed XML " + b''.join(bin_lines).decode('ASCII'))
99102
continue
100103

101-
if self.debug:
102-
ElementTree.dump(tree)
104+
for tree in root:
103105

104-
response_type = tree.tag
105-
klass = response_entities.Entity.tag_to_class(response_type)
106-
if klass is None:
107106
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)
112117

113118
# Issue a command to the device. Pass the command name as the first
114119
# argument, and any additional params as a dict. Will return immediately
@@ -171,6 +176,15 @@ def _format_yn(self, value):
171176
def _format_hex(self, num, digits=8):
172177
return "0x{:0{digits}x}".format(num, digits=digits)
173178

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+
174188
# The following are convenience methods for sending commands. Commands
175189
# can also be sent manually using the generic issue_command method.
176190

@@ -192,18 +206,12 @@ def get_device_info(self):
192206
return self.issue_command('get_device_info', return_class=response_entities.DeviceInfo)
193207

194208
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)
199210
opts = {'MeterMacId': mac, 'Event': event}
200211
return self.issue_command('get_schedule', opts, return_class=response_entities.ScheduleInfo)
201212

202213
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)
207215
opts = {
208216
'MeterMacId': mac,
209217
'Event': event,
@@ -213,10 +221,7 @@ def set_schedule(self, mac=None, event=None, frequency=10, enabled=True):
213221
return self.issue_command('set_schedule', opts)
214222

215223
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)
220225
opts = {'MeterMacId': mac, 'Event': event}
221226
return self.issue_command('set_schedule_default', opts)
222227

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
setuptools.setup(
77
name="emu-power",
8-
version="1.2",
8+
version="1.4",
99
author="Steven Bertolucci",
1010
author_email="[email protected]",
1111
description="A Python3 API to interface with Rainforest Automation's EMU-2",

0 commit comments

Comments
 (0)