view iec1107.py @ 10:c1892bd1460a

Handle SI unit prefixes.
author Daniel O'Connor <darius@dons.net.au>
date Thu, 21 Nov 2013 12:34:13 +1030
parents 08b192a6e189
children 156313694bbb
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 = 1
        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()