From fb3820e5770b3dcb9c377507a5aac49d2fdf42d9 Mon Sep 17 00:00:00 2001 From: Daniel Demus Date: Thu, 26 Mar 2026 21:46:37 +0100 Subject: [PATCH] Systemd timer unit for to run huami-token on a schedule --- README-user-systemd.md | 20 +++ configuration/configure-zepp-timer | 105 ++++++++++++++ configuration/enable-zepp-timer | 4 + configuration/huami-token-generator | 42 ++++++ systemd/huami-token.service | 9 ++ systemd/huami-token.timer | 8 ++ test/10-configure-zepp-timer-test.bats | 178 ++++++++++++++++++++++++ test/20-huami-token-generator-test.bats | 110 +++++++++++++++ test/test_helper.bash | 30 ++++ 9 files changed, 506 insertions(+) create mode 100644 README-user-systemd.md create mode 100644 configuration/configure-zepp-timer create mode 100644 configuration/enable-zepp-timer create mode 100644 configuration/huami-token-generator create mode 100644 systemd/huami-token.service create mode 100644 systemd/huami-token.timer create mode 100644 test/10-configure-zepp-timer-test.bats create mode 100644 test/20-huami-token-generator-test.bats create mode 100644 test/test_helper.bash diff --git a/README-user-systemd.md b/README-user-systemd.md new file mode 100644 index 0000000..7b4ac8d --- /dev/null +++ b/README-user-systemd.md @@ -0,0 +1,20 @@ +# Set up hf-cli for automatic daily downloads + +To enable daily runs, you first need to generate a config.json file defining what +to download and where to put it: + +```bash +configure-zepp-timer +``` + +then you need to enable the timer for the user and enable lingering, so the timer +is executed even when the user is not logged in: + +```bash +enable-zepp-timer +``` + +You can test the service by running it manually: +```bash +systemctl --user start huami-token.service +``` diff --git a/configuration/configure-zepp-timer b/configuration/configure-zepp-timer new file mode 100644 index 0000000..c3e49fa --- /dev/null +++ b/configuration/configure-zepp-timer @@ -0,0 +1,105 @@ +#!/usr/bin/env bash +XDG_CONFIG_HOME=${XDG_CONFIG_HOME:-$HOME/.config} +CONFIG="$XDG_CONFIG_HOME/huami-token.env" + +_load_existing_config() { + if [ -s "$1" ]; then + # shellcheck disable=SC1090 + source "$1" + fi +} + +_request_credentials() { + local -a CREDS + # shellcheck disable=SC2046 + eval $(__credentials_dialog "$1" "$2") + if [ ${#CREDS[@]} -eq 0 ]; then + echo "Cancelled. $CONFIG was not changed." + exit 0 + fi + ZEPP_USER="${CREDS[0]//[$'\t'$'\n'$'\r']/}" + ZEPP_PASS="${CREDS[1]//[$'\t'$'\n'$'\r']/}" + if [ -z "$ZEPP_USER" ] || [ -z "$ZEPP_PASS" ]; then + echo "Username or password wasn't provided. Not changing $CONFIG" + exit 1 + fi +} + +_test_credentials() { + local auth_result + auth_result=$(__call_huami_token "$1" "$2") + + if [ -z "$auth_result" ] || [ "$auth_result" -ne "0" ]; then + echo Failed to login with the given user and password. Aborting. + exit 1 + fi +} + +_request_target_directory() { + local TARGET_DIR + if [ "$1" == "%h" ]; then + TARGET_DIR=$HOME + else + TARGET_DIR=$1 + fi + TARGET_DIR=$(__target_directory_dialog "$TARGET_DIR") + if [ -z "$TARGET_DIR" ]; then + echo "Cancelled. $CONFIG was not changed." + exit 0 + fi + TARGET_DIR=${TARGET_DIR//\\n|\\t|\\r/} + if [ -n "$TARGET_DIR" ] && [ "$(realpath "${TARGET_DIR}")" == "$HOME" ]; then + TARGET_DIR=%h + else + TARGET_DIR=$(realpath "$TARGET_DIR") + fi + ZEPP_AGPS_TARGET_DIR="$TARGET_DIR" +} + +_save_config() { + if [ ! -d "$XDG_CONFIG_HOME" ]; then + mkdir "$XDG_CONFIG_HOME" + fi + cat <"$CONFIG" +# Generated by configure-zepp-timer +ZEPP_USER=${ZEPP_USER@Q} +ZEPP_PASS=${ZEPP_PASS@Q} +ZEPP_AGPS_TARGET_DIR=${ZEPP_AGPS_TARGET_DIR@Q} +EOF +} + +__credentials_dialog() { + local -a CREDS + readarray -t CREDS < <(dialog --output-fd 1 --defaultno --title "Provide Zepp login credentials" --visit-items --keep-tite --insecure --mixedform "Please enter your credentials:" 8 60 2 "Username/Email:" 1 1 "$1" 1 18 38 0 0 "Password:" 2 1 "$2" 2 18 38 0 1) + # shellcheck disable=SC2068 + echo ${CREDS[@]@A} +} + +__call_huami_token() { + huami-token --method amazfit --email "$1" --password "$2" |& dialog --title 'Remote check' --keep-tite --programbox 'Trying to log into Zepp' 32 "$(tput cols)" 3>&1 1>&2 2>&3 3>&- + echo "${PIPESTATUS[0]}" +} + +__target_directory_dialog() { + local RET + RET="$(dialog --output-fd 1 --erase-on-exit --visit-items --keep-tite --clear --title "Choose a directory to put the downloaded files into" --dselect "$1/" 3 60)" + if [ "${PIPESTATUS[0]}" -ne 0 ]; then + RET="" + fi + echo "$RET" +} + +main() { + _load_existing_config "$CONFIG" + _request_credentials "$ZEPP_USER" "$ZEPP_PASS" + _test_credentials "$ZEPP_USER" "$ZEPP_PASS" + _request_target_directory "$ZEPP_AGPS_TARGET_DIR" + _save_config + + echo "The config at $CONFIG was updated" +} + +# Check if the script is being run directly, not sourced +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + main "$@" +fi diff --git a/configuration/enable-zepp-timer b/configuration/enable-zepp-timer new file mode 100644 index 0000000..0284ac5 --- /dev/null +++ b/configuration/enable-zepp-timer @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +systemctl --user enable huami-token.timer huami-token.service +loginctl enable-linger +echo The timer is now enabled diff --git a/configuration/huami-token-generator b/configuration/huami-token-generator new file mode 100644 index 0000000..d5976b8 --- /dev/null +++ b/configuration/huami-token-generator @@ -0,0 +1,42 @@ +#!/usr/bin/env bash +function _is_timer_enabled { + echo "$(systemctl is-enabled --user huami-token.timer)" +} + +function _disable_timer { + systemctl disable --now --user huami-token.timer +} + +function disable_unit_and_exit() { + if [ "$(_is_timer_enabled)" == "enabled" ]; then + _disable_timer + fi + if [ -f "$DROPIN_PATH" ]; then + rm -f "$DROPIN_PATH" + fi + exit 0 +} + +main() { + DROPIN_PATH="$1/01-WorkingDirectory.conf" + + # Generate the WorkingDir value for the huami-token user service unit + local CONFIG + CONFIG=${XDG_CONFIG_HOME:-$HOME/.config}/huami-token.env + if [ ! -s "$CONFIG" ]; then + disable_unit_and_exit + fi + + # shellcheck disable=SC1090 + source "$CONFIG" + if [ -z "${ZEPP_AGPS_TARGET_DIR//\\n|\\t|\\r/}" ]; then + disable_unit_and_exit + fi + + echo "WorkingDirectory=${ZEPP_AGPS_TARGET_DIR}" >"$DROPIN_PATH" +} + +# Check if the script is being run directly, not sourced +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + main "$@" +fi diff --git a/systemd/huami-token.service b/systemd/huami-token.service new file mode 100644 index 0000000..e94f0fe --- /dev/null +++ b/systemd/huami-token.service @@ -0,0 +1,9 @@ +[Unit] +Description=Run huami-token from saved config +After=network.target +ConditionFileNotEmpty=%E/huami-token.env + +[Service] +Type=oneshot +EnvironmentFile=%E/huami-token.env +ExecStart=/usr/bin/huami-token --method amazfit --gps --email "${ZEPP_USER}" --password "${ZEPP_PASS}" diff --git a/systemd/huami-token.timer b/systemd/huami-token.timer new file mode 100644 index 0000000..6520f55 --- /dev/null +++ b/systemd/huami-token.timer @@ -0,0 +1,8 @@ +[Unit] +Description=Download fresh gps roms for ZeppOS gadgets + +[Timer] +OnCalendar=*-*-* 01:10:00 + +[Install] +WantedBy=timers.target diff --git a/test/10-configure-zepp-timer-test.bats b/test/10-configure-zepp-timer-test.bats new file mode 100644 index 0000000..b4d3fed --- /dev/null +++ b/test/10-configure-zepp-timer-test.bats @@ -0,0 +1,178 @@ +#!/usr/bin/env bats + +load 'test_helper' + +setup() { + load ../configuration/configure-zepp-timer + common_setup +} + +# bats file_tags=configure +@test "The happy path" { + unset -f __credentials_dialog __call_huami_token __target_directory_dialog + stub __credentials_dialog "fnuggi ugh : printf 'declare -a CREDS=([0]=\"gurli\" [1]=\"pass1234\")'" + stub __call_huami_token 'printf 0' + stub __target_directory_dialog "echo \"$TEST_TEMP_DIR\"" + + run main + + unstub __target_directory_dialog + unstub __call_huami_token + unstub __credentials_dialog + + assert_output "The config at $TEST_TEMP_DIR/test-config.env was updated" + assert_file_exists "$TEST_TEMP_DIR/test-config.env" + assert_file_contains "$TEST_TEMP_DIR/test-config.env" "ZEPP_USER='gurli'" + assert_file_contains "$TEST_TEMP_DIR/test-config.env" "ZEPP_PASS='pass1234'" + assert_file_contains "$TEST_TEMP_DIR/test-config.env" "ZEPP_AGPS_TARGET_DIR='$TEST_TEMP_DIR'" +} + +# bats file_tags=configure +@test "User cancels credential dialog" { + unset -f __credentials_dialog + stub __credentials_dialog "fnuggi ugh : printf 'declare -a CREDS'" + + run main + + unstub __credentials_dialog + assert_output "Cancelled. $TEST_TEMP_DIR/test-config.env was not changed." + assert_file_exists "$TEST_TEMP_DIR/test-config.env" + assert_file_contains "$TEST_TEMP_DIR/test-config.env" "ZEPP_USER='fnuggi'" + assert_file_contains "$TEST_TEMP_DIR/test-config.env" "ZEPP_PASS='ugh'" + assert_file_contains "$TEST_TEMP_DIR/test-config.env" "ZEPP_AGPS_TARGET_DIR='/tmp/tmp'" +} + +# bats file_tags=configure +@test "No username was entered" { + unset -f __credentials_dialog + stub __credentials_dialog "fnuggi ugh : printf 'declare -a CREDS=([0]=\"\" [1]=\"pass1234\")'" + + run main + + unstub __credentials_dialog + assert_output "Username or password wasn't provided. Not changing $TEST_TEMP_DIR/test-config.env" + assert_file_exists "$TEST_TEMP_DIR/test-config.env" + assert_file_contains "$TEST_TEMP_DIR/test-config.env" "ZEPP_USER='fnuggi'" + assert_file_contains "$TEST_TEMP_DIR/test-config.env" "ZEPP_PASS='ugh'" + assert_file_contains "$TEST_TEMP_DIR/test-config.env" "ZEPP_AGPS_TARGET_DIR='/tmp/tmp'" +} + +# bats file_tags=configure +@test "No password was entered" { + unset -f __credentials_dialog + stub __credentials_dialog "fnuggi ugh : printf 'declare -a CREDS=([0]=\"gurli\" [1]=\"\")'" + + run main + + unstub __credentials_dialog + assert_output "Username or password wasn't provided. Not changing $TEST_TEMP_DIR/test-config.env" + assert_file_exists "$TEST_TEMP_DIR/test-config.env" + assert_file_contains "$TEST_TEMP_DIR/test-config.env" "ZEPP_USER='fnuggi'" + assert_file_contains "$TEST_TEMP_DIR/test-config.env" "ZEPP_PASS='ugh'" + assert_file_contains "$TEST_TEMP_DIR/test-config.env" "ZEPP_AGPS_TARGET_DIR='/tmp/tmp'" +} + +# bats file_tags=configure +@test "Username/password couldn't log in" { + unset -f __credentials_dialog __call_huami_token + stub __credentials_dialog "fnuggi ugh : printf 'declare -a CREDS=([0]=\"gurli\" [1]=\"pass1234\")'" + stub __call_huami_token 'printf 1' + + run main + + unstub __call_huami_token + unstub __credentials_dialog + assert_output "Failed to login with the given user and password. Aborting." + assert_file_exists "$TEST_TEMP_DIR/test-config.env" + assert_file_contains "$TEST_TEMP_DIR/test-config.env" "ZEPP_USER='fnuggi'" + assert_file_contains "$TEST_TEMP_DIR/test-config.env" "ZEPP_PASS='ugh'" + assert_file_contains "$TEST_TEMP_DIR/test-config.env" "ZEPP_AGPS_TARGET_DIR='/tmp/tmp'" +} + +# bats file_tags=configure +@test "Username is correctly normalised" { + unset -f __credentials_dialog __call_huami_token + stub __credentials_dialog "fnuggi ugh : printf 'declare -a CREDS=([0]=\"gur$'\n' li$'\r' \" [1]=\"pass1234\")'" + stub __call_huami_token '"gur li " "pass1234" : printf 1' + + cat /tmp/__credentials_dialog-stub-plan + run main + + unstub __call_huami_token + unstub __credentials_dialog +} + +# bats file_tags=configure +@test "Password is correctly normalised" { + unset -f __credentials_dialog __call_huami_token + stub __credentials_dialog "fnuggi ugh : printf 'declare -a CREDS=([0]=\"gurli\" [1]=\"pass$'\r' _ 1234\")'" + stub __call_huami_token '"gurli" "pass _ 1234" : printf 1' + + run main + + unstub __call_huami_token + unstub __credentials_dialog + assert_output "Failed to login with the given user and password. Aborting." +} + +# bats file_tags=configure +@test "User cancels target directory dialog" { + unset -f __credentials_dialog __call_huami_token __target_directory_dialog + stub __credentials_dialog "fnuggi ugh : printf 'declare -a CREDS=([0]=\"gurli\" [1]=\"pass1234\")'" + stub __call_huami_token 'printf 0' + stub __target_directory_dialog 'echo ""' + + run main + + unstub __target_directory_dialog + unstub __call_huami_token + unstub __credentials_dialog + assert_output "Cancelled. $TEST_TEMP_DIR/test-config.env was not changed." + assert_file_exists "$TEST_TEMP_DIR/test-config.env" + assert_file_contains "$TEST_TEMP_DIR/test-config.env" "ZEPP_USER='fnuggi'" + assert_file_contains "$TEST_TEMP_DIR/test-config.env" "ZEPP_PASS='ugh'" + assert_file_contains "$TEST_TEMP_DIR/test-config.env" "ZEPP_AGPS_TARGET_DIR='/tmp/tmp'" +} + +# bats file_tags=configure +@test "The path to the home directory is converted to the systemd version, %h" { + unset -f __credentials_dialog __call_huami_token __target_directory_dialog + stub __credentials_dialog "fnuggi ugh : printf 'declare -a CREDS=([0]=\"gurli\" [1]=\"pass1234\")'" + stub __call_huami_token 'printf 0' + stub __target_directory_dialog "echo $HOME" + + run main + + unstub __target_directory_dialog + unstub __call_huami_token + unstub __credentials_dialog + assert_output "The config at $TEST_TEMP_DIR/test-config.env was updated" + assert_file_exists "$TEST_TEMP_DIR/test-config.env" + assert_file_contains "$TEST_TEMP_DIR/test-config.env" "ZEPP_USER='gurli'" + assert_file_contains "$TEST_TEMP_DIR/test-config.env" "ZEPP_PASS='pass1234'" + assert_file_contains "$TEST_TEMP_DIR/test-config.env" "ZEPP_AGPS_TARGET_DIR='%h'" +} + +# bats file_tags=configure +@test "An existing systemd home path, %h, is converted to a real path in the target directory dialog" { + cat <"$CONFIG" +ZEPP_USER=fnuggi +ZEPP_PASS=ugh +ZEPP_AGPS_TARGET_DIR=%h +EOF + unset -f __credentials_dialog __call_huami_token __target_directory_dialog + stub __credentials_dialog "fnuggi ugh : printf 'declare -a CREDS=([0]=\"gurli\" [1]=\"pass1234\")'" + stub __call_huami_token 'printf 0' + stub __target_directory_dialog "$HOME : echo /tmp/tmp" + + run main + + unstub __target_directory_dialog + unstub __call_huami_token + unstub __credentials_dialog + assert_output "The config at $TEST_TEMP_DIR/test-config.env was updated" + assert_file_exists "$TEST_TEMP_DIR/test-config.env" + assert_file_contains "$TEST_TEMP_DIR/test-config.env" "ZEPP_USER='gurli'" + assert_file_contains "$TEST_TEMP_DIR/test-config.env" "ZEPP_PASS='pass1234'" + assert_file_contains "$TEST_TEMP_DIR/test-config.env" "ZEPP_AGPS_TARGET_DIR='/tmp/tmp'" +} diff --git a/test/20-huami-token-generator-test.bats b/test/20-huami-token-generator-test.bats new file mode 100644 index 0000000..22c1c44 --- /dev/null +++ b/test/20-huami-token-generator-test.bats @@ -0,0 +1,110 @@ +#!/usr/bin/env bats + +load 'test_helper' + +setup() { + load ../configuration/huami-token-generator + common_setup + + export XDG_CONFIG_HOME="$TEST_TEMP_DIR/.config" + mkdir -p "$XDG_CONFIG_HOME" +} + +# bats file_tags=generate +@test "The happy path" { + cat <"$XDG_CONFIG_HOME/huami-token.env" +ZEPP_USER=fnuggi +ZEPP_PASS=ugh +ZEPP_AGPS_TARGET_DIR=/tmp/tmp +EOF + unset -f _is_timer_disabled _disable_timer + + run main "$TEST_TEMP_DIR" + + assert_file_exists "$TEST_TEMP_DIR/01-WorkingDirectory.conf" + assert_file_contains "$TEST_TEMP_DIR/01-WorkingDirectory.conf" "WorkingDirectory=/tmp/tmp" +} + +# bats file_tags=generate +@test "The systemd conf is updated with new value" { + cat <"$XDG_CONFIG_HOME/huami-token.env" +ZEPP_USER=fnuggi +ZEPP_PASS=ugh +ZEPP_AGPS_TARGET_DIR=/tmp/ugh +EOF + echo "WorkingDirectory=/tmp/tmp" > "$TEST_TEMP_DIR/01-WorkingDirectory.conf" + unset -f _is_timer_disabled _disable_timer + + run main "$TEST_TEMP_DIR" + + assert_file_exists "$TEST_TEMP_DIR/01-WorkingDirectory.conf" + assert_file_contains "$TEST_TEMP_DIR/01-WorkingDirectory.conf" "WorkingDirectory=/tmp/ugh" +} + +# bats file_tags=generate +@test "Config file is missing, the timer is disabled" { + rm -f "$XDG_CONFIG_HOME/huami-token.env" + echo "WorkingDirectory=/tmp/tmp" > "$TEST_TEMP_DIR/01-WorkingDirectory.conf" + + unset -f _is_timer_enabled + stub _is_timer_enabled 'echo disabled' + + run main "$TEST_TEMP_DIR" + + assert_file_not_exists "$TEST_TEMP_DIR/01-WorkingDirectory.conf" + unstub _is_timer_enabled +} + +# bats file_tags=generate +@test "Config file is missing, the timer is enabled" { + rm -f "$XDG_CONFIG_HOME/huami-token.env" + echo "WorkingDirectory=/tmp/tmp" > "$TEST_TEMP_DIR/01-WorkingDirectory.conf" + + unset -f _is_timer_enabled _disable_timer + stub _is_timer_enabled 'echo enabled' + stub _disable_timer 'true' + + run main "$TEST_TEMP_DIR" + + assert_file_not_exists "$TEST_TEMP_DIR/01-WorkingDirectory.conf" + unstub _is_timer_enabled + unstub _disable_timer +} + +# bats file_tags=generate +@test "No target dir is set, the timer is disabled" { + cat <"$XDG_CONFIG_HOME/huami-token.env" +ZEPP_USER=fnuggi +ZEPP_PASS=ugh +ZEPP_AGPS_TARGET_DIR= +EOF + echo "WorkingDirectory=/tmp/tmp" > "$TEST_TEMP_DIR/01-WorkingDirectory.conf" + + unset -f _is_timer_enabled + stub _is_timer_enabled 'echo disabled' + + run main "$TEST_TEMP_DIR" + + assert_file_not_exists "$TEST_TEMP_DIR/01-WorkingDirectory.conf" + unstub _is_timer_enabled +} + +# bats file_tags=generate +@test "No target dir is set, the timer is enabled" { + cat <"$XDG_CONFIG_HOME/huami-token.env" +ZEPP_USER=fnuggi +ZEPP_PASS=ugh +ZEPP_AGPS_TARGET_DIR= +EOF + echo "WorkingDirectory=/tmp/tmp" > "$TEST_TEMP_DIR/01-WorkingDirectory.conf" + + unset -f _is_timer_enabled _disable_timer + stub _is_timer_enabled 'echo enabled' + stub _disable_timer 'true' + + run main "$TEST_TEMP_DIR" + + assert_file_not_exists "$TEST_TEMP_DIR/01-WorkingDirectory.conf" + unstub _is_timer_enabled + unstub _disable_timer +} diff --git a/test/test_helper.bash b/test/test_helper.bash new file mode 100644 index 0000000..38fbf44 --- /dev/null +++ b/test/test_helper.bash @@ -0,0 +1,30 @@ +bats_require_minimum_version 1.5.0 +bats_load_library 'bats-support' +bats_load_library 'bats-assert' +bats_load_library 'bats-mock' +bats_load_library 'bats-file' + +setup_file() { + BATS_TEST_TIMEOUT=3 +} + +teardown() { + temp_del "$TEST_TEMP_DIR" + unset TEST_TEMP_DIR +} + +common_setup() { + export TEST_TEMP_DIR="$(temp_make)" + + export CONFIG="$TEST_TEMP_DIR/test-config.env" + cat <"$CONFIG" +ZEPP_USER='fnuggi' +ZEPP_PASS='ugh' +ZEPP_AGPS_TARGET_DIR='/tmp/tmp' +EOF +} + +bats::on_failure() { + echo "Failed test run output:" + echo "$output" +}