#!/usr/bin/python

# Outback Mate interface
# Copyright 2010 Joe Martine

# Class implements a 10-slot list of dictionaries.  The dictionaries get
# populated with the appropriate values based on what device is plugged
# in to that port on the comm hub.  (Direct-connected devices will go in
# the first slot.)

# Each dict will have a 'dev' key that says what is plugged in there - 
# FX, CC (MX or FM look the same), DC.  Subsequent keys of course will
# be device-specific.

import serial

serport = '/dev/mate'

class Mate():
    devices = []
    ser = ''

    def __init__ (self, serport):
        # Init devices array to "nothing"
        for i in range(10):
            self.devices.append ({'dev':'none'})

        self.ser = serial.Serial (serport, 19200, timeout=5)
        self.ser.setRTS (0)
        self.ser.setDTR (1)
        self.ser.flushInput()

    def scan (self):
        # Reads the next available line of data.  Must be called repeatedly
        # to get all data.  (I know there has to be a better way to do this,
        # just don't know what it is yet!)

        # Last character of each string is CR (dec 13).
        data = self.ser.readline (eol='\r')

        # Validating checksum
        if data[0] == '\n' and len(data) == 49:
            cksum = 0
            for c in data[:-5]:
                if str.isdigit (c):
                    cksum = cksum + int(c)
                if str.isalpha (c):
                    cksum = cksum + (ord(c) - 48)

            if cksum == int(data[45:48]):

                if str.isdigit(data[1]):
                    self.getFX (data)

                if str.isupper (data[1]):
                    self.getCC (data)

                if str.islower (data[1]):
                    self.getDC (data)

            else:
                print 'Checksum invalid!'
        else:
            print 'String length bad!'


    def getFX (self, data):
        
        # FX address will report 0 if direct-connect.  On hub, will report
        # 1-9 or ':' for 10th slot.
        addr = data[1]
        if addr == ':':
            addr = 10
        else:
            addr = int(addr)

        if addr > 0:
            addr = addr - 1

        self.devices[addr]['dev'] = 'FX'
        self.devices[addr]['invAmps'] = int(data[3:5])
        self.devices[addr]['chgAmps'] = int(data[6:8])
        self.devices[addr]['buyAmps'] = int(data[9:11])
        self.devices[addr]['acInVolts'] = int(data[12:15])
        self.devices[addr]['acOutVolts'] = int(data[16:19])
        self.devices[addr]['sellAmps'] = int(data[20:22])
        
        self.devices[addr]['battVolts'] = float(data[33:35] + '.' + data[35])

        # FX Mode
        modes = {'00':'Inverter Off', '01':'Search', '02':'Inverter On', 
                '03':'Charge', '04':'Silent', '05':'Float', '06':'Equalize',
                '07':'Charger Off', '08':'Support', '09':'Sell Enabled',
                '10':'PassThru', '90':'FX Error', '91':'AGS Error',
                '92':'Comm Error'}

        mode = data[23:25]
        self.devices[addr]['fxMode'] = modes[mode]
        
        # Error Mode
        mode = int(data[26:29])
        if mode > 0:
            errors = []
            if mode & 128:
                errors.append ('Backfeed')

            if mode & 64:
                errors.append ('Shorted Output')

            if mode & 32:
                errors.append ('High Battery')

            if mode & 16:
                errors.append ('Phase Loss')

            if mode & 8:
                errors.append ('Low Battery')

            if mode & 4:
                errors.append ('Over Temp')

            if mode & 2:
                errors.append ('Stack Error')

            if mode & 1:
                errors.append ('Low AC Output')

        else:
            errors = ['None']

        self.devices[addr]['errModes'] = errors

        # AC Mode
        modes = {'00':'No AC', '01':'AC Drop', '02':'AC Use'}

        mode = data[30:32]
        self.devices[addr]['acMode'] = modes[mode]

        # FX Misc
        # An 8-bit field, but only two bits valid right now.
        # Bit 1 indicates a 230V unit, which means voltages must be doubled
        # and currents must be halved.  Will leave that up to client for now...
        mode = int (data[37:40])
        
        misc = []
        if mode & 128:
            misc.append ('Aux Output On')
        else:
            misc.append ('Aux Output Off')

        if mode & 1:
            misc.append ('230V Unit')

        self.devices[addr]['fxMisc'] = misc

        # Warning Mode
        mode = int (data[41:44])
        if mode > 0:
            errors = []
            if mode & 128:
                errors.append ('Fan Failure')

            if mode & 64:
                errors.append ('Comm Error')

            if mode & 32:
                errors.append ('Temp Sensor Failed')

            if mode & 16:
                errors.append ('Buy Amps > Input Size')

            if mode & 8:
                errors.append ('Input VAC Low')

            if mode & 4:
                errors.append ('Input VAC High')

            if mode & 2:
                errors.append ('AC In Freq Low')

            if mode & 1:
                errors.append ('AC In Freq High')

        else:
            errors = ['None']

        self.devices[addr]['warnModes'] = errors
        


    def getCC (self, data):
        
        # CC reports address as 'A' thru 'K'.  'A' for direct-connect,
        # 'B' thru 'K' for ports 1-10.

        addr = ord(data[1])
        if addr == 65:
            addr = 0
        else:
            addr = addr - 66

        self.devices[addr]['dev'] = 'CC'
        self.devices[addr]['chgAmps'] = float (data[6:8] + '.' + data[21])
        self.devices[addr]['pvAmps'] = int (data[9:11])
        self.devices[addr]['pvVolts'] = int (data[12:15])
        self.devices[addr]['dailykWh'] = float (data[16:18] + '.' + data[18])
        self.devices[addr]['battVolts'] = float (data[33:35] + '.' + data[35])
        self.devices[addr]['dailyAH'] = int (data[37:41])

        # Aux mode
        modes = {'00':'Disabled', '01':'Diversion', '02':'Remote', 
                '03':'Manual', '04':'Vent Fan', '05':'PV Trigger', 
                '06':'Float', '07':'Error Output', '08':'Night Light',
                '09':'PWM Diversion', '10':'Low Battery'}

        mode = data[23:25]
        self.devices[addr]['auxMode'] = modes[mode]

        # Error mode
        mode = int (data[26:29])
        if mode > 0:
            errors = []
            if mode & 128:
                errors.append ('High VOC')

            if mode & 64:
                errors.append ('Too Hot')

            if mode & 32:
                errors.append ('Shorted Batt Sensor')

        else:
            errors = ['None']

        self.devices[addr]['errMode'] = errors

        # Charge mode
        modes = {'00':'Silent', '01':'Float', '02':'Bulk', '03':'Absorb',
                '04':'EQ'}

        mode = data[30:32]
        self.devices[addr]['chgMode'] = modes[mode]


    def getDC (self, data):

        # DC reports addresses 'a' to 'j'.  Can't be direct-connected.
        addr = ord (data[1]) - 97
        
        self.devices[addr]['dev'] = 'DC'
        # Amps sign depends on Status Flags below...
        aAmps = float (data[3:6] + '.' + data[6])
        bAmps = float (data[8:11] + '.' + data[11])
        cAmps = float (data[13:16] + '.' + data[16])
        self.devices[addr]['battVolts'] = float (data[27:29] + '.' + data[29])
        self.devices[addr]['soc'] = int (data[31:34])
        # Batt temp range -10-60 C, must offset 10.
        self.devices[addr]['battTemp'] = int (data[42:44]) - 10

        # Status Flags
        mode = int (data[39:41])
        if mode & 32:
            cAmps = cAmps * -1

        if mode & 16:
            bAmps = bAmps * -1

        if mode & 8:
            aAmps = aAmps * -1

        self.devices[addr]['aAmps'] = aAmps
        self.devices[addr]['bAmps'] = bAmps
        self.devices[addr]['cAmps'] = cAmps

        if mode & 4:
            self.devices[addr]['relayMode'] = 'Manual'
        else:
            self.devices[addr]['relayMode'] = 'Automatic'

        if mode & 2:
            self.devices[addr]['relayState'] = 'Closed'
        else:
            self.devices[addr]['relayState'] = 'Open'

        if mode & 1:
            self.devices[addr]['chgParmsMet'] = True
        else:
            self.devices[addr]['chgParmsMet'] = False
        
        # Extra Data Identifier & Extra Data.
        # Only 11 modes listed in manual, I do get data for 12 & 13.  12 
        # appears to be "Battery Net In/Out", not sure about 13.  Adding
        # placeholders for others Just In Case...

        modes = ['aAccumAH', 'aAccumKWH', 'bAccumAH', 'bAccumKWH', 'cAccumAH',
                'cAccumKWH', 'daysSinceFull', 'todayMinSOC', 'todayNetInAH',
                'todayNetOutAH', 'todayNetInKWH', 'todayNetOutKWH', 
                'battNetInOut', '13', '14', '15', '16', '17', '18', '19', '20',
                '21', '22', '23', '24', '25','26','27','28','29','30','31',
                '32','33','34','35']

        edi = int (data[18:20])
        if edi > 63:
            sign = -1
            edi = edi - 64
        else:
            sign = 1

        # No decimal places...
        ints = [0, 2, 4, 7, 8, 9, 12]
        if (edi in ints) or (edi > 12):
            value = int (data[21:26]) * sign

        # Days Since Full has one decimal place
        if edi == 6:
            value = float (data[21:26]) * sign * 0.1

        # All kWh values have two decimal places
        x100 = [1, 3, 5, 10, 11]
        if edi in x100:
            value = float (data[21:26]) * sign * 0.01

        self.devices[addr][modes[edi]] = value

        # Shunt enables - logic backwards.
        self.devices[addr]['aEnable'] = not bool (int (data[35]))
        self.devices[addr]['bEnable'] = not bool (int (data[36]))
        self.devices[addr]['cEnable'] = not bool (int (data[37]))


    def getDevices (self):
        return self.devices



