Increased length of the first column
[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                 if not self.read_data():
106                     break
107                 self._append_latest_traffic_status()
108                 if len(self.datas) < 2:
109                     self._cancel_and_show_message('Try again later', 
110                     "Unfortunately there isn't enough data in the "\
111                     "database yet. Try again after few minutes.")
112                     break
113             elif i == 3:
114                 self._generate_hourly()
115             elif i == 4:
116                 self._generate_daily()
117             elif i == 5:
118                 self._generate_weekly()
119                 self._generate_monthly()
120                 self._generate_summary()
121                 
122             if self.progress.wasCanceled():
123                 break
124             self.progress.setValue(value)
125             QtCore.QCoreApplication.processEvents()
126             
127         self.progress.setValue(100)
128         self.progress.reset()
129                    
130     def read_data(self):
131         self.datas = []
132         try:
133             f = open(self.file, 'r')
134             for line in f:
135                 QtCore.QCoreApplication.processEvents()
136                 if self._if_canceled():
137                     return False
138                 if len(line) > 5:
139                     parts = line.split(',')
140                     try:
141                         self.datas.append(TrafficLogLine(parts[0], parts[1], 
142                                                         parts[2]))
143                     except TypeError, e:
144                         print 'Error in: %s (%s)' % (self.file, str(e))
145                     except ValueError, e:
146                         print 'Error in: %s (%s)' % (self.file, str(e))
147         except IOError, e:
148             self._cancel_and_show_message('Error', str(e))
149             return False
150         return True
151         
152     def _cancel_and_show_message(self, title, message):
153         self.progress.cancel()
154         QtGui.QMessageBox.about(self, title, message)
155         QtCore.QCoreApplication.processEvents()
156         
157     def _if_canceled(self):
158         """Checks cheaply from long loop if Cancel was pushed."""
159         self.loop += 1
160         if self.loop % 500 == 0:
161             QtCore.QCoreApplication.processEvents()
162             if self.progress.wasCanceled():
163                 return True
164         return False
165
166     def _append_latest_traffic_status(self):
167         try:
168             download, upload = netstoryd.read_counters()
169             if netstoryd.check(download) and netstoryd.check(upload):
170                 now = datetime.now().strftime(settings.DATA_TIME_FORMAT)
171                 self.datas.append(TrafficLogLine(now, download, upload))
172             else:
173                 QtGui.QMessageBox.about(self, 'Problem', "Your N900 " \
174                     "isn't currently probably compatible with NetStory " \
175                     "(only PR1.2 is tested)")
176         except NameError, e:
177             print 'Windows testing: %s' % str(e)
178             
179     def _generate_hourly(self):
180         self.hourly = []
181         for i, data in enumerate(self.datas[1:]):
182             if self._if_canceled():
183                 return
184             traffic_row = TrafficRow()
185             traffic_row.calculate_between_log_lines(self.datas[i], data)
186             self.hourly.append(traffic_row)
187         
188         table = self.ui.table_hourly
189         self._init_table(table, len(self.hourly))
190         
191         for i, hour in enumerate(reversed(self.hourly[-self.max_rows:])):
192             if self._if_canceled():
193                 return
194             if hour.start_time.day != hour.end_time.day and \
195                hour.end_time.hour != 0:
196                 # Phone has been off or there is some other reason why
197                 # end time date is different. Anyhow show end time with date.
198                 end_time = hour.end_time.strftime('%H:%M (%d.%m.%Y)')
199             else:
200                 end_time = hour.end_time.strftime('%H:%M')
201             hour.set_description_cell('%s - %s' % 
202                                 (hour.start_time.strftime('%d.%m.%Y %H:%M'), 
203                                 end_time), i)
204             # This is expensive operation if there are thousands of lines
205             self._set_table_row(table, i, hour)
206             
207     def _generate_daily(self):
208         self.daily = {}
209         for hour in self.hourly:
210             if self._if_canceled():
211                 return
212             key = hour.start_time.isocalendar()
213             self.daily[key] = self.daily.get(key, TrafficRow())
214             self.daily[key].add(hour)
215         
216         table = self.ui.table_daily
217         self._init_table(table, len(self.daily))
218         
219         keys = self.daily.keys()
220         keys.sort()
221         
222         for i, key in enumerate(reversed(keys[-self.max_rows:])):
223             if self._if_canceled():
224                 return
225             day = self.daily[key]
226             day.set_total()
227             day.set_representation()
228             day.set_description_cell(\
229                 day.start_time.strftime('%d.%m.%Y'), i)
230             self._set_table_row(table, i, day)
231             
232     def _generate_weekly(self):
233         self.weekly = {}
234         for day in self.daily.itervalues():
235             # Following works beatifully, 
236             # because: datetime(2011, 1, 1).isocalendar()[0] == 2010
237             key = '%d / %02d' % (day.start_time.isocalendar()[0], 
238                                         day.start_time.isocalendar()[1])
239             self.weekly[key] = self.weekly.get(key, TrafficRow())
240             self.weekly[key].add(day)
241         
242         table = self.ui.table_weekly
243         self._init_table(table, len(self.weekly))
244         
245         keys = self.weekly.keys()
246         keys.sort()
247         
248         for i, key in enumerate(reversed(keys[-self.max_rows:])):
249             week = self.weekly[key]
250             week.set_total()
251             week.set_representation()
252             if week.end_time.isocalendar()[1] != \
253                week.start_time.isocalendar()[1]: 
254                 # it's probably following situation: 
255                 # e.g. start time is 7.6.2010 0:00 (week 23) 
256                 # and end time is 14.6.2010 0:00 (week 24)
257                 week.end_time -= timedelta(days=1)
258             week.set_description_cell('%d (%s - %s)' % 
259                                 (week.start_time.isocalendar()[1], 
260                                 week.start_time.strftime('%d.%m'), 
261                                 week.end_time.strftime('%d.%m.%Y')), i)
262             self._set_table_row(table, i, week)
263             
264     def _generate_monthly(self):
265         self.monthly = {}
266         for day in self.daily.itervalues():
267             key = day.start_time.strftime('%Y %m')
268             self.monthly[key] = self.monthly.get(key, TrafficRow())
269             self.monthly[key].add(day)
270         
271         table = self.ui.table_monthly
272         self._init_table(table, len(self.monthly))
273         
274         keys = self.monthly.keys()
275         keys.sort()
276         
277         for i, key in enumerate(reversed(keys[-self.max_rows:])):
278             month = self.monthly[key]
279             month.set_total()
280             month.set_representation()
281             month.set_description_cell(month.start_time.strftime('%Y: %B'), i)
282             self._set_table_row(table, i, month)
283             
284     def _generate_summary(self):
285         table = self.ui.table_summary
286         self._init_table(table, 5)
287         
288         for i, string, traffic_rows in [(0, 'Hourly average', self.hourly), 
289                 (1, 'Daily average', self.daily.itervalues()), 
290                 (2, 'Weekly average', self.weekly.itervalues()), 
291                 (3, 'Monthly average', self.monthly.itervalues())]:
292             averages =  self.calculate_averages(traffic_rows)
293             average = TrafficRow()
294             average.download_bytes = averages['download']
295             average.upload_bytes = averages['upload']
296             average.total_bytes = averages['total']
297             average.set_representation()
298             average.set_description_cell(string, i)
299             self._set_table_row(table, i, average)
300         
301         totals = self.calculate_total(self.monthly.itervalues())
302         total = TrafficRow()
303         total.download_bytes = sum(totals['download'])
304         total.upload_bytes = sum(totals['upload'])
305         total.total_bytes = sum(totals['total'])
306         total.set_representation()
307         total.set_description_cell(\
308             self.datas[0].time.strftime('Total since %d.%m.%Y %H:%M'), 0)
309         self._set_table_row(table, 4, total)
310             
311     def _init_table(self, table, rows):
312         table.clearContents()
313         table.sortItems(0)
314         if rows < self.max_rows:
315             table.setRowCount(rows)
316         else:
317             table.setRowCount(self.max_rows)
318         table.horizontalHeader().resizeSection(0, 315)
319         table.horizontalHeader().setVisible(True)
320         
321     def _set_table_row(self, table, row_number, traffic_row):
322         table.setItem(row_number, 0, 
323                       SortTableWidgetItem(traffic_row.description, 
324                                           traffic_row.sort_key))
325         table.setItem(row_number, 1, 
326                       SortTableWidgetItem(traffic_row.download_string, 
327                                           traffic_row.download_bytes))
328         table.setItem(row_number, 2, 
329                       SortTableWidgetItem(traffic_row.upload_string, 
330                                           traffic_row.upload_bytes))
331         table.setItem(row_number, 3, 
332                       SortTableWidgetItem(traffic_row.total_string, 
333                                           traffic_row.total_bytes))
334     
335     def calculate_averages(self, traffic_rows=[]):
336         total = self.calculate_total(traffic_rows)
337         averages = {}
338         for key, l in total.items():
339             averages[key] = sum(l) / len(l)
340         return averages
341         
342     def calculate_total(self, traffic_rows=[]):
343         total = {'download': [], 'upload': [], 'total': []}
344         for traffic_row in traffic_rows:
345             total['download'].append(traffic_row.download_bytes)
346             total['upload'].append(traffic_row.upload_bytes)
347             total['total'].append(traffic_row.total_bytes)   
348         return total
349
350
351 class TrafficLogLine:
352     def __init__(self, time='', download='', upload=''):
353         #self.time = datetime.strptime(time.strip(), settings.DATA_TIME_FORMAT)
354         # this is about 4 times faster than above
355         self.time = datetime(int(time[0:4]), int(time[5:7]), int(time[8:10]), 
356                 int(time[11:13]), int(time[14:16]), int(time[17:19]))
357         self.download = int(download.strip())
358         self.upload = int(upload.strip())
359
360         
361 class TrafficRow:
362     def __init__(self):
363         self.download_bytes = 0
364         self.upload_bytes = 0
365         self.total_bytes = 0
366         self.start_time = None
367         self.end_time = None
368         
369     def calculate_between_log_lines(self, start_data, end_data):
370         self.start_time = start_data.time
371         self.end_time = end_data.time
372         self.download_bytes = self.traffic_difference(start_data.download, \
373                                                       end_data.download)
374         self.upload_bytes = self.traffic_difference(start_data.upload, \
375                                                     end_data.upload)
376         self.set_total()
377         self.set_representation()
378         
379     def traffic_difference(self, start, end):
380         if end >= start:
381             return end - start
382         else:
383             return end #This value is probably inaccurate compared to reality
384         
385     def set_total(self):
386         self.total_bytes = self.download_bytes + self.upload_bytes
387         
388     def set_representation(self):
389         self.download_string = self.bytes_representation(self.download_bytes)
390         self.upload_string = self.bytes_representation(self.upload_bytes)
391         self.total_string = self.bytes_representation(self.total_bytes)
392         
393     def bytes_representation(self, number):
394         if number > 999999:
395             s = '%.1f MB' % round(number / 1000000.0, 1)
396         elif number > 999:
397             s = '%d kB' % round(number / 1000.0, 0)
398         else:
399             s = '%d B' % (number)
400         return s
401         
402     def add(self, other):
403         """
404         Adds traffic values from other row into self
405         and also sets start and end times properly.
406         """
407         self.download_bytes += other.download_bytes
408         self.upload_bytes += other.upload_bytes
409         if not self.start_time or other.start_time < self.start_time:
410             self.start_time = other.start_time
411         if not self.end_time or other.end_time > self.end_time:
412             self.end_time = other.end_time
413     
414     def set_description_cell(self, description, sort_key):
415         self.description = description
416         self.sort_key = sort_key
417
418
419 class SortTableWidgetItem(QtGui.QTableWidgetItem):
420     def __init__(self, text, sort_key):
421         # call custom constructor with UserType item type
422         QtGui.QTableWidgetItem.__init__(self, text, \
423                                         QtGui.QTableWidgetItem.UserType)
424         self.sort_key = sort_key
425
426     # Qt uses a simple < check for sorting items, 
427     # override this to use the sort_key
428     def __lt__(self, other):
429         return self.sort_key < other.sort_key
430
431
432 if __name__ == "__main__":
433     app = QtGui.QApplication(sys.argv)   
434     dataform = DataForm()
435     dataform.show()
436     sys.exit(app.exec_())