3 # This file is part of NetStory.
4 # Author: Jere Malinen <jeremmalinen@gmail.com>
9 from datetime import datetime, timedelta
11 from PyQt4 import QtCore, QtGui
13 from netstory_ui import Ui_MainWindow
17 except ImportError, e:
18 print 'Windows testing: %s' % str(e)
21 class DataForm(QtGui.QMainWindow):
22 def __init__(self, parent=None):
23 QtGui.QWidget.__init__(self, parent)
24 self.ui = Ui_MainWindow()
28 self.ui.combo_box_max_rows.addItems(['100', '1000', '10000',
31 QtCore.QObject.connect(self.ui.combo_box_max_rows,
32 QtCore.SIGNAL('currentIndexChanged(QString)'),
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)
43 self.progress = QtGui.QProgressDialog('Please wait...',
45 self.progress.setWindowTitle('Generating tables')
47 # This is gives time for UI to show up before updating tables
48 self.timer = QtCore.QBasicTimer()
49 self.timer.start(100, self)
51 def timerEvent(self, event):
53 self.generate_traffic_tables()
55 def change_max_rows(self):
57 self.max_rows = int(self.ui.combo_box_max_rows.currentText())
59 self.max_rows = 999999999 # should be as good as unlimited
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.')
71 def show_db_info(self):
73 db_size = os.path.getsize(self.file)
75 QtGui.QMessageBox.about(self, 'Error', str(e))
78 size = str(db_size / 1000) + ' kB'
80 size = str(db_size) + ' B'
81 QtGui.QMessageBox.about(self, 'Database info',
82 'Records: %d\nSize: %s' % (len(self.datas) - 1, size))
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:
90 f = open(self.file, 'w')
92 download, upload = netstoryd.read_counters()
93 netstoryd.write_data(f, download, upload)
96 QtGui.QMessageBox.about(self, 'Error', str(e))
98 self.generate_traffic_tables()
100 def generate_traffic_tables(self):
101 self.file = settings.DATA
103 for i, value in [(1, 5), (2, 33), (3, 60), (4, 90), (5, 100)]:
105 if not self.read_data():
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.")
114 self._generate_hourly()
116 self._generate_daily()
118 self._generate_weekly()
119 self._generate_monthly()
120 self._generate_summary()
122 if self.progress.wasCanceled():
124 self.progress.setValue(value)
125 QtCore.QCoreApplication.processEvents()
127 self.progress.setValue(100)
128 self.progress.reset()
133 f = open(self.file, 'r')
135 QtCore.QCoreApplication.processEvents()
136 if self._if_canceled():
139 parts = line.split(',')
141 self.datas.append(TrafficLogLine(parts[0], parts[1],
144 print 'Error in: %s (%s)' % (self.file, str(e))
145 except ValueError, e:
146 print 'Error in: %s (%s)' % (self.file, str(e))
148 self._cancel_and_show_message('Error', str(e))
152 def _cancel_and_show_message(self, title, message):
153 self.progress.cancel()
154 QtGui.QMessageBox.about(self, title, message)
155 QtCore.QCoreApplication.processEvents()
157 def _if_canceled(self):
158 """Checks cheaply from long loop if Cancel was pushed."""
160 if self.loop % 500 == 0:
161 QtCore.QCoreApplication.processEvents()
162 if self.progress.wasCanceled():
166 def _append_latest_traffic_status(self):
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))
173 QtGui.QMessageBox.about(self, 'Problem', "Your N900 " \
174 "isn't currently probably compatible with NetStory " \
175 "(only PR1.2 is tested)")
177 print 'Windows testing: %s' % str(e)
179 def _generate_hourly(self):
181 for i, data in enumerate(self.datas[1:]):
182 if self._if_canceled():
184 traffic_row = TrafficRow()
185 traffic_row.calculate_between_log_lines(self.datas[i], data)
186 self.hourly.append(traffic_row)
188 table = self.ui.table_hourly
189 self._init_table(table, len(self.hourly))
191 for i, hour in enumerate(reversed(self.hourly[-self.max_rows:])):
192 if self._if_canceled():
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)')
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'),
204 # This is expensive operation if there are thousands of lines
205 self._set_table_row(table, i, hour)
207 def _generate_daily(self):
209 for hour in self.hourly:
210 if self._if_canceled():
212 key = hour.start_time.isocalendar()
213 self.daily[key] = self.daily.get(key, TrafficRow())
214 self.daily[key].add(hour)
216 table = self.ui.table_daily
217 self._init_table(table, len(self.daily))
219 keys = self.daily.keys()
222 for i, key in enumerate(reversed(keys[-self.max_rows:])):
223 if self._if_canceled():
225 day = self.daily[key]
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)
232 def _generate_weekly(self):
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)
242 table = self.ui.table_weekly
243 self._init_table(table, len(self.weekly))
245 keys = self.weekly.keys()
248 for i, key in enumerate(reversed(keys[-self.max_rows:])):
249 week = self.weekly[key]
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)
264 def _generate_monthly(self):
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)
271 table = self.ui.table_monthly
272 self._init_table(table, len(self.monthly))
274 keys = self.monthly.keys()
277 for i, key in enumerate(reversed(keys[-self.max_rows:])):
278 month = self.monthly[key]
280 month.set_representation()
281 month.set_description_cell(month.start_time.strftime('%Y: %B'), i)
282 self._set_table_row(table, i, month)
284 def _generate_summary(self):
285 table = self.ui.table_summary
286 self._init_table(table, 5)
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)
301 totals = self.calculate_total(self.monthly.itervalues())
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)
311 def _init_table(self, table, rows):
312 table.clearContents()
314 if rows < self.max_rows:
315 table.setRowCount(rows)
317 table.setRowCount(self.max_rows)
318 table.horizontalHeader().resizeSection(0, 315)
319 table.horizontalHeader().setVisible(True)
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))
335 def calculate_averages(self, traffic_rows=[]):
336 total = self.calculate_total(traffic_rows)
338 for key, l in total.items():
339 averages[key] = sum(l) / len(l)
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)
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())
363 self.download_bytes = 0
364 self.upload_bytes = 0
366 self.start_time = None
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, \
374 self.upload_bytes = self.traffic_difference(start_data.upload, \
377 self.set_representation()
379 def traffic_difference(self, start, end):
383 return end #This value is probably inaccurate compared to reality
386 self.total_bytes = self.download_bytes + self.upload_bytes
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)
393 def bytes_representation(self, number):
395 s = '%.1f MB' % round(number / 1000000.0, 1)
397 s = '%d kB' % round(number / 1000.0, 0)
399 s = '%d B' % (number)
402 def add(self, other):
404 Adds traffic values from other row into self
405 and also sets start and end times properly.
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
414 def set_description_cell(self, description, sort_key):
415 self.description = description
416 self.sort_key = sort_key
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
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
432 if __name__ == "__main__":
433 app = QtGui.QApplication(sys.argv)
434 dataform = DataForm()
436 sys.exit(app.exec_())