Mercurial > ~darius > hgwebdir.cgi > iec1107
view iec1107.py @ 13:156313694bbb default tip
Use the correct exponent for Watts (ie 0 not 1).
author | Daniel O'Connor <darius@dons.net.au> |
---|---|
date | Sat, 18 Jan 2014 05:23:02 +0000 |
parents | c1892bd1460a |
children |
line wrap: on
line source
#!/usr/bin/env python # # Copyright (c) 2013 # Daniel O'Connor <darius@dons.net.au>. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions # are met: # 1. Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED BY AUTHOR AND CONTRIBUTORS ``AS IS'' AND # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL AUTHOR OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY # OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF # SUCH DAMAGE. # # Most of this is derived from the extremely helpful post at # http://www.domoticaforum.eu/viewtopic.php?f=71&t=7489 # import datetime import exceptions import optparse import re import serial import sys import time baudtable = {'0' : 300, '1' : 600, '2' : 1200, '3' : 2400, '4' : 4800, '5' : 9600, '6' : 19200} parsere = re.compile('([0-9A-Z](\.[0-9A-Z]){1,})\((.*)\)') class Error(exceptions.BaseException): pass class IEC1107Reading(object): def __init__(self, port, force300bps = True): # Open port s = serial.Serial(port, baudrate = 300, bytesize = 7, parity = 'E', stopbits = 1) s.timeout = 2.5 # Send ident message s.write('/?!\r\n') rtn = s.readline() if len(rtn) == 0: raise Error('No reply to probe') if len(rtn) < 6 or rtn[0] != '/' or rtn[-1] != '\n' or rtn[-2] != '\r': raise Error('Invalid line "%s"' % (rtn)) rtn = rtn.strip() self.mfg = rtn[1:4] if self.mfg[2].isupper(): self.restime = 0.2 else: self.restime = 0.02 if force300bps: self.baudid = '0' else: self.baudid = rtn[4] if self.baudid not in baudtable: raise Error('Invalid baud rate %c from "%s"' % (selfbaudid, rtn)) else: self.baud = baudtable[self.baudid] if rtn[5] == '/': self.mode = rtn[6] self.mfg = rtn[7:] else: self.mode = None self.mfg = rtn[5:] # Send ACK/option message # Byte Meaning # 0 ACK (0x06) # 1 Protocol character ('0' = normal, '1' = secondary, '2' = HDLC protocol) # 2 Baud rate ID ('0', '1', etc) # 3 Mode control('0' = read data, '1' = device prog) s.write('\x060%c0\r\n' % (self.baudid)) time.sleep(self.restime) s.setBaudrate(self.baud) lines = [] cksum = 0 # Read STX head = s.read(1) if len(head) == 0: raise Error('No reply to query') if head != '\x02': raise Error('Invalid reply header 0x%02x' % (ord(head))) # Read result lines while True: line = s.readline() if len(line) == 0: raise Error('Timeout during message') cksum ^= reduce(lambda x, y: x ^ y, map(ord, line)) if line.strip() == '!': break lines.append(line) # Read trailer fin = s.read(2) if len(fin) != 2: raise Error('Timeout reading trailer') if fin[0] != '\x03': raise Error('Trailer malformed, expected 0x03, got 0x%02x' % (ord(fin[0]))) # Validate checksum cksum ^= ord(fin[0]) if cksum != ord(fin[1]): raise Error('Checksum mismatch, expected 0x%02x, got 0x%02x' % (cksum, ord(fin[1]))) self.rawreading = lines del s self.parse() self.readdate = datetime.datetime.now() def parse(self): for l in self.rawreading: m = parsere.match(l) if m == None: raise Error('Unable to parse result \"%s\"' % (l)) (code, xxx, value) = m.groups() if code == 'C.1': self.meterid, date = value.split('(') # XXX: The meter doesn't handle DST, assume the PC is correct self.meterdate = datetime.datetime.strptime(date, '%H:%M %d-%m-%y') elif code == '1.8.0': self.importWh = self.parsevalue(value) elif code[0:4] == '1.8.': # Differing tarrifs which I don't care about pass elif code == '2.8.0': self.exportWh = self.parsevalue(value) else: print 'Unknown code', code @staticmethod def parsevalue(value): count, units = value.split('*') if units[0] == 'm': exp = -3 elif units[0] == 'u': exp = -6 elif units[0] == 'n': exp = -9 elif units[0] == 'k': exp = 3 elif units[0] == 'M': exp = 6 elif units[0] == 'G': exp = 9 elif units[1] == 'T': exp = 12 else: exp = 0 return float(count) * 10 ** exp def __str__(self): return 'Time: %s, Meter: %s, Import: %d Wh, Export: %d Wh' % (self.readdate.strftime('%Y/%m/%d %H:%M'), self.meterid, self.importWh, self.exportWh) def main(): parser = optparse.OptionParser(usage = 'usage: %prog [options] port', epilog = 'Read out from an IEC1107 meter') parser.add_option('-r', '--rrd', dest = 'rrd', action = 'store_true', default = False, help = 'Output in a format suitable for rrdtool') (opt, args) = parser.parse_args() if len(args) != 1: parser.error('Need to specify port') res = IEC1107Reading(args[0]) if opt.rrd: print '%s:%d:%d' % (res.readdate.strftime('%s'), res.importWh, res.exportWh) else: print res if __name__ == '__main__': main()