pyP2Monitor  1.0.0
Monitor a P2 furnace activity reading data on serial port
 All Data Structures Namespaces Functions Variables Groups Pages
p2data.py
1 # -*- coding: utf-8 -*-#
2 
3 # Copyright 2013, 2014 Weber Yann, Weber Laurent
4 #
5 # This file is part of pyP2Monitor.
6 #
7 # pyP2Monitor is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU General Public License as published by
9 # the Free Software Foundation, either version 3 of the License, or
10 # (at your option) any later version.
11 #
12 # pyP2Monitor is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU General Public License for more details.
16 #
17 # You should have received a copy of the GNU General Public License
18 # along with pyP2Monitor. If not, see <http://www.gnu.org/licenses/>.
19 #
20 
21 
22 ##@package p2data Store functions and object handling P2's data processing
23 #
24 # Stores functions used to format raw data from the furnace
25 
26 
27 import time
28 import datetime
29 import json
30 import logging
31 import tempfile
32 
33 
34 import os, string, tempfile, types, sys, time
35 
36 import p2msg
37 import p2dbstore
38 import p2data
39 import utils
40 
41 ##Use to log
42 #@see utils.getLogger()
43 logger = utils.getLogger()
44 
45 ##Class used to store a range of data in tuples like (time, value)
46 #
47 # This class is used by pyP2DataReader to store query result.
48 # @ingroup msgprocess
49 class P2Query:
50 
51  ##Instanciate a new P2Query object
52  #
53  # @param dateFormat The format used to convert beg and end
54  # @param colNum The data's column number handled by this object
55  # @param beg A timestamp representing the lower handled date and time
56  # @param end A timestamp representing the biggest handled date and time
57  # @param time Used if one of param beg or end is set to None. Represent length of the time range.
58  def __init__(self,dateFormat, colNum, firstTs, beg=None, end=None, time=None, ):
59 
60  ##The higher date and time of the date range. Stored as a timestamp.
61  self.beg = None
62  ##The smaller date and time of the date range. Stored as a timestamp.
63  self.end = None
64  ##A dict storing query's data. Keys are timestamp, and value are data
65  self.datas=dict()
66  ##Store the column number
67  self.colNum=int(colNum)
68 
69 
70  #store the first timestamp
71  if beg != None:
72  if beg == 'first':
73  self.beg = firstTs
74  else:
75  if dateFormat == 'diff':
76  secs=P2Query.fromInterval(beg)
77  self.beg = datetime.datetime.now()
78  self.beg = int(self.beg.strftime("%s")) + secs
79  else:
80  self.beg=datetime.strptime(beg,dateFormat)
81  self.beg = int(self.beg.strftime("%s"))
82 
83  #store the last timestamp
84  if end != None:
85  if end == 'now':
86  self.end=datetime.datetime.now()
87  self.end = int(self.end.strftime("%s"))
88  else:
89  if dateFormat == 'diff':
90  secs=P2Query.fromInterval(end)
91  self.end = datetime.datetime.now()
92  self.end = int(self.end.strftime("%s")) + secs
93  else:
94  self.end=datetime.strptime(end,dateFormat)
95  self.end = int(self.end.strftime("%s"))
96 
97  #Calculate from time interval if needed
98  if time != None:
99  secs=P2Query.fromInterval(time)
100 
101  if beg == None and not end == None:
102  self.beg = self.end + secs
103 
104  elif end == None and not beg == None:
105  self.end = self.beg + secs
106 
107  #Check that date are correct
108  if self.end<self.beg:
109  logger.critical('End date is smaller than begin date')
110  exit(1)
111 
112  pass
113 
114  ##Return a number of seconds from an interval
115  #
116  # Return a number of seconds from a interval composed by a signed integer and a suffix.
117  # Allowed suffixes are 's' for seconds, 'm' for minutes, 'h' for hours and 'd' for days
118  #
119  #@param interval A string representing a time interval
120  #@return The number of seconds represented by this interval
121  @staticmethod
122  def fromInterval(interval):
123 
124  secs = 0
125  unit = interval[len(interval)-1]
126  secs=int(interval[:-1])
127 
128  if unit == 's':
129  secs = secs
130  elif unit == 'm':
131  secs *= 60
132  elif unit == 'h':
133  secs *= 3600
134  elif unit == 'd':
135  secs *= 3600 * 24
136  else:
137  logger.critical('Invalid time interval : '+time)
138  exit(1)
139 
140  return secs
141 
142  ##Set the data's content of the query object
143  # Set the content with an array of the form [[timestamp0,val0],[timestamp1,val1], ... ]
144  # with val the array of value associated with a data
145  #
146  #@param content An array storing arrays of the form [timestamp,val]
147  #@return True
148  def setContent(self, content):
149  self.datas=dict()
150  for (ts,vals) in content:
151  self.datas[ts] = vals[self.colNum]
152  return True
153 
154 
155  ##Return a value given a timestamp
156  #
157  #@param timestamp The wanted timestamp
158  #@return An integer value or False if there is no data's associated with this timestamp
159  def getVal(self,timestamp):
160  if timestamp not in self.datas:
161  return False
162  else:
163  return self.datas[timestamp]
164 
165 
166  ##Return the first timestamp ( P2Query::beg )
167  def getBeg(self):
168  return self.beg
169 
170  ##Return the last timestamp ( P2Query::end )
171  def getEnd(self):
172  return self.end
173 
174  def getKeys(self):
175  return self.datas.keys();
176 
177 
178 
179 ##Used to get datas from queries
180 #
181 # This object handle multiple query's and make request to the Sqlite database.
182 # @ingroup msgprocess
183 class P2Datas:
184 
185  ##Alias for one letter query parameter to string parameter
186  argsShort = {'b' : 'begin', 'e' : 'end', 't' : 'time', 'f' : 'format', 'd':'data', 'n':'num', 'y':'yaxe', 's':'style', 'c':'color', 'l':'label', 'sc':'scale', 'a':'add' }
187  ##List of argument that have to be set to None after the P2Query creation
188  argsToNone = ['begin','end','time']
189 
190  ##Instanciate a new P2Datas object
191  #
192  #@param dbFile is the sqlite db file
193  #@param queries is a query string array
194  #@param queryArgSep is the separator between two query's argument
195  def __init__(self, dbFile, queries, queryArgSep):
196 
197  ##The Sqlite database object
198  self.db = p2dbstore.P2DbStore(dbFile)
199  ##The query list
200  self.queries = []
201  ##The larger range between a begin and a end in seconds
202  self.maxDiff=0
203  ##Set to True if every handled query have the same date range
204  self.sameRange=True
205  ##Store query's arguments
206  self.qArgs = []
207  ##Store the GnuPlot's data temporary file name
208  self.tmpfile = None
209  first = True
210 
211  #oldest data's timestamp
212  firstTs = self.db.getFirst()
213 
214  for query in queries:
215 
216  args = P2Datas.queryToDict(query, queryArgSep)
217  self.qArgs.append(args)
218  if 'format' not in args:
219  print >> sys.stderr, 'Error, no date format specified in query '+query
220  exit(1)
221  elif 'num' not in args:
222  print >> sys.stderr, 'Error no data id number specified in query '+query
223  self.queries.append(P2Query(args['format'], args['num'], firstTs, args['begin'],args['end'],args['time']))
224  curq = self.queries[-1:][0]
225  diff = curq.getEnd() - curq.getBeg()
226  if diff > self.maxDiff:
227  self.maxDiff = diff
228  if first:
229  first = False
230  base = [curq.getBeg(),curq.getEnd()]
231  else:
232  if base[0] != curq.getBeg() or base[1] != curq.getEnd():
233  self.sameRange = False
234 
235  pass
236 
237  ##Return a dict of "option"=>"value" from a query string
238  #
239  #@param query The query string
240  #@param sep The query's argument separator
241  #@return A dict representing each argument as 'option'=>'value'
242  @staticmethod
243  def queryToDict(query, sep):
244  spl = query.split(sep)
245  res = dict()
246  for s in spl:
247  spl2 = s.split('=',1)
248  name = spl2[0]
249  if name in P2Datas.argsShort:
250  name = P2Datas.argsShort[name]
251  res[name] = spl2[1]
252 
253  for n in P2Datas.argsToNone:
254  if n not in res:
255  res[n] = None
256  return res
257 
258 
259  ##Fill a query with datas
260  #
261  #@param datas An array of data
262  #@param queries An array of query to fill
263  @staticmethod
264  def fillQuery(datas,queries):
265  #format content
266  content = []
267  for data in datas:
268  dataList = p2msg.P2Msg.hex2list(data[1])
269  if len(dataList) != 48:
270  logger.warning('Data as not the waited length ('+str(len(dataList))+' \''+data[1]+'\'')
271  else:
272  dataList = data2List(data[0], dataList )
273  content.append([data[0],dataList])
274 
275  #fill queries
276  for query in queries:
277  query.setContent(content)
278 
279  ##Trigger the queries filling with data from the database
280  #
281  # When called this function tells to the P2Datas object to get datas from the database and to fill its handled P2Query object.
282  def populate(self):
283 
284  if self.sameRange:
285  #Get th data
286  datas = self.db.getData(self.queries[0].getBeg(),self.queries[0].getEnd())
287  P2Datas.fillQuery(datas,self.queries)
288  else:
289  filled = []
290  """The goal is to look for queries with same begin and end
291  to make only one db query per group"""
292  for i in range(len(self.queries)):
293  if i not in filled:
294  tofill=[i]
295  #Look for same range
296  for j in range(i,len(self.queries)):
297  if self.queries[j].getBeg() == self.queries[i].getBeg() and self.queries[j].getEnd() == self.queries[i].getEnd():
298  tofill.append(i)
299  #Fill the group
300  filled += tofill
301  datas = self.db.getData(self.queries[i].getBeg(),self.queries[i].getEnd())
302  tmp = tofill
303  tofill = []
304  for t in tmp:
305  tofill.append(self.queries[t])
306  P2Datas.fillQuery(datas, tofill)
307  pass
308 
309 
310  ##Write GnuPlot's datas temporary file
311  #
312  # Use handled query to create and populate GnuPlot's temporary files.
313  def getPlotData(self, csv = False, outfd = None, sep = None):
314 
315  if not csv:
316  if self.tmpfile != None:
317  for t in self.tmpfile:
318  t.close()
319  self.tmpfile = None
320 
321  self.tmpfile = []
322  tmpfd = []
323  for i in range(len(self.queries)):
324  self.tmpfile.append(tempfile.NamedTemporaryFile('w+b',-1,'pyP2gnuplotdatas'))
325  tmpfd=self.tmpfile
326 
327  #Creating a dataSet foreach query
328 
329 
330  #Formating datas from P2Query
331  datasRes = []
332 
333  #Processing optimisation if same range
334  if self.sameRange:
335  trange = sorted(self.queries[0].getKeys())
336  else:
337  trange = range(0,self.maxDiff)
338 
339  for t in trange:
340 
341  #vars for csv output
342  dataBuff = ''
343  okData = False
344 
345  #Then put data
346  for i in range(len(self.queries)):
347 
348  query = self.queries[i] #the query
349  args = self.qArgs[i] #The query args
350 
351  ts = t
352  if not self.sameRange:
353  ts = ts + query.getBeg()
354  ts = int(ts)
355 
356  #Retrieving scale and correction
357  if 'add' in args:
358  add = args['add']
359  else:
360  add=0
361  if 'scale' in args:
362  scale = args['scale']
363  else:
364  scale = 1
365  val = query.getVal(ts)
366 
367 
368  if val is not False:
369  if csv:
370  #csv formating
371  okData = True
372  dataBuff+=sep+str(float(val)*float(scale)+float(add))
373  else:
374  #gnuplot formating
375  tmpfd[i].write(str(ts)+' '+str(float(val)*float(scale)+float(add))+'\n')
376  tmpfd[i].flush()
377  elif csv:
378  dataBuff+=sep
379 
380  if csv and okData:
381  #csv line output
382  outfd.write(str(ts)+dataBuff+'\n');
383  pass
384 
385  ##Return the GnuPlot's plot command with the good arguments
386  #
387  #@return A string representing a GnuPlot's plot command
388  def getPlotCommand(self):
389 
390  first = True
391  res = ''
392 
393  for i in range(len(self.queries)):
394  query = self.queries[i] #the query
395  args = self.qArgs[i] #The query args
396  if first:
397  first = False
398  res += 'plot '
399  else:
400  res += ' , '
401 
402  if self.tmpfile == None:
403  self.getPlotData()
404 
405  res += '"'+self.tmpfile[i].name+'" using 1:2 '
406 
407  if 'label' in args:
408  label = args['label']+' '
409  else:
410  label = colNames()[int(args['num'])]+' '
411 
412  #Retrieving scale and correction
413  if 'add' in args:
414  add = args['add']
415  else:
416  add=0
417  if 'scale' in args:
418  scale = args['scale']
419  else:
420  scale = 1
421 
422  #Adding scale and correction to label
423  if scale != 1.0:
424  label += '*'+str(scale)
425  if add != 0:
426  if add > 0:
427  label += '+'
428  label += str(add)
429 
430  if 'style' in args:
431  style = args['style']
432  else:
433  style = 'points'
434 
435  if 'yaxe' in args and args['yaxe'] == '2':
436  res += ' axes x1y2 '
437  label += '(y2)'
438  else:
439  res += 'axes x1y1 '
440 
441  res += 'title "'+label+'" '
442  res += ' with '+style+' '
443  if 'color' in args:
444  res += 'lt rgb "'+args['color']+'" '
445 
446  res += '\n'
447  logger.debug('Plot command : '+res)
448  return res
449 
450  ##Return the time format to use giving the queries
451  #
452  # Find a good dateTimeFormat for GnuPlot's date display giving the date range of each query
454  if not self.sameRange:
455  return False
456  inFmt = '%s'
457  outFmt = '%S'
458 
459  if self.maxDiff > 60 * 5:
460  outFmt = '%H:%M:%s'
461  #outFmt = '%S'
462  if self.maxDiff > 3600 * 24:
463  outFmt = '%d-%m %H:%M'
464 
465 
466  return (inFmt,outFmt)
467 
468 
469 ##Take data and return a well formated integer array
470 #
471 # Take a string representing a huge hexadecimal number (the data field of a data frame from the furnace)
472 # and format it as an integer array applying number correction on some fields
473 #
474 #@param timestamp The timestamp to associate with this datas
475 #@param data The datas
476 #@param dateFormat The date's display format
477 #@return An integer array (except for the first item wich is a date as a string)
478 def data2List(timestamp, data, dateFormat="%Y/%m/%d_%H:%M:%S"):
479  res = []
480  date = datetime.datetime.fromtimestamp(float(timestamp))
481 
482  #Adding timestamp
483  res.append(date.strftime(dateFormat))
484  #Adding datas
485  """
486  for d in data:
487  res.append(d)
488  """
489  for i in range(0,len(data),2):
490  res.append(data[i]*0x100+data[i+1])
491  #Applying number corrections on datas
492  res[5] /= 2.0
493  res[12] /= 10.0
494  res[14] *= 0.0029
495  res[15] /= 2.0
496 
497  if res[16] & 0x8000 != 0:
498  #negative temperature
499  res[16] = res[16] - ( 1 << 16 ) #twos complement on 16 bits
500  res[16] /= 2.0
501 
502  res[17] /= 2.0
503  res[18] /= 2.0
504  res[24] /= 2.0
505 
506  return res
507 
508 
509 ##Return an array with data column's name
510 #
511 # Return an array with P2 furnace data's column's name.
512 #
513 #@see data2List
514 def colNames():
515  res = []
516 
517  res.append("Date et heure")
518  res.append("a")
519  res.append("Etat")
520  res.append("c")
521  res.append("d")
522  res.append("Temp chaudiere")
523  res.append("Temp fumee")
524  res.append("Temp gaz brules")
525  res.append("Puissance momentanee")
526  res.append("Ventil. depart")
527  res.append("Ventil. air combustion")
528  res.append("Alimentation")
529  res.append("O2 residuel")
530  res.append("Regulation O2")
531  res.append("Pellets restants (kg)")
532  res.append("o")
533  res.append("Temp exterieur")
534  res.append("Temp consigne depart 1")
535  res.append("Temp depart 1")
536  res.append("s")
537  res.append("t")
538  res.append("Demarages")
539  res.append("Duree fonctionnement (h)")
540  res.append("Temp tableau")
541  res.append("Consigne temp chaudiere")
542  res.append("y")
543  res.append("z")
544 
545  return res
546 
547 ##Return a json string from datas (OBSOLETE)
548 def datas2Json(datas):
549 
550  res = [colNames()]
551 
552  for data in datas:
553  dataList = p2msg.P2Msg.hex2list(data[1])
554  if len(dataList) == 48:
555  res.append(data2List(data[0], dataList ))
556 
557  return json.dumps(res)
558 
559 ##Dump the database in csv format
560 #
561 # @param filename The filename to write csv in. If - output to stdout
562 # @param headers If true put a header with colnames
563 #
564 # @return a string representing the db dump in csv format
565 
566 def csvDump(dbname, filename = '-', header = True, sep="; "):
567 
568  db = p2dbstore.P2DbStore(dbname)
569 
570  fd = None
571 
572  if filename == '-':
573  fdout = sys.stdout
574  else:
575  fdout = open(filename, "w+")
576 
577  #put header
578  if header:
579  hnames = colNames()
580  for i in range(len(hnames)):
581  fdout.write(hnames[i])
582  if i < len(hnames)-1:
583  fdout.write(sep)
584  fdout.write("\n")
585 
586  datas = db.getLastData() #fetch all datas
587 
588  for (timestamp,data) in datas:
589  csvOutputData(fdout, timestamp, data, sep)
590 
591 ##Print the last data on stdout formated in csv
592 #
593 # @param lfname The name of the file storing the latest data
594 # @param sep The csv field separator
595 def csvLastDataDump(lfname, sep = ";"):
596  ts = ""
597  data = ""
598 
599  while len(ts) == 0 or len(data) == 0:
600  lfile = open(lfname, "r")
601  ts = lfile.readline().strip("\n")
602  data = lfile.readline().strip("\n")
603 
604  #put header
605  hnames = colNames()
606  for i in range(len(hnames)):
607  sys.stdout.write(hnames[i])
608  if i < len(hnames)-1:
609  sys.stdout.write(sep)
610  sys.stdout.write("\n")
611 
612  csvOutputData(sys.stdout,ts, data, sep)
613 
614 ##Output data in csv format
615 #
616 # @param fdout The file where we will write the data
617 # @param data The datas
618 # @param sep The csv separator
619 def csvOutputData(fdout, timestamp, data, sep):
620  dataList = p2msg.P2Msg.hex2list(data)
621 
622  if len(dataList) != 48:
623  #bad len
624  logger.warning("Bad data length "+str(len(dataList))+" : '"+(str(timestamp)+" "+data)+"'")
625  else:
626  dataNums = data2List(timestamp, dataList)
627  for i in range(len(dataNums)):
628  fdout.write(str(dataNums[i]))
629  if i < len(dataNums)-1:
630  fdout.write(sep)
631  fdout.write("\n")
632 
633