From ff789db499468e0b5a6aa158592c10449e4d00fc Mon Sep 17 00:00:00 2001 From: Andrea Ciceri Date: Thu, 25 Jul 2024 16:42:39 +0200 Subject: [PATCH] `garmin-collector` --- hosts/default.nix | 1 + modules/garmin-collector/default.nix | 46 ++++++++++ packages/garmin-collector/default.nix | 12 +++ packages/garmin-collector/garmin-collector.py | 82 ++++++++++++++++++ secrets/garmin-collector-environment.age | Bin 0 -> 1713 bytes secrets/secrets.nix | 1 + 6 files changed, 142 insertions(+) create mode 100644 modules/garmin-collector/default.nix create mode 100644 packages/garmin-collector/default.nix create mode 100644 packages/garmin-collector/garmin-collector.py create mode 100644 secrets/garmin-collector-environment.age diff --git a/hosts/default.nix b/hosts/default.nix index 27ff71f..16a5f4f 100644 --- a/hosts/default.nix +++ b/hosts/default.nix @@ -182,6 +182,7 @@ # "matrix-registration-shared-secret".owner = "matrix-synapse"; # "matrix-sliding-sync-secret".owner = "matrix-synapse"; "autistici-password".owner = "forgejo"; + "garmin-collector-environment".owner = "garmin-collector"; }; }; }; diff --git a/modules/garmin-collector/default.nix b/modules/garmin-collector/default.nix new file mode 100644 index 0000000..b4ac0e1 --- /dev/null +++ b/modules/garmin-collector/default.nix @@ -0,0 +1,46 @@ +{ + pkgs, + lib, + fleetFlake, + config, + ... +}: { + users.users.garmin-collector = { + isSystemUser = true; + group = "garmin-collector"; + extraGroups = ["garmin-collector"]; + home = "/var/lib/garmin-collector"; + }; + + users.groups.garmin-collector = {}; + + systemd.services.garmin-collector = { + description = "Garmin collector pushing to Prometheus Pushgateway"; + wantedBy = ["multi-user.target"]; + environment = { + PUSHGATEWAY_ADDRESS = config.services.prometheus.pushgateway.web.listen-address; + }; + serviceConfig = { + Group = "garmin-collector"; + User = "garmin-collector"; + WorkingDirectory = "/var/lib/garmin-collector"; + ExecStart = '' + ${lib.getExe fleetFlake.packages.${pkgs.system}.garmin-collector} + ''; + EnvironmentFile = config.age.secrets.garmin-collector-environment.path; + }; + }; + + systemd.timers."garmin-collector" = { + wantedBy = ["timers.target"]; + timerConfig = { + OnBootSec = "5m"; + OnUnitActiveSec = "4h"; + Unit = "garmin-collector.service"; + }; + }; + + environment.persistence."/persist".directories = [ + "/var/lib/garmin-collector" + ]; +} diff --git a/packages/garmin-collector/default.nix b/packages/garmin-collector/default.nix new file mode 100644 index 0000000..2e19330 --- /dev/null +++ b/packages/garmin-collector/default.nix @@ -0,0 +1,12 @@ +{ + writers, + python3Packages, + ... +}: +writers.writePython3Bin "garmin-collector" { + libraries = with python3Packages; [ + prometheus-client + garminconnect + ]; + flakeIgnore = ["E501"]; +} (builtins.readFile ./garmin-collector.py) diff --git a/packages/garmin-collector/garmin-collector.py b/packages/garmin-collector/garmin-collector.py new file mode 100644 index 0000000..f296638 --- /dev/null +++ b/packages/garmin-collector/garmin-collector.py @@ -0,0 +1,82 @@ +# !/usr/bin/env python3 + +import datetime +import os + +from garth.exc import GarthHTTPError + +from garminconnect import ( + Garmin, + GarminConnectAuthenticationError, +) + + +from prometheus_client import CollectorRegistry, push_to_gateway +from prometheus_client.core import GaugeMetricFamily + +email = os.getenv("GARMIN_EMAIL") +password = os.getenv("GARMIN_PASSWORD") +tokenstore = os.getenv("GARMINTOKENS") or "~/.garminconnect" +tokenstore_base64 = os.getenv("GARMINTOKENS_BASE64") or "~/.garminconnect_base64" +gateway_address = os.getenv("PUSHGATEWAY_ADDRESS") + +today = datetime.date.today() + + +def init_api(email=email, password=password): + """Initialize Garmin API with your credentials.""" + + try: + print( + f"Trying to login to Garmin Connect using token data from directory '{tokenstore}'...\n" + ) + + garmin = Garmin() + garmin.login(tokenstore) + except (FileNotFoundError, GarthHTTPError, GarminConnectAuthenticationError): + # Session is expired. You'll need to log in again + print( + "Login tokens not present, login with your Garmin Connect credentials to generate them.\n" + f"They will be stored in '{tokenstore}' for future use.\n" + ) + garmin = Garmin(email=email, password=password, is_cn=False) + garmin.login() + # Save Oauth1 and Oauth2 token files to directory for next login + garmin.garth.dump(tokenstore) + print( + f"Oauth tokens stored in '{tokenstore}' directory for future use. (first method)\n" + ) + # Encode Oauth1 and Oauth2 tokens to base64 string and safe to file for next login (alternative way) + token_base64 = garmin.garth.dumps() + dir_path = os.path.expanduser(tokenstore_base64) + with open(dir_path, "w") as token_file: + token_file.write(token_base64) + print( + f"Oauth tokens encoded as base64 string and saved to '{dir_path}' file for future use. (second method)\n" + ) + + return garmin + + +class GarminCollector: + def __init__(self): + super().__init__() + self.api = init_api() + + def collect(self): + try: + body = self.api.get_daily_weigh_ins(today.isoformat())["totalAverage"] + metric_gauge = GaugeMetricFamily("body_composition", "Body composition and weight", labels=["metric"]) + for k in ["weight", "bmi", "bodyFat", "bodyWater", "boneMass", "muscleMass", "physiqueRating", "visceralFat"]: + metric_gauge.add_metric([k], body[k]) + except Exception as e: + print(f"Something went wrong while fetching body composition data\n{e}") + + yield metric_gauge + + +if __name__ == "__main__": + registry = CollectorRegistry() + registry.register(GarminCollector()) + + push_to_gateway(gateway_address, job='garmin', registry=registry) diff --git a/secrets/garmin-collector-environment.age b/secrets/garmin-collector-environment.age new file mode 100644 index 0000000000000000000000000000000000000000..efc5779ffef174d6bdd9c322235c021118feb077 GIT binary patch literal 1713 zcmYk)Im`420RUhNWg&%NY4^f{i19VaOilzL$7GV3+{fgo%zfY2Bx<*?TWljLf_OC| zHoBD{B3LdqTgjHLg%*NhDOh~k2tL2z39{rhX`^xP=b~%B(v2CkBEaj{UZyF39jD+0 zqJxat0B`80+m(hRB9F=}HRaS;;xyuL3MH~FV-}yC_GhnqC%45J+#aFI&MvlKHe&Ng zbP>>etnM7{GlHb8f zsa4q(1Xh=dTMotA4ljBqSajp1yq`RV3{1wR_YG~R{LXZP0=Q*3Me>SF8wLHaSz>H3 z@lNx7N;_UDM+#yw0rsRb#8frnsMUa%G3pe#Hoe$Bj#5AoBB7(l^0wgA8B>xdH{865?%Q2rU<9WCg0NxXWaHr0MQ;PIZY8erd=h)k% zd2`$TPCVR{w0R_U24Pdb95 zJwn_PqL$FZo8on;VRSQCXU+ltu2~dZ<@tXzMiP>#(8=<7l3^%?*p?L!-HSRo&{RKm zyxCx#J$ZXlH{~=WSJUw;U@l`(cot#h=JHA<0p`X=_*fQFwgioPsI%8_7zwiC>Oz$V zhBw3HmdiaN>w6Q=)6H<}6 zXXDo7R~2COU36;KC?#bwJS*NkNL8h?cY^iAC5)wCt&ws(Wl^pVwodN-LJG;FKXKSqcFqi@4E}n? zEr5{nYJO^46f?baT(*Q3r3T+{JeW0>B}c7iaedG_D=rjsx^jw-$BD@}Ez3^LJ);4F zajX?gpPtXA2NRf&#Y}F2!<&H-2BI&4VrMTP0GyF7`!lu=1S?EgJZ;uD4;)C;)AMoYh(I9V21@aZqK zRwJiWgZrSy2l)XC8UaUCR}>ZvCePlA0G7D2waLm}D0{`CP9w3Wy%UEXe(+rsA3EXA zx>MMr9s#efuP-A^V#VYe3OJ9A(`(s@D9RAt!zJT5n}&m6vxqb))SL!>{xjvH--n<6 z;m3db22gdFNN(bp8SUYyZZdhVQ-q_s`OQ9)I$MAH4Z({q6@} z|Ke{x_rdSJODkW_-=4g`ewdNwkL$0lqV_51N1y-F+n*Tc{FCc1tG9mfZ$$m*+x+T$ O|K%?q|Mb1de(OKN<3c0= literal 0 HcmV?d00001 diff --git a/secrets/secrets.nix b/secrets/secrets.nix index 8f3c4ef..fb8e4df 100644 --- a/secrets/secrets.nix +++ b/secrets/secrets.nix @@ -27,6 +27,7 @@ in "matrix-sliding-sync-secret.age".publicKeys = [ccr-ssh ccr-gpg sisko]; "forgejo-runners-token.age".publicKeys = [ccr-ssh ccr-gpg picard]; "forgejo-nix-access-tokens.age".publicKeys = [ccr-ssh ccr-gpg picard]; + "garmin-collector-environment.age".publicKeys = [ccr-ssh ccr-gpg sisko]; # WireGuard "picard-wireguard-private-key.age".publicKeys = [ccr-ssh ccr-gpg picard];