view iec1107.py @ 4:10a16898903d

Add license.
author Daniel O'Connor <darius@dons.net.au>
date Wed, 20 Nov 2013 14:48:44 +1030
parents 535076e31660
children b686ad203c1e
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 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 I have is an hour slow
                self.meterdate = datetime.datetime.strptime(date, '%H:%M %d-%m-%y')
            elif code == '1.8.0':
                self.importWh = int(value[0:-3])
            elif code[0:4] == '1.8.':
                # Differing tarrifs which I don't care about
                pass
            elif code == '2.8.0':
                self.exportWh = int(value[0:-3])
            else:
                print 'Unknown code', code

    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():
    if len(sys.argv) != 2:
        print 'Bad usage'
        print '\t%s portname' % (sys.argv[0])
        sys.exit(1)

    res = IEC1107Reading(sys.argv[1])
    map(sys.stdout.write, res.rawreading)
    print res
if __name__ == '__main__':
    main()

# Meter number is 1288004
# 1.8.0 is import
# 1.8.1 is ??
# 1.8.2 is ??
# 1.8.3 is ??
# 2.8.0 is export

# C.1(12880041.0(22:25 18-11-13)
# 1.8.1(0000000597*Wh)
# 1.8.2(0000000000*Wh)
# 1.8.3(0000264238*Wh)
# 1.8.0(0000264835*Wh)
# 2.8.0(0000511354*Wh)

# ==> /?!<0D><0A>
# <== /ACE5SMLCD
# ==> <06>050<0D><0A>
# <==  -- STX -- 
# <== C.1(12880041.0(22:48 18-11-13)
# <== 1.8.1(0000000597*Wh)
# <== 1.8.2(0000000000*Wh)
# <== 1.8.3(0000264460*Wh)
# <== 1.8.0(0000265057*Wh)
# <== 2.8.0(0000511354*Wh)
# <== !
# <==  -- ETX -- 
# <==  -- BCC --