30c755455153eeabd9c7c7b57e5015f1ba007871
[netstory] / src / opt / netstory / netstory.py
1 #!/usr/bin/env python
2
3 # This file is part of NetStory.
4 # Author: Jere Malinen <jeremmalinen@gmail.com>
5
6
7 import sys
8 import os
9 from datetime import datetime, timedelta
10
11 from PyQt4 import QtCore, QtGui
12
13 from netstory_ui import Ui_MainWindow
14 import settings
15 try:
16     import netstoryd
17 except ImportError, e:
18     print 'Windows testing: %s' % str(e)
19
20
21 class DataForm(QtGui.QMainWindow):
22     def __init__(self, parent=None):
23         QtGui.QWidget.__init__(self, parent)
24         self.ui = Ui_MainWindow()
25         self.ui.setupUi(self)
26         
27         self.max_rows = 100
28         self.ui.combo_box_max_rows.addItems(['100', '1000', '10000', 
29                                              'unlimited'])
30         
31         QtCore.QObject.connect(self.ui.combo_box_max_rows, 
32             QtCore.SIGNAL('currentIndexChanged(QString)'), 
33             self.change_max_rows)
34         QtCore.QObject.connect(self.ui.button_reload, 
35             QtCore.SIGNAL('clicked()'), self.generate_traffic_tables)
36         QtCore.QObject.connect(self.ui.actionDatabaseInfo, 
37             QtCore.SIGNAL('triggered()'), self.show_db_info)
38         QtCore.QObject.connect(self.ui.actionEmptyDatabase, 
39             QtCore.SIGNAL('triggered()'), self.empty_db)
40         QtCore.QObject.connect(self.ui.actionAbout, 
41             QtCore.SIGNAL('triggered()'), self.show_about)
42             
43         self.progress = QtGui.QProgressDialog('Please wait...', 
44                                               'Stop', 0, 100, self)
45         self.progress.setWindowTitle('Generating tables')
46         
47         # This is gives time for UI to show up before updating tables
48         self.timer = QtCore.QBasicTimer()
49         self.timer.start(100, self)
50         
51     def timerEvent(self, event):
52         self.timer.stop()
53         self.generate_traffic_tables()
54     
55     def change_max_rows(self):
56         try:
57             self.max_rows = int(self.ui.combo_box_max_rows.currentText())
58         except ValueError:
59             self.max_rows = 999999999 # should be as good as unlimited
60     
61     def show_about(self):
62         QtGui.QMessageBox.about(self, 'About', 'NetStory consists of two '\
63             'parts: a daemon that records network data counters in '\
64             'background and this GUI application to view hourly, daily, '\
65             'weekly and monthly net traffics.\n\n'\
66             'Currently NetStory records '\
67             'only "Home network data counter".\n\nNote that some numbers '\
68             'might be inaccurate and probably will be if you change date '\
69             'or time or clear data counter.')
70             
71     def show_db_info(self):
72         try:
73             db_size = os.path.getsize(self.file)
74         except OSError, e:
75             QtGui.QMessageBox.about(self, 'Error', str(e))
76             return
77         if db_size > 1000:
78             size = str(db_size / 1000) + ' kB'
79         else:
80             size = str(db_size) + ' B'
81         QtGui.QMessageBox.about(self, 'Database info', 
82             'Records: %d\nSize: %s' % (len(self.datas) - 1, size))
83             
84     def empty_db(self):
85         reply = QtGui.QMessageBox.question(self, 'Confirmation',
86             "Are you absolutely sure that you want to empty database?", 
87             QtGui.QMessageBox.Yes, QtGui.QMessageBox.No)
88         if reply == QtGui.QMessageBox.Yes:
89             try:
90                 f = open(self.file, 'w')
91                 f.write('')
92                 download, upload = netstoryd.read_counters()
93                 netstoryd.write_data(f, download, upload)
94                 f.close()
95             except IOError, e:
96                 QtGui.QMessageBox.about(self, 'Error', str(e))
97                 return
98             self.generate_traffic_tables()
99             
100     def generate_traffic_tables(self):
101         self.file = settings.DATA
102         self.loop = 0
103         for i, value in [(1, 5), (2, 33), (3, 60), (4, 90), (5, 100)]:
104             if i == 2:
105                 print str(datetime.now()) + ' self.read_data()'
106                 if not self.read_data():
107                     break
108                 print str(datetime.now()) + ' ohi'
109                 self._append_latest_traffic_status()
110                 if len(self.datas) < 2:
111                     self._cancel_and_show_message('Try again later', 
112                     "Unfortunately there isn't enough data in the "\
113                     "database yet. Try again after few minutes.")
114                     break
115             elif i == 3:
116                 print str(datetime.now()) + ' self._generate_hourly()'
117                 self._generate_hourly()
118             elif i == 4:
119                 print str(datetime.now()) + ' self._generate_daily()'
120                 self._generate_daily()
121             elif i == 5:
122                 print str(datetime.now()) + ' self._generate_weekly()'
123                 self._generate_weekly()
124                 print str(datetime.now()) + ' self._generate_monthly()'
125                 self._generate_monthly()
126                 print str(datetime.now()) + ' self._generate_summary()'
127                 self._generate_summary()
128                 print str(datetime.now()) + ' ohi'
129                 
130             if self.progress.wasCanceled():
131                 break
132             self.progress.setValue(value)
133             QtCore.QCoreApplication.processEvents()
134             
135         self.progress.setValue(100)
136         self.progress.reset()
137                    
138     def read_data(self):
139         self.datas = []
140         try:
141             f = open(self.file, 'r')
142             for line in f:
143                 QtCore.QCoreApplication.processEvents()
144                 if self._if_canceled():
145                     return False
146                 if len(line) > 5:
147                     parts = line.split(',')
148                     try:
149                         self.datas.append(TrafficLogLine(parts[0], parts[1], 
150                                                         parts[2]))
151                     except TypeError, e:
152                         print 'Error in: %s (%s)' % (self.file, str(e))
153                     except ValueError, e:
154                         print 'Error in: %s (%s)' % (self.file, str(e))
155         except IOError, e:
156             self._cancel_and_show_message('Error', str(e))
157             return False
158         return True
159         
160     def _cancel_and_show_message(self, title, message):
161         self.progress.cancel()
162         QtGui.QMessageBox.about(self, title, message)
163         QtCore.QCoreApplication.processEvents()
164         
165     def _if_canceled(self):
166         """Checks cheaply from long loop if Cancel was pushed."""
167         self.loop += 1
168         if self.loop % 500 == 0:
169             QtCore.QCoreApplication.processEvents()
170             if self.progress.wasCanceled():
171                 return True
172         return False
173
174     def _append_latest_traffic_status(self):
175         try:
176             download, upload = netstoryd.read_counters()
177             if netstoryd.check(download) and netstoryd.check(upload):
178                 now = datetime.now().strftime(settings.DATA_TIME_FORMAT)
179                 self.datas.append(TrafficLogLine(now, download, upload))
180             else:
181                 QtGui.QMessageBox.about(self, 'Problem', "Your N900 " \
182                     "isn't currently probably compatible with NetStory " \
183                     "(only PR1.2 is tested)")
184         except NameError, e:
185             print 'Windows testing: %s' % str(e)
186             
187     def _generate_hourly(self):
188         self.hourly = []
189         for i, data in enumerate(self.datas[1:]):
190             if self._if_canceled():
191                 return
192             traffic_row = TrafficRow()
193             traffic_row.calculate_between_log_lines(self.datas[i], data)
194             self.hourly.append(traffic_row)
195         
196         table = self.ui.table_hourly
197         self._init_table(table, len(self.hourly))
198         
199         for i, hour in enumerate(reversed(self.hourly[-self.max_rows:])):
200             if self._if_canceled():
201                 return
202             if hour.start_time.day != hour.end_time.day and \
203                hour.end_time.hour != 0:
204                 # Phone has been off or there is some other reason why
205                 # end time date is different. Anyhow show end time with date.
206                 end_time = hour.end_time.strftime('%H:%M (%d.%m.%Y)')
207             else:
208                 end_time = hour.end_time.strftime('%H:%M')
209             hour.set_description_cell('%s - %s' % 
210                                 (hour.start_time.strftime('%d.%m.%Y %H:%M'), 
211                                 end_time), i)
212             # This is expensive operation if there are thousands of lines
213             self._set_table_row(table, i, hour)
214             
215     def _generate_daily(self):
216         self.daily = {}
217         for hour in self.hourly:
218             if self._if_canceled():
219                 return
220             key = hour.start_time.isocalendar()
221             self.daily[key] = self.daily.get(key, TrafficRow())
222             self.daily[key].add(hour)
223         
224         table = self.ui.table_daily
225         self._init_table(table, len(self.daily))
226         
227         keys = self.daily.keys()
228         keys.sort()
229         
230         for i, key in enumerate(reversed(keys[-self.max_rows:])):
231             if self._if_canceled():
232                 return
233             day = self.daily[key]
234             day.set_total()
235             day.set_representation()
236             day.set_description_cell(\
237                 day.start_time.strftime('%d.%m.%Y'), i)
238             self._set_table_row(table, i, day)
239             
240     def _generate_weekly(self):
241         self.weekly = {}
242         for day in self.daily.itervalues():
243             # Following works beatifully, 
244             # because: datetime(2011, 1, 1).isocalendar()[0] == 2010
245             key = '%d / %02d' % (day.start_time.isocalendar()[0], 
246                                         day.start_time.isocalendar()[1])
247             self.weekly[key] = self.weekly.get(key, TrafficRow())
248             self.weekly[key].add(day)
249         
250         table = self.ui.table_weekly
251         self._init_table(table, len(self.weekly))
252         
253         keys = self.weekly.keys()
254         keys.sort()
255         
256         for i, key in enumerate(reversed(keys[-self.max_rows:])):
257             week = self.weekly[key]
258             week.set_total()
259             week.set_representation()
260             if week.end_time.isocalendar()[1] != \
261                week.start_time.isocalendar()[1]: 
262                 # it's probably following situation: 
263                 # e.g. start time is 7.6.2010 0:00 (week 23) 
264                 # and end time is 14.6.2010 0:00 (week 24)
265                 week.end_time -= timedelta(days=1)
266             week.set_description_cell('%d (%s - %s)' % 
267                                 (week.start_time.isocalendar()[1], 
268                                 week.start_time.strftime('%d.%m'), 
269                                 week.end_time.strftime('%d.%m.%Y')), i)
270             self._set_table_row(table, i, week)
271             
272     def _generate_monthly(self):
273         self.monthly = {}
274         for day in self.daily.itervalues():
275             key = day.start_time.strftime('%Y %m')
276             self.monthly[key] = self.monthly.get(key, TrafficRow())
277             self.monthly[key].add(day)
278         
279         table = self.ui.table_monthly
280         self._init_table(table, len(self.monthly))
281         
282         keys = self.monthly.keys()
283         keys.sort()
284         
285         for i, key in enumerate(reversed(keys[-self.max_rows:])):
286             month = self.monthly[key]
287             month.set_total()
288             month.set_representation()
289             month.set_description_cell(month.start_time.strftime('%Y: %B'), i)
290             self._set_table_row(table, i, month)
291             
292     def _generate_summary(self):
293         table = self.ui.table_summary
294         self._init_table(table, 5)
295         
296         for i, string, traffic_rows in [(0, 'Hourly average', self.hourly), 
297                 (1, 'Daily average', self.daily.itervalues()), 
298                 (2, 'Weekly average', self.weekly.itervalues()), 
299                 (3, 'Monthly average', self.monthly.itervalues())]:
300             averages =  self.calculate_averages(traffic_rows)
301             average = TrafficRow()
302             average.download_bytes = averages['download']
303             average.upload_bytes = averages['upload']
304             average.total_bytes = averages['total']
305             average.set_representation()
306             average.set_description_cell(string, i)
307             self._set_table_row(table, i, average)
308         
309         totals = self.calculate_total(self.monthly.itervalues())
310         total = TrafficRow()
311         total.download_bytes = sum(totals['download'])
312         total.upload_bytes = sum(totals['upload'])
313         total.total_bytes = sum(totals['total'])
314         total.set_representation()
315         total.set_description_cell(\
316             self.datas[0].time.strftime('Total since %d.%m.%Y %H:%M'), 0)
317         self._set_table_row(table, 4, total)
318             
319     def _init_table(self, table, rows):
320         table.clearContents()
321         table.sortItems(0)
322         if rows < self.max_rows:
323             table.setRowCount(rows)
324         else:
325             table.setRowCount(self.max_rows)
326         table.horizontalHeader().resizeSection(0, 300)
327         table.horizontalHeader().setVisible(True)
328         
329     def _set_table_row(self, table, row_number, traffic_row):
330         table.setItem(row_number, 0, 
331                       SortTableWidgetItem(traffic_row.description, 
332                                           traffic_row.sort_key))
333         table.setItem(row_number, 1, 
334                       SortTableWidgetItem(traffic_row.download_string, 
335                                           traffic_row.download_bytes))
336         table.setItem(row_number, 2, 
337                       SortTableWidgetItem(traffic_row.upload_string, 
338                                           traffic_row.upload_bytes))
339         table.setItem(row_number, 3, 
340                       SortTableWidgetItem(traffic_row.total_string, 
341                                           traffic_row.total_bytes))
342     
343     def calculate_averages(self, traffic_rows=[]):
344         total = self.calculate_total(traffic_rows)
345         averages = {}
346         for key, l in total.items():
347             averages[key] = sum(l) / len(l)
348         return averages
349         
350     def calculate_total(self, traffic_rows=[]):
351         total = {'download': [], 'upload': [], 'total': []}
352         for traffic_row in traffic_rows:
353             total['download'].append(traffic_row.download_bytes)
354             total['upload'].append(traffic_row.upload_bytes)
355             total['total'].append(traffic_row.total_bytes)   
356         return total
357
358
359 class TrafficLogLine:
360     def __init__(self, time='', download='', upload=''):
361         #self.time = datetime.strptime(time.strip(), settings.DATA_TIME_FORMAT)
362         # this is about 4 times faster than above
363         self.time = datetime(int(time[0:4]), int(time[5:7]), int(time[8:10]), 
364                 int(time[11:13]), int(time[14:16]), int(time[17:19]))
365         self.download = int(download.strip())
366         self.upload = int(upload.strip())
367
368         
369 class TrafficRow:
370     def __init__(self):
371         self.download_bytes = 0
372         self.upload_bytes = 0
373         self.total_bytes = 0
374         self.start_time = None
375         self.end_time = None
376         
377     def calculate_between_log_lines(self, start_data, end_data):
378         self.start_time = start_data.time
379         self.end_time = end_data.time
380         self.download_bytes = self.traffic_difference(start_data.download, \
381                                                       end_data.download)
382         self.upload_bytes = self.traffic_difference(start_data.upload, \
383                                                     end_data.upload)
384         self.set_total()
385         self.set_representation()
386         
387     def traffic_difference(self, start, end):
388         if end >= start:
389             return end - start
390         else:
391             return end #This value is probably inaccurate compared to reality
392         
393     def set_total(self):
394         self.total_bytes = self.download_bytes + self.upload_bytes
395         
396     def set_representation(self):
397         self.download_string = self.bytes_representation(self.download_bytes)
398         self.upload_string = self.bytes_representation(self.upload_bytes)
399         self.total_string = self.bytes_representation(self.total_bytes)
400         
401     def bytes_representation(self, number):
402         s = str(number)
403         if len(s) > 6:
404             s = '%s.%s MB' % (s[0:-6], s[-5])
405         elif len(s) > 3:
406             s = '%s kB' % (s[0:-3])
407         else:
408             s = '%s B' % (s)
409         return s
410         
411     def add(self, other):
412         """
413         Adds traffic values from other row into self
414         and also sets start and end times properly.
415         """
416         self.download_bytes += other.download_bytes
417         self.upload_bytes += other.upload_bytes
418         if not self.start_time or other.start_time < self.start_time:
419             self.start_time = other.start_time
420         if not self.end_time or other.end_time > self.end_time:
421             self.end_time = other.end_time
422     
423     def set_description_cell(self, description, sort_key):
424         self.description = description
425         self.sort_key = sort_key
426
427
428 class SortTableWidgetItem(QtGui.QTableWidgetItem):
429     def __init__(self, text, sort_key):
430         # call custom constructor with UserType item type
431         QtGui.QTableWidgetItem.__init__(self, text, \
432                                         QtGui.QTableWidgetItem.UserType)
433         self.sort_key = sort_key
434
435     # Qt uses a simple < check for sorting items, 
436     # override this to use the sort_key
437     def __lt__(self, other):
438         return self.sort_key < other.sort_key
439
440
441 if __name__ == "__main__":
442     app = QtGui.QApplication(sys.argv)   
443     dataform = DataForm()
444     dataform.show()
445     sys.exit(app.exec_())