From b4d4d844fd6938fa0ef2852db33986194c8ac79f Mon Sep 17 00:00:00 2001
From: Thomas Klaehn
Date: Thu, 26 Jul 2018 14:20:15 +0200
Subject: [PATCH] bicycle_statistics: Transformed to file system event driven
service
Signed-off-by: Thomas Klaehn
---
bicycle_statistics/__main__.py | 48 +++++++-
gpx2html/__init__.py | 198 ++++++++++++++++++---------------
input_observer/__init__.py | 31 ++++++
setup.py | 2 +-
4 files changed, 185 insertions(+), 94 deletions(-)
create mode 100644 input_observer/__init__.py
diff --git a/bicycle_statistics/__main__.py b/bicycle_statistics/__main__.py
index 44b5ca5..e661c61 100644
--- a/bicycle_statistics/__main__.py
+++ b/bicycle_statistics/__main__.py
@@ -1,6 +1,12 @@
import argparse
import sys
+import threading
+import time
+
+from watchdog.observers import Observer
+
from gpx2html import Gpx2Html
+from input_observer import InputObserver
def parse_args():
'''Shell argument parser.'''
@@ -9,10 +15,48 @@ def parse_args():
parser.add_argument('outfolder', help='Specify the out folder.')
return parser.parse_args()
+class myLoop(threading.Thread):
+ def __init__(self, infolder, outfolder):
+ super(myLoop, self).__init__()
+ self.run_condition = True
+ self.infolder = infolder
+ self.outfolder = outfolder
+ self.observer = Observer()
+ self.in_obs = InputObserver(patterns=["*.gpx"])
+
+ def run(self):
+ gpx2html = Gpx2Html(self.infolder, self.outfolder)
+ gpx2html.process()
+
+ self.observer.schedule(self.in_obs, self.infolder)
+ self.observer.start()
+ while self.run_condition is True:
+ new_file = self.in_obs.get_new_file()
+ if new_file:
+ gpx2html.process()
+ print "processed"
+
+ def stop(self):
+ self.run_condition = False
+ self.in_obs.stop()
+ self.observer.stop()
+ self.observer.join()
+
+
def main():
args = parse_args()
- gpx2html = Gpx2Html(args.infolder, args.outfolder)
- gpx2html.process()
+
+ my_loop = myLoop(args.infolder, args.outfolder)
+ my_loop.start()
+
+ try:
+ while True:
+ time.sleep(1)
+ except KeyboardInterrupt:
+ my_loop.stop()
+ my_loop.join()
+ return 0
+
if __name__ == '__main__':
sys.exit(main())
diff --git a/gpx2html/__init__.py b/gpx2html/__init__.py
index a0524f2..ffae2ad 100755
--- a/gpx2html/__init__.py
+++ b/gpx2html/__init__.py
@@ -47,99 +47,37 @@ class Gpx2Html(object):
def __init__(self, infolder, outfolder):
self.infolder = infolder
- self.outfolder = outfolder
+ self.outfolder = os.path.abspath(outfolder)
+ self.distance_diag_file = 'distance.png'
+ self.distance_diag_abs = os.path.join(self.outfolder, self.distance_diag_file)
+ self.avg_spd_diag_file = 'avg_spd.png'
+ self.avg_spd_diag_abs = os.path.join(self.outfolder, self.avg_spd_diag_file)
+ self.last_n_days_diag_file = None
+ self.html_file = os.path.join(self.outfolder, 'index.html')
+ self.years_distance = list()
+ self.years_avg_spd = list()
+ self.years = list()
- def process(self):
- self.tracks = gpx_parser.Tracks(self.infolder)
- years_distance = list()
- years_avg_spd = list()
- years = list()
- for year in range(2017, datetime.datetime.now().year + 1):
- month_avg_spd = {1:0, 2:0, 3:0, 4:0, 5:0, 6:0, 7:0, 8:0, 9:0, 10:0, 11:0, 12:0}
- month_distance = {1:0, 2:0, 3:0, 4:0, 5:0, 6:0, 7:0, 8:0, 9:0, 10:0, 11:0, 12:0}
- month_duration = {1:0, 2:0, 3:0, 4:0, 5:0, 6:0, 7:0, 8:0, 9:0, 10:0, 11:0, 12:0}
- for month in range(1, 13):
- if month == 12:
- max_day = (datetime.date(year + 1, 1, 1) - datetime.timedelta(days=1)).day
- else:
- max_day = (datetime.date(year, month + 1, 1) - datetime.timedelta(days=1)).day
- for day in range(1, max_day + 1):
- date_tracks = self.tracks.get(year, month, day)
- for track in date_tracks:
- month_distance[month] += (track.distance / 1000) # km
- month_duration[month] += track.duration.total_seconds() / 3600 # h
- for i in range(1, 13):
- if month_duration[i] > 0:
- month_avg_spd[i] = month_distance[i] / month_duration[i]
- years_distance.append(month_distance.values())
- years_avg_spd.append(month_avg_spd.values())
- years.append(str(year))
+ def plot_distance_diagram(self):
+ plot_bar_chart(self.years, self.MONTH_LABELS, self.years_distance,
+ 'Distance', 'Month', 'km', self.distance_diag_abs)
- out_folder = os.path.abspath(self.outfolder)
- dst_file_name = 'distance.png'
- plot_bar_chart(years, self.MONTH_LABELS, years_distance, 'Distance', 'Month', 'km', os.path.join(out_folder, dst_file_name))
+ def plot_avg_spd_diagram(self):
+ plot_bar_chart(self.years, self.MONTH_LABELS, self.years_avg_spd,
+ 'Average Speed', 'Month', 'km/h', self.avg_spd_diag_abs)
- avg_file_name = 'avg_spd.png'
- plot_bar_chart(years, self.MONTH_LABELS, years_avg_spd, 'Average Speed', 'Month', 'km/h', os.path.join(out_folder, avg_file_name))
- # last n days
- n = 14
- end_date = datetime.date.today()
- start_date = end_date - datetime.timedelta(days=n)
- dates = pd.date_range(start_date, end_date)
+ def plot_last_n_days(self, day_count):
+ plot_bar_chart(["Distance", "Average speed"], self.date_distance.keys(),
+ [self.date_distance.values(), self.date_avg_spd.values()],
+ 'Last {} days'.format(day_count), 'Date', 'km, km/h',
+ os.path.join(self.outfolder, self.last_n_days_diag_file), 90)
- date_distance = dict()
- date_duration = dict()
- date_avg_spd = dict()
- for date in dates:
- date_str = "{0:04d}-{1:02d}-{2:02d}".format(date.year, date.month, date.day)
- date_tracks = self.tracks.get(date.year, date.month, date.day)
- for track in date_tracks:
- try:
- current_dist = date_distance[date_str]
- current_duration = date_duration[date_str]
- except KeyError:
- current_dist = 0
- current_duration = 0
- current_dist += track.distance / 1000
- date_distance.update({date_str:current_dist})
- current_duration += track.duration.total_seconds() / 3600
- date_duration.update({date_str:current_duration})
- # check for empty dates
- try:
- current_dist = date_distance[date_str]
- current_duration = date_duration[date_str]
- except KeyError:
- date_distance.update({date_str:0})
- date_duration.update({date_str:0})
-
- date_duration = collections.OrderedDict(sorted(date_duration.items()))
-
- for key, value in date_duration.items():
- if value == 0:
- date_avg_spd.update({key:0})
- else:
- avg_spd = date_distance[key] / value
- date_avg_spd.update({key:avg_spd})
-
- date_avg_spd = collections.OrderedDict(sorted(date_avg_spd.items()))
- date_distance = collections.OrderedDict(sorted(date_distance.items()))
-
- dst_n_file_name = "distance_last_{}_days.png".format(n)
- plot_bar_chart(["Distance", "Average speed"],
- date_distance.keys(),
- [date_distance.values(), date_avg_spd.values()],
- 'Last {} days'.format(n),
- 'Date',
- 'km, km/h',
- os.path.join(out_folder, dst_n_file_name),
- 90)
-
- html_file = os.path.join(out_folder, 'index.html')
- with open(html_file, 'w') as handle:
+ def write_html_file(self):
+ with open(self.html_file, 'w') as handle:
handle.write('\n')
handle.write('\n')
handle.write('\n')
@@ -161,24 +99,102 @@ class Gpx2Html(object):
handle.write('\n')
handle.write('\n')
- for year in years:
+ for year in self.years:
handle.write('{} | \n'.format(year))
handle.write('
\n')
handle.write('\n')
- for i in range(len(years_distance)):
- handle.write('{} km | \n'.format(round(sum(years_distance[i]), 1)))
+ for i in range(len(self.years_distance)):
+ handle.write('{} km | \n'.format(round(sum(self.years_distance[i]), 1)))
handle.write('
\n')
handle.write('
\n')
handle.write('
\n')
handle.write('\n')
- handle.write('\n'.format(dst_file_name))
- handle.write('\n'.format(avg_file_name))
- handle.write('\n'.format(dst_n_file_name))
+ handle.write('\n'.format(self.distance_diag_file))
+ handle.write('\n'.format(self.avg_spd_diag_file))
+ handle.write('\n'.format(self.last_n_days_diag_file))
handle.write('
\n')
handle.write('\n')
handle.write('\n')
handle.write('\n')
+
+
+ def process(self):
+ self.years[:] = []
+ self.years_avg_spd[:] = []
+ self.years_distance[:] = []
+ self.tracks = gpx_parser.Tracks(self.infolder)
+ for year in range(2017, datetime.datetime.now().year + 1):
+ month_avg_spd = {1:0, 2:0, 3:0, 4:0, 5:0, 6:0, 7:0, 8:0, 9:0, 10:0, 11:0, 12:0}
+ month_distance = {1:0, 2:0, 3:0, 4:0, 5:0, 6:0, 7:0, 8:0, 9:0, 10:0, 11:0, 12:0}
+ month_duration = {1:0, 2:0, 3:0, 4:0, 5:0, 6:0, 7:0, 8:0, 9:0, 10:0, 11:0, 12:0}
+ for month in range(1, 13):
+ if month == 12:
+ max_day = (datetime.date(year + 1, 1, 1) - datetime.timedelta(days=1)).day
+ else:
+ max_day = (datetime.date(year, month + 1, 1) - datetime.timedelta(days=1)).day
+ for day in range(1, max_day + 1):
+ date_tracks = self.tracks.get(year, month, day)
+ for track in date_tracks:
+ month_distance[month] += (track.distance / 1000) # km
+ month_duration[month] += track.duration.total_seconds() / 3600 # h
+ for i in range(1, 13):
+ if month_duration[i] > 0:
+ month_avg_spd[i] = month_distance[i] / month_duration[i]
+ self.years_distance.append(month_distance.values())
+ self.years_avg_spd.append(month_avg_spd.values())
+ self.years.append(str(year))
+
+ self.plot_distance_diagram()
+ self.plot_avg_spd_diagram()
+
+ # last n days
+ n = 14
+ end_date = datetime.date.today()
+ start_date = end_date - datetime.timedelta(days=n)
+ dates = pd.date_range(start_date, end_date)
+
+ self.date_distance = dict()
+ date_duration = dict()
+ self.date_avg_spd = dict()
+
+ for date in dates:
+ date_str = "{0:04d}-{1:02d}-{2:02d}".format(date.year, date.month, date.day)
+ date_tracks = self.tracks.get(date.year, date.month, date.day)
+ for track in date_tracks:
+ try:
+ current_dist = self.date_distance[date_str]
+ current_duration = date_duration[date_str]
+ except KeyError:
+ current_dist = 0
+ current_duration = 0
+ current_dist += track.distance / 1000
+ self.date_distance.update({date_str:current_dist})
+ current_duration += track.duration.total_seconds() / 3600
+ date_duration.update({date_str:current_duration})
+ # check for empty dates
+ try:
+ current_dist = self.date_distance[date_str]
+ current_duration = date_duration[date_str]
+ except KeyError:
+ self.date_distance.update({date_str:0})
+ date_duration.update({date_str:0})
+
+ date_duration = collections.OrderedDict(sorted(date_duration.items()))
+
+ for key, value in date_duration.items():
+ if value == 0:
+ self.date_avg_spd.update({key:0})
+ else:
+ avg_spd = self.date_distance[key] / value
+ self.date_avg_spd.update({key:avg_spd})
+
+ self.date_avg_spd = collections.OrderedDict(sorted(self.date_avg_spd.items()))
+ self.date_distance = collections.OrderedDict(sorted(self.date_distance.items()))
+
+ self.last_n_days_diag_file = "distance_last_{}_days.png".format(n)
+ self.plot_last_n_days(n)
+ self.write_html_file()
diff --git a/input_observer/__init__.py b/input_observer/__init__.py
new file mode 100644
index 0000000..5e9438b
--- /dev/null
+++ b/input_observer/__init__.py
@@ -0,0 +1,31 @@
+from watchdog.events import PatternMatchingEventHandler
+
+import threading
+
+class InputObserver(PatternMatchingEventHandler):
+ def __init__(self, patterns=None, ignore_patterns=None,
+ ignore_directories=False, case_sensitive=False):
+ super(InputObserver, self).__init__(patterns, ignore_patterns,
+ ignore_directories, case_sensitive)
+
+ self.lock = threading.Lock()
+ self.lock.acquire()
+ self.new_filename = None
+ self.run_condition = True
+
+
+ def on_created(self, event):
+ self.new_filename = event.src_path
+ self.lock.release()
+
+
+ def get_new_file(self):
+ self.lock.acquire() # don't release - will be released with next on_created
+ if self.run_condition == True:
+ return self.new_filename
+ else:
+ return None
+
+ def stop(self):
+ self.run_condition = False
+ self.lock.release()
diff --git a/setup.py b/setup.py
index fb543b7..31b875e 100755
--- a/setup.py
+++ b/setup.py
@@ -10,7 +10,7 @@ NAME = 'bicycle-statistics'
VERSION = '0.1.0'
AUTHOR = 'Thomas Klaehn'
EMAIL = 'tkl@blackfinn.de'
-PACKAGES = [NAME, 'gpx_parser', 'gpx2html']
+PACKAGES = ['bicycle_statistics', 'gpx_parser', 'gpx2html', 'input_observer']
SCRIPTS = ['example-gpx-parser', 'bicycle-stat']
DAEMON_START_SCRIPT = os.path.join("/lib/systemd/system", "bicycle-stat.service")