#!/usr/bin/env python

import os
import sys
import argh
from argh import arg
from subprocess import call
import re
import glob
import time
import ConfigParser
import json as JSON

VERSION = '0.1.2'
boardsDefaults = {'inverted':False}
outletsDefaults = {'description': 'Generic outlet','startpower':False}
_boards = []
_outlets = []


def version():
    'show program version and quit'
    return 'OpenPDU version %s - Copyright (C) 2018 by Paolo Asperti.' % VERSION

@arg('-j', '--json', help="output in json")
def boards(json=False):
    'dump boards configuration'
    if json:
        b = [board.toJSON() for board in _boards]
        return JSON.dumps({'boards':b})
    else:
        b = ''
        for board in _boards:
            b += board.toSTR()
        return '\nBoards:\n' + b


def initboards():
    'initialize boards'
    for board in _boards:
        board.init()
    return

def initoutlets():
    'initialize outlets'
    for outlet in _outlets:
        outlet.init()
    return

@arg('-j', '--json', help="output in json")
def outlets(json=False):
    'dump outlets configuration'
    if json:
        o = [outlet.toJSON() for outlet in _outlets]
        return JSON.dumps({'outlets':o})
    else:
        o = ''
        if len(_outlets)>0:
            for outlet in _outlets:
                o += outlet.toSTR()
        return '\nOutlets:\n' + o


@arg("outlet", help="outlet number")
@arg("onoff", help="1=on [everything else]=off")
@arg('-j', '--json', help="output in json")
def setpower(outlet, onoff, json=False):
    'enable or disable power to an outlet'
    outlet=int(outlet)
    o = [o for o in _outlets if o.outletnum==outlet]
    if len(o) != 1:
        msg = 'wrong outlet number: %s' % str(outlet)
        print JSON.dumps({'message':msg}) if json else msg
        sys.exit(1)
    theOutlet = o[0]
    status = (onoff == '1')
    out = theOutlet.setpower(status)
    if out is None:
        msg = "Cannot set power status for outlet %s" % outlet
        return JSON.dumps({'message':msg,'outlet':outlet}) if json else msg
    else:
        pwrstr = 'on' if out==1 else 'off'
        msg = "Outlet #%s powered %s" % (outlet, pwrstr)
        return JSON.dumps({'powerstatus':out==1,'outlet':outlet}) if json else msg


@arg("outlet", help="outlet number")
@arg('-j', '--json', help="output in json")
def getpower(outlet, json=False):
    'get outlet power status'
    outlet=int(outlet)
    o = [o for o in _outlets if o.outletnum==outlet]
    if len(o) != 1:
        msg = 'wrong outlet number: %s' % str(outlet)
        print JSON.dumps({'message':msg}) if json else msg
        sys.exit(1)
    theOutlet = o[0]
    out = theOutlet.getpower()
    pwrstr = 'on' if out else 'off'
    msg = "Outlet #%s powered %s" % (outlet, pwrstr)
    return JSON.dumps({'powerstatus':out,'outlet':outlet}) if json else msg


# class Abstract1( object ):
#     """Some description that tells you it's abstract,
#     often listing the methods you're expected to supply."""
#     def aMethod( self ):
#         raise NotImplementedError( "Should have implemented this" )
#
# class Cipo(Abstract1):
#     pass


class BoardDummy(object):
    channels = []

    def __init__(self, boardnum, channels=None, filename=None):
        self.boardnum = boardnum
        if channels is None:
            self.channels = 1
        else:
            self.channels = int(channels)
        self.parser = ConfigParser.SafeConfigParser()
        self.filename = filename
        if os.path.isfile(filename) and os.access(filename, os.R_OK):
            pass
        else:
            with open(self.filename, 'wb') as theFile:
                self.parser.add_section('STATUS')
                for c in range(0,self.channels):
                    self.parser.set('STATUS', 'channel%s' % c, '0')
                self.parser.write(theFile)

    def toJSON(self):
      return {'boardnum':self.boardnum,'type':'dummy','channels':self.channels}

    def toSTR(self):
      return ' Board %s\n Type: dummy\n Channels: %s\n\n' % (self.boardnum,self.channels)

    def setpower(self, channel, power):
        self.parser.read(self.filename)
        p = '1' if power else '0'
        s = self.parser.set('STATUS', 'channel%s' % channel, p)
        with open(self.filename, 'wb') as theFile:
            return self.parser.write(theFile)
        return False

    def getpower(self, channel):
        self.parser.read(self.filename)
        s = self.parser.get('STATUS', 'channel%s' % channel)
        return int(s)==1

    def init(self):
        pass








# MCP23008
class BoardI2COut(object):
    data = 0
    next_refresh = 0
    lifetime_sec = 2

    def __init__(self, boardnum, channels=None, address=None, bus=None, inverted=False):
        self.boardnum = boardnum
        if channels is None:
            self.channels = 0
        else:
            self.channels = channels
        if address is None:
            self.address = 0x20
        else:
            self.address = address
        if bus is None:
            self.bus = 1
        else:
            self.bus = bus
        self.inverted = inverted
        if not glob.glob('/dev/i2c*'):
            raise OSError('Cannot access I2C. Please ensure I2C is enabled')

    def toJSON(self):
        return {'boardnum':self.boardnum,'type':'i2c-out','channels':self.channels,'address':self.address}

    def toSTR(self):
        return ' Board %s\n Type: i2c-out\n Channels: %s\n Address: %s\n\n' % (self.boardnum,self.channels,self.address)

    def setpower(self, channel, power):
        old_data = data = self.getdata()
        mask = 1 << channel
        data &= ~mask
        if self.inverted:
            power = not power
        if power:
            data |= mask
        if old_data != data:
            self.next_refresh = 0
        return os.popen("/usr/sbin/i2cset -y %s %s 0x09 0x%s" % (self.bus, self.address, format(data, '02x'))).read()

    def getpower(self, channel):
        data = self.getdata()
        d = ( data >> channel ) & 1
        c = 0 if self.inverted else 1
        return d == c

    def getdata(self):
        now = time.time()
        if now > self.next_refresh:
            self.data = int(os.popen("/usr/sbin/i2cget -y %s %s 0x0A" % (self.bus, self.address)).read(),0)
            self.next_refresh = now + self.lifetime_sec
        return self.data

    def init(self):
        call(["/usr/sbin/i2cset", "-y", self.bus, self.address, "0x00", "0x00"])


class BoardGpioOut(object):
    gpios = []

    def __init__(self, boardnum, channels=None, gpios=None, inverted=False):
        self.boardnum = boardnum
        if channels is None:
            self.channels = 0
        else:
            self.channels = int(channels)
        self.inverted = inverted
        if not isinstance(gpios, list):
            print 'No gpios specified for gpio board %s' % self.boardnum
        if len(gpios) != self.channels:
            print 'Wrong number of gpios specified for gpio board %s' % self.boardnum
        self.gpios = [int(gpio) for gpio in gpios]

    def toJSON(self):
      return {'boardnum':self.boardnum,'type':'gpio-out','channels':self.channels}

    def toSTR(self):
      return ' Board %s\n Type: gpio-out\n Channels: %s\n\n' % (self.boardnum,self.channels)

    def setpower(self, channel, power):
        io = self.gpios[channel]
        fn = '/sys/class/gpio/gpio%s/value' % io
        f = open(fn,'w')
        if self.inverted:
            power = not power
        out = '1' if power else '0'
        return f.write(out)

    def getpower(self, channel):
        io = self.gpios[channel]
        fn = '/sys/class/gpio/gpio%s/value' % io
        f = open(fn,'r')
        e = f.read()
        power = int('0'+e) == 1
        if self.inverted:
            power = not power
        return power

    def init(self):
        for gpio in self.gpios:
            if not os.path.isdir('/sys/class/gpio/gpio%s/' % gpio):
                open('/sys/class/gpio/export','w').write(str(gpio))
            fn = '/sys/class/gpio/gpio%s/direction' % gpio
            open(fn,'w').write('out')
        return


class Outlet(object):
    def __init__(self, outletnum, boardnum, channel, startpower=False):
        self.outletnum = int(outletnum)
        b = [b for b in _boards if b.boardnum==int(boardnum)]
        self.board = b[0]
        self.channel = int(channel)
        self.description = 'Outlet # %s' % self.outletnum
        self.startpower = startpower

    def init(self):
        self.setpower(self.startpower)
        return

    def setpower(self, power):
        self.board.setpower(self.channel,power)
        if self.board.getpower(self.channel) == power:
            return power
        else:
            return None

    def getpower(self):
        return self.board.getpower(self.channel)

    def toSTR(self):
      status = self.board.getpower(self.channel)
      return ' Outlet %s\n Description: %s\n Board #: %s\n Channel: %s\n Power Status:%s\n\n' % (self.outletnum,self.description,self.board.boardnum,self.channel,status)

    def toJSON(self):
      status = self.board.getpower(self.channel)
      return {'outlet':self.outletnum,'description':self.description,'board':self.board.boardnum,'channel':self.channel,'powerstatus':status}











boardsConfigParser = ConfigParser.SafeConfigParser(defaults=boardsDefaults)
boardsConfigFile = '/etc/openpdu/boards.conf'
if os.path.isfile(boardsConfigFile) and os.access(boardsConfigFile, os.R_OK):
  boardsConfigParser.read(boardsConfigFile)

for s in boardsConfigParser.sections():
    if re.match('^board.*',s):
        bType = boardsConfigParser.get(s, 'type')
        num = int(re.sub(r'^board','',s))
        inverted = int('0' + boardsConfigParser.get(s, 'inverted')) == 1
        channels = int(boardsConfigParser.get(s, 'channels'))
        if bType=='gpio-out':
            gpios = []
            for g in range(0,channels):
                gpios.append(int(boardsConfigParser.get(s, 'channel%s' % g)))
            b = BoardGpioOut(boardnum=num, channels=channels, gpios=gpios, inverted=inverted)
            _boards.append(b)
        elif bType=='i2c-out':
            address = boardsConfigParser.get(s, 'address')
            bus = boardsConfigParser.get(s, 'bus')
            b = BoardI2COut(boardnum=num, channels=channels, address=address, bus=bus, inverted=inverted)
            _boards.append(b)
        elif bType=='dummy':
            filename = boardsConfigParser.get(s, 'filename')
            b = BoardDummy(boardnum=num, channels=channels, filename=filename)
            _boards.append(b)

outletsConfigParser = ConfigParser.SafeConfigParser(defaults=outletsDefaults)
outletsConfig = '/etc/openpdu/outlets.conf'
if os.path.isfile(outletsConfig) and os.access(outletsConfig, os.R_OK):
  outletsConfigParser.read(outletsConfig)
for s in outletsConfigParser.sections():
    if re.match('^outlet.*',s):
        num = int(re.sub(r'^outlet','',s))
        description = outletsConfigParser.get(s, 'description')
        boardnum = outletsConfigParser.get(s, 'board')
        channel = outletsConfigParser.get(s, 'channel')
        startpower = int('0' + outletsConfigParser.get(s, 'startpower')) == 1
        o = Outlet(outletnum=num, boardnum=boardnum, channel=channel, startpower=startpower)
        o.description = description
        _outlets.append(o)



parser = argh.ArghParser()
parser.add_commands([setpower, getpower, outlets, boards, initboards, version, initoutlets])

# dispatching:

if __name__ == '__main__':
    parser.dispatch()
