#!/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.

# 27-Mar-10:
# Changing the way some values are reported - the inital method was 
# plaintext, but that doesn't work well for passing to the Jace.  This
# one will use enums instead.
#
# Also, since most everything is string handling on the output side, 
# this gets rid of the int() casts, to eliminate unnecessary loss of 
# precision...

# 21 Oct 11:
# Updating name to '-gl' to indicate it's intended to work with the logger
# on Ginny.
#
# Adding returns to getXX and scan functions to propagate which address
# in the devices array was just updated by a scan.  This is so my new
# logging program doesn't have to search the entire array every time it
# scans for updates.
#
# Also going to go back to ints and floats for numeric values - will be 
# more directly usable later on.


import serial

serport = '/dev/mate'

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

    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, fxCmd='', auxCmd=''):
        # 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')
        
        self.sendCmds(fxCmd, auxCmd)
        
        addr = -1

        # Validating checksum
        if len(data) == 49 and data[0] == '\n':
            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]):
                    addr = self.getFX (data)
                    
                if str.isupper (data[1]):
                    addr = self.getCC (data)

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

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

        return addr

    def sendCmds (self, fxCmd, auxCmd):
        # Send commands to FX.  One catch - it only listens for a single
        # command at a time, so this will only update on if both inputs
        # change at once.

        # The Mate listens *while* sending data, and total for about 250ms
        # so hopefully there's enough time to send after decoding above...
        if fxCmd != '' and 'acMode' in self.devices[0]:
            value = ''
            if fxCmd == 'Use' and self.devices[0]['acMode'] == 1:
                value = 'UU'
            if fxCmd == 'Drop' and self.devices[0]['acMode'] == 2:
                value = 'DD'

            if value != '':
                self.ser.write (value)
                return

        if auxCmd != '' and 'fxMisc' in self.devices[0]:
            value = ''
            if auxCmd == 'true' and self.devices[0]['fxMisc'][0] == 0:
                value = 'ZZ'
            if auxCmd == 'false' and self.devices[0]['fxMisc'][0] == 1:
                value = 'XX'

            if value != '':
                self.ser.write (value)



    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'}

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

            if mode & 64:           # Shorted Output
                errors[1] = 1

            if mode & 32:           # High battery
                errors[2] = 1

            if mode & 16:           # Phase loss
                errors[3] = 1

            if mode & 8:            # Low Battery
                errors[4] = 1

            if mode & 4:            # Overtemp
                errors[5] = 1

            if mode & 2:            # Stack error
                errors[6] = 1

            if mode & 1:            # Low AC output
                errors[7] = 1

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

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

        self.devices[addr]['acMode'] = int(data[30:32])

        # 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 = [0, 0]
        if mode & 128:          # Aux Output on
            misc[0] = 1

        if mode & 1:            # 230V unit
            misc[1] = 1

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

        # Warning Mode
        mode = int (data[41:44])
        errors = [0, 0, 0, 0, 0, 0, 0, 0]
        if mode > 0:
            if mode & 128:          # Fan failure
                errors[0] = 1

            if mode & 64:           # Comm error
                errors[1] = 1

            if mode & 32:           # Temp sensor failed
                errors[2] = 1

            if mode & 16:           # Buy amps > input size
                errors[3] = 1

            if mode & 8:            # Input VAC low
                errors[4] = 1

            if mode & 4:            # Input VAC high
                errors[5] = 1

            if mode & 2:            # AC in freq low
                errors[6] = 1

            if mode & 1:            # AC in freq high
                errors[7] = 1

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

        return addr
        


    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'}

        self.devices[addr]['auxMode'] = int(data[23:25])

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

            if mode & 64:           # Too hot
                errors[1] = 1

            if mode & 32:           # Shorted batt sensor
                errors[2] = 1

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

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

        self.devices[addr]['chgMode'] = int(data[30:32])

        return addr


    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

        if mode & 16:
            bAmps = -bAmps

        if mode & 8:
            aAmps = -aAmps

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

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

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

        if mode & 1:
            self.devices[addr]['chgParmsMet'] = 1       # True
        else:
            self.devices[addr]['chgParmsMet'] = 0       #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 = '-'
            edi = edi - 64
        else:
            sign = ''

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

        # Days Since Full has one decimal place
        if edi == 6:
            value = float(sign + data[21:25] + '.' + data[25])

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

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

        # Shunt enables - logic backwards.
        if int(data[35]):
            self.devices[addr]['aEnable'] = 0
        else:
            self.devices[addr]['aEnable'] = 1

        if int(data[36]):
            self.devices[addr]['bEnable'] = 0
        else:
            self.devices[addr]['bEnable'] = 1

        if int(data[37]):
            self.devices[addr]['cEnable'] = 0
        else:
            self.devices[addr]['cEnable'] = 1

        return addr



    def getDevices (self):
        return self.devices


    def scanGet (self, fxC='', auxC=''):
        # Perform a scan, then return the dictionary that was just updated.
        addr = self.scan(fxC, auxC)
        
        if addr != -1:
            return self.devices[addr]
        else:
            return {'dev':'none'}

