diff --git a/machines/lasting-integrity/default.nix b/machines/lasting-integrity/default.nix index 4d77c94e..c4fa6c68 100644 --- a/machines/lasting-integrity/default.nix +++ b/machines/lasting-integrity/default.nix @@ -59,6 +59,7 @@ tetris.server = true; }; services = { + garmin-scraper.enable = true; grafana.enable = true; mail.enable = true; matrix.enable = true; diff --git a/modules/services/default.nix b/modules/services/default.nix index 4dbccb0f..78ca58c6 100644 --- a/modules/services/default.nix +++ b/modules/services/default.nix @@ -6,6 +6,7 @@ ./containers ./data-access ./deluge + ./garmin-scraper ./grafana ./mail ./matrix diff --git a/modules/services/garmin-scraper/default.nix b/modules/services/garmin-scraper/default.nix new file mode 100644 index 00000000..6fefde55 --- /dev/null +++ b/modules/services/garmin-scraper/default.nix @@ -0,0 +1,37 @@ +{ config, lib, pkgs, ... }: + +let + garmin2influx = pkgs.writers.writePython3Bin "garmin2influx" { + libraries = with pkgs.python3Packages; [ garminconnect influxdb-client ]; + } (builtins.readFile ./garmin2influx.py); +in +{ + options.chvp.services.garmin-scraper.enable = lib.mkEnableOption "garmin scraper"; + + config = lib.mkIf config.chvp.services.garmin-scraper.enable { + # Install in environment to allow manual data collection + environment.systemPackages = [ garmin2influx ]; + systemd = { + services.garmin2influx = { + description = "Garmin health data importer"; + restartIfChanged = false; + unitConfig.X-StopOnRemoval = false; + serviceConfig = { + EnvironmentFile = config.age.secrets."passwords/services/garmin2influx-env".path; + Type = "oneshot"; + User = "charlotte"; + Group = "users"; + ExecStart = "${garmin2influx}/bin/garmin2influx"; + RestartSec = "5s"; + Restart = "on-failure"; + }; + startAt = "02/4:00"; + }; + timers.garmin2influx.timerConfig.RandomizedDelaySec = "30min"; + }; + age.secrets."passwords/services/garmin2influx-env" = { + file = ../../../secrets/passwords/services/garmin2influx-env.age; + owner = "charlotte"; + }; + }; +} diff --git a/modules/services/garmin-scraper/garmin2influx.py b/modules/services/garmin-scraper/garmin2influx.py new file mode 100644 index 00000000..d5133910 --- /dev/null +++ b/modules/services/garmin-scraper/garmin2influx.py @@ -0,0 +1,66 @@ +import os +import sys +from datetime import date, datetime, timedelta, timezone +from garminconnect import ( + Garmin, + GarminConnectConnectionError, + GarminConnectTooManyRequestsError, + GarminConnectAuthenticationError, + ) +from influxdb_client import InfluxDBClient, Point, WritePrecision +from influxdb_client.client.write_api import SYNCHRONOUS + +email = os.getenv('EMAIL') +password = os.getenv('PASSWORD') +token = os.getenv('TOKEN') +org = 'default' +bucket = 'default' + + +def hr2point(time, val): + return Point("health") \ + .field("heart_rate", val) \ + .time( + datetime.fromtimestamp(time / 1000, timezone.utc), + WritePrecision.S + ) + + +def hr_for_date(api, date_to_fetch): + return api.get_heart_rates(date_to_fetch.isoformat())['heartRateValues'] + + +date_to_fetch = date.today().isoformat() +if len(sys.argv) > 1: + date_to_fetch = sys.argv[1] + +date_to_fetch = date.fromisoformat(date_to_fetch) + +try: + api = Garmin(email, password) + api.login() + points = list(map( + lambda p: hr2point(*p), + hr_for_date(api, date_to_fetch - timedelta(days=1)) + )) + points += list(map( + lambda p: hr2point(*p), + hr_for_date(api, date_to_fetch) + )) + with InfluxDBClient( + url="https://stats.chvp.be:8086", + token=token, + org=org + ) as client: + write_api = client.write_api(write_options=SYNCHRONOUS) + write_api.write(bucket, org, points) +except ( + GarminConnectConnectionError, + GarminConnectAuthenticationError, + GarminConnectTooManyRequestsError, +) as err: + print( + f'Error occured during Garmin Connect communication: {err}', + file=sys.stderr + ) + sys.exit(1) diff --git a/secrets.nix b/secrets.nix index efd67548..01374b1e 100644 --- a/secrets.nix +++ b/secrets.nix @@ -53,6 +53,7 @@ in "secrets/passwords/services/acme.age".publicKeys = servers ++ users; + "secrets/passwords/services/garmin2influx-env.age".publicKeys = [ lasting-integrity ] ++ users; "secrets/passwords/services/grafana/smtp.age".publicKeys = [ lasting-integrity ] ++ users; "secrets/passwords/services/grafana/admin-password.age".publicKeys = [ lasting-integrity ] ++ users; "secrets/passwords/services/grafana/secret-key.age".publicKeys = [ lasting-integrity ] ++ users; diff --git a/secrets/passwords/services/garmin2influx-env.age b/secrets/passwords/services/garmin2influx-env.age new file mode 100644 index 00000000..7a750f84 --- /dev/null +++ b/secrets/passwords/services/garmin2influx-env.age @@ -0,0 +1,13 @@ +age-encryption.org/v1 +-> ssh-ed25519 hKAFvQ Hx4N76JCCrv/Ybwgl2J0xrDJYcW4QUALyjQAGSfaeVw +IZijVgrusaqo0OlFI3aCmDe6dfmdVoKUWbqOYXEetsg +-> ssh-ed25519 s9rb8g dBblA0A3YJd0IkFgTTg/eQt6gUzQtZxRr6J2TU6NJwg +05rIEPNBVBIEK14JJ6LKLV8e9F7DmYuuKBg/ufXWYTs +-> ssh-ed25519 yad4VQ eZ4pWpGclpfgFsawKOEXa2AgPBkjOHZp9WmBVhfNXQA +FgSLcpVfIJsIgAAdH7rjJCFVBQ50qMHBXr6AMkhjSUw +-> ..-grease [IY! +3IuyW18ISiewloiloc1TZ+N7mfZaf+1TOrBJAnsNvvey0nzGSprmt8JCBrSBKKQF +yJ2Mcb2HMbe+ +--- /xUIYvscrvdSBlSv9a5E+8Chv5VkhF1CDLuj5cPV/YY +J`?)0k|kneKH1ytY4hߝ-V}fڭiƚEcög]FtqC&@ + ŢޕL7aK^~o!" y .C pcoXVHYO|D\JC .Hh6 \ No newline at end of file