Initial release
[netstory] / src / opt / netstory / netstory.py
diff --git a/src/opt/netstory/netstory.py b/src/opt/netstory/netstory.py
new file mode 100644 (file)
index 0000000..30c7554
--- /dev/null
@@ -0,0 +1,445 @@
+#!/usr/bin/env python
+
+# This file is part of NetStory.
+# Author: Jere Malinen <jeremmalinen@gmail.com>
+
+
+import sys
+import os
+from datetime import datetime, timedelta
+
+from PyQt4 import QtCore, QtGui
+
+from netstory_ui import Ui_MainWindow
+import settings
+try:
+    import netstoryd
+except ImportError, e:
+    print 'Windows testing: %s' % str(e)
+
+
+class DataForm(QtGui.QMainWindow):
+    def __init__(self, parent=None):
+        QtGui.QWidget.__init__(self, parent)
+        self.ui = Ui_MainWindow()
+        self.ui.setupUi(self)
+        
+        self.max_rows = 100
+        self.ui.combo_box_max_rows.addItems(['100', '1000', '10000', 
+                                             'unlimited'])
+        
+        QtCore.QObject.connect(self.ui.combo_box_max_rows, 
+            QtCore.SIGNAL('currentIndexChanged(QString)'), 
+            self.change_max_rows)
+        QtCore.QObject.connect(self.ui.button_reload, 
+            QtCore.SIGNAL('clicked()'), self.generate_traffic_tables)
+        QtCore.QObject.connect(self.ui.actionDatabaseInfo, 
+            QtCore.SIGNAL('triggered()'), self.show_db_info)
+        QtCore.QObject.connect(self.ui.actionEmptyDatabase, 
+            QtCore.SIGNAL('triggered()'), self.empty_db)
+        QtCore.QObject.connect(self.ui.actionAbout, 
+            QtCore.SIGNAL('triggered()'), self.show_about)
+            
+        self.progress = QtGui.QProgressDialog('Please wait...', 
+                                              'Stop', 0, 100, self)
+        self.progress.setWindowTitle('Generating tables')
+        
+        # This is gives time for UI to show up before updating tables
+        self.timer = QtCore.QBasicTimer()
+        self.timer.start(100, self)
+        
+    def timerEvent(self, event):
+        self.timer.stop()
+        self.generate_traffic_tables()
+    
+    def change_max_rows(self):
+        try:
+            self.max_rows = int(self.ui.combo_box_max_rows.currentText())
+        except ValueError:
+            self.max_rows = 999999999 # should be as good as unlimited
+    
+    def show_about(self):
+        QtGui.QMessageBox.about(self, 'About', 'NetStory consists of two '\
+            'parts: a daemon that records network data counters in '\
+            'background and this GUI application to view hourly, daily, '\
+            'weekly and monthly net traffics.\n\n'\
+            'Currently NetStory records '\
+            'only "Home network data counter".\n\nNote that some numbers '\
+            'might be inaccurate and probably will be if you change date '\
+            'or time or clear data counter.')
+            
+    def show_db_info(self):
+        try:
+            db_size = os.path.getsize(self.file)
+        except OSError, e:
+            QtGui.QMessageBox.about(self, 'Error', str(e))
+            return
+        if db_size > 1000:
+            size = str(db_size / 1000) + ' kB'
+        else:
+            size = str(db_size) + ' B'
+        QtGui.QMessageBox.about(self, 'Database info', 
+            'Records: %d\nSize: %s' % (len(self.datas) - 1, size))
+            
+    def empty_db(self):
+        reply = QtGui.QMessageBox.question(self, 'Confirmation',
+            "Are you absolutely sure that you want to empty database?", 
+            QtGui.QMessageBox.Yes, QtGui.QMessageBox.No)
+        if reply == QtGui.QMessageBox.Yes:
+            try:
+                f = open(self.file, 'w')
+                f.write('')
+                download, upload = netstoryd.read_counters()
+                netstoryd.write_data(f, download, upload)
+                f.close()
+            except IOError, e:
+                QtGui.QMessageBox.about(self, 'Error', str(e))
+                return
+            self.generate_traffic_tables()
+            
+    def generate_traffic_tables(self):
+        self.file = settings.DATA
+        self.loop = 0
+        for i, value in [(1, 5), (2, 33), (3, 60), (4, 90), (5, 100)]:
+            if i == 2:
+                print str(datetime.now()) + ' self.read_data()'
+                if not self.read_data():
+                    break
+                print str(datetime.now()) + ' ohi'
+                self._append_latest_traffic_status()
+                if len(self.datas) < 2:
+                    self._cancel_and_show_message('Try again later', 
+                    "Unfortunately there isn't enough data in the "\
+                    "database yet. Try again after few minutes.")
+                    break
+            elif i == 3:
+                print str(datetime.now()) + ' self._generate_hourly()'
+                self._generate_hourly()
+            elif i == 4:
+                print str(datetime.now()) + ' self._generate_daily()'
+                self._generate_daily()
+            elif i == 5:
+                print str(datetime.now()) + ' self._generate_weekly()'
+                self._generate_weekly()
+                print str(datetime.now()) + ' self._generate_monthly()'
+                self._generate_monthly()
+                print str(datetime.now()) + ' self._generate_summary()'
+                self._generate_summary()
+                print str(datetime.now()) + ' ohi'
+                
+            if self.progress.wasCanceled():
+                break
+            self.progress.setValue(value)
+            QtCore.QCoreApplication.processEvents()
+            
+        self.progress.setValue(100)
+        self.progress.reset()
+                   
+    def read_data(self):
+        self.datas = []
+        try:
+            f = open(self.file, 'r')
+            for line in f:
+                QtCore.QCoreApplication.processEvents()
+                if self._if_canceled():
+                    return False
+                if len(line) > 5:
+                    parts = line.split(',')
+                    try:
+                        self.datas.append(TrafficLogLine(parts[0], parts[1], 
+                                                        parts[2]))
+                    except TypeError, e:
+                        print 'Error in: %s (%s)' % (self.file, str(e))
+                    except ValueError, e:
+                        print 'Error in: %s (%s)' % (self.file, str(e))
+        except IOError, e:
+            self._cancel_and_show_message('Error', str(e))
+            return False
+        return True
+        
+    def _cancel_and_show_message(self, title, message):
+        self.progress.cancel()
+        QtGui.QMessageBox.about(self, title, message)
+        QtCore.QCoreApplication.processEvents()
+        
+    def _if_canceled(self):
+        """Checks cheaply from long loop if Cancel was pushed."""
+        self.loop += 1
+        if self.loop % 500 == 0:
+            QtCore.QCoreApplication.processEvents()
+            if self.progress.wasCanceled():
+                return True
+        return False
+
+    def _append_latest_traffic_status(self):
+        try:
+            download, upload = netstoryd.read_counters()
+            if netstoryd.check(download) and netstoryd.check(upload):
+                now = datetime.now().strftime(settings.DATA_TIME_FORMAT)
+                self.datas.append(TrafficLogLine(now, download, upload))
+            else:
+                QtGui.QMessageBox.about(self, 'Problem', "Your N900 " \
+                    "isn't currently probably compatible with NetStory " \
+                    "(only PR1.2 is tested)")
+        except NameError, e:
+            print 'Windows testing: %s' % str(e)
+            
+    def _generate_hourly(self):
+        self.hourly = []
+        for i, data in enumerate(self.datas[1:]):
+            if self._if_canceled():
+                return
+            traffic_row = TrafficRow()
+            traffic_row.calculate_between_log_lines(self.datas[i], data)
+            self.hourly.append(traffic_row)
+        
+        table = self.ui.table_hourly
+        self._init_table(table, len(self.hourly))
+        
+        for i, hour in enumerate(reversed(self.hourly[-self.max_rows:])):
+            if self._if_canceled():
+                return
+            if hour.start_time.day != hour.end_time.day and \
+               hour.end_time.hour != 0:
+                # Phone has been off or there is some other reason why
+                # end time date is different. Anyhow show end time with date.
+                end_time = hour.end_time.strftime('%H:%M (%d.%m.%Y)')
+            else:
+                end_time = hour.end_time.strftime('%H:%M')
+            hour.set_description_cell('%s - %s' % 
+                                (hour.start_time.strftime('%d.%m.%Y %H:%M'), 
+                                end_time), i)
+            # This is expensive operation if there are thousands of lines
+            self._set_table_row(table, i, hour)
+            
+    def _generate_daily(self):
+        self.daily = {}
+        for hour in self.hourly:
+            if self._if_canceled():
+                return
+            key = hour.start_time.isocalendar()
+            self.daily[key] = self.daily.get(key, TrafficRow())
+            self.daily[key].add(hour)
+        
+        table = self.ui.table_daily
+        self._init_table(table, len(self.daily))
+        
+        keys = self.daily.keys()
+        keys.sort()
+        
+        for i, key in enumerate(reversed(keys[-self.max_rows:])):
+            if self._if_canceled():
+                return
+            day = self.daily[key]
+            day.set_total()
+            day.set_representation()
+            day.set_description_cell(\
+                day.start_time.strftime('%d.%m.%Y'), i)
+            self._set_table_row(table, i, day)
+            
+    def _generate_weekly(self):
+        self.weekly = {}
+        for day in self.daily.itervalues():
+            # Following works beatifully, 
+            # because: datetime(2011, 1, 1).isocalendar()[0] == 2010
+            key = '%d / %02d' % (day.start_time.isocalendar()[0], 
+                                        day.start_time.isocalendar()[1])
+            self.weekly[key] = self.weekly.get(key, TrafficRow())
+            self.weekly[key].add(day)
+        
+        table = self.ui.table_weekly
+        self._init_table(table, len(self.weekly))
+        
+        keys = self.weekly.keys()
+        keys.sort()
+        
+        for i, key in enumerate(reversed(keys[-self.max_rows:])):
+            week = self.weekly[key]
+            week.set_total()
+            week.set_representation()
+            if week.end_time.isocalendar()[1] != \
+               week.start_time.isocalendar()[1]: 
+                # it's probably following situation: 
+                # e.g. start time is 7.6.2010 0:00 (week 23) 
+                # and end time is 14.6.2010 0:00 (week 24)
+                week.end_time -= timedelta(days=1)
+            week.set_description_cell('%d (%s - %s)' % 
+                                (week.start_time.isocalendar()[1], 
+                                week.start_time.strftime('%d.%m'), 
+                                week.end_time.strftime('%d.%m.%Y')), i)
+            self._set_table_row(table, i, week)
+            
+    def _generate_monthly(self):
+        self.monthly = {}
+        for day in self.daily.itervalues():
+            key = day.start_time.strftime('%Y %m')
+            self.monthly[key] = self.monthly.get(key, TrafficRow())
+            self.monthly[key].add(day)
+        
+        table = self.ui.table_monthly
+        self._init_table(table, len(self.monthly))
+        
+        keys = self.monthly.keys()
+        keys.sort()
+        
+        for i, key in enumerate(reversed(keys[-self.max_rows:])):
+            month = self.monthly[key]
+            month.set_total()
+            month.set_representation()
+            month.set_description_cell(month.start_time.strftime('%Y: %B'), i)
+            self._set_table_row(table, i, month)
+            
+    def _generate_summary(self):
+        table = self.ui.table_summary
+        self._init_table(table, 5)
+        
+        for i, string, traffic_rows in [(0, 'Hourly average', self.hourly), 
+                (1, 'Daily average', self.daily.itervalues()), 
+                (2, 'Weekly average', self.weekly.itervalues()), 
+                (3, 'Monthly average', self.monthly.itervalues())]:
+            averages =  self.calculate_averages(traffic_rows)
+            average = TrafficRow()
+            average.download_bytes = averages['download']
+            average.upload_bytes = averages['upload']
+            average.total_bytes = averages['total']
+            average.set_representation()
+            average.set_description_cell(string, i)
+            self._set_table_row(table, i, average)
+        
+        totals = self.calculate_total(self.monthly.itervalues())
+        total = TrafficRow()
+        total.download_bytes = sum(totals['download'])
+        total.upload_bytes = sum(totals['upload'])
+        total.total_bytes = sum(totals['total'])
+        total.set_representation()
+        total.set_description_cell(\
+            self.datas[0].time.strftime('Total since %d.%m.%Y %H:%M'), 0)
+        self._set_table_row(table, 4, total)
+            
+    def _init_table(self, table, rows):
+        table.clearContents()
+        table.sortItems(0)
+        if rows < self.max_rows:
+            table.setRowCount(rows)
+        else:
+            table.setRowCount(self.max_rows)
+        table.horizontalHeader().resizeSection(0, 300)
+        table.horizontalHeader().setVisible(True)
+        
+    def _set_table_row(self, table, row_number, traffic_row):
+        table.setItem(row_number, 0, 
+                      SortTableWidgetItem(traffic_row.description, 
+                                          traffic_row.sort_key))
+        table.setItem(row_number, 1, 
+                      SortTableWidgetItem(traffic_row.download_string, 
+                                          traffic_row.download_bytes))
+        table.setItem(row_number, 2, 
+                      SortTableWidgetItem(traffic_row.upload_string, 
+                                          traffic_row.upload_bytes))
+        table.setItem(row_number, 3, 
+                      SortTableWidgetItem(traffic_row.total_string, 
+                                          traffic_row.total_bytes))
+    
+    def calculate_averages(self, traffic_rows=[]):
+        total = self.calculate_total(traffic_rows)
+        averages = {}
+        for key, l in total.items():
+            averages[key] = sum(l) / len(l)
+        return averages
+        
+    def calculate_total(self, traffic_rows=[]):
+        total = {'download': [], 'upload': [], 'total': []}
+        for traffic_row in traffic_rows:
+            total['download'].append(traffic_row.download_bytes)
+            total['upload'].append(traffic_row.upload_bytes)
+            total['total'].append(traffic_row.total_bytes)   
+        return total
+
+
+class TrafficLogLine:
+    def __init__(self, time='', download='', upload=''):
+        #self.time = datetime.strptime(time.strip(), settings.DATA_TIME_FORMAT)
+        # this is about 4 times faster than above
+        self.time = datetime(int(time[0:4]), int(time[5:7]), int(time[8:10]), 
+                int(time[11:13]), int(time[14:16]), int(time[17:19]))
+        self.download = int(download.strip())
+        self.upload = int(upload.strip())
+
+        
+class TrafficRow:
+    def __init__(self):
+        self.download_bytes = 0
+        self.upload_bytes = 0
+        self.total_bytes = 0
+        self.start_time = None
+        self.end_time = None
+        
+    def calculate_between_log_lines(self, start_data, end_data):
+        self.start_time = start_data.time
+        self.end_time = end_data.time
+        self.download_bytes = self.traffic_difference(start_data.download, \
+                                                      end_data.download)
+        self.upload_bytes = self.traffic_difference(start_data.upload, \
+                                                    end_data.upload)
+        self.set_total()
+        self.set_representation()
+        
+    def traffic_difference(self, start, end):
+        if end >= start:
+            return end - start
+        else:
+            return end #This value is probably inaccurate compared to reality
+        
+    def set_total(self):
+        self.total_bytes = self.download_bytes + self.upload_bytes
+        
+    def set_representation(self):
+        self.download_string = self.bytes_representation(self.download_bytes)
+        self.upload_string = self.bytes_representation(self.upload_bytes)
+        self.total_string = self.bytes_representation(self.total_bytes)
+        
+    def bytes_representation(self, number):
+        s = str(number)
+        if len(s) > 6:
+            s = '%s.%s MB' % (s[0:-6], s[-5])
+        elif len(s) > 3:
+            s = '%s kB' % (s[0:-3])
+        else:
+            s = '%s B' % (s)
+        return s
+        
+    def add(self, other):
+        """
+        Adds traffic values from other row into self
+        and also sets start and end times properly.
+        """
+        self.download_bytes += other.download_bytes
+        self.upload_bytes += other.upload_bytes
+        if not self.start_time or other.start_time < self.start_time:
+            self.start_time = other.start_time
+        if not self.end_time or other.end_time > self.end_time:
+            self.end_time = other.end_time
+    
+    def set_description_cell(self, description, sort_key):
+        self.description = description
+        self.sort_key = sort_key
+
+
+class SortTableWidgetItem(QtGui.QTableWidgetItem):
+    def __init__(self, text, sort_key):
+        # call custom constructor with UserType item type
+        QtGui.QTableWidgetItem.__init__(self, text, \
+                                        QtGui.QTableWidgetItem.UserType)
+        self.sort_key = sort_key
+
+    # Qt uses a simple < check for sorting items, 
+    # override this to use the sort_key
+    def __lt__(self, other):
+        return self.sort_key < other.sort_key
+
+
+if __name__ == "__main__":
+    app = QtGui.QApplication(sys.argv)   
+    dataform = DataForm()
+    dataform.show()
+    sys.exit(app.exec_())
\ No newline at end of file