diff --git a/APKBUILD b/APKBUILD index 2c0a0f7..d850ccf 100644 --- a/APKBUILD +++ b/APKBUILD @@ -1,13 +1,13 @@ # Contributor: Paolo Asperti # Maintainer: Paolo Asperti pkgname=openpdu -pkgver=0.1.2 +pkgver=0.2.0 pkgrel=1 pkgdesc="OpenPDU project - main binary" url="https://github.com/openpdu/openpdu" arch="noarch" license="GPL2" -depends="python py-argh apk-cron i2c-tools" +depends="python py-argh apk-cron py-bottle i2c-tools" makedepends="" install="openpdu.post-install" subpackages="" @@ -25,4 +25,5 @@ package() { install -Dm755 etc/local.d/openpdu.start "$pkgdir"/etc/local.d/openpdu.start install -Dm755 openpdu "$pkgdir"/usr/bin/openpdu + install -Dm755 openpdud "$pkgdir"/usr/bin/openpdud } diff --git a/etc/openpdu/boards.conf b/etc/openpdu/boards.conf index ea44876..0bb3426 100644 --- a/etc/openpdu/boards.conf +++ b/etc/openpdu/boards.conf @@ -18,6 +18,13 @@ # EXAMPLE I2C BOARD # [board1] # type = i2c-out -# address = 20 +# address = 0x27 # channels = 8 -# bus = 1 +# bus = 0 + +# EXAMPLE I2C CURRENT BOARD +# [board2] +# type = i2c-current +# address = 0x48 +# channels = 4 +# bus = 0 diff --git a/etc/openpdu/outlets.conf b/etc/openpdu/outlets.conf index 8210655..6a42f28 100644 --- a/etc/openpdu/outlets.conf +++ b/etc/openpdu/outlets.conf @@ -20,6 +20,8 @@ startpower = 1 # [outlet3] # board=1 # channel=1 +# currentboard=2 +# currentboardchannel=1 # description = MailServer # [outlet4] diff --git a/openpdud b/openpdud new file mode 100755 index 0000000..31bfee8 --- /dev/null +++ b/openpdud @@ -0,0 +1,475 @@ +#!/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//power/') +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//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//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) + +