Skip to content
Open
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
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,24 +28,24 @@ from emu_power import Emu
api = Emu(synchronous=True)
api.start_serial("/dev/tty.usbmodem146101")

# This will return an instance of InstantaneousUsage, or None on timeout.
response = api.get_instantaneous_usage()
# This will return an instance of InstantaneousDemand, or None on timeout.
response = api.get_instantaneous_demand()
```

#### Asynchronous
```
from emu_power import Emu
from emu_power.response_entities import InstantaneousUsage
from emu_power.response_entities import InstantaneousDemand
import time

api = Emu()
api.start_serial("/dev/tty.usbmodem146101")

# This will return immediately. The response data will become available
# when the device responds.
api.get_instantaneous_usage()
api.get_instantaneous_demand()
time.sleep(5)
response = api.get_data(InstantaneousUsage)
response = api.get_data(InstantaneousDemand)
```
Note: In real programs using asynchronous mode, it would probably make sense to make
use of the schedule function of the EMU-2. This sets the frequency that certain events
Expand Down
31 changes: 18 additions & 13 deletions emu_power/__init__.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,29 @@
import serial
import itertools
import platform
import threading
from xml.etree import ElementTree
import time
import itertools
from xml.etree import ElementTree

import serial

from emu_power import response_entities


class Emu:
if platform.system() == 'Darwin':
_DEFAULT_DEVICE = '/dev/tty.usbmodem11'
elif platform.system() == 'Linux':
_DEFAULT_DEVICE = '/dev/ttyACM0'
else:
_DEFAULT_DEVICE = None


class Emu:
# Construct a new Emu object. Set synchronous to true to to attempt to
# return results synchronously if possible. Timeout is the time period
# in seconds until a request is considered failed. Poll factor indicates
# the fraction of a second to check for a response. Set fresh_only to True
# to only return fresh responses from get_data. Only useful in asynchronous mode.
def __init__(self, debug=False, fresh_only=False, synchronous=False, timeout=10, poll_factor=2):

# Internal communication
self._channel_open = False
self._serial_port = None
Expand All @@ -39,7 +48,6 @@ def __init__(self, debug=False, fresh_only=False, synchronous=False, timeout=10,
# Get the most recent fresh response that has come in. This
# should be used in asynchronous mode.
def get_data(self, klass):

res = self._data.get(klass.tag_name())
if not self.fresh_only:
return res
Expand All @@ -51,7 +59,9 @@ def get_data(self, klass):
return res

# Open communication channel
def start_serial(self, port_name):
def start_serial(self, port_name=_DEFAULT_DEVICE):
assert port_name, (
"Must specify a port name; cannot determine default for your OS")

if self._channel_open:
return True
Expand All @@ -68,7 +78,6 @@ def start_serial(self, port_name):

# Close the communication channel
def stop_serial(self):

if not self._channel_open:
return True

Expand All @@ -82,7 +91,6 @@ def stop_serial(self):
# Main communication thread - handles all asynchronous messaging
def _communication_thread(self):
while True:

if self._stop_thread:
self._stop_thread = False
return
Expand Down Expand Up @@ -120,7 +128,6 @@ def _communication_thread(self):
# unless the synchronous attribute on the library is true, in which case
# it will return data when available, or None if the timeout has elapsed.
def issue_command(self, command, params=None, return_class=None):

if not self._channel_open:
raise ValueError("Serial port is not open")

Expand Down Expand Up @@ -198,7 +205,7 @@ def restart(self):
return self.issue_command('restart')

# Dangerous! Will decommission device!
def factory_reset(self):
def factory_reset_warning_dangerous(self):
return self.issue_command('factory_reset')

def get_connection_status(self):
Expand Down Expand Up @@ -266,7 +273,6 @@ def get_message(self, mac=None, refresh=True):
return self.issue_command('get_message', opts, return_class=response_entities.MessageCluster)

def confirm_message(self, mac=None, message_id=None):

if message_id is None:
raise ValueError('Message id is required')

Expand All @@ -283,7 +289,6 @@ def get_current_price(self, mac=None, refresh=True):

# Price is in cents, w/ decimals (e.g. "24.373")
def set_current_price(self, mac=None, price="0.0"):

parts = price.split(".", 1)
if len(parts) == 1:
trailing = 2
Expand Down
30 changes: 21 additions & 9 deletions emu_power/response_entities.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import ctypes
import datetime
from xml.etree import ElementTree


Expand Down Expand Up @@ -30,7 +32,17 @@ def find_text(self, tag):
return node.text

def find_hex(self, text):
return int(self.find_text(text) or "0x00", 16)
"""Parse hex text into a signed int32."""
return ctypes.c_int(int(self.find_text(text) or "0x00", 16)).value

def find_time(self, text):
"""Parse the hex value as seconds since jan 1, 2000."""
time_since_2000 = self.find_hex(text)
if not time_since_2000:
return None

delta = 946684800 # seconds between jan 1, 1970 and jan 1, 2000.
return datetime.datetime.fromtimestamp(time_since_2000 + delta)

# The root element associated with this class
@classmethod
Expand Down Expand Up @@ -134,7 +146,7 @@ def _parse(self):
class MessageCluster(Entity):
def _parse(self):
self.meter_mac = self.find_text("MeterMacId")
self.timestamp = self.find_text("TimeStamp")
self.timestamp = self.find_time("TimeStamp")
self.id = self.find_text("Id")
self.text = self.find_text("Text")
self.confirmation_required = self.find_text("ConfirmationRequired")
Expand All @@ -149,7 +161,7 @@ def _parse(self):
class PriceCluster(Entity):
def _parse(self):
self.meter_mac = self.find_text("MeterMacId")
self.timestamp = self.find_text("TimeStamp")
self.timestamp = self.find_time("TimeStamp")
self.price = self.find_text("Price")
self.currency = self.find_text("Currency") # ISO-4217
self.trailing_digits = self.find_text("TrailingDigits")
Expand All @@ -165,7 +177,7 @@ def _parse(self):
class InstantaneousDemand(Entity):
def _parse(self):
self.meter_mac = self.find_text("MeterMacId")
self.timestamp = self.find_text("TimeStamp")
self.timestamp = self.find_time("TimeStamp")
self.demand = self.find_hex("Demand")
self.multiplier = self.find_hex("Multiplier")
self.divisor = self.find_hex("Divisor")
Expand All @@ -183,7 +195,7 @@ def _parse(self):
class CurrentSummationDelivered(Entity):
def _parse(self):
self.meter_mac = self.find_text("MeterMacId")
self.timestamp = self.find_text("TimeStamp")
self.timestamp = self.find_time("TimeStamp")
self.summation_delivered = self.find_hex("SummationDelivered")
self.summation_received = self.find_hex("SummationReceived")
self.multiplier = self.find_hex("Multiplier")
Expand All @@ -202,14 +214,14 @@ def _parse(self):
class CurrentPeriodUsage(Entity):
def _parse(self):
self.meter_mac = self.find_text("MeterMacId")
self.timestamp = self.find_text("TimeStamp")
self.timestamp = self.find_time("TimeStamp")
self.current_usage = self.find_hex("CurrentUsage")
self.multiplier = self.find_hex("Multiplier")
self.divisor = self.find_hex("Divisor")
self.digits_right = self.find_hex("DigitsRight")
self.digits_left = self.find_hex("DigitsLeft")
self.suppress_leading_zero = self.find_text("SuppressLeadingZero")
self.start_date = self.find_text("StartDate")
self.start_date = self.find_time("StartDate")

# Compute actual reading (protecting from divide-by-zero)
if self.divisor != 0:
Expand All @@ -227,8 +239,8 @@ def _parse(self):
self.digits_right = self.find_hex("DigitsRight")
self.digits_left = self.find_hex("DigitsLeft")
self.suppress_leading_zero = self.find_text("SuppressLeadingZero")
self.start_date = self.find_text("StartDate")
self.end_date = self.find_text("EndDate")
self.start_date = self.find_time("StartDate")
self.end_date = self.find_time("EndDate")


# TODO: IntervalData may appear more than once
Expand Down