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('\n'.format(round(sum(years_distance[i]), 1))) + for i in range(len(self.years_distance)): + handle.write('\n'.format(round(sum(self.years_distance[i]), 1))) handle.write('\n') handle.write('
{}
{} km{} km
\n') handle.write('

\n') handle.write('

\n') - handle.write('Distance\n'.format(dst_file_name)) - handle.write('Distance\n'.format(avg_file_name)) - handle.write('Distance\n'.format(dst_n_file_name)) + handle.write('Distance\n'.format(self.distance_diag_file)) + handle.write('Distance\n'.format(self.avg_spd_diag_file)) + handle.write('Distance\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")