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