From cb534d253ea6f67d9f72ca7fa8eb46ff8f020ea6 Mon Sep 17 00:00:00 2001 From: k0rventen Date: Sat, 17 Jun 2023 12:28:16 +0200 Subject: [PATCH 1/4] feat: add logic for parsing sleep data and viz panels --- ingester/app.py | 52 +- .../dashboards/dashs/specific-metrics.json | 541 +++++++++++++++++- 2 files changed, 578 insertions(+), 15 deletions(-) diff --git a/ingester/app.py b/ingester/app.py index 3a399ae..12aa5ee 100644 --- a/ingester/app.py +++ b/ingester/app.py @@ -5,7 +5,7 @@ import os import time import xml.etree.ElementTree as etree -from datetime import datetime as dt +from datetime import datetime,timedelta from shutil import unpack_archive from typing import Any, Union @@ -30,7 +30,7 @@ def parse_float_with_try(v: Any) -> Union[float, int]: def parse_date_as_timestamp(v: Any) -> int: - return int(dt.fromisoformat(v).timestamp()) + return int(datetime.fromisoformat(v).timestamp()) def format_route_point( @@ -56,6 +56,29 @@ def format_route_point( datapoint["fields"]["distance"] = point.distance_3d(next_point) return datapoint +def compute_sleep_times(record): + start_date = datetime.fromisoformat(record.get("startDate")) + start_date = start_date.replace(second=0) + end_date = datetime.fromisoformat(record.get("endDate")) + end_date = end_date.replace(second=0) + device = record.get("sourceName", "unknown") + minutes_in_bed = [] + sleep_seconds = int(end_date.timestamp() - start_date.timestamp()) + while start_date <= end_date: + minutes_in_bed.append({ + 'measurement':"SleepAnalysisTimes", + "time":int(start_date.timestamp()), + "fields": {"value": 1 if record.get("value") == "HKCategoryValueSleepAnalysisInBed" else 2,}, + "tags": {"unit": 'minutes', "device": device} + }) + start_date += timedelta(minutes=1) + minutes_in_bed.append({ + 'measurement':'SleepAnalysis', + "time":start_date, + "fields": {"value": sleep_seconds}, + "tags": {"unit": 'seconds', "device": device,'state':record.get("value")} + }) + return minutes_in_bed def format_record(record: dict[str, Any]) -> dict[str, Any]: """format a export health xml record for influx""" @@ -65,17 +88,20 @@ def format_record(record: dict[str, Any]) -> dict[str, Any]: .removeprefix("HKCategoryTypeIdentifier") .removeprefix("HKDataType") ) + if measurement == "SleepAnalysis": + return compute_sleep_times(record) + date = parse_date_as_timestamp(record.get("startDate", 0)) value = parse_float_with_try(record.get("value", 1)) unit = record.get("unit", "unit") device = record.get("sourceName", "unknown") - return { + return [{ "measurement": measurement, "time": date, "fields": {"value": value}, "tags": {"unit": unit, "device": device}, - } + }] def format_workout(record: dict[str, Any]) -> dict[str, Any]: @@ -133,16 +159,18 @@ def process_health_data(client: InfluxDBClient) -> None: records = [] total_count = 0 for _, elem in etree.iterparse(EXPORT_PATH): - if elem.tag == "Record": - records.append(format_record(elem)) - elem.clear() - elif elem.tag == "Workout": - records.append(format_workout(elem)) + if elem.tag == "Record": + if rec := format_record(elem): + for r in rec: + records.append(r) elem.clear() + # elif elem.tag == "Workout": + # records.append(format_workout(elem)) + # elem.clear() # batch push every 10000 - if len(records) == 10000: - total_count += 10000 + if len(records) >= 10000: + total_count += len(records) client.write_points(records, time_precision="s") del records @@ -176,6 +204,6 @@ def process_health_data(client: InfluxDBClient) -> None: print("Waiting on influx to be ready..") time.sleep(1) - process_workout_routes(client) + #process_workout_routes(client) process_health_data(client) print("All done! You can now check grafana.") diff --git a/provisioning/dashboards/dashs/specific-metrics.json b/provisioning/dashboards/dashs/specific-metrics.json index 4ba7d8d..65d10a4 100644 --- a/provisioning/dashboards/dashs/specific-metrics.json +++ b/provisioning/dashboards/dashs/specific-metrics.json @@ -99,7 +99,7 @@ }, "textMode": "value_and_name" }, - "pluginVersion": "9.3.13", + "pluginVersion": "9.5.3", "targets": [ { "alias": "Cycled distance this year ($tag_unit)", @@ -2479,10 +2479,545 @@ ], "title": "Heart rate through the day", "type": "timeseries" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 47 + }, + "id": 24, + "panels": [], + "title": "Sleep data", + "type": "row" + }, + { + "datasource": { + "type": "influxdb", + "uid": "influx" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 61, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineStyle": { + "fill": "solid" + }, + "lineWidth": 3, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "decimals": 0, + "mappings": [ + { + "options": { + "1": { + "index": 0, + "text": "In bed" + }, + "2": { + "index": 1, + "text": "Sleeping" + } + }, + "type": "value" + } + ], + "max": 2, + "min": 1, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "semi-dark-blue", + "value": null + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 13, + "w": 10, + "x": 0, + "y": 48 + }, + "id": 25, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": false + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "influxdb", + "uid": "influx" + }, + "groupBy": [ + { + "params": [ + "1m" + ], + "type": "time" + }, + { + "params": [ + "device::tag" + ], + "type": "tag" + }, + { + "params": [ + "null" + ], + "type": "fill" + } + ], + "measurement": "SleepAnalysisTimes", + "orderByTime": "ASC", + "policy": "autogen", + "refId": "A", + "resultFormat": "time_series", + "select": [ + [ + { + "params": [ + "value" + ], + "type": "field" + }, + { + "params": [], + "type": "last" + } + ] + ], + "tags": [ + { + "key": "device::tag", + "operator": "=~", + "value": "/^$source$/" + } + ] + } + ], + "title": "Times in bed/sleeping", + "type": "timeseries" + }, + { + "datasource": { + "type": "influxdb", + "uid": "influx" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "points", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + } + }, + "gridPos": { + "h": 13, + "w": 11, + "x": 10, + "y": 48 + }, + "id": 27, + "interval": "1d", + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "influxdb", + "uid": "influx" + }, + "groupBy": [ + { + "params": [ + "1d" + ], + "type": "time" + }, + { + "params": [ + "unit" + ], + "type": "tag" + }, + { + "params": [ + "device::tag" + ], + "type": "tag" + }, + { + "params": [ + "none" + ], + "type": "fill" + } + ], + "hide": false, + "measurement": "SleepAnalysis", + "orderByTime": "ASC", + "policy": "default", + "refId": "B", + "resultFormat": "time_series", + "select": [ + [ + { + "params": [ + "value" + ], + "type": "field" + }, + { + "params": [], + "type": "sum" + } + ] + ], + "tags": [ + { + "key": "device::tag", + "operator": "=~", + "value": "/^$source$/" + } + ] + } + ], + "title": "Durations of time in bed", + "type": "timeseries" + }, + { + "datasource": { + "type": "influxdb", + "uid": "influx" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "purple", + "value": null + } + ] + }, + "unit": "h" + }, + "overrides": [] + }, + "gridPos": { + "h": 5, + "w": 3, + "x": 21, + "y": 48 + }, + "id": 26, + "options": { + "colorMode": "background", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "9.5.3", + "targets": [ + { + "alias": "Sleep Goal", + "datasource": { + "type": "influxdb", + "uid": "influx" + }, + "groupBy": [], + "measurement": "SleepDurationGoal", + "orderByTime": "ASC", + "policy": "autogen", + "query": "SELECT \"value\" FROM \"autogen\".\"SleepDurationGoal\" ", + "rawQuery": true, + "refId": "A", + "resultFormat": "time_series", + "select": [ + [ + { + "params": [ + "value" + ], + "type": "field" + } + ] + ], + "tags": [] + } + ], + "type": "stat" + }, + { + "datasource": { + "type": "influxdb", + "uid": "influx" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "dark-blue", + "value": null + } + ] + }, + "unit": "s" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*bed.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "light-blue", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 3, + "x": 21, + "y": 53 + }, + "id": 29, + "options": { + "colorMode": "background", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "9.5.3", + "targets": [ + { + "alias": "Total time spent in bed ($tag_device)", + "datasource": { + "type": "influxdb", + "uid": "influx" + }, + "groupBy": [ + { + "params": [ + "device::tag" + ], + "type": "tag" + } + ], + "hide": false, + "measurement": "SleepAnalysis", + "orderByTime": "ASC", + "policy": "autogen", + "query": "SELECT sum(\"value\") FROM \"autogen\".\"SleepAnalysis\" WHERE (\"state\"::tag = 'HKCategoryValueSleepAnalysisInBed') AND $timeFilter GROUP BY \"device\"::tag", + "rawQuery": false, + "refId": "A", + "resultFormat": "time_series", + "select": [ + [ + { + "params": [ + "value" + ], + "type": "field" + }, + { + "params": [], + "type": "sum" + } + ] + ], + "tags": [ + { + "key": "state::tag", + "operator": "=", + "value": "HKCategoryValueSleepAnalysisInBed" + } + ] + }, + { + "alias": "Total time spent asleep ($tag_device)", + "datasource": { + "type": "influxdb", + "uid": "influx" + }, + "groupBy": [ + { + "params": [ + "device::tag" + ], + "type": "tag" + } + ], + "hide": false, + "measurement": "SleepAnalysis", + "orderByTime": "ASC", + "policy": "autogen", + "query": "SELECT sum(\"value\") FROM \"autogen\".\"SleepAnalysis\" WHERE (\"state\"::tag = 'HKCategoryValueSleepAnalysisInBed') AND $timeFilter GROUP BY \"device\"::tag", + "rawQuery": false, + "refId": "B", + "resultFormat": "time_series", + "select": [ + [ + { + "params": [ + "value" + ], + "type": "field" + }, + { + "params": [], + "type": "sum" + } + ] + ], + "tags": [ + { + "key": "state::tag", + "operator": "=", + "value": "HKCategoryValueSleepAnalysisAsleepUnspecified" + } + ] + } + ], + "type": "stat" } ], "refresh": "30s", - "schemaVersion": 37, + "schemaVersion": 38, "style": "dark", "tags": [], "templating": { @@ -2517,7 +3052,7 @@ ] }, "time": { - "from": "now-2y", + "from": "now-90d", "to": "now" }, "timepicker": {}, From 5bdfc1feb685a982922d85af229ee6e57b7352df Mon Sep 17 00:00:00 2001 From: coco <49311792+k0rventen@users.noreply.github.com> Date: Sun, 6 Aug 2023 11:44:41 +0200 Subject: [PATCH 2/4] feat: add workflow for creating per-branch docker images --- .github/workflows/branch_builds.yaml | 41 ++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 .github/workflows/branch_builds.yaml diff --git a/.github/workflows/branch_builds.yaml b/.github/workflows/branch_builds.yaml new file mode 100644 index 0000000..bf115e1 --- /dev/null +++ b/.github/workflows/branch_builds.yaml @@ -0,0 +1,41 @@ +name: Build an image for each non-main branch with tag : + +permissions: + contents: write + +on: + push: + branches: + - '**' # matches every branch + - '!main' # excludes master + +jobs: + build-and-release: + runs-on: ubuntu-latest + steps: + - + name: Checkout + uses: actions/checkout@v3 + - + name: Dockerhub login + uses: docker/login-action@v2 + with: + username: ${{ github.repository_owner }} + password: ${{ secrets.DOCKER_TOKEN }} + - + name: Set up QEMU + uses: docker/setup-qemu-action@v2 + - + name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + - + name: Inject slug/short variables + uses: rlespinasse/github-slug-action@v4 + - + name: Docker build & push + uses: docker/build-push-action@v4 + with: + context: ./ingester + push: true + platforms: linux/amd64,linux/arm64 + tags: ${{ github.repository }}-ingester:${GITHUB_REF_SLUG} From af45514a68ba06c7ab73c499f7881c6578b3dbfc Mon Sep 17 00:00:00 2001 From: coco <49311792+k0rventen@users.noreply.github.com> Date: Sun, 6 Aug 2023 12:51:21 +0200 Subject: [PATCH 3/4] fix: use correct env var for slug branch --- .github/workflows/branch_builds.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/branch_builds.yaml b/.github/workflows/branch_builds.yaml index bf115e1..4f4a759 100644 --- a/.github/workflows/branch_builds.yaml +++ b/.github/workflows/branch_builds.yaml @@ -38,4 +38,4 @@ jobs: context: ./ingester push: true platforms: linux/amd64,linux/arm64 - tags: ${{ github.repository }}-ingester:${GITHUB_REF_SLUG} + tags: ${{ github.repository }}-ingester:${{ env.GITHUB_REF_SLUG }} From dd00a1e6bc25f469e51b287888cd627cb9ea3992 Mon Sep 17 00:00:00 2001 From: k0rventen Date: Sat, 19 Aug 2023 21:38:39 +0200 Subject: [PATCH 4/4] feat: better sleep tracking support --- docker-compose.yml | 2 +- ingester/Dockerfile | 4 +- ingester/app.py | 73 +- ingester/formatters.py | 78 ++ .../dashboards/dashs/sleep-tracking.json | 1174 +++++++++++++++++ 5 files changed, 1280 insertions(+), 51 deletions(-) create mode 100644 ingester/formatters.py create mode 100644 provisioning/dashboards/dashs/sleep-tracking.json diff --git a/docker-compose.yml b/docker-compose.yml index cd37e51..67ee21e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,7 +1,7 @@ version: "3" services: grafana: - image: grafana/grafana:9.5.5 + image: grafana/grafana:10.0.3 ports: - 3000:3000 environment: diff --git a/ingester/Dockerfile b/ingester/Dockerfile index 3781ef4..29a4476 100644 --- a/ingester/Dockerfile +++ b/ingester/Dockerfile @@ -3,5 +3,5 @@ FROM python:3.11-alpine COPY requirements.txt . RUN pip3 install -r requirements.txt -COPY app.py . -ENTRYPOINT python3 -u app.py \ No newline at end of file +COPY *.py . +ENTRYPOINT python3 -uO app.py \ No newline at end of file diff --git a/ingester/app.py b/ingester/app.py index 33a3b17..6f23dba 100644 --- a/ingester/app.py +++ b/ingester/app.py @@ -5,11 +5,13 @@ import os import re import time - from lxml import etree -from datetime import datetime,timedelta +from datetime import datetime as dt from shutil import unpack_archive from typing import Any, Union +import subprocess + +from formatters import parse_date_as_timestamp, parse_float_with_try, AppleStandHourFormatter, SleepAnalysisFormatter import gpxpy from gpxpy.gpx import GPXTrackPoint @@ -20,21 +22,6 @@ EXPORT_PATH = "/export/apple_health_export" EXPORT_XML_REGEX = re.compile("export.xml",re.IGNORECASE) -def parse_float_with_try(v: Any) -> Union[float, int]: - """convert v to float or 0""" - try: - return float(v) - except ValueError: - try: - return int(v) - except Exception: - return 0 - - -def parse_date_as_timestamp(v: Any) -> int: - return int(datetime.fromisoformat(v).timestamp()) - - def format_route_point( name: str, point: GPXTrackPoint, next_point=None ) -> dict[str, Any]: @@ -58,29 +45,6 @@ def format_route_point( datapoint["fields"]["distance"] = point.distance_3d(next_point) return datapoint -def compute_sleep_times(record): - start_date = datetime.fromisoformat(record.get("startDate")) - start_date = start_date.replace(second=0) - end_date = datetime.fromisoformat(record.get("endDate")) - end_date = end_date.replace(second=0) - device = record.get("sourceName", "unknown") - minutes_in_bed = [] - sleep_seconds = int(end_date.timestamp() - start_date.timestamp()) - while start_date <= end_date: - minutes_in_bed.append({ - 'measurement':"SleepAnalysisTimes", - "time":int(start_date.timestamp()), - "fields": {"value": 1 if record.get("value") == "HKCategoryValueSleepAnalysisInBed" else 2,}, - "tags": {"unit": 'minutes', "device": device} - }) - start_date += timedelta(minutes=1) - minutes_in_bed.append({ - 'measurement':'SleepAnalysis', - "time":start_date, - "fields": {"value": sleep_seconds}, - "tags": {"unit": 'seconds', "device": device,'state':record.get("value")} - }) - return minutes_in_bed def format_record(record: dict[str, Any]) -> dict[str, Any]: """format a export health xml record for influx""" @@ -90,9 +54,12 @@ def format_record(record: dict[str, Any]) -> dict[str, Any]: .removeprefix("HKCategoryTypeIdentifier") .removeprefix("HKDataType") ) + + if measurement == "AppleStandHour": + return AppleStandHourFormatter(record) if measurement == "SleepAnalysis": - return compute_sleep_times(record) - + return SleepAnalysisFormatter(record) + date = parse_date_as_timestamp(record.get("startDate", 0)) value = parse_float_with_try(record.get("value", 1)) unit = record.get("unit", "unit") @@ -159,22 +126,32 @@ def process_health_data(client: InfluxDBClient) -> None: if not export_xml_files: print("No export file found, skipping...") return - export_file = os.path.join(EXPORT_PATH,export_xml_files[0]) print("Export file is",export_file) + + print("Removing potentially malformed XML..") + p = subprocess.run("sed -i '/= 10000: - total_count += len(records) + if len(records) == 10000: + total_count += 10000 client.write_points(records, time_precision="s") del records @@ -208,6 +185,6 @@ def process_health_data(client: InfluxDBClient) -> None: print("Waiting on influx to be ready..") time.sleep(1) - #process_workout_routes(client) + process_workout_routes(client) process_health_data(client) - print("All done! You can now check grafana.") + print("All done! You can now check grafana.") \ No newline at end of file diff --git a/ingester/formatters.py b/ingester/formatters.py new file mode 100644 index 0000000..0e37a82 --- /dev/null +++ b/ingester/formatters.py @@ -0,0 +1,78 @@ +from datetime import datetime as dt +from datetime import timedelta +from typing import Any, Union + + +def parse_float_with_try(v: Any) -> Union[float, int]: + """convert v to float or 0""" + try: + return float(v) + except ValueError: + try: + return int(v) + except Exception: + return 0 + + +def parse_date_as_timestamp(v: Any) -> int: + return int(dt.fromisoformat(v).timestamp()) + + +def AppleStandHourFormatter(record: dict) -> dict: + date = parse_date_as_timestamp(record.get("startDate", 0)) + unit = record.get("unit", "unit") + device = record.get("sourceName", "unknown") + value = 1 if record.get("value") == "HKCategoryValueAppleStandHourStood" else 0 + + return { + "measurement": "AppleStandHour", + "time": date, + "fields": {"value": value}, + "tags": {"unit": unit, "device": device}, + } + + +sleep_states_lookup={ + "HKCategoryValueSleepAnalysisAsleepDeep":0, + "HKCategoryValueSleepAnalysisAsleepCore":1, + "HKCategoryValueSleepAnalysisAsleepREM":2, + #"HKCategoryValueSleepAnalysisAsleepUnspecified":3, + "HKCategoryValueSleepAnalysisInBed": 3, + "HKCategoryValueSleepAnalysisAwake":4, +} + +sleep_states_short_lookup={ + "HKCategoryValueSleepAnalysisAsleepDeep":"Deep", + "HKCategoryValueSleepAnalysisAsleepCore":"Core", + "HKCategoryValueSleepAnalysisAsleepREM":"REM", + #"HKCategoryValueSleepAnalysisAsleepUnspecified":"Asleep", + "HKCategoryValueSleepAnalysisInBed": "Asleep", + "HKCategoryValueSleepAnalysisAwake":"Awake", +} + +def SleepAnalysisFormatter(record: dict) -> dict: + start_date = dt.fromisoformat(record.get("startDate")) + start_date.replace(second=0) + end_date = dt.fromisoformat(record.get("endDate")) + device = record.get("sourceName", "unknown") + state = sleep_states_lookup.get(record.get("value"),5) + if "Apple Watch" in device: + print(start_date,end_date,device,state) + + minutes_in_bed = [] + while start_date <= end_date: + minutes_in_bed.append({ + 'measurement':"SleepAnalysisTimes-{}".format(device), + "time":int(start_date.timestamp()), + "fields": {"value":state}, + "tags": {} + }) + start_date += timedelta(minutes=1) + + minutes_in_bed.append({ + 'measurement':'SleepAnalysis', + "time":start_date, + "fields": {"start": int(dt.fromisoformat(record.get("startDate")).timestamp()),"stop":int(dt.fromisoformat(record.get("endDate")).timestamp())}, + "tags": {"unit": 'seconds', "device": device,'state':sleep_states_short_lookup.get(record.get("value"),"Unspecified")} + }) + return minutes_in_bed diff --git a/provisioning/dashboards/dashs/sleep-tracking.json b/provisioning/dashboards/dashs/sleep-tracking.json new file mode 100644 index 0000000..4013b2b --- /dev/null +++ b/provisioning/dashboards/dashs/sleep-tracking.json @@ -0,0 +1,1174 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "description": "Pull from an Apple watch sleep tracking data", + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": 5, + "links": [], + "liveNow": false, + "panels": [ + { + "datasource": { + "type": "influxdb", + "uid": "influx" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds", + "seriesBy": "last" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "series", + "axisGridShow": false, + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "points", + "fillOpacity": 0, + "gradientMode": "scheme", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 3, + "pointSize": 10, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "decimals": 0, + "mappings": [ + { + "options": { + "0": { + "color": "purple", + "index": 0, + "text": "Deep sleep" + }, + "1": { + "color": "semi-dark-blue", + "index": 1, + "text": "Core Sleep" + }, + "2": { + "color": "light-blue", + "index": 2, + "text": "REM Sleep" + }, + "3": { + "color": "super-light-blue", + "index": 3, + "text": "Asleep" + }, + "4": { + "color": "light-orange", + "index": 4, + "text": "Awake" + } + }, + "type": "value" + } + ], + "max": 4, + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "purple", + "value": null + }, + { + "color": "semi-dark-blue", + "value": 1 + }, + { + "color": "blue", + "value": 2 + }, + { + "color": "super-light-blue", + "value": 3 + }, + { + "color": "yellow", + "value": 4 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 13, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 1, + "interval": "10s", + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": false + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "influxdb", + "uid": "influx" + }, + "groupBy": [], + "measurement": "/^$sleep_sources$/", + "orderByTime": "ASC", + "policy": "autogen", + "refId": "A", + "resultFormat": "time_series", + "select": [ + [ + { + "params": [ + "value" + ], + "type": "field" + } + ] + ], + "tags": [] + } + ], + "title": "Sleep analysis", + "type": "timeseries" + }, + { + "datasource": { + "type": "influxdb", + "uid": "influx" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "purple", + "value": null + } + ] + }, + "unit": "m" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 15, + "x": 0, + "y": 13 + }, + "id": 5, + "options": { + "colorMode": "background", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "vertical", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "10.0.3", + "targets": [ + { + "alias": "REM Sleep", + "datasource": { + "type": "influxdb", + "uid": "influx" + }, + "groupBy": [], + "measurement": "SleepAnalysisTimes-Apple Watch", + "orderByTime": "ASC", + "policy": "autogen", + "query": "SELECT count(\"value\") FROM \"autogen\"./^$sleep_sources$/ WHERE (\"value\"::field = 2) AND $timeFilter", + "rawQuery": true, + "refId": "A", + "resultFormat": "time_series", + "select": [ + [ + { + "params": [ + "value" + ], + "type": "field" + } + ] + ], + "tags": [ + { + "key": "value::field", + "operator": "=", + "value": "2" + } + ] + }, + { + "alias": "Core Sleep", + "datasource": { + "type": "influxdb", + "uid": "influx" + }, + "groupBy": [], + "hide": false, + "measurement": "SleepAnalysisTimes-Apple Watch", + "orderByTime": "ASC", + "policy": "autogen", + "query": "SELECT count(\"value\") FROM \"autogen\"./^$sleep_sources$/ WHERE (\"value\"::field = 1) AND $timeFilter", + "rawQuery": true, + "refId": "B", + "resultFormat": "time_series", + "select": [ + [ + { + "params": [ + "value" + ], + "type": "field" + } + ] + ], + "tags": [ + { + "key": "value::field", + "operator": "=", + "value": "2" + } + ] + }, + { + "alias": "Deep Sleep", + "datasource": { + "type": "influxdb", + "uid": "influx" + }, + "groupBy": [], + "hide": false, + "measurement": "SleepAnalysisTimes-Apple Watch", + "orderByTime": "ASC", + "policy": "autogen", + "query": "SELECT count(\"value\") FROM \"autogen\"./^$sleep_sources$/ WHERE (\"value\"::field = 0) AND $timeFilter", + "rawQuery": true, + "refId": "C", + "resultFormat": "time_series", + "select": [ + [ + { + "params": [ + "value" + ], + "type": "field" + } + ] + ], + "tags": [ + { + "key": "value::field", + "operator": "=", + "value": "2" + } + ] + }, + { + "alias": "Asleep", + "datasource": { + "type": "influxdb", + "uid": "influx" + }, + "groupBy": [], + "hide": false, + "measurement": "SleepAnalysisTimes-Apple Watch", + "orderByTime": "ASC", + "policy": "autogen", + "query": "SELECT count(\"value\") FROM \"autogen\"./^$sleep_sources$/ WHERE (\"value\"::field = 3) AND $timeFilter", + "rawQuery": true, + "refId": "D", + "resultFormat": "time_series", + "select": [ + [ + { + "params": [ + "value" + ], + "type": "field" + } + ] + ], + "tags": [ + { + "key": "value::field", + "operator": "=", + "value": "2" + } + ] + }, + { + "alias": "Awake", + "datasource": { + "type": "influxdb", + "uid": "influx" + }, + "groupBy": [], + "hide": false, + "measurement": "SleepAnalysisTimes-Apple Watch", + "orderByTime": "ASC", + "policy": "autogen", + "query": "SELECT count(\"value\") FROM \"autogen\"./^$sleep_sources$/ WHERE (\"value\"::field = 4) AND $timeFilter", + "rawQuery": true, + "refId": "E", + "resultFormat": "time_series", + "select": [ + [ + { + "params": [ + "value" + ], + "type": "field" + } + ] + ], + "tags": [ + { + "key": "value::field", + "operator": "=", + "value": "2" + } + ] + }, + { + "alias": "Total sleep time", + "datasource": { + "type": "influxdb", + "uid": "influx" + }, + "groupBy": [], + "hide": false, + "measurement": "SleepAnalysisTimes-Apple Watch", + "orderByTime": "ASC", + "policy": "autogen", + "query": "SELECT count(\"value\") FROM \"autogen\"./^$sleep_sources$/ WHERE $timeFilter", + "rawQuery": true, + "refId": "F", + "resultFormat": "time_series", + "select": [ + [ + { + "params": [ + "value" + ], + "type": "field" + } + ] + ], + "tags": [ + { + "key": "value::field", + "operator": "=", + "value": "2" + } + ] + } + ], + "title": "Time in each sleep category ", + "type": "stat" + }, + { + "datasource": { + "type": "influxdb", + "uid": "influx" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "yellow", + "value": null + } + ] + }, + "unit": "percentunit" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 2, + "x": 15, + "y": 13 + }, + "id": 10, + "options": { + "colorMode": "background", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "10.0.3", + "targets": [ + { + "alias": "Awake", + "datasource": { + "type": "influxdb", + "uid": "influx" + }, + "groupBy": [], + "measurement": "SleepAnalysisTimes-Apple Watch", + "orderByTime": "ASC", + "policy": "autogen", + "query": "SELECT count(\"value\") FROM \"autogen\"./^$sleep_sources$/ WHERE (\"value\"::field = 4) AND $timeFilter", + "rawQuery": true, + "refId": "A", + "resultFormat": "time_series", + "select": [ + [ + { + "params": [ + "value" + ], + "type": "field" + } + ] + ], + "tags": [ + { + "key": "value::field", + "operator": "=", + "value": "2" + } + ] + }, + { + "alias": "Total", + "datasource": { + "type": "influxdb", + "uid": "influx" + }, + "groupBy": [], + "hide": false, + "measurement": "SleepAnalysisTimes-Apple Watch", + "orderByTime": "ASC", + "policy": "autogen", + "query": "SELECT count(\"value\") FROM \"autogen\"./^$sleep_sources$/ WHERE $timeFilter", + "rawQuery": true, + "refId": "B", + "resultFormat": "time_series", + "select": [ + [ + { + "params": [ + "value" + ], + "type": "field" + } + ] + ], + "tags": [ + { + "key": "value::field", + "operator": "=", + "value": "2" + } + ] + } + ], + "transformations": [ + { + "id": "joinByField", + "options": { + "byField": "Time", + "mode": "outer" + } + }, + { + "id": "calculateField", + "options": { + "alias": "Awake", + "binary": { + "left": "Awake", + "operator": "/", + "reducer": "sum", + "right": "Total" + }, + "mode": "binary", + "reduce": { + "reducer": "sum" + }, + "replaceFields": true + } + } + ], + "type": "stat" + }, + { + "datasource": { + "type": "influxdb", + "uid": "influx" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "light-blue", + "value": null + } + ] + }, + "unit": "percentunit" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 3, + "x": 17, + "y": 13 + }, + "id": 8, + "options": { + "colorMode": "background", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "10.0.3", + "targets": [ + { + "alias": "Normal Sleep", + "datasource": { + "type": "influxdb", + "uid": "influx" + }, + "groupBy": [], + "measurement": "SleepAnalysisTimes-Apple Watch", + "orderByTime": "ASC", + "policy": "autogen", + "query": "SELECT count(\"value\") FROM \"autogen\"./^$sleep_sources$/ WHERE (\"value\"::field = 3) AND $timeFilter", + "rawQuery": true, + "refId": "A", + "resultFormat": "time_series", + "select": [ + [ + { + "params": [ + "value" + ], + "type": "field" + } + ] + ], + "tags": [ + { + "key": "value::field", + "operator": "=", + "value": "2" + } + ] + }, + { + "alias": "Total", + "datasource": { + "type": "influxdb", + "uid": "influx" + }, + "groupBy": [], + "hide": false, + "measurement": "SleepAnalysisTimes-Apple Watch", + "orderByTime": "ASC", + "policy": "autogen", + "query": "SELECT count(\"value\") FROM \"autogen\"./^$sleep_sources$/ WHERE $timeFilter", + "rawQuery": true, + "refId": "B", + "resultFormat": "time_series", + "select": [ + [ + { + "params": [ + "value" + ], + "type": "field" + } + ] + ], + "tags": [ + { + "key": "value::field", + "operator": "=", + "value": "2" + } + ] + } + ], + "transformations": [ + { + "id": "joinByField", + "options": { + "byField": "Time", + "mode": "outer" + } + }, + { + "id": "calculateField", + "options": { + "alias": "Normal Sleep", + "binary": { + "left": "Normal Sleep", + "operator": "/", + "reducer": "sum", + "right": "Total" + }, + "mode": "binary", + "reduce": { + "reducer": "sum" + }, + "replaceFields": true + } + } + ], + "type": "stat" + }, + { + "datasource": { + "type": "influxdb", + "uid": "influx" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "blue", + "value": null + } + ] + }, + "unit": "percentunit" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 20, + "y": 13 + }, + "id": 6, + "options": { + "colorMode": "background", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "10.0.3", + "targets": [ + { + "alias": "REM Sleep", + "datasource": { + "type": "influxdb", + "uid": "influx" + }, + "groupBy": [], + "measurement": "SleepAnalysisTimes-Apple Watch", + "orderByTime": "ASC", + "policy": "autogen", + "query": "SELECT count(\"value\") FROM \"autogen\"./^$sleep_sources$/ WHERE (\"value\"::field = 2) AND $timeFilter", + "rawQuery": true, + "refId": "A", + "resultFormat": "time_series", + "select": [ + [ + { + "params": [ + "value" + ], + "type": "field" + } + ] + ], + "tags": [ + { + "key": "value::field", + "operator": "=", + "value": "2" + } + ] + }, + { + "alias": "Total", + "datasource": { + "type": "influxdb", + "uid": "influx" + }, + "groupBy": [], + "hide": false, + "measurement": "SleepAnalysisTimes-Apple Watch", + "orderByTime": "ASC", + "policy": "autogen", + "query": "SELECT count(\"value\") FROM \"autogen\"./^$sleep_sources$/ WHERE $timeFilter", + "rawQuery": true, + "refId": "B", + "resultFormat": "time_series", + "select": [ + [ + { + "params": [ + "value" + ], + "type": "field" + } + ] + ], + "tags": [ + { + "key": "value::field", + "operator": "=", + "value": "2" + } + ] + } + ], + "transformations": [ + { + "id": "joinByField", + "options": { + "byField": "Time", + "mode": "outer" + } + }, + { + "id": "calculateField", + "options": { + "alias": "REM Sleep", + "binary": { + "left": "REM Sleep", + "operator": "/", + "reducer": "sum", + "right": "Total" + }, + "mode": "binary", + "reduce": { + "reducer": "sum" + }, + "replaceFields": true + } + } + ], + "type": "stat" + }, + { + "datasource": { + "type": "influxdb", + "uid": "influx" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "semi-dark-blue", + "value": null + } + ] + }, + "unit": "percentunit" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 17, + "y": 17 + }, + "id": 9, + "options": { + "colorMode": "background", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "10.0.3", + "targets": [ + { + "alias": "Core Sleep", + "datasource": { + "type": "influxdb", + "uid": "influx" + }, + "groupBy": [], + "measurement": "SleepAnalysisTimes-Apple Watch", + "orderByTime": "ASC", + "policy": "autogen", + "query": "SELECT count(\"value\") FROM \"autogen\"./^$sleep_sources$/ WHERE (\"value\"::field = 1) AND $timeFilter", + "rawQuery": true, + "refId": "A", + "resultFormat": "time_series", + "select": [ + [ + { + "params": [ + "value" + ], + "type": "field" + } + ] + ], + "tags": [ + { + "key": "value::field", + "operator": "=", + "value": "2" + } + ] + }, + { + "alias": "Total", + "datasource": { + "type": "influxdb", + "uid": "influx" + }, + "groupBy": [], + "hide": false, + "measurement": "SleepAnalysisTimes-Apple Watch", + "orderByTime": "ASC", + "policy": "autogen", + "query": "SELECT count(\"value\") FROM \"autogen\"./^$sleep_sources$/ WHERE $timeFilter", + "rawQuery": true, + "refId": "B", + "resultFormat": "time_series", + "select": [ + [ + { + "params": [ + "value" + ], + "type": "field" + } + ] + ], + "tags": [ + { + "key": "value::field", + "operator": "=", + "value": "2" + } + ] + } + ], + "transformations": [ + { + "id": "joinByField", + "options": { + "byField": "Time", + "mode": "outer" + } + }, + { + "id": "calculateField", + "options": { + "alias": "Core Sleep ", + "binary": { + "left": "Core Sleep", + "operator": "/", + "reducer": "sum", + "right": "Total" + }, + "mode": "binary", + "reduce": { + "reducer": "sum" + }, + "replaceFields": true + } + } + ], + "type": "stat" + }, + { + "datasource": { + "type": "influxdb", + "uid": "influx" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "dark-blue", + "value": null + } + ] + }, + "unit": "percentunit" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 3, + "x": 21, + "y": 17 + }, + "id": 7, + "options": { + "colorMode": "background", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "10.0.3", + "targets": [ + { + "alias": "Deep Sleep", + "datasource": { + "type": "influxdb", + "uid": "influx" + }, + "groupBy": [], + "measurement": "SleepAnalysisTimes-Apple Watch", + "orderByTime": "ASC", + "policy": "autogen", + "query": "SELECT count(\"value\") FROM \"autogen\"./^$sleep_sources$/ WHERE (\"value\"::field = 1) AND $timeFilter", + "rawQuery": true, + "refId": "A", + "resultFormat": "time_series", + "select": [ + [ + { + "params": [ + "value" + ], + "type": "field" + } + ] + ], + "tags": [ + { + "key": "value::field", + "operator": "=", + "value": "2" + } + ] + }, + { + "alias": "Total", + "datasource": { + "type": "influxdb", + "uid": "influx" + }, + "groupBy": [], + "hide": false, + "measurement": "SleepAnalysisTimes-Apple Watch", + "orderByTime": "ASC", + "policy": "autogen", + "query": "SELECT count(\"value\") FROM \"autogen\"./^$sleep_sources$/ WHERE $timeFilter", + "rawQuery": true, + "refId": "B", + "resultFormat": "time_series", + "select": [ + [ + { + "params": [ + "value" + ], + "type": "field" + } + ] + ], + "tags": [ + { + "key": "value::field", + "operator": "=", + "value": "2" + } + ] + } + ], + "transformations": [ + { + "id": "joinByField", + "options": { + "byField": "Time", + "mode": "outer" + } + }, + { + "id": "calculateField", + "options": { + "alias": "Deep Sleep", + "binary": { + "left": "Deep Sleep", + "operator": "/", + "reducer": "sum", + "right": "Total" + }, + "mode": "binary", + "reduce": { + "reducer": "sum" + }, + "replaceFields": true + } + } + ], + "type": "stat" + } + ], + "refresh": false, + "schemaVersion": 38, + "style": "dark", + "tags": [], + "templating": { + "list": [ + { + "current": { + "selected": false, + "text": "SleepAnalysisTimes-Apple Watch", + "value": "SleepAnalysisTimes-Apple Watch" + }, + "datasource": { + "type": "influxdb", + "uid": "influx" + }, + "definition": "show measurements WITH MEASUREMENT =~ /SleepAnalysisTimes.*/", + "hide": 0, + "includeAll": false, + "multi": false, + "name": "sleep_sources", + "options": [], + "query": "show measurements WITH MEASUREMENT =~ /SleepAnalysisTimes.*/", + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 0, + "type": "query" + } + ] + }, + "time": { + "from": "now-7d", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "Sleep tracking dashboard", + "uid": "a46f7443-1bc1-478d-8100-40c05aef654c", + "version": 9, + "weekStart": "" +} \ No newline at end of file