def print_info(): dir = config.get_download_dir() if dir is None: print("No load directory configured. To set, use 'heroscript config --load_directory PATH'.") return if not path.exists(dir): exit_on_error("Not a valid directory: '{}'. " "To change, use 'heroscript config --load_directory PATH'.".format(dir)) types = ('*.TCX', '*.tcx') files_grabbed = [] for files in types: files_grabbed.extend(glob.glob(path.join(dir, files))) files_grabbed.sort() if len(files_grabbed) == 0: print("No track file found in {}".format(dir)) return load = read_load() print(f"Found {len(files_grabbed)} track file(s) in '{dir}':") for file in files_grabbed: if load and file == load.file_name: status = "*" else: status = " " print(f" {status} {path.basename(file)}")
def process_load(args): log("process_load", "start") if args.info: print_info() return if args.file: create_load(args.file) elif args.directory: create_load(get_next_track_file(args.directory)) else: dir = config.get_download_dir() if dir is None: exit_on_error("No load directory configured. Use 'heroscript config --load_directory PATH' or use " "an optional argument '--file FILE' or '--directory DIRECTORY'.") else: create_load(get_next_track_file(dir)) if args.strava: load_strava_activity() log("process_load", "end")
def _execute_archive(preparation, load): dest = path.join(preparation['dest_dir'], Path(load.file_name).name) if preparation['ftp']: try: with FTP(host=preparation['host'], user=preparation['username'], passwd=preparation['password']) as connection: with open(load.file_name, 'rb') as f: connection.storlines(f"STOR {dest}", f) human_dest = f"{preparation['host']}/{dest}" except all_errors as e: utility.exit_on_error(f"Archiving failed: {e}") os.remove(load.file_name) else: utility.log("os.replace from {} to".format(load.file_name), preparation['dest_dir']) dest = path.join(preparation['dest_dir'], Path(load.file_name).name) shutil.move(load.file_name, dest) human_dest = dest load.set_archived_to(dest) delete_load() print("Archived to: '{}'".format(human_dest))
def find_equipment_by_name(name): """ Query for a equipment. Exit script with message, if it doesn't match exactly one item. :param name: String with name or a Substring :return: masterdata item (dictionary) :except exit, if not found """ hits = [] # log("find_equipment_by_name", read_masterdata()['equipments']) # log("name", name) regex = re.compile("^" + name + ".*", re.IGNORECASE) for equipment in [ e for e in read_masterdata()['equipments'] if regex.match(e['name']) ]: hits.append(equipment) if len(hits) == 0: exit_on_error( f"No Equipment found for '{name}' (use 'masterdata --list/--refresh')" ) if len(hits) > 1: exit_on_error(f"'{name}' not unique: Found {hits}") return hits[0]
def get_config(key): """ Get the value for the given key. The key must exist otherwise the exit is processed. :param key String with key name :return: Found value """ if key in read_config(): return read_config()[key] else: exit_on_error(f"Config key '{key}' not found !")
def _get_activity_type(gear): """ Maps strava gear to a heroscript activity type. First try is to findin the description heroscript.activity_type='..'. If not found, the activity type will be set by frame type (for bikes) and 'run' for shoes. :param gear: Shoe or Bike :return: item from utility.activity_type_list :exception: Mapping failed """ regex_activity_type = re.compile(".*heroscript\.activity_type=\'(.*)\'.*") log("gear.description", gear.description) if regex_activity_type.match(gear.description): activity_type = regex_activity_type.match( gear.description).group(1).strip().lower() log("Strava", f"Found activity type {activity_type} in gear {gear.name}") if activity_type in utility.activity_type_list: return activity_type else: exit_on_error( f"Gear {gear.name}: Unknown activity type '{activity_type}'. " f"Please update your Strava equipment description with heroscript.activity_type='{utility.activity_type_list}'" ) # Only bikes have a frame type if hasattr(gear, 'frame_type'): # MTB if gear.frame_type == 1: return utility.activity_type_mtb # Cross Bike elif gear.frame_type == 2: return utility.activity_type_roadbike # Race Bike elif gear.frame_type == 3: return utility.activity_type_roadbike # Triathlon Bike elif gear.frame_type == 4: return utility.activity_type_roadbike else: exit_on_error( f"Bike '{gear.name}' (ID={gear.id}) has an unknown frame type '{gear.frame_type}'. " "You have to edit this Equipment in Strava.") # Shoe else: # In der Schuh-Beschreibung einen Text z.B. activity_type='hiking' aber auch heroscript._type='roadbike' return utility.activity_type_run
def delete_item(key): """ delete one item in the config list :param item: Item to append or replace """ myconfig = read_config() if key in myconfig: log(f"Delete setting", key) myconfig.pop(key) _save_config(myconfig) else: exit_on_error(f"'{key}' not found !")
def _authorize_app(): log("_authorize_app", "start") dameon = threading.Thread(name='herscript-server', target=_start_server) dameon.setDaemon(True) dameon.start() client = stravalib.Client() url = get_strava_authorization_url('http://{}:{}/authorize'.format( host_name, get_config(key_port))) # url = "https://www.strava.com/oauth/authorize?client_id={client_id}"\ # "&redirect_uri=http://{host_name}:{port}"\ # "&response_type=code"\ # "&approval_prompt=auto"\ # "&scope=activity:write,read".format(client_id="43527", host_name=host_name, port=port) # log("url", url) print( "Starting webbrowser, please authorize heroscript for STRAVA access.") # No glue, why this doesn't work with chromium (no redirection, but normal login) webbrowser.get('firefox').open_new(url) log("webbrowser", "called") i = 10 print(f"Waiting {i} sec for your authorization: {i:2}", end="", flush=True) while i >= 0 and threaded_authorize_code is None: i -= 1 print(f"\b\b{i:2}", end="", flush=True) # if authorize_code is None: if i == 0: exit_on_error("\nTimeout, authorization failed !") else: time.sleep(1) print("") if threaded_authorize_code is None: exit_on_error("Timeout, Authorization failed.") else: print("Done.") log("Go on now with code", threaded_authorize_code) if not key_strava_client_id in read_config(): exit_on_error( "STRAVA client ID not set. Set it with: heroscript config --strava_client_id ID-OF-YOUR-API" ) if not key_strava_client_secret in read_config(): exit_on_error( "STRAVA client secret not set. Set it with: heroscript config --strava_client_secret SECRET-OF-YOUR-API" ) obtain_new_token(threaded_authorize_code) log("_authorize_app", "end")
def _get_training_type(gear): regex = re.compile(".*heroscript\.training_type=\'(.*)\'.*") types = masterdata.get_types() log("gear.description", gear.description) if regex.match(gear.description): type = regex.match(gear.description).group(1).strip().lower() log("Strava", f"Found training type {type} in gear {gear.name}") if type.lower() in [t.lower() for t in types]: return [t for t in types if t.lower() == type.lower()][0] else: exit_on_error( f"Gear {gear.name}: Unknown training type '{type}'. " f"Please update your Strava equipment description with heroscript.training_type=" f"'{types}'")
def _delete_argument(key): if config.find(key) is None: exit_on_error(f"Key '{key}' not found!") elif key == config.key_load_dir: config.save_item(key, config.default_load_dir) reset = True elif key == config.key_archive_dir: config.save_item(key, config.default_archive_dir) reset = True else: config.delete_item(key) if reset: print(f"{key} resetted.")
def velohero_do_upload(file_name): """ File must exist and of a valid route Returns workout ID or False """ log("upload file", file_name) with open(file_name, "rb") as file: # content = file.read() # log("content", content) r = requests.post("https://app.velohero.com/upload/file", headers={ 'user-agent': my_user_agent, }, files={ 'file': (file_name, file.read()), }, data={ 'sso': sso_id, 'view': "json", }) # log("request.headers", r.request.headers) # log("headers", r.headers) log("text", r.text) # text = json.loads(r.text) # log("id", text["id"]) # 200 Created if r.status_code == 200: log("Upload successful", r.status_code) log("Response", str(r)) return json.loads(r.text)["id"] # 403 Forbidden if r.status_code == 403: exit_on_error("Login forbidden - {}: ".format(r.status_code)) return False # Other error exit_on_error("HTTP error - {}: ".format(r.status_code)) return False
def find_training_type_by_name(text): """ Try to find the type by a (sub-) string. :param: text :return: S """ hits = [] regex = re.compile("^" + text.strip() + ".*", re.IGNORECASE) for t in [t for t in read_masterdata()['types'] if regex.match(t['name'])]: hits.append(t['name']) if len(hits) == 0: exit_on_error( "Training Type input doesn't match. Use 'heroscript masterdata --list' to list all training types." ) if len(hits) > 1: exit_on_error(f"Training Type input not unique, found: {hits}") return hits[0]
def _download_count(count): username = config.get_config(config.key_garmin_username) password = config.get_config(config.key_garmin_password) # log("sys.executable", sys.executable) load_dir = config.get_config(config.key_load_dir) gce_script = path.join(os.path.dirname(os.path.realpath(__file__)), '..', 'garmin-connect-export', 'gcexport.py') if not os.path.exists(gce_script): exit_on_error(f"Script not found: {gce_script} !") with subprocess.Popen([ 'python2', gce_script, "--username", f"{username}", "--password", f"{password}", "-d", load_dir, "-f", "tcx", "-s", config.load_subdir, "-fp", "-c", f"{count}" ], stdout=subprocess.PIPE, stderr=subprocess.PIPE, bufsize=1, universal_newlines=True, text=True) as p: # Doesn't flush for garmin but flushs for pings. Why? for line in p.stdout: print(line, end='', flush=True) # Print the rest for line in p.stdout.readlines(): print(line, end='', flush=True) for line in p.stderr.readlines(): print(line, end='', flush=True) # Doesn't work # if p.returncode != 0: # exit_on_error(f"Download failed") log("donwload", "end")
def velohero_check_sso_login(): log("check_sso_login", "begin") global sso_id sso_id = get_config(key_velohero_sso_id) log("sso_id", sso_id) if sso_id is None: exit_on_error( "Missing SSO Key. Use 'config --velohero_sso_id YOUR_KEY' to set the key once." ) try: r = requests.post("https://app.velohero.com/sso", data={'sso': sso_id}) except ConnectionError as e: exit_on_error("Velohero SSO failed: {}".format(e.strerror)) # 200 Created if r.status_code == 200: log("Authentification successful", r.status_code) return # 403 Forbidden if r.status_code == 403: exit_on_error("Login forbidden - {}: ".format(r.status_code)) return
def _obtain_new_token(func, token): """ Call STRAVA for getting new access token. There are two different methods for that in the STRAVA Client API. Either while app authorization and for an expired token. :param func: Either client.exchange_code_for_token() or client.refresh_access_token() :param token: Either the authorize_token or the request_token. """ token_response = None try: token_response = func(token) # While authorization except stravalib.exc.AccessUnauthorized as e: log("stravalib.exc.AccessUnauthorized", e) exit_on_error("Unauthorized") # While reqest expired token except stravalib.exc.Fault as e: log("stravalib.exc.Fault", e) exit_on_error("Obtaining new STRAVA token failed") if not 'access_token' in token_response: exit_on_error( "Unexpected Response: Didn't got the access_toke from STRAVA.") if not 'refresh_token' in token_response: exit_on_error( "Unexpected Response: Didn't got the refresh_token from STRAVA.") if not 'expires_at' in token_response: utility.exit_on_error( "Unexpected Response: Didn't got the expires_at from STRAVA.") save_item(key_strava_access_token, token_response['access_token']) save_item(key_strava_refresh_token, token_response['refresh_token']) save_item(key_strava_expired_at, token_response['expires_at']) log("access_token", token_response['access_token']) log("refresh_token", token_response['refresh_token']) log("expires_at", token_response['expires_at'])
def get_next_track_file(directory): """ Get apth of the name track file :param directory: String with directorz=y :return: String with path or None """ if not path.exists(directory): exit_on_error("Not a valid directory: '{}'".format(directory)) types = ('*.TCX', '*.tcx') files_grabbed = [] for files in types: files_grabbed.extend(glob.glob(path.join(directory, files))) files_grabbed.sort() log("files_grabbed", files_grabbed) if len(files_grabbed) == 0: exit_on_error("No track file found in {}".format(directory)) else: return files_grabbed[0]
def get_type(name): """ Returns type dictionary by name. Name must match an existing type, but can be case independent Precondition: Type must exists, otherwise exit will processed :return: E.g. dict(name='Pendel', tag='commute', velohero_id='7433') """ if not 'types' in read_masterdata(): exit_on_error( "Master data not found. Use 'heroscript masterdata --refresh' to load data from Velohero" ) types = [ t for t in read_masterdata()['types'] if t['name'].lower() == name.lower() ] if len(types) == 0: exit_on_error( f"Trainging Type '{name}' not found. Use 'masterdata --list' to see existing trainging types" ) return types[0]
def process_transfer(args): # utility.log("process_transfer", "start") if not args.velohero and not args.archive and not args.strava and not args.dir and not args.purge: utility.exit_on_error( "Missing transfer destination(s). Use --help to see possible arguments" ) load = read_load() if load is None: utility.exit_on_error( "No file loaded! Use 'load' to stage the next activity.") if args.purge: if args.purge != path.basename(load.file_name): utility.exit_on_error( f"Wrong filename. You can only purge the loaded file {path.basename(load.file_name)}" ) else: remove(load.file_name) delete_load() print("File and Load purged") return if args.strava and load.strava_activity_id is None: utility.exit_on_error( "STRAVA activity not loaded, so STRAVA can't be updated. If you want to update your STRAVA activity, " "first 'load --strava' to get the activity ID from STRAVA.") if not path.exists(load.file_name): utility.exit_on_error("File not found: '{}'".format(load.file_name)) if load.new_activity_type is None: utility.exit_on_error( f"Activity type not set! Use heroscript set --activity_type '{utility.activity_type_list}'" ) archive_preparation = None if args.dir or args.archive: if args.dir: archive_preparation = _prepare_archive(args.dir, load) if args.archive: archive_preparation = _prepare_archive( get_config(config.key_archive_dir), load) if archive_preparation['overwrite']: print(f"[WARN] Archive file already exists, will be overwritten.") if args.velohero: # Velohero step 2: Upload, if not already done if load.velohero_workout_id is None: print("Upload to velohero.", end='', flush=True) velohero_check_sso_login() print(".", end='', flush=True) velohero_workout_id = velohero_do_upload(load.file_name) load.set_velohero_workout_id(velohero_workout_id) print(".", end='', flush=True) save_load(load) velohero_do_update(velohero_workout_id, None, load) print("Done (new Workout ID %s created)" % velohero_workout_id) # Velohero step update else: print("Update velohero.", end='', flush=True) velohero_check_sso_login() print(".", end='', flush=True) velohero_workout_id = load.velohero_workout_id # Velohero step update velohero_do_update(velohero_workout_id, None, load) print("Done (Workout ID %s updated)" % velohero_workout_id) if args.strava: strava_do_update() if args.dir or args.archive: _execute_archive(archive_preparation, load)
def _prepare_archive(destination, load): """ Checks if destination exists and returns a user human path. If destination directory is missing, it will be created. Exit in error case. :param: destination String with destination information, from Config or command line :param: load Load with file name :returns: dict with ftp [true\false] overwrite [true\false] dest_dir path without file name host FTP Host address or None username FTP Username or None password FTP Password or None """ ret = dict() ret['ftp'] = destination.strip().lower().startswith("use ftp") ret['overwrite'] = None ret['dest_dir'] = None ret['host'] = None ret['username'] = None ret['password'] = None if ret['ftp']: regex = re.match(".* username=(.*?)( .*|$)", destination) if regex and regex.group(1): ret['username'] = regex.group(1) else: utility.exit_on_error( f"Missing username in destination configuration '{destination}'" ) regex = re.match(".* password=(.*?)( .*|$)", destination) if regex and regex.group(1): ret['password'] = regex.group(1) else: utility.exit_on_error( f"Missing password in destination configuration '{destination}'" ) regex = re.match(".* host=(.*?)( .*|$)", destination) if regex and regex.group(1): ret['host'] = regex.group(1) else: utility.exit_on_error( f"Missing host in destination configuration '{destination}'") regex = re.match(".* dir=(.*?)( .*|$)", destination) if regex and regex.group(1): ret['dest_dir'] = utility.resolve_path(regex.group(1), load.started_at) else: utility.exit_on_error( f"Missing dir in destination configuration '{destination}'") with FTP(host=ret['host'], user=ret['username'], passwd=ret['password']) as connection: utility.log("FTP", connection.getwelcome()) # Check if directory exists try: connection.cwd(ret['dest_dir']) exists = True except all_errors: exists = False # Let's try to create it if not exists: try: connection.mkd(ret['dest_dir']) connection.cwd(ret['dest_dir']) except all_errors: utility.exit_on_error( f"Could not create directory via FTP: '{ret['dest_dir']}'" ) return # Now we have an existing destination dir try: pathlist = connection.nlst() ret['overwrite'] = Path(load.file_name).name in pathlist except all_errors as e: utility.exit_on_error(e) return ret else: ret['dest_dir'] = utility.resolve_path(destination, load.started_at) if not path.exists(ret['dest_dir']): print(f"Directory not there, create it: {ret['dest_dir']}") Path(ret['dest_dir']).mkdir(parents=True, exist_ok=True) if not path.exists(ret['dest_dir']): utility.exit_on_error( f"Destination doesn't exist:'{ret['dest_dir']}'") ret['overwrite'] = path.exists( path.join(ret['dest_dir'], load.file_name)) return ret
def _set_argument(args): """ Must be expanded for every new key, """ cnt = 0 if args.velohero_sso_id: config.save_item(config.key_velohero_sso_id, args.velohero_sso_id) cnt += 1 if args.port: config.save_item(config.key_port, args.port) cnt += 1 if args.strava_client_id: config.save_item(config.key_strava_client_id, args.strava_client_id) cnt += 1 if args.load_dir: config.save_item(config.key_load_dir, args.load_dir) cnt += 1 if args.archive_dir: config.save_item(config.key_archive_dir, args.archive_dir) cnt += 1 if args.garmin_connect_username: config.save_item(config.key_garmin_username, args.garmin_connect_username) cnt += 1 if args.garmin_connect_password: config.save_item(config.key_garmin_password, args.garmin_connect_password) cnt += 1 if args.strava_reset: config.save_item(config.key_strava_access_token, None) cnt += 1 config.save_item(config.key_strava_refresh_token, None) cnt += 1 config.save_item(config.key_strava_expired_at, None) cnt += 1 if args.strava_description: regex1 = re.compile("(strava_name)\((.*?)\)\?(.*)") res1 = regex1.match(args.strava_description) regex2 = re.compile("(training_type)\((.*?)\)\?(.*)") res2 = regex2.match(args.strava_description) if res1 and len(res1.group(3).strip()) > 0: config.save_item( "{}{}".format(config.key_strava_description_prefix, res1.group(1)), "{}?{}".format(res1.group(2), res1.group(3))) cnt += 1 elif res2: training_type = masterdata.get_type(res2.group(2)) config.save_item( "{}{}".format(config.key_strava_description_prefix, res2.group(1)), "{}?{}".format(training_type['name'], res2.group(3))) cnt += 1 else: exit_on_error("Invalid config value. See 'config --help.'") if cnt == 0: print( "No config value set. Did you set proper key? For a value description see: 'config --help'" ) else: print(f"Set {cnt} value(s).")
def strava_do_update(): """ Precondition: In Load there is the STRAVA activity ID """ print("Updating STRAVA...", end='', flush=True) messages = [] try: client = _setup_client() load = read_load() type = masterdata.get_type(load.training_type)['name'] if type == masterdata.get_competition_type()['name']: messages.append(( "[WARN] Can't set the STRAVA workout type to 'competition', please do this manually " "(missing this feature in stravalib API)")) if load.strava_descriptions is None: description = None else: description = "\n".join(load.strava_descriptions) log("description", description) if len(load.equipment_names) == 9: messages.append( "[WARN] No equipment in stage, equipment on Strava will not be be updated" ) client.update_activity( activity_id=load.strava_activity_id, name=load.strava_activity_name, commute=is_commute_training_type(type), trainer=is_indoor_training_type(type), description=description, ) else: equipment = masterdata.find_first_strava_equipment( load.equipment_names) if equipment is None: messages.append( "No equipment with Strava ID found! Are the master data up-to-date " "(use 'masterdata --refresh' to update)? ") gear_id = False else: gear_id = equipment['strava_id'] log("Take equipment ID", f"{equipment['strava_id']} '{equipment['name']}'") # There can ve prints inside, so print afterwards client.update_activity( activity_id=load.strava_activity_id, name=load.strava_activity_name, commute=is_commute_training_type(type), trainer=is_indoor_training_type(type), gear_id=gear_id, description=description, ) for message in messages: print(message) print(f" Done (Activity ID {load.strava_activity_id} updated)") except stravalib.exc.AccessUnauthorized as e: log("Exception occurred", e) exit_on_error( f"STRAVA access failed: Unauthorized. Maybe you have removed the app permission in your STRAVA profile!? " "Use 'heroscript config --strava_reset' to remove all login data.py. Then try the command again and you " "will be askt for app authorization.")
def load_strava_activity(): """ Precondition: Load is staged. """ # log("load_strava_activity", "start") try: client = _setup_client() load = read_load() log("Searching for an activity at", load.started_at) # client.get_activity(1).tr for a in client.get_activities(after=load.started_at_datetime + datetime.timedelta(seconds=-1), before=load.started_at_datetime + datetime.timedelta(seconds=1)): log( f"Found STRAVA activity: started at={a.start_date}, " f"name='{a.name}' ID", f"'{a.id}', commute={a.commute}, trainer={a.trainer}, " f"workout type={a.workout_type}") if len(a.name.split(": ", 1)) == 2: title = a.name.split(": ", 1)[1] else: title = a.name if a.commute: type = masterdata.get_commute_type() elif a.trainer: type = masterdata.get_indoor_type() # Type decorates the name as prefix elif re.compile(".*: .*").match(a.name): type = masterdata.find_type_by_name(a.name.split(": ", 1)[0]) # Run or bike competition elif a.workout_type == strava_workout_run_competition_type or \ a.workout_type == strava_workout_bicycle_competition_type: type = masterdata.get_competition_type() # Strava doesn't have a type else: type = masterdata.get_default_type() log("Detected STRAVA activity type", type['name']) equipment_name = None if a.gear_id: log("gear_id", a.gear_id) equipment = masterdata.find_equipment_by_strava_id(a.gear_id) if equipment is None: print( f"[WARN] STRAVA equipment with ID {a.gear_id} not found ! To update master data use " f"'masterdata --refresh'") else: equipment_name = equipment['name'] if equipment['training_type'] is not None: type = masterdata.find_type_by_name( equipment['training_type']) else: print("[WARN] STRAVA activity hasn't got an equipment") myconfig = config.get_strava_description_items() descriptions = [] log("a.distance", f"{a.distance.num} {a.distance.unit}") log("a.total_elevation_gain", f"{a.total_elevation_gain.num} {a.total_elevation_gain.unit}") elevation = a.total_elevation_gain.num / (a.distance.num / 1000) descriptions.append( "\u25B2 {elevation:.1f} {unit1}/k{unit2}".format( elevation=elevation, unit1=a.total_elevation_gain.unit, unit2=a.distance.unit)) for item in myconfig: log("description rule", item) if item['condition_field'] == 'strava_name': if item['condition_value'].lower().strip() == title.lower( ).strip(): descriptions.append(item['text']) if item['condition_field'] == 'training_type': if item['condition_value'].lower().strip( ) == type['name'].lower(): descriptions.append(item['text']) descriptions.append( "Powered by https://github.com/chs8691/heroscript") log("descriptions", descriptions) load.add_strava( a.id, a.name, type['name'], title, equipment_name, descriptions, masterdata.find_activity_type_by_equipment(equipment_name)) save_load(load) print(f"Found activity on Strava") return print("[WARN] Strava activity not found !") except stravalib.exc.AccessUnauthorized as e: log("Exception occurred", e) exit_on_error( f"STRAVA access failed: Unauthorized. Maybe you have removed the app permission in your STRAVA profile!? " "Use 'heroscript config --strava_reset' to remove all login data.py. Then try the command again and you " "will be askt for app authorization.")
def parse_args(): """ Examples: Show info of an workout file: main.py info --file=20191221-150315-activity_4355570485.tcx main.py info --directory=/home/chris/newActivities Upload an workout file and set an attribute: main.py upload --file=20191221-150315-activity_4355570485.tcx --sport_id='Mountainbike' Pick next file from a directory, upload and set value: main.py upload --directory=/home/chris/newActivities --sport_id='Mountainbike' Show workout's attributes: main.py show --id=4075724 Update attribute of an an existing workout: main.py set --id=4075724 --sport_id=Mountainbike """ global args parser = argparse.ArgumentParser( description="Upload activities to velohero an set proper attributes.") parser.add_argument("-l", "--log", action='store_true', help="Print log to the console") sub_parsers = parser.add_subparsers() # ######### masterdata ######### data_parser = sub_parsers.add_parser( 'masterdata', help="Set or show master data (equipment and training type).", description="Set or show master data (equipment and training type)." "Equipments will be downloaded from Velohero and from Strava " "and then merged together by its name. In consequence, to use an " "equipment in heroscript you have to create it twice, once in " "velohero and once in Strava. " "The second master data is Training Type (Trainingsarten), " "imported from Velohero. To set the training type manually, use " "the set command. But more comfortable could be to customize it " "directly in your Strava activity before the load (see load " "command).") data_parser.add_argument( "-l", "--list", required=False, action='store_true', help="Print all master data as a list. This is the default argument.") data_parser.add_argument( "-r", "--refresh", required=False, action='store_true', help= "Update all master data from Velohero (equipments and training types). " "A EQUIPMENT is compared by name: A equipment will only added to the master data, if" "it is found in Velohero and Strava, all other equipments will be ignored. " "In Strava, the equipment can be used to specify activity type and training type:" "For bikes, the frame type will be mapped to the activity type (Mountainbike to mtb, etc.). " "Shoes, will be mapped to run. In addition, this can be overridden by a value heroscript.activity_type='YOUR_CHOOSEN_TYPE' " "in the description, for instance heroscript.activity_type='fitness'. " "The name in Velohero must be a substring in STRAVA's name, but spaces and case will be ignored." "A TYPE is configured by the user in Velohero (Training Type). There is a type specific" "behavior: A type named 'Competition/Wettkampf' will be used to set the Competition flag in STRAVA," "same for 'Commute/Pendel' and 'Indoor/Rolle'. 'Training' is the standard type and has not specific behavior." "All other types will be used to tag STRAVA's activity name.") data_parser.add_argument("-v", "--validate", required=False, action='store_true', help="Check, if master data are actual.") data_parser.set_defaults(func=execute_masterdata) # ######### download ######### download_parser = sub_parsers.add_parser( 'download', help="Download track files from Garmin Connect", description= "Download new or a specified number of activity files in TCX format. " "The working directory is .download. The downloaded activitiy " "files will be moved to the load_dir (set with 'config --load_dir'). ") download_parser.add_argument( "-c", "--count", required=False, help= "Specify a fixed number of latest activities to download. If not given, all " "new activity files will be loaded. Must be used for the very first downloadb " "to avoid downloading all existing activities from Garmin Connect. " "Example: 'download --count 3' will load the last three activities from Garmin " "Connect.") download_parser.add_argument("-i", "--info", required=False, action='store_true', help="Prints info about the last download.") download_parser.set_defaults(func=execute_download) # ######### config ######### config_parser = sub_parsers.add_parser( 'config', help="Set and show script settings.", description= "Set the configuration for Heroscript. This will be stored in the " ".heroscript directory of the user's home directory. Some configurations" "will be handled by the script but there are also values the user can " "maintain. Calling config without paramater, all values will be listed.", ) config_parser.add_argument( "-l", "--list", action='store_true', help="Print all settings as a list. This is the default argument.", ) config_parser.add_argument( "-d", "--delete", help= "Delete item. Mandatory items will not be deleted but replaced by the default value." " Examples 'config --delete strava_description__BY__strava_name' will delete the " "item, but 'config --delete strava_client_id' will reset to default.", ) # For every new argument added here, cmd_config.py must be enhanced: # - Define a constant key_.... # - Enhance _set_argument() # - If argument has a default value, update _init_config config_parser.add_argument( "-sc", "--strava_client_id", required=False, help= "Set the STRAVA client ID for the used API. This can be found in STAVA / Settings / My API. " "If you havn't got alreay a created an API, you have to to this first." "Example: --strava_client_id 43527 ") config_parser.add_argument( "-sr", "--strava_reset", required=False, action='store_true', help="Unset STRAVA access ids. " "Useful, if there is trouble with the authentication process.") config_parser.add_argument( "-vs", "--velohero_sso_id", required=False, help="Set the velohero SSO key. " "Example: --velohero_sso_id kdRfmIHT6IVH1GI9SD32BIhaUpwTaQguuzE7XYt4 ") config_parser.add_argument( "-gu", "--garmin_connect_username", required=False, help="Garmin connect user name " "Example: --garmin_connect_username='******' ") config_parser.add_argument( "-gp", "--garmin_connect_password", required=False, help="Garmin connect password " "Example: --garmin_connect_password='******' ") config_parser.add_argument( "-p", "--port", required=False, help= "Port for internal webserver to show a map, default is {}. Example: --port 1234" .format(config.default_port)) config_parser.add_argument( "-ld", "--load_dir", required=False, help= f"Change the root directory for the activity files to load, default is " f"'{config.default_load_dir}'. This will be used to download " "activities from Garmin. The track files will be stored in the subdirectory " f"'{config.load_subdir}'. Example: --load_dir '/garmin_import'. " "Use '--delete load_dir' to reset to default.") config_parser.add_argument( "-ad", "--archive_dir", required=False, help= "Change the directory path to transfer the track file to a archive directory, default is " f"'{config.default_archive_dir}'. FTP upload is supported." "Supported placeholder is '{YYYY}'." "Examples: '../archive' or '/home/chris/tracks/{YYYY}' or, for FTP: " "'use ftp host=192.168.178.30 username=hugo password=topsecrectpassword dir=/home/chris/tracks/{YYYY}'. " "Use '--delete archvie_dir' to reset to default.") config_parser.add_argument( "-sd", "--strava_description", required=False, help= "Add a description line automatically to the Strava activity, if the value condition maps. " "Supported values for the condition are 'strava_name(TEXT TO SEARCH FOR)' " "and training_type((TRAINING_TYPE)) followed by a questionmark '?' and the text. " "The original description will be purged, if exists." "Example 1: --strava_description=strava_name(die runden stunde)?https://www.instagram.com/explore/tags/dierundestunde/ " "will add the link to the description, if the Activity name is 'Die Runden Stunde'. " "Example 2: --strava_description=training_type(pendel)?https://flic.kr/s/aHsm3QRWjT " "will create the link, if the training type is set to 'Pendel'.") config_parser.set_defaults(func=execute_config) # ######### load ######### load_parser = sub_parsers.add_parser( 'load', description= "First step of the workflow: Load an activity file from a local " "directory into the stage." "With no arguments, the next file from the default directory " "will be loaded. For this the default directory must be set once " "by velohero config --load_dir 'your_path'. Or the directory " "or a file can be set by an optional argument.", # help="", ) load_parser.add_argument( "-f", "--file", required=False, help= "Name (path) to the track file to load. This parameters excludes --directory." ) load_parser.add_argument( "-s", "--strava", required=False, action='store_true', help= "If set, values will be loaded from the existing Strava activity (matching by start " "tinme): ID and name. The training type will be defined by the flags for commute and " "trainer (indoor), the setting of the workout type to 'Competition' or the prefix in " " name (separated by a colon ':'). ") load_parser.add_argument( "-d", "--directory", required=False, help= "Path to the directory which contains one or more track files. In alphanumerical order," "the first track file will be loaded. This parameters excludes --file." ) load_parser.add_argument( "-i", "--info", required=False, action='store_true', help="Prints info about the track files in the configured directory.") load_parser.set_defaults(func=execute_load) # ######### set ######### set_parser = sub_parsers.add_parser( 'set', help="Set attributes for a loaded track file", description= "After loading an activity file into the stash, values can be" "changed with the set command. This can be useful, if the activity " "is not proper configured in Strava. Use the show command to list" "the actual settings.", ) set_parser.add_argument( "-at", "--activity_type", required=False, choices=utility.activity_type_list, help= "Set the activity type to Running, Mountain Biking, Road Cycling, Fitness or Hiking. " "Will be used to set the workout type in Velohero.") set_parser.add_argument( "-tt", "--training_type", required=False, help="Set the training type by its name (unset with ''). " "The input must match exactly one master data item " "(which must be created with 'masterdata --refresh'). Heroscript is expecting, that" "there are training types for competition ('Competition' or 'Wettkampf'), Commute " "('Commute' or 'Pendel'), Indoor Cycling ('Indoor', 'Trainer' or 'Rolle') and " "'Training' as the default type. And there can be additional types, too. This types" "must be defined in Velohero and downloaded with 'masterdata --refresh'. If you need" "other training types, you have to fork the scripting for you." "Examples: 'Pendel', 'Training' or '', but also 'pendel' or 'p'") set_parser.add_argument( "-r", "--route_name", required=False, help="Set value for the route by its name. Unset with ''. " "The name must match (case insensitive) the route name exactly or a unique substring of it. " "Will be used to update Velohero, not used for STRAVA. " "Examples: 'Goldbach Wintercross 10 KM', 'goldbach', ' ") set_parser.add_argument( "-e", "--equipment_names", required=False, help= "Set values for Equipments by its names, delimeted by a comma (','). Unset with ''. " "The name must match (case insensitive) the material name exactly or a unique substring of it. " "Examples: 'Laufschuhe Saucony Ride ISO, MTB Focus', ''") set_parser.add_argument( "-t", "--title", required=False, help= "Set the activity title (unset with ''). In Velhero this will be saved as " "comment, in Strava this will be part of the name (without the optional training type). " "Examples: 'Wonderful weather tour', or 'Day 1 of 5'") set_parser.add_argument( "-c", "--comment", required=False, help= "Set the comment (unset with '') in Velhero and overwrites the name in STRAVA" "Examples: 'Wonderful weather', or ''") set_parser.add_argument( "-v", "--velohero_workout_id", required=False, help= "Velohero workout ID. If set, the upload command only udpates the existing workout " "in Velohero. Set ID to '0' to force an upload. Examples: '4124109' or '0'" ) set_parser.set_defaults(func=execute_set) # ######### show ######### show_parser = sub_parsers.add_parser( 'show', help="Show actual attributes for a loaded track file", description="Shows the values of a loaded activity.") show_parser.add_argument("-m", "--map", required=False, action='store_true', help="Show Track in a map.") show_parser.set_defaults(func=execute_show) # ######### transfer ######### transfer_parser = sub_parsers.add_parser( 'transfer', help="Upload/Update the track file with the actual setting", description= "Finalized the workflow for a loaded activity by exporting it" "the specified destinations: Upload to Velohero, update " "Strava and moving the track file to a local archive" "directory.", ) transfer_parser.add_argument( "-v", "--velohero", required=False, action='store_true', help= "Upload file to Velohero and set values or, ff the Velohero Workout ID is set, " "just update the values of the existing Workout.") transfer_parser.add_argument( "-s", "--strava", required=False, action='store_true', help= "Update STRAVA activity. The activity must exist in STRAVA and loaded with 'load --strava'" ) transfer_parser.add_argument( "-a", "--archive", action='store_true', required=False, help="Move the track file to the default archive directory. " "For this it must be set once by velohero config --archive_dir 'your_path'." "Supported placeholder is '{YYYY}'." "Examples: config --archive_dir='../archive' or config -ad '/home/chris/tracks/{YYYY}'" ) transfer_parser.add_argument( "-d", "--dir", required=False, help="Move the track file to this archive directory. " "Supported placeholder is '{YYYY}'." "Examples: --dir='../archive' or -d '/home/chris/tracks/{YYYY}'") transfer_parser.add_argument( "--purge", required=False, help= "Delete the activity and remove the load. This option can be useful to get rid of " "double downloaded files. For security reason the filename is the parameter. " "Be careful, this can not be undone! " "Example: 'transfer --purge 20200403-122359.tcx'") transfer_parser.set_defaults(func=execute_transfer) # ######### velohero show ######### vh_show_parser = sub_parsers.add_parser( 'vh-show', help="Show existing workout in Velohero") vh_show_parser.add_argument("-i", "--workout_id", required=True, help="Velohero workout ID.") vh_show_parser.set_defaults(func=execute_velohero_show) # shared arguments for set and upload vh_field_parser = argparse.ArgumentParser(add_help=False) vh_field_parser.add_argument( "-date", "--workout_date", required=False, help= "Set Date (field 'workout_date'). Example: '31.12.2020' or '2020-12-31" ) vh_field_parser.add_argument( "-time", "--workout_start_time", required=False, help="Set Start Time (Field 'workout_start_time'). Example: '17:59:00'" ) vh_field_parser.add_argument( "-dur", "--workout_dur_time", required=False, help="Set Duration (field 'workout_dur_time'). Example: '2:23:00'") vh_field_parser.add_argument( "-sport", "--sport_id", required=False, help= "Set Sport (field 'sport_id'). Value can be sport's id or name (case insensitive). " "Examples: '1', 'Mountainbike'") vh_field_parser.add_argument( "-type", "--type_id", required=False, help= "Set value Training type (field 'type_id'). Value can be id or name " "(case sensitive). Examples: '7431', 'Training") vh_field_parser.add_argument( "-route", "--route_id", required=False, help="Set value Route (field 'route_id'). Value can be id or name " "(case sensitive). Examples: '12345', 'Berlin Marathon") vh_field_parser.add_argument( "-dist", "--workout_dist_km", required=False, help= "Set Distance (field 'workout_dist_km') in your unit (configured in Velohero). " "Example: '12345'") vh_field_parser.add_argument( "-asc", "--workout_asc_m", required=False, help= "Set Ascent (Field 'workout_asc_m') in your unit (configured in Velohero)." " Example: '1234'") vh_field_parser.add_argument( "-dsc_m", "--workout_dsc_m", required=False, help= "Set Descent (field 'workout_dsc_m') in your unit (configured in Velohero)." " Example: '1234'") vh_field_parser.add_argument( "-alt_min", "--workout_alt_min_m", required=False, help="Set Minimum Altitude (field 'aworkout_alt_min_m')" " in your unit (configured in Velohero). Example: '100'") vh_field_parser.add_argument( "-alt_max", "--workout_alt_max_m", required=False, help="Set Maximum Altitude )field 'workout_alt_max_m') " "in your unit (configured in Velohero). Example: '1000'") vh_field_parser.add_argument( "-spd_avg", "--workout_spd_avg_kph", required=False, help="Set Average Speed (field 'workout_spd_avg_kph') " "in your unit (configured in Velohero). Example: '23.4'") vh_field_parser.add_argument( "-spd_max_kph", "--workout_spd_max_kph", required=False, help="Set Maximum Speed (field 'workout_spd_max_kph') " "in your unit (configured in Velohero). Example: '45.6'") vh_field_parser.add_argument( "-hr_avg_bpm", "--workout_hr_avg_bpm", required=False, help= "Set Average Heart Rate (field 'workout_hr_avg_bpm'). Example: '123'") vh_field_parser.add_argument( "-hr_max_bpm", "--workout_hr_max_bpm", required=False, help= "Set Maximum Heart Rate (field 'workout_hr_max_bpm'). Example: '171'") vh_field_parser.add_argument( "-equipment", "--equipment_ids", required=False, help="Set values for Equipments (field 'equipments_ids'). " "Examples: '29613, 12345', ''") vh_field_parser.add_argument( "-comment", "--workout_comment", required=False, help="Field 'workout_comment'. Example: 'Got a bonk.'") # # ######### velohero update ######### vh_update_parser = sub_parsers.add_parser( 'vh-update', parents=[vh_field_parser], help="Set attributes for an existing workout in Velohero directly" " (independent of a load)") vh_update_parser.add_argument( "-i", "--workout_id", required=True, help="Velohero workout ID. Example: '4075724'") vh_update_parser.set_defaults(func=execute_velohero_update) # ######### velohero upload ######### vh_upload_parser = sub_parsers.add_parser( 'vh-upload', parents=[vh_field_parser], help="Upload workout file to Velohero directly" " (independent of a load)") vh_upload_parser.add_argument( "-f", "--file", required=True, help="Name (path) to the track file to upload") vh_upload_parser.set_defaults(func=execute_velohero_upload) args = parser.parse_args() # There must be choosen an argument if len(sys.argv) == 1: exit_on_error("Missing argument, see --help.") if args.log: set_log_switch(True) args.func()
def process_show(args): utility.log("process_show", "start") load = read_load() if load is None: utility.exit_on_error("No activity loaded yet.") if args.map: show_map() # print("Opening map in default Browser...", end='', flush=True) # tracksfer_gps.map("track.gps") # print("Done.") return if not path.exists(load.file_name): file_message = "<--------- MISSING !!!" else: file_message = "" print('File Name : {} {}'.format(load.file_name, file_message)) print('--- ATTRIBUTES ---') if load.new_activity_type is None: print(f"Activity Type : ({load.original_activity_type}) " f"<=== Use set --activity_type {utility.activity_type_list}") else: print('Activity Type : %s (original: %s)' % (load.new_activity_type, load.original_activity_type)) print('Training Type : %s' % load.training_type) print('Route Name : %s' % load.route_name) print('Equipment Names : %s' % load.equipment_names) print(f"Name : '{load.title}'") print("Comment : '%s'" % load.comment) print("") print("Started at (GMT) : {}".format(utility.get_human_date(load.started_at, "%a %y-%m-%d %H:%M"))) print("Distance : {0:.1f} k{1}".format(load.distance/1000, load.distance_unit_abbreviation())) print("Duration : {} h".format(time.strftime('%H:%M:%S', time.gmtime(load.duration)))) print("Velocity - Pace (total): {0:.1f} k{1}/h - {2}/k{3}".format( load.velocity_average, load.distance_unit_abbreviation(), load.pace, load.distance_unit_abbreviation())) print("Altitude : \u25B2 {0:.0f} \u25bc {1:.0f} [{2:.0f}..{3:.0f}] {4}".format( load.ascent, load.descent, load.altitude_min, load.altitude_max, load.distance_units)) print('--- STRAVA ---') print(f'Activity ID : {load.strava_activity_id}') print(f'Activity Name : {load.strava_activity_name}') if(load.strava_descriptions is None): print('Description (generated): None') else: print(f'Description (generated):') for description in load.strava_descriptions: print(f' {description}') print('--- STATUS ---') print('Velohero Workout ID : %s' % load.velohero_workout_id) print("Archived to : {}".format(load.archived_to)) utility.log("process_show", "end")