#!/usr/bin/env python

import os
import sys
import argh
#from argh import arg
import re
import glob
import time
import ConfigParser
import json as JSON
from bottle import route, run, template
import smbus2
#import math
from numpy import mean, sqrt, square



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




@route('/version')
def index():
    return VERSION

@route('/boards')
def index():
  b = [board.toJSON() for board in _boards]
  return JSON.dumps({'boards':b})

@route('/initboards')
def index():
    for board in _boards:
        board.init()
    return

@route('/initoutlets')
def index():
    for outlet in _outlets:
        outlet.init()
    return

@route('/outlets')
def index():
    o = [outlet.toJSON() for outlet in _outlets]
    return JSON.dumps({'outlets':o})

@route('/outlet/<outlet>/power/<onoff>')
def index(outlet, onoff):
    outlet=int(outlet)
    o = [o for o in _outlets if o.outletnum==outlet]
    if len(o) != 1:
        msg = 'wrong outlet number: %s' % str(outlet)
        return JSON.dumps({'message':msg})
    theOutlet = o[0]
    status = (onoff == 'on')
    out = theOutlet.setpower(status)
    if out is None:
        msg = "Cannot set power status for outlet %s" % outlet
        return JSON.dumps({'message':msg,'outlet':outlet})
    else:
        pwrstr = 'on' if out==1 else 'off'
        msg = "Outlet #%s powered %s" % (outlet, pwrstr)
        return JSON.dumps({'powerstatus':out==1,'outlet':outlet})

@route('/outlet/<outlet>/power')
def index(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)
        return JSON.dumps({'message':msg})
    theOutlet = o[0]
    out = theOutlet.getpower()
    return JSON.dumps({'powerstatus':out,'outlet':outlet})

@route('/outlet/<outlet>/current')
def index(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)
        return JSON.dumps({'message':msg})
    theOutlet = o[0]
    out = theOutlet.getcurrent()
    return JSON.dumps({'current':out,'outlet':outlet})







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 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 = int(address, 16)
      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')
      self._bus = smbus2.SMBus(self.bus)

  def toJSON(self):
      return {'boardnum':self.boardnum,'type':'i2c-out','channels':self.channels,'address':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 self._bus.write_byte_data(self.address, 0x09,data)

  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 = self._bus.read_byte_data(self.address, 0x0A)
          self.next_refresh = now + self.lifetime_sec
      return self.data

  def init(self):
      return self._bus.write_byte_data(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,'inverted':self.inverted}

  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
      self.currentboard = None
      self.currentboardchannel = None

  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 getcurrent(self):
      if self.currentboard is None:
          return 0
      return self.currentboard.readcurrent(self.currentboardchannel)

  def setcurrentboard(self, boardnum, channel):
      b = [b for b in _boards if b.boardnum==int(boardnum)]
      self.currentboard = b[0]
      self.currentboardchannel = channel


  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}




class BoardI2Ccurrent(object):
  data = 0
  next_refresh = 0
  lifetime_sec = 2

  def __init__(self, boardnum, channels=None, address=None, bus=None):
      self.boardnum = boardnum
      if channels is None:
          self.channels = 0
      else:
          self.channels = channels
      if address is None:
          self.address = 0x48
      else:
          self.address = int(address, 16)
      if bus is None:
          self.bus = 1
      else:
          self.bus = bus
      if not glob.glob('/dev/i2c*'):
          raise OSError('Cannot access I2C. Please ensure I2C is enabled')
      self._bus = smbus2.SMBus(self.bus)
      self.my_zero = 19920
      self.num_samples = 100

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

  def _readvalue(self, channel):
    # 0 = no effect / 1 = start a single conversion
    cfg_OS = 1

    # multiplexer configurations:
    cfg_chan = 0b100
    if channel == 0:
        # AIN0 -> GND
        cfg_chan = 0b100
    elif channel == 1:
        # AIN1 -> GND
        cfg_chan = 0b101
    elif channel == 2:
        # AIN2 -> GND
        cfg_chan = 0b110
    elif channel == 3:
        # AIN3 -> GND
        cfg_chan = 0b111
    # unused multiplexer configurations:
    # 0b000 AIN0 -> AIN1
    # 0b001 AIN0 -> AIN3
    # 0b010 AIN1 -> AIN3
    # 0b011 AIN2 -> AIN3

    # gain amplifier configuration
    #  000 = +- 6.144 V
    #  001 = +- 4,096 V
    #  010 = +- 2,048 V
    #  011 = +- 1,024 V
    #  100 = +- 0,512 V
    #  101 = +- 0,256 V
    #  110 = +- 0,256 V
    #  111 = +- 0,256 V
    cfg_amplifier = 0b000

    # 0 = continuous conversion / 1 = single-shot or power-down
    cfg_mode = 1

    # data rate
    #  000 = 128 sps
    #  001 = 250 sps
    #  010 = 490 sps
    #  011 = 920 sps
    #  100 = 1600 sps
    #  101 = 2400 sps
    #  110 = 3300 sps
    #  111 = 3300 sps
    cfg_sps = 0b110

    # 0 = traditional comparator / 1 = window comparator
    cfg_comp = 0

    # 0 = comparator active low / 1 = comparator active high
    cfg_comp_pol = 0

    # 0 = comparator non-latching / 1 = comparator latching
    cfg_comp_latch = 0

    # comparator queue and disable
    #  00 = assert after one conversion
    #  01 = assert after two conversions
    #  10 = assert after three conversions
    #  11 = disable comparator
    cfg_comp_queue = 0b11

    config = cfg_sps << 13 | cfg_comp << 12 | cfg_comp_pol << 11 | cfg_comp_latch << 10 | cfg_comp_queue << 8 | cfg_OS << 7 | cfg_chan << 4 | cfg_amplifier << 1 | cfg_mode

    self._bus.write_word_data(self.address, 0x01, config)
    while True:
	    d1 = self._bus.read_word_data(self.address, 0x01)
            if d1 == config:
                break
    d1 = self._bus.read_word_data(self.address, 0x00)
    msb = d1 & 0x00FF
    lsb = d1 & 0xFF00
    val = msb << 8 | lsb >> 8
    return (val - self.my_zero) >> 4

  def readcurrent(self, channel):
	samples = []
	for i in range(1,self.num_samples):
		v = self._readvalue(channel)
		samples.append(v)
	rms = sqrt(mean(square(samples)))
	return rms




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=='i2c-current':
          address = boardsConfigParser.get(s, 'address')
          bus = boardsConfigParser.get(s, 'bus')
          b = BoardI2Ccurrent(boardnum=num, channels=channels, address=address, bus=bus)
          _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
      if outletsConfigParser.has_option(s,'currentboard') and outletsConfigParser.has_option(s,'currentboardchannel'):
          currentboard = outletsConfigParser.get(s, 'currentboard')
          currentboardchannel = int(outletsConfigParser.get(s, 'currentboardchannel'))
          o.setcurrentboard(currentboard, currentboardchannel)
      _outlets.append(o)



for sec in range(1,100):
    for outlet in range(0,4):
        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})
        theOutlet = o[0]
        out = theOutlet.getcurrent()
        print JSON.dumps({'current':out,'outlet':outlet})
    print "---"
#run(host='0.0.0.0', port=5000)


