Пример #1
0
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)}")
Пример #2
0
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")
Пример #3
0
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))
Пример #4
0
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]
Пример #5
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 !")
Пример #6
0
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
Пример #7
0
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 !")
Пример #8
0
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")
Пример #9
0
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}'")
Пример #10
0
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.")
Пример #11
0
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
Пример #12
0
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]
Пример #13
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")
Пример #14
0
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
Пример #15
0
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'])
Пример #16
0
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]
Пример #17
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]
Пример #18
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)
Пример #19
0
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
Пример #20
0
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).")
Пример #21
0
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.")
Пример #22
0
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.")
Пример #23
0
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()
Пример #24
0
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")