#!/usr/bin/env python # # Company: EPSCom AG # # $Id: dss_script.py $ import sys, os, tempfile, time, datetime, string, Image, urllib import xml.sax.handler from xml.dom.minidom import * from xml.dom import minidom #---------------------------------------------------------------------------------------------- # GLOBAL PARAMS #---------------------------------------------------------------------------------------------- WORK_DIR = os.path.dirname(__file__) # current directory path (where script is) OUTPUT_FILE = '' # output file path (picture) dSS_IP = '127.0.0.1' # default IP of dSS server dSS_PORT = '8080' # default port of dSS server T_PARAM = '' # default time interval of interest T_TEXT = "invalid interval" # text description of the interval T_DAYS = 0 # number of days per interval (for > 1 day) T_SECONDS = 0 # number of seconds per interval (for < 1 day) T_FREQINTERVAL = 0 # interval for each X-axis unit #---------------------------------------------------------------------------------------------- # DOM PARSER #---------------------------------------------------------------------------------------------- class dssMeteringRetriever: def __init__(self, dssServerIP, dssServerPort): self.dssServerPath = "http://" + dssServerIP + ":" + dssServerPort + "/metering/" self.filenames = { '5minutes' : 'metering_consumption_seconds.xml', 'hour' : 'metering_consumption_10seconds.xml', 'day' : 'metering_consumption_5minutely.xml', 'week' : 'metering_consumption_halfhourly.xml', 'month' : 'metering_consumption_2hourly.xml', 'year' : 'metering_consumption_daily.xml' } def fetchInterval(self, interval): try: fullPath = self.dssServerPath + self.filenames[interval] dom = minidom.parse( urllib.urlopen( fullPath ) ) except: return {} try: dom.normalize() config_node = dom.getElementsByTagName('config')[0] # parse config config = {'Name' : '', 'unit' : 'power consumption', 'Customer-ID' : '', 'Contract' : ''} for element in config_node.getElementsByTagName('Name'): config['Name'] = element.childNodes[0].data for element in config_node.getElementsByTagName('unit'): config['unit'] = element.childNodes[0].data for element in config_node.getElementsByTagName('Customer-ID'): config['Customer-ID'] = element.childNodes[0].data for element in config_node.getElementsByTagName('Contract'): config['Contract'] = element.childNodes[0].data # parse values result_vector = [] startTS = '' endTS = '' minVal = -1.0 maxVal = 0.0 values_node = dom.getElementsByTagName('values')[0] for sample in values_node.getElementsByTagName('value'): ts = sample.getAttribute('timestamp') val = '' for data in sample.getElementsByTagName('value'): val = data.childNodes[0].data if ts != '': val_numeric = string.atof(val)/1000.0 if minVal < 0.0: minVal = val_numeric if minVal > val_numeric: minVal = val_numeric if maxVal < val_numeric: maxVal = val_numeric result_vector.append({ts : '%.5f' % val_numeric}) if startTS == '': startTS = ts endTS = ts config['minVal'] = minVal config['maxVal'] = maxVal config['startTS'] = startTS config['endTS'] = endTS return { 'config' : config, 'values' : result_vector } except: return {} #---------------------------------------------------------------------------------------------- # SAX PARSER #---------------------------------------------------------------------------------------------- class ConfigFinder(xml.sax.handler.ContentHandler): def __init__(self,parser,configLogger): self.parser = parser self.logger = configLogger def startElement(self,name,attributes): if name == 'config': self.parser.setContentHandler(self.logger) class ConfigLogger(xml.sax.handler.ContentHandler): def __init__(self,parser,config, metExtractor): self.parser = parser self.config = config self.extractor = metExtractor self.activeTag = '' def startElement(self,name,attributes): self.activeTag = name def characters(self,string): string = strip(string, '\n\t' ) partStr = self.config[self.activeTag] self.config.update({self.activeTag : partStr + string}) def endElement(self,name): if name == 'config': self.parser.setContentHandler(self.extractor) class MeteringsExtractor(xml.sax.handler.ContentHandler): def __init__(self, interval, values): self.timeTagFound = 0 self.valueTagFound = 0 self.values = values self.interl = interval self.ts = '' self.val = '' self.pts = '' def outOfRange(self, tstop, tsbottom, interval): top = time.strptime(tstop, "%Y-%m-%d %H:%M:%S") bot = time.strptime(tsbottom, "%Y-%m-%d %H:%M:%S") top = datetime.datetime(top.tm_year, top.tm_mon, top.tm_mday, top.tm_hour, top.tm_min, top.tm_sec) bot = datetime.datetime(bot.tm_year, bot.tm_mon, bot.tm_mday, bot.tm_hour, bot.tm_min, bot.tm_sec) lowerBound = top - interval if bot < lowerBound: return True return False def startElement(self,name,attributes): if name == 'value': if self.timeTagFound == 0: if attributes['timestamp'] != '': self.timeTagFound = 1 self.ts = attributes['timestamp'] if self.pts == '' : self.pts = self.ts else: self.valueTagFound = 1 def characters(self,string): if self.valueTagFound == 1: self.val += string def endElement(self,name): if name == 'value' : if self.valueTagFound == 1: self.valueTagFound = 0 self.values.append({self.ts : self.val}) self.val = '' while self.outOfRange(self.ts, self.pts, self.interl): del self.values[0] self.pts = self.values[0].keys()[0] else: if self.timeTagFound == 1: self.timeTagFound = 0 self.ts = '' class dssSaxMeteringRetriever(dssMeteringRetriever): def __init__(self, dssServerIP, dssServerPort): dssMeteringRetriever.__init__(self, dssServerIP, dssServerPort) self.config = {'Name' : '', 'unit' : '', 'Customer-ID' : '', 'Contract' : '', '' : '', 'comment' : '', 'startTS' : '', 'endTS' : ''} self.result_vector = [] def fetchInterval(self, interval): fullPath = self.dssServerPath + self.filenames[interval] try: dateDelta = { '5minutes': datetime.timedelta(minutes=5), 'hour' : datetime.timedelta(hours=1), 'day' : datetime.timedelta(days=1), 'week' : datetime.timedelta(weeks=1), 'month' : datetime.timedelta(days=30), 'year' : datetime.timedelta(days=365) }[interval] parser = xml.sax.make_parser() metExtractor = MeteringsExtractor(dateDelta, self.result_vector) configLogger = ConfigLogger(parser,self.config, metExtractor) configFinder = ConfigFinder(parser, configLogger) parser.setContentHandler(configFinder) parser.parse(urllib.urlopen( fullPath )) siz = len(self.result_vector) if siz != 0: self.config['startTS'] = self.result_vector[0].keys()[0] self.config['endTS'] = self.result_vector[siz-1].keys()[0] return { 'config' : self.config, 'values' : self.result_vector } except: return {} #---------------------------------------------------------------------------------------------- # GRAPH RENDERER #---------------------------------------------------------------------------------------------- class MeteringGraphGenerator: def __init__(self, datafile, outputfile): self.datafile = datafile self.outputfile = outputfile self.tmp = '' self.formatX = "%H:%M:%S\\n%d-%m-%y" def setXRange(self, start, end): self.start = start self.end = end def setYRange(self, floor, ceil): self.floor = floor self.ceil = ceil def setYUnits(self, units ): self.Yunits = units def setTimeInterval(self, intervaltext, freqinterval): self.intervaltext = intervaltext self.freqinterval = freqinterval def setTitle(self, title): self.title = title def __del__(self): self.tmp.unlink(self.tmp.name) def setFormatX(self, fmt): self.formatX = fmt def createScript(self): string = "set autoscale\n" \ "unset log\n" \ "unset label\n" \ "set terminal jpeg size 640,430\n" \ "set output \"" + self.outputfile + "\"\n" \ "set xdata time\n" \ "set timefmt \"%Y-%m-%d %H:%M:%S\"\n" \ "set xrange [ \""+self.start+"\" : \""+self.end+"\" ]\n" \ "set yrange [ \""+str(self.floor)+"\" : \""+str(self.ceil)+"\" ]\n" \ "set format x \"" + self.formatX + "\"\n" \ "set title \"" + self.title + "\"\n" \ "set xlabel \"time\"\n" \ "set ylabel \"" + self.Yunits + "\"\n" \ "set grid\n" \ "set xtics autofreq "+self.freqinterval+"\n" \ "set mxtics 6\n" \ "plot \"" + self.datafile + "\" using 1:3 title \'"+ self.intervaltext+"\' with lines\n" return string def dumpToAFile(self): self.tmp = tempfile.NamedTemporaryFile(delete=False) self.tmp.write('set rmargin 7;') #denondock workout self.tmp.write('set lmargin 7;') #denondock workout self.tmp.write( self.createScript() ) self.tmp.close() return self.tmp.name def unlinkFile(self): self.tmp.unlink(self.tmp.name) #---------------------------------------------------------------------------------------------- # MEDIATOMB INTERFACE #---------------------------------------------------------------------------------------------- class Item: def __init__(self, input): self.xml = parseString(input) def __getNodeData(self, data): try: title = self.xml.getElementsByTagName(data)[0] if (len(title.childNodes) > 0): if (title.childNodes[0].nodeType == title.childNodes[0].TEXT_NODE): return title.childNodes[0].data return '' except: return '' def __getNode(self, data): try: title = self.xml.getElementsByTagName(data)[0] if (len(title.childNodes) > 0): if (title.childNodes[0].nodeType == title.childNodes[0].TEXT_NODE): return title.childNodes[0] else: text = self.xml.createTextNode("") title.appendChild(text) return title.childNodes[0] except: return None def getTitle(self): return self.__getNodeData("dc:title") def setTitle(self, data): node = self.__getNode("dc:title") node.data = data def getClass(self): return self.__getNodeData("upnp:class") def setClass(self, data): node = self.__getNode("upnp:class") node.data = data def getAction(self): return self.__getNodeData("action") def setAction(self, data): node = self.__getNode("action") node.data = data def getState(self): return self.__getNodeData("state") def setState(self, data): node = self.__getNode("state") node.data = data def getLocation(self): return self.__getNodeData("location") def setLocation(self, data): node = self.__getNode("location") node.data = data def getMimeType(self): return self.__getNodeData("mime-type") def setMimeType(self, data): node = self.__getNode("mime-type") node.data = data def render(self): return self.xml.toxml() def getDescription(self): return self.__getNodeData("dc:description") def setDescription(self, data): node = self.__getNode("dc:description") node.data = data #---------------------------------------------------------------------------------------------- # SCRIPT ROUTINES #---------------------------------------------------------------------------------------------- #---------------------------------- # Adds logo to the output jpeg file #---------------------------------- def addLogo(fileIn, fileOut): logoFile = os.path.join(WORK_DIR, 'box_logo.jpg') imLogo = Image.open(logoFile) if imLogo == None: sys.stderr.write("addLogo: Could not open logo file for reading!\n") raise Exception() imMetering = Image.open(fileIn) if imMetering == None: sys.stderr.write("addLogo: Could not open metering graph file for reading!\n") raise Exception() logoSizeX, logoSizeY = imLogo.size meteringSizeX, meteringSizeY = imMetering.size boxLogoInOut = (0, 0, logoSizeX, logoSizeY) boxMeteringIn = (0, 0, meteringSizeX, meteringSizeY) boxMeteringOut = (0, logoSizeY, meteringSizeX, logoSizeY + meteringSizeY) logoReg = imLogo.crop(boxLogoInOut) meteringReg = imMetering.crop(boxMeteringIn) imResult = Image.new( "RGB", (meteringSizeX, logoSizeY + meteringSizeY) ) if imResult == None: sys.stderr.write("addLogo: Could not create output file!\n") raise Exception() try: imResult.paste(logoReg, boxLogoInOut) imResult.paste(meteringReg, boxMeteringOut) imResult.save(fileOut, "JPEG") except: sys.stderr.write("addLogo: Could not paste into the output file!\n") raise Exception() #------------------------------------------------------------------ # Process the config parameters and feed them to the graf generator #------------------------------------------------------------------ def initializeGraphGenerator( config, generator ): # calulate X range startTS = config['startTS'] endTS = config['endTS'] t1 = datetime.datetime.strptime( startTS, "%Y-%m-%d %H:%M:%S" ) t2 = datetime.datetime.strptime( endTS, "%Y-%m-%d %H:%M:%S" ) timeInterval = datetime.timedelta( days=T_DAYS, seconds=T_SECONDS ) t1 = t2 - timeInterval startTS = t1.strftime( "%Y-%m-%d %H:%M:%S" ) endTS = t2.strftime( "%Y-%m-%d %H:%M:%S" ) generator.setXRange( startTS, endTS ) minVal = 0.0 maxVal = 0.0 try: minVal = config['minVal'] maxVal = config['maxVal'] except KeyError: raise Exception('MIN/MAX values are not specified') margin = (maxVal - minVal)/20.0 if margin == 0: margin = (maxVal + minVal) / 2.0 / 20.0 if margin < 1: margin = 1.0 generator.setYRange( minVal - margin, maxVal + margin ) #sOut = "min = " + str(minVal) + " | max = " + str(maxVal) + "\n" #sOut = "YRange = " + str(minVal-margin) + " | " + str(maxVal+margin) + "\n" name = config['Name'] contract = config['Contract'] customerId = config['Customer-ID'] title = "Power consumption metering graph\\nZone: Apartment Time:" + endTS if name != '': title +='\\n' + 'Name: ' + name if contract != '': title +='\\n' + 'Contract: ' + contract if customerId != '': title +=' ' + 'Customer-ID:' + customerId generator.setTitle( title ) generator.setTimeInterval( T_TEXT, T_FREQINTERVAL ) units = config['unit'] generator.setYUnits( 'Watt' ) if T_PARAM == '5minutes': generator.setFormatX("%H:%M:%S") elif T_PARAM == 'hour' or T_PARAM == 'day': generator.setFormatX("%H:%M:%S\\n%d-%m-%y") elif T_PARAM == 'week' or T_PARAM == 'month' or T_PARAM == 'year': generator.setFormatX("%d-%m-%y") else: raise Exception('Invalid time interval\n') ############################################################################################## # SCRIPT START POINT ############################################################################################## # retrieve the item data item = Item(sys.stdin.read()) OUTPUT_FILE = os.path.join(WORK_DIR, 'dSSError.jpg') try: availableIntevals = [] availableIntevals.append( {'tparam':'5minutes', 'text':'Last 5 minutes of metetring data...', 'days':0, 'seconds':300, 'freq':'60'} ) availableIntevals.append( {'tparam':'hour', 'text':'Last hour of metering data...', 'days':0, 'seconds':3600, 'freq':'720'} ) availableIntevals.append( {'tparam':'day', 'text':'Last day of metering data...', 'days':1, 'seconds':0, 'freq':'14400'} ) availableIntevals.append( {'tparam':'week', 'text':'Last week of metering data...', 'days':7, 'seconds':0, 'freq':'86400'} ) availableIntevals.append( {'tparam':'month', 'text':'Last month of metering data...', 'days':31, 'seconds':0, 'freq':'432000'} ) availableIntevals.append( {'tparam':'year', 'text':'Last year of metering data...', 'days':365, 'seconds':0, 'freq':'5184000'} ) # retrieve the input params (from state field) state = item.getState() params = string.split(state,';') for param in params: pair = string.split(param,':') if len(pair) > 1: if pair[0] == "t": T_PARAM = pair[1] elif pair[0] == "dSS_ip": dSS_IP = pair[1] elif pair[0] == "dSS_port": dSS_PORT = pair[1] # retrieve the rest of the params for interval in availableIntevals: if interval['tparam'] == T_PARAM: T_TEXT = interval['text'] # text description of the interval T_DAYS = interval['days'] # number of days per interval (for > 1 day) T_SECONDS = interval['seconds'] # number of seconds per interval (for < 1 day) T_FREQINTERVAL = interval['freq'] # interval for each X-axis unit # choose the XML parser: # SAX #retriever = dssSaxMeteringRetriever( dSS_IP, dSS_PORT ) # ... or DOM try: retriever = dssMeteringRetriever( dSS_IP, dSS_PORT ) # request the data from dSS and parse it result = retriever.fetchInterval(T_PARAM) except: raise Exception() # result contains both: the data and the config params (such as contract, name, units, etc...) if result == {}: # metering request has failed sys.stderr.write('Metering request has failed - no data') #OUTPUT_FILE = os.path.join(WORK_DIR, 'box_logo.jpg') raise Exception() else: # metering request succeded - write data into a temp file scriptpath = '' tmpGraph = 0 tmp = 0 config = result['config'] try: tmp = tempfile.NamedTemporaryFile(delete=False) for sample in result['values']: ( ts , val ) = ( sample.keys()[0], sample.values()[0] ) tmp.write( ts + " " + val + "\n" ) tmp.close() except: sys.stderr.write('Could not write the metering information to a text file\n') raise Exception() try: tmpGraph = tempfile.NamedTemporaryFile(delete=False) generator = MeteringGraphGenerator( tmp.name, tmpGraph.name ) initializeGraphGenerator( config, generator ) scriptpath = generator.dumpToAFile() except: sys.stderr.write('Could not process gnuplot params\n') raise Exception() try: os.system("gnuplot \'"+scriptpath+"\'") except: sys.stderr.write('Could not execute gnuplot command with path: \'' + scriptpath + '\'\n') raise Exception() tmp.unlink(tmp.name) OUTPUT_FILE = os.path.join(WORK_DIR, 'dSSMetering.jpg') try: addLogo(tmpGraph.name, OUTPUT_FILE) except: sys.stderr.write('Could add logo to metering graph\n') raise Exception() tmpGraph.close() tmpGraph.unlink(tmpGraph.name) except: sys.stderr.write('dSS Metering script exit with exception\n') OUTPUT_FILE = os.path.join(WORK_DIR, 'dSSError.jpg') finally: # generate output item # item.setTitle("DSS Metering") # item.setDescription("Your toggle item is turned ON, press PLAY to turn it OFF.") item.setLocation(OUTPUT_FILE) sys.stdout.write(item.render())