From 2f35f24c678f9acade4b30a35c1982f042d65e5e Mon Sep 17 00:00:00 2001 From: Dan H Date: Fri, 17 Jul 2026 15:42:40 +0100 Subject: [PATCH 1/6] Add Google Health plugin Integrates the Google Health API (v4, successor to the Fitbit Web API) with Google OAuth 2.0 auth-code flow and automatic token refresh. - Imports the authenticated user and paired devices as objects - 13 data streams: daily/intraday activity metrics, nutrition and per-item food log, macro breakdown, sleep sessions, workouts, heart rate (intraday + daily zones), body measurements, profile and devices - Personal goals (steps, calories, macros, sleep, water) set at config level; gauges, monitors, status columns and chart colours all derive from them - 7 single-pane dashboards: Today, Activity, Diet & Nutrition, Trends Over Time, Sleep, plus User and Device perspectives - Sub-24h timeframes snap to the current civil day so "24 hours" means today, matching the app's daily view Co-Authored-By: Claude Opus 4.8 --- plugins/GoogleHealth/v1/configValidation.json | 11 + plugins/GoogleHealth/v1/custom_types.json | 16 + .../v1/dataStreams/bodyMetric.json | 118 ++++ .../v1/dataStreams/dailyHealthMetric.json | 126 ++++ .../v1/dataStreams/dailyMetric.json | 153 +++++ .../GoogleHealth/v1/dataStreams/foodLog.json | 154 +++++ .../v1/dataStreams/heartRateIntraday.json | 33 + .../v1/dataStreams/heartRateZonesDaily.json | 117 ++++ .../v1/dataStreams/intradayMetric.json | 165 +++++ .../v1/dataStreams/macroBreakdown.json | 94 +++ .../v1/dataStreams/nutritionDaily.json | 239 +++++++ .../v1/dataStreams/pairedDevices.json | 25 + .../v1/dataStreams/scripts/bodyMetric.js | 41 ++ .../dataStreams/scripts/dailyHealthMetric.js | 39 ++ .../v1/dataStreams/scripts/dailyMetric.js | 124 ++++ .../v1/dataStreams/scripts/foodLog.js | 38 ++ .../dataStreams/scripts/heartRateIntraday.js | 21 + .../scripts/heartRateZonesDaily.js | 39 ++ .../v1/dataStreams/scripts/intradayMetric.js | 66 ++ .../v1/dataStreams/scripts/macroBreakdown.js | 45 ++ .../v1/dataStreams/scripts/nutritionDaily.js | 63 ++ .../v1/dataStreams/scripts/pairedDevices.js | 25 + .../v1/dataStreams/scripts/sleepSessions.js | 58 ++ .../v1/dataStreams/scripts/userProfile.js | 39 ++ .../v1/dataStreams/scripts/workouts.js | 47 ++ .../v1/dataStreams/sleepSessions.json | 197 ++++++ .../v1/dataStreams/userProfile.json | 25 + .../GoogleHealth/v1/dataStreams/workouts.json | 153 +++++ .../v1/defaultContent/activity.dash.json | 595 ++++++++++++++++++ .../devicePerspective.dash.json | 85 +++ .../v1/defaultContent/dietNutrition.dash.json | 584 +++++++++++++++++ .../v1/defaultContent/manifest.json | 32 + .../v1/defaultContent/scopes.json | 32 + .../v1/defaultContent/sleep.dash.json | 293 +++++++++ .../v1/defaultContent/today.dash.json | 527 ++++++++++++++++ .../v1/defaultContent/trends.dash.json | 266 ++++++++ .../defaultContent/userPerspective.dash.json | 174 +++++ plugins/GoogleHealth/v1/docs/README.md | 79 +++ plugins/GoogleHealth/v1/icon.svg | 9 + .../v1/indexDefinitions/default.json | 33 + plugins/GoogleHealth/v1/metadata.json | 63 ++ plugins/GoogleHealth/v1/ui.json | 49 ++ 42 files changed, 5092 insertions(+) create mode 100644 plugins/GoogleHealth/v1/configValidation.json create mode 100644 plugins/GoogleHealth/v1/custom_types.json create mode 100644 plugins/GoogleHealth/v1/dataStreams/bodyMetric.json create mode 100644 plugins/GoogleHealth/v1/dataStreams/dailyHealthMetric.json create mode 100644 plugins/GoogleHealth/v1/dataStreams/dailyMetric.json create mode 100644 plugins/GoogleHealth/v1/dataStreams/foodLog.json create mode 100644 plugins/GoogleHealth/v1/dataStreams/heartRateIntraday.json create mode 100644 plugins/GoogleHealth/v1/dataStreams/heartRateZonesDaily.json create mode 100644 plugins/GoogleHealth/v1/dataStreams/intradayMetric.json create mode 100644 plugins/GoogleHealth/v1/dataStreams/macroBreakdown.json create mode 100644 plugins/GoogleHealth/v1/dataStreams/nutritionDaily.json create mode 100644 plugins/GoogleHealth/v1/dataStreams/pairedDevices.json create mode 100644 plugins/GoogleHealth/v1/dataStreams/scripts/bodyMetric.js create mode 100644 plugins/GoogleHealth/v1/dataStreams/scripts/dailyHealthMetric.js create mode 100644 plugins/GoogleHealth/v1/dataStreams/scripts/dailyMetric.js create mode 100644 plugins/GoogleHealth/v1/dataStreams/scripts/foodLog.js create mode 100644 plugins/GoogleHealth/v1/dataStreams/scripts/heartRateIntraday.js create mode 100644 plugins/GoogleHealth/v1/dataStreams/scripts/heartRateZonesDaily.js create mode 100644 plugins/GoogleHealth/v1/dataStreams/scripts/intradayMetric.js create mode 100644 plugins/GoogleHealth/v1/dataStreams/scripts/macroBreakdown.js create mode 100644 plugins/GoogleHealth/v1/dataStreams/scripts/nutritionDaily.js create mode 100644 plugins/GoogleHealth/v1/dataStreams/scripts/pairedDevices.js create mode 100644 plugins/GoogleHealth/v1/dataStreams/scripts/sleepSessions.js create mode 100644 plugins/GoogleHealth/v1/dataStreams/scripts/userProfile.js create mode 100644 plugins/GoogleHealth/v1/dataStreams/scripts/workouts.js create mode 100644 plugins/GoogleHealth/v1/dataStreams/sleepSessions.json create mode 100644 plugins/GoogleHealth/v1/dataStreams/userProfile.json create mode 100644 plugins/GoogleHealth/v1/dataStreams/workouts.json create mode 100644 plugins/GoogleHealth/v1/defaultContent/activity.dash.json create mode 100644 plugins/GoogleHealth/v1/defaultContent/devicePerspective.dash.json create mode 100644 plugins/GoogleHealth/v1/defaultContent/dietNutrition.dash.json create mode 100644 plugins/GoogleHealth/v1/defaultContent/manifest.json create mode 100644 plugins/GoogleHealth/v1/defaultContent/scopes.json create mode 100644 plugins/GoogleHealth/v1/defaultContent/sleep.dash.json create mode 100644 plugins/GoogleHealth/v1/defaultContent/today.dash.json create mode 100644 plugins/GoogleHealth/v1/defaultContent/trends.dash.json create mode 100644 plugins/GoogleHealth/v1/defaultContent/userPerspective.dash.json create mode 100644 plugins/GoogleHealth/v1/docs/README.md create mode 100644 plugins/GoogleHealth/v1/icon.svg create mode 100644 plugins/GoogleHealth/v1/indexDefinitions/default.json create mode 100644 plugins/GoogleHealth/v1/metadata.json create mode 100644 plugins/GoogleHealth/v1/ui.json diff --git a/plugins/GoogleHealth/v1/configValidation.json b/plugins/GoogleHealth/v1/configValidation.json new file mode 100644 index 00000000..46f659dc --- /dev/null +++ b/plugins/GoogleHealth/v1/configValidation.json @@ -0,0 +1,11 @@ +{ + "steps": [ + { + "displayName": "Authenticate", + "dataStream": { "name": "userProfile" }, + "required": true, + "error": "Could not authenticate with Google Health. Check the OAuth client ID and secret, that the Google Health API is enabled in your Google Cloud project, and that you completed Google sign-in granting the requested health permissions.", + "success": "Connected to Google Health successfully." + } + ] +} diff --git a/plugins/GoogleHealth/v1/custom_types.json b/plugins/GoogleHealth/v1/custom_types.json new file mode 100644 index 00000000..1ce0193f --- /dev/null +++ b/plugins/GoogleHealth/v1/custom_types.json @@ -0,0 +1,16 @@ +[ + { + "name": "Google Health User", + "sourceType": "Google Health User", + "icon": "heart-pulse", + "singular": "User", + "plural": "Users" + }, + { + "name": "Google Health Device", + "sourceType": "Google Health Device", + "icon": "watch-fitness", + "singular": "Device", + "plural": "Devices" + } +] diff --git a/plugins/GoogleHealth/v1/dataStreams/bodyMetric.json b/plugins/GoogleHealth/v1/dataStreams/bodyMetric.json new file mode 100644 index 00000000..eb8a3e00 --- /dev/null +++ b/plugins/GoogleHealth/v1/dataStreams/bodyMetric.json @@ -0,0 +1,118 @@ +{ + "name": "bodyMetric", + "displayName": "Body Measurement", + "description": "Body measurements over time for a chosen metric such as weight or body fat, from individual logged readings", + "tags": [ + "Body", + "Health" + ], + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "get", + "endpointPath": "users/me/dataTypes/{{metric}}/dataPoints", + "getArgs": [ + { + "key": "filter", + "value": "{{metric.replace(/-/g,'_')}}.sample_time.civil_time >= \"{{new Date(new Date(timeframe.end).getTime()-new Date(timeframe.start).getTime()<=90000000 ? timeframe.end : timeframe.start).toISOString().slice(0,10)}}\" AND {{metric.replace(/-/g,'_')}}.sample_time.civil_time < \"{{new Date(new Date(timeframe.end).getTime()+86400000).toISOString().slice(0,10)}}\"" + } + ], + "paging": { + "mode": "token", + "pageSize": { + "realm": "queryArg", + "path": "pageSize", + "value": "500" + }, + "in": { + "realm": "payload", + "path": "nextPageToken" + }, + "out": { + "realm": "queryArg", + "path": "pageToken" + } + }, + "postRequestScript": "bodyMetric.js" + }, + "matches": "none", + "ui": [ + { + "type": "autocomplete", + "name": "metric", + "label": "Metric", + "allowCustomValues": false, + "isMulti": false, + "defaultValue": "weight", + "data": { + "source": "fixed", + "values": [ + { + "value": "weight", + "label": "Weight (kg)" + }, + { + "value": "body-fat", + "label": "Body Fat (%)" + } + ] + }, + "validation": { + "required": true + } + } + ], + "metadata": [ + { + "name": "date", + "displayName": "Date", + "shape": [ + "date", + { + "format": "dd/MM" + } + ], + "role": "timestamp" + }, + { + "name": "value", + "displayName": "Value", + "shape": [ + "number", + { + "decimalPlaces": 1 + } + ], + "role": "value" + }, + { + "name": "metric", + "displayName": "Metric", + "shape": "string" + }, + { + "name": "unit", + "displayName": "Unit", + "shape": "string", + "role": "unitLabel" + } + ], + "timeframes": [ + "last12hours", + "last24hours", + "last7days", + "last30days", + "thisMonth", + "thisQuarter", + "thisYear" + ], + "defaultShaping": { + "sort": { + "by": [ + [ + "date", + "asc" + ] + ] + } + } +} \ No newline at end of file diff --git a/plugins/GoogleHealth/v1/dataStreams/dailyHealthMetric.json b/plugins/GoogleHealth/v1/dataStreams/dailyHealthMetric.json new file mode 100644 index 00000000..ab6291fc --- /dev/null +++ b/plugins/GoogleHealth/v1/dataStreams/dailyHealthMetric.json @@ -0,0 +1,126 @@ +{ + "name": "dailyHealthMetric", + "displayName": "Daily Health Metric", + "description": "Daily health summary values for a chosen metric such as resting heart rate, heart rate variability, respiratory rate or oxygen saturation", + "tags": [ + "Heart Rate", + "Health" + ], + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "get", + "endpointPath": "users/me/dataTypes/{{metric}}/dataPoints", + "getArgs": [ + { + "key": "filter", + "value": "{{metric.replace(/-/g,'_')}}.date >= \"{{new Date(new Date(timeframe.end).getTime()-new Date(timeframe.start).getTime()<=90000000 ? timeframe.end : timeframe.start).toISOString().slice(0,10)}}\" AND {{metric.replace(/-/g,'_')}}.date < \"{{new Date(new Date(timeframe.end).getTime()+86400000).toISOString().slice(0,10)}}\"" + } + ], + "paging": { + "mode": "token", + "pageSize": { + "realm": "queryArg", + "path": "pageSize", + "value": "370" + }, + "in": { + "realm": "payload", + "path": "nextPageToken" + }, + "out": { + "realm": "queryArg", + "path": "pageToken" + } + }, + "postRequestScript": "dailyHealthMetric.js" + }, + "matches": "none", + "ui": [ + { + "type": "autocomplete", + "name": "metric", + "label": "Metric", + "allowCustomValues": false, + "isMulti": false, + "defaultValue": "daily-resting-heart-rate", + "data": { + "source": "fixed", + "values": [ + { + "value": "daily-resting-heart-rate", + "label": "Resting Heart Rate" + }, + { + "value": "daily-heart-rate-variability", + "label": "Heart Rate Variability" + }, + { + "value": "daily-respiratory-rate", + "label": "Respiratory Rate" + }, + { + "value": "daily-oxygen-saturation", + "label": "Oxygen Saturation (SpO2)" + } + ] + }, + "validation": { + "required": true + } + } + ], + "metadata": [ + { + "name": "date", + "displayName": "Date", + "shape": [ + "date", + { + "format": "dd/MM" + } + ], + "role": "timestamp" + }, + { + "name": "value", + "displayName": "Value", + "shape": [ + "number", + { + "decimalPlaces": 1 + } + ], + "role": "value" + }, + { + "name": "metric", + "displayName": "Metric", + "shape": "string" + }, + { + "name": "unit", + "displayName": "Unit", + "shape": "string", + "role": "unitLabel" + } + ], + "timeframes": [ + "last12hours", + "last24hours", + "last7days", + "last30days", + "thisMonth", + "thisQuarter", + "thisYear" + ], + "defaultShaping": { + "sort": { + "by": [ + [ + "date", + "asc" + ] + ] + } + } +} \ No newline at end of file diff --git a/plugins/GoogleHealth/v1/dataStreams/dailyMetric.json b/plugins/GoogleHealth/v1/dataStreams/dailyMetric.json new file mode 100644 index 00000000..0d3a94c9 --- /dev/null +++ b/plugins/GoogleHealth/v1/dataStreams/dailyMetric.json @@ -0,0 +1,153 @@ +{ + "name": "dailyMetric", + "displayName": "Daily Metric", + "description": "Daily total or average for a chosen health metric such as steps, distance, calories, weight, body fat, resting heart rate or VO2 max", + "tags": [ + "Activity", + "Health" + ], + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "post", + "endpointPath": "users/me/dataTypes/{{metric}}/dataPoints:dailyRollUp", + "postBody": { + "range": { + "start": { + "date": { + "year": "{{new Date(new Date(timeframe.end).getTime()-new Date(timeframe.start).getTime()<=90000000 ? timeframe.end : timeframe.start).getUTCFullYear()}}", + "month": "{{new Date(new Date(timeframe.end).getTime()-new Date(timeframe.start).getTime()<=90000000 ? timeframe.end : timeframe.start).getUTCMonth()+1}}", + "day": "{{new Date(new Date(timeframe.end).getTime()-new Date(timeframe.start).getTime()<=90000000 ? timeframe.end : timeframe.start).getUTCDate()}}" + } + }, + "end": { + "date": { + "year": "{{new Date(new Date(timeframe.end).getTime()+86400000).getUTCFullYear()}}", + "month": "{{new Date(new Date(timeframe.end).getTime()+86400000).getUTCMonth()+1}}", + "day": "{{new Date(new Date(timeframe.end).getTime()+86400000).getUTCDate()}}" + } + } + }, + "windowSizeDays": 1 + }, + "paging": { + "mode": "none" + }, + "postRequestScript": "dailyMetric.js" + }, + "matches": "none", + "ui": [ + { + "type": "autocomplete", + "name": "metric", + "label": "Metric", + "allowCustomValues": false, + "isMulti": false, + "defaultValue": "steps", + "data": { + "source": "fixed", + "values": [ + { + "value": "steps", + "label": "Steps" + }, + { + "value": "distance", + "label": "Distance (km)" + }, + { + "value": "floors", + "label": "Floors" + }, + { + "value": "active-zone-minutes", + "label": "Active Zone Minutes" + }, + { + "value": "active-energy-burned", + "label": "Active Calories" + }, + { + "value": "hydration-log", + "label": "Hydration" + } + ] + }, + "validation": { + "required": true + } + } + ], + "metadata": [ + { + "name": "date", + "displayName": "Date", + "shape": [ + "date", + { + "format": "dd/MM" + } + ], + "role": "timestamp" + }, + { + "name": "value", + "displayName": "Value", + "shape": "number", + "role": "value" + }, + { + "name": "metric", + "displayName": "Metric", + "shape": "string" + }, + { + "name": "unit", + "displayName": "Unit", + "shape": "string", + "role": "unitLabel" + }, + { + "name": "goal", + "displayName": "Goal", + "shape": "number", + "visible": false + }, + { + "name": "pctOfGoal", + "displayName": "% of Goal", + "shape": [ + "percent", + { + "decimalPlaces": 0 + } + ], + "visible": false, + "formatExpression": "{{ $['value'] }} {{ $['unit'] }}" + }, + { + "name": "status", + "displayName": "Goal Status", + "computed": true, + "valueExpression": "{{ $[\"pctOfGoal\"] === undefined || $[\"pctOfGoal\"] === null ? \"unknown\" : $[\"pctOfGoal\"] >= 100 ? \"success\" : $[\"pctOfGoal\"] >= 75 ? \"warning\" : \"error\" }}", + "shape": "state", + "visible": false + } + ], + "timeframes": [ + "last12hours", + "last24hours", + "last7days", + "last30days", + "thisMonth" + ], + "defaultShaping": { + "sort": { + "by": [ + [ + "date", + "asc" + ] + ] + } + } +} \ No newline at end of file diff --git a/plugins/GoogleHealth/v1/dataStreams/foodLog.json b/plugins/GoogleHealth/v1/dataStreams/foodLog.json new file mode 100644 index 00000000..37c2745d --- /dev/null +++ b/plugins/GoogleHealth/v1/dataStreams/foodLog.json @@ -0,0 +1,154 @@ +{ + "name": "foodLog", + "displayName": "Food Log", + "description": "Individual logged food entries with meal type, calories and macronutrient breakdown per item", + "tags": [ + "Nutrition", + "Diet" + ], + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "get", + "endpointPath": "users/me/dataTypes/nutrition-log/dataPoints", + "getArgs": [ + { + "key": "filter", + "value": "nutrition_log.interval.civil_start_time >= \"{{new Date(new Date(timeframe.end).getTime()-new Date(timeframe.start).getTime()<=90000000 ? timeframe.end : timeframe.start).toISOString().slice(0,10)}}\" AND nutrition_log.interval.civil_start_time < \"{{new Date(new Date(timeframe.end).getTime()+86400000).toISOString().slice(0,10)}}\"" + } + ], + "paging": { + "mode": "token", + "pageSize": { + "realm": "queryArg", + "path": "pageSize", + "value": "200" + }, + "in": { + "realm": "payload", + "path": "nextPageToken" + }, + "out": { + "realm": "queryArg", + "path": "pageToken" + } + }, + "postRequestScript": "foodLog.js" + }, + "matches": "none", + "metadata": [ + { + "name": "time", + "displayName": "Logged", + "shape": [ + "date", + { + "format": "dd MMM HH:mm" + } + ], + "role": "timestamp" + }, + { + "name": "food", + "displayName": "Food", + "shape": "string", + "role": "label" + }, + { + "name": "meal", + "displayName": "Meal", + "shape": "string" + }, + { + "name": "calories", + "displayName": "Calories", + "shape": [ + "number", + { + "decimalPlaces": 0 + } + ], + "role": "value" + }, + { + "name": "carbsG", + "displayName": "Carbs (g)", + "shape": [ + "number", + { + "decimalPlaces": 1 + } + ] + }, + { + "name": "fatG", + "displayName": "Fat (g)", + "shape": [ + "number", + { + "decimalPlaces": 1 + } + ] + }, + { + "name": "proteinG", + "displayName": "Protein (g)", + "shape": [ + "number", + { + "decimalPlaces": 1 + } + ] + }, + { + "name": "sugarG", + "displayName": "Sugar (g)", + "shape": [ + "number", + { + "decimalPlaces": 1 + } + ] + }, + { + "name": "fibreG", + "displayName": "Fibre (g)", + "shape": [ + "number", + { + "decimalPlaces": 1 + } + ] + }, + { + "name": "mealOrder", + "displayName": "Meal Order", + "computed": true, + "valueExpression": "{{ ({ 'Before Breakfast': 1, 'Breakfast': 2, 'Before Lunch': 3, 'Lunch': 4, 'Before Dinner': 5, 'Dinner': 6, 'After Dinner': 7, 'Snack': 8, 'Anytime': 9 })[$['meal']] || 10 }}", + "shape": "number", + "visible": false + } + ], + "timeframes": [ + "last12hours", + "last24hours", + "last7days", + "last30days", + "thisMonth", + "thisQuarter", + "thisYear" + ], + "defaultShaping": { + "sort": { + "by": [ + [ + "mealOrder", + "asc" + ], + [ + "time", + "asc" + ] + ] + } + } +} \ No newline at end of file diff --git a/plugins/GoogleHealth/v1/dataStreams/heartRateIntraday.json b/plugins/GoogleHealth/v1/dataStreams/heartRateIntraday.json new file mode 100644 index 00000000..274d3890 --- /dev/null +++ b/plugins/GoogleHealth/v1/dataStreams/heartRateIntraday.json @@ -0,0 +1,33 @@ +{ + "name": "heartRateIntraday", + "displayName": "Heart Rate (Intraday)", + "description": "Heart rate through the day aggregated into fixed time windows, with average, minimum and maximum beats per minute", + "tags": ["Heart Rate", "Health"], + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "post", + "endpointPath": "users/me/dataTypes/heart-rate/dataPoints:rollUp", + "postBody": { + "range": { + "startTime": "{{new Date(timeframe.start).toISOString()}}", + "endTime": "{{new Date(timeframe.end).toISOString()}}" + }, + "windowSize": "900s" + }, + "paging": { + "mode": "token", + "in": { "realm": "payload", "path": "nextPageToken" }, + "out": { "realm": "body", "path": "pageToken" } + }, + "postRequestScript": "heartRateIntraday.js" + }, + "matches": "none", + "metadata": [ + { "name": "time", "displayName": "Time", "shape": ["date", { "format": "dd MMM HH:mm" }], "role": "timestamp" }, + { "name": "avgBpm", "displayName": "Avg (bpm)", "shape": ["number", { "decimalPlaces": 0 }], "role": "value" }, + { "name": "minBpm", "displayName": "Min (bpm)", "shape": ["number", { "decimalPlaces": 0 }] }, + { "name": "maxBpm", "displayName": "Max (bpm)", "shape": ["number", { "decimalPlaces": 0 }] } + ], + "timeframes": ["last12hours", "last24hours", "last7days"], + "defaultShaping": { "sort": { "by": [["time", "asc"]] } } +} diff --git a/plugins/GoogleHealth/v1/dataStreams/heartRateZonesDaily.json b/plugins/GoogleHealth/v1/dataStreams/heartRateZonesDaily.json new file mode 100644 index 00000000..84662cc0 --- /dev/null +++ b/plugins/GoogleHealth/v1/dataStreams/heartRateZonesDaily.json @@ -0,0 +1,117 @@ +{ + "name": "heartRateZonesDaily", + "displayName": "Heart Rate Zones (Daily)", + "description": "Minutes spent in each heart rate zone per day (light, moderate, vigorous and peak)", + "tags": [ + "Heart Rate", + "Activity" + ], + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "post", + "endpointPath": "users/me/dataTypes/time-in-heart-rate-zone/dataPoints:dailyRollUp", + "postBody": { + "range": { + "start": { + "date": { + "year": "{{new Date(new Date(timeframe.end).getTime()-new Date(timeframe.start).getTime()<=90000000 ? timeframe.end : timeframe.start).getUTCFullYear()}}", + "month": "{{new Date(new Date(timeframe.end).getTime()-new Date(timeframe.start).getTime()<=90000000 ? timeframe.end : timeframe.start).getUTCMonth()+1}}", + "day": "{{new Date(new Date(timeframe.end).getTime()-new Date(timeframe.start).getTime()<=90000000 ? timeframe.end : timeframe.start).getUTCDate()}}" + } + }, + "end": { + "date": { + "year": "{{new Date(new Date(timeframe.end).getTime()+86400000).getUTCFullYear()}}", + "month": "{{new Date(new Date(timeframe.end).getTime()+86400000).getUTCMonth()+1}}", + "day": "{{new Date(new Date(timeframe.end).getTime()+86400000).getUTCDate()}}" + } + } + }, + "windowSizeDays": 1 + }, + "paging": { + "mode": "none" + }, + "postRequestScript": "heartRateZonesDaily.js" + }, + "matches": "none", + "metadata": [ + { + "name": "date", + "displayName": "Date", + "shape": [ + "date", + { + "format": "dd/MM" + } + ], + "role": "timestamp" + }, + { + "name": "lightMin", + "displayName": "Light (min)", + "shape": [ + "number", + { + "decimalPlaces": 0 + } + ] + }, + { + "name": "moderateMin", + "displayName": "Moderate (min)", + "shape": [ + "number", + { + "decimalPlaces": 0 + } + ] + }, + { + "name": "vigorousMin", + "displayName": "Vigorous (min)", + "shape": [ + "number", + { + "decimalPlaces": 0 + } + ] + }, + { + "name": "peakMin", + "displayName": "Peak (min)", + "shape": [ + "number", + { + "decimalPlaces": 0 + } + ] + }, + { + "name": "totalMin", + "displayName": "Total Active (min)", + "shape": [ + "number", + { + "decimalPlaces": 0 + } + ], + "role": "value" + } + ], + "timeframes": [ + "last12hours", + "last24hours", + "last7days" + ], + "defaultShaping": { + "sort": { + "by": [ + [ + "date", + "asc" + ] + ] + } + } +} \ No newline at end of file diff --git a/plugins/GoogleHealth/v1/dataStreams/intradayMetric.json b/plugins/GoogleHealth/v1/dataStreams/intradayMetric.json new file mode 100644 index 00000000..5faa5743 --- /dev/null +++ b/plugins/GoogleHealth/v1/dataStreams/intradayMetric.json @@ -0,0 +1,165 @@ +{ + "name": "intradayMetric", + "displayName": "Intraday Metric", + "description": "A chosen metric such as steps, distance, calories or heart rate aggregated into intraday time windows across the selected period", + "tags": [ + "Activity", + "Health" + ], + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "post", + "endpointPath": "users/me/dataTypes/{{metric}}/dataPoints:rollUp", + "postBody": { + "range": { + "startTime": "{{new Date(timeframe.start).toISOString()}}", + "endTime": "{{new Date(timeframe.end).toISOString()}}" + }, + "windowSize": "{{window || '3600s'}}" + }, + "paging": { + "mode": "token", + "in": { + "realm": "payload", + "path": "nextPageToken" + }, + "out": { + "realm": "body", + "path": "pageToken" + } + }, + "postRequestScript": "intradayMetric.js" + }, + "matches": "none", + "ui": [ + { + "type": "autocomplete", + "name": "metric", + "label": "Metric", + "allowCustomValues": false, + "isMulti": false, + "defaultValue": "steps", + "data": { + "source": "fixed", + "values": [ + { + "value": "steps", + "label": "Steps" + }, + { + "value": "distance", + "label": "Distance" + }, + { + "value": "active-energy-burned", + "label": "Active Calories" + }, + { + "value": "floors", + "label": "Floors" + }, + { + "value": "active-zone-minutes", + "label": "Active Zone Minutes" + }, + { + "value": "heart-rate", + "label": "Heart Rate" + } + ] + }, + "validation": { + "required": true + } + }, + { + "type": "choiceChips", + "name": "window", + "label": "Interval", + "options": [ + { + "value": "900s", + "label": "15 min" + }, + { + "value": "1800s", + "label": "30 min" + }, + { + "value": "3600s", + "label": "1 hour" + } + ] + } + ], + "metadata": [ + { + "name": "time", + "displayName": "Time", + "shape": [ + "date", + { + "format": "dd MMM HH:mm" + } + ], + "role": "timestamp" + }, + { + "name": "value", + "displayName": "Value", + "shape": "number", + "role": "value" + }, + { + "name": "metric", + "displayName": "Metric", + "shape": "string" + }, + { + "name": "unit", + "displayName": "Unit", + "shape": "string", + "role": "unitLabel" + }, + { + "name": "goal", + "displayName": "Goal", + "shape": "number", + "visible": false + }, + { + "name": "pctOfGoal", + "displayName": "% of Goal", + "shape": [ + "percent", + { + "decimalPlaces": 0 + } + ], + "visible": false + }, + { + "name": "status", + "displayName": "Goal Status", + "computed": true, + "valueExpression": "{{ $[\"pctOfGoal\"] === undefined || $[\"pctOfGoal\"] === null ? \"unknown\" : $[\"pctOfGoal\"] >= 100 ? \"success\" : $[\"pctOfGoal\"] >= 75 ? \"warning\" : \"error\" }}", + "shape": "state", + "visible": false + } + ], + "timeframes": [ + "last12hours", + "last24hours", + "last7days" + ], + "defaultShaping": { + "sort": { + "by": [ + [ + "time", + "asc" + ] + ] + } + } +} \ No newline at end of file diff --git a/plugins/GoogleHealth/v1/dataStreams/macroBreakdown.json b/plugins/GoogleHealth/v1/dataStreams/macroBreakdown.json new file mode 100644 index 00000000..65a91be8 --- /dev/null +++ b/plugins/GoogleHealth/v1/dataStreams/macroBreakdown.json @@ -0,0 +1,94 @@ +{ + "name": "macroBreakdown", + "displayName": "Macro Breakdown", + "description": "Total grams and calorie contribution of each macronutrient (carbohydrate, fat, protein) across the selected period", + "tags": [ + "Nutrition", + "Diet" + ], + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "post", + "endpointPath": "users/me/dataTypes/nutrition-log/dataPoints:dailyRollUp", + "postBody": { + "range": { + "start": { + "date": { + "year": "{{new Date(new Date(timeframe.end).getTime()-new Date(timeframe.start).getTime()<=90000000 ? timeframe.end : timeframe.start).getUTCFullYear()}}", + "month": "{{new Date(new Date(timeframe.end).getTime()-new Date(timeframe.start).getTime()<=90000000 ? timeframe.end : timeframe.start).getUTCMonth()+1}}", + "day": "{{new Date(new Date(timeframe.end).getTime()-new Date(timeframe.start).getTime()<=90000000 ? timeframe.end : timeframe.start).getUTCDate()}}" + } + }, + "end": { + "date": { + "year": "{{new Date(new Date(timeframe.end).getTime()+86400000).getUTCFullYear()}}", + "month": "{{new Date(new Date(timeframe.end).getTime()+86400000).getUTCMonth()+1}}", + "day": "{{new Date(new Date(timeframe.end).getTime()+86400000).getUTCDate()}}" + } + } + }, + "windowSizeDays": 1 + }, + "paging": { + "mode": "none" + }, + "postRequestScript": "macroBreakdown.js" + }, + "matches": "none", + "metadata": [ + { + "name": "macro", + "displayName": "Macronutrient", + "shape": "string", + "role": "label" + }, + { + "name": "grams", + "displayName": "Total (g)", + "shape": [ + "number", + { + "decimalPlaces": 0 + } + ] + }, + { + "name": "calories", + "displayName": "Calories", + "shape": [ + "number", + { + "decimalPlaces": 0 + } + ], + "role": "value" + }, + { + "name": "percent", + "displayName": "% of Calories", + "shape": [ + "percent", + { + "decimalPlaces": 0 + } + ] + } + ], + "timeframes": [ + "last12hours", + "last24hours", + "last7days", + "last30days", + "thisMonth" + ], + "defaultShaping": { + "sort": { + "by": [ + [ + "calories", + "desc" + ] + ] + } + } +} \ No newline at end of file diff --git a/plugins/GoogleHealth/v1/dataStreams/nutritionDaily.json b/plugins/GoogleHealth/v1/dataStreams/nutritionDaily.json new file mode 100644 index 00000000..0be680e2 --- /dev/null +++ b/plugins/GoogleHealth/v1/dataStreams/nutritionDaily.json @@ -0,0 +1,239 @@ +{ + "name": "nutritionDaily", + "displayName": "Daily Nutrition", + "description": "Daily logged nutrition totals including calories consumed, carbohydrates, fat, protein, sugar, fibre and sodium", + "tags": [ + "Nutrition", + "Diet" + ], + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "post", + "endpointPath": "users/me/dataTypes/nutrition-log/dataPoints:dailyRollUp", + "postBody": { + "range": { + "start": { + "date": { + "year": "{{new Date(new Date(timeframe.end).getTime()-new Date(timeframe.start).getTime()<=90000000 ? timeframe.end : timeframe.start).getUTCFullYear()}}", + "month": "{{new Date(new Date(timeframe.end).getTime()-new Date(timeframe.start).getTime()<=90000000 ? timeframe.end : timeframe.start).getUTCMonth()+1}}", + "day": "{{new Date(new Date(timeframe.end).getTime()-new Date(timeframe.start).getTime()<=90000000 ? timeframe.end : timeframe.start).getUTCDate()}}" + } + }, + "end": { + "date": { + "year": "{{new Date(new Date(timeframe.end).getTime()+86400000).getUTCFullYear()}}", + "month": "{{new Date(new Date(timeframe.end).getTime()+86400000).getUTCMonth()+1}}", + "day": "{{new Date(new Date(timeframe.end).getTime()+86400000).getUTCDate()}}" + } + } + }, + "windowSizeDays": 1 + }, + "paging": { + "mode": "none" + }, + "postRequestScript": "nutritionDaily.js" + }, + "matches": "none", + "metadata": [ + { + "name": "date", + "displayName": "Date", + "shape": [ + "date", + { + "format": "dd/MM" + } + ], + "role": "timestamp" + }, + { + "name": "calories", + "displayName": "Calories", + "shape": [ + "number", + { + "decimalPlaces": 0 + } + ], + "role": "value" + }, + { + "name": "carbsG", + "displayName": "Carbs (g)", + "shape": [ + "number", + { + "decimalPlaces": 0 + } + ] + }, + { + "name": "fatG", + "displayName": "Fat (g)", + "shape": [ + "number", + { + "decimalPlaces": 0 + } + ] + }, + { + "name": "proteinG", + "displayName": "Protein (g)", + "shape": [ + "number", + { + "decimalPlaces": 0 + } + ] + }, + { + "name": "sugarG", + "displayName": "Sugar (g)", + "shape": [ + "number", + { + "decimalPlaces": 0 + } + ] + }, + { + "name": "fibreG", + "displayName": "Fibre (g)", + "shape": [ + "number", + { + "decimalPlaces": 0 + } + ] + }, + { + "name": "sodiumMg", + "displayName": "Sodium (mg)", + "shape": [ + "number", + { + "decimalPlaces": 0 + } + ] + }, + { + "name": "calorieGoalCol", + "displayName": "Calorie Goal", + "shape": "number", + "visible": false + }, + { + "name": "proteinGoalCol", + "displayName": "Protein Goal", + "shape": "number", + "visible": false + }, + { + "name": "carbGoalCol", + "displayName": "Carb Goal", + "shape": "number", + "visible": false + }, + { + "name": "fatGoalCol", + "displayName": "Fat Goal", + "shape": "number", + "visible": false + }, + { + "name": "caloriesPct", + "displayName": "Calories % of Goal", + "shape": [ + "percent", + { + "decimalPlaces": 0 + } + ], + "visible": false + }, + { + "name": "proteinPct", + "displayName": "Protein % of Goal", + "shape": [ + "percent", + { + "decimalPlaces": 0 + } + ], + "visible": false + }, + { + "name": "carbsPct", + "displayName": "Carbs % of Goal", + "shape": [ + "percent", + { + "decimalPlaces": 0 + } + ], + "visible": false + }, + { + "name": "fatPct", + "displayName": "Fat % of Goal", + "shape": [ + "percent", + { + "decimalPlaces": 0 + } + ], + "visible": false + }, + { + "name": "caloriesStatus", + "displayName": "calories Status", + "computed": true, + "valueExpression": "{{ $[\"caloriesPct\"] === undefined || $[\"caloriesPct\"] === null ? \"unknown\" : $[\"caloriesPct\"] <= 100 ? \"success\" : $[\"caloriesPct\"] <= 110 ? \"warning\" : \"error\" }}", + "shape": "state", + "visible": false + }, + { + "name": "proteinStatus", + "displayName": "protein Status", + "computed": true, + "valueExpression": "{{ $[\"proteinPct\"] === undefined || $[\"proteinPct\"] === null ? \"unknown\" : $[\"proteinPct\"] >= 100 ? \"success\" : $[\"proteinPct\"] >= 75 ? \"warning\" : \"error\" }}", + "shape": "state", + "visible": false + }, + { + "name": "carbsStatus", + "displayName": "carbs Status", + "computed": true, + "valueExpression": "{{ $[\"carbsPct\"] === undefined || $[\"carbsPct\"] === null ? \"unknown\" : $[\"carbsPct\"] <= 100 ? \"success\" : $[\"carbsPct\"] <= 110 ? \"warning\" : \"error\" }}", + "shape": "state", + "visible": false + }, + { + "name": "fatStatus", + "displayName": "fat Status", + "computed": true, + "valueExpression": "{{ $[\"fatPct\"] === undefined || $[\"fatPct\"] === null ? \"unknown\" : $[\"fatPct\"] <= 100 ? \"success\" : $[\"fatPct\"] <= 110 ? \"warning\" : \"error\" }}", + "shape": "state", + "visible": false + } + ], + "timeframes": [ + "last12hours", + "last24hours", + "last7days", + "last30days", + "thisMonth" + ], + "defaultShaping": { + "sort": { + "by": [ + [ + "date", + "asc" + ] + ] + } + } +} \ No newline at end of file diff --git a/plugins/GoogleHealth/v1/dataStreams/pairedDevices.json b/plugins/GoogleHealth/v1/dataStreams/pairedDevices.json new file mode 100644 index 00000000..7c72129f --- /dev/null +++ b/plugins/GoogleHealth/v1/dataStreams/pairedDevices.json @@ -0,0 +1,25 @@ +{ + "name": "pairedDevices", + "displayName": "Paired Devices", + "description": "The user's paired Fitbit trackers, watches and scales with battery level, last sync time and supported feature count", + "tags": ["Device", "Hardware"], + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "get", + "endpointPath": "users/me/pairedDevices", + "paging": { "mode": "none" }, + "postRequestScript": "pairedDevices.js" + }, + "matches": "none", + "metadata": [ + { "name": "deviceName", "displayName": "Device", "shape": "string", "role": "label" }, + { "name": "deviceType", "displayName": "Type", "shape": "string" }, + { "name": "batteryLevel", "displayName": "Battery", "shape": ["percent", { "decimalPlaces": 0 }] }, + { "name": "batteryStatus", "displayName": "Battery Status", "shape": ["state", { "map": { "success": ["High", "Medium"], "warning": ["Low"], "error": ["Empty"] } }] }, + { "name": "lastSyncTime", "displayName": "Last Sync", "shape": ["date", { "format": "dd MMM yyyy HH:mm" }], "role": "timestamp" }, + { "name": "featureCount", "displayName": "Features", "shape": ["number", { "decimalPlaces": 0 }] }, + { "name": "deviceId", "displayName": "Device ID", "shape": "string", "role": "id", "visible": false }, + { "name": "macAddress", "displayName": "MAC", "shape": "string", "visible": false } + ], + "timeframes": false +} diff --git a/plugins/GoogleHealth/v1/dataStreams/scripts/bodyMetric.js b/plugins/GoogleHealth/v1/dataStreams/scripts/bodyMetric.js new file mode 100644 index 00000000..0a0d7ef3 --- /dev/null +++ b/plugins/GoogleHealth/v1/dataStreams/scripts/bodyMetric.js @@ -0,0 +1,41 @@ +// list of body-measurement dataPoints (sample types). +// weight: { weightGrams, sampleTime:{ civilTime:{ date:{y,m,d} } } } +// bodyFat: { percentage, sampleTime:{ civilTime:{ date:{y,m,d} } } } +const pts = data?.dataPoints || []; +const metricName = context?.config?.metric || ""; + +const N = (v) => { + const n = Number(v); + return Number.isFinite(n) ? n : undefined; +}; +const civilToDate = (ct) => + ct && ct.date && ct.date.year + ? new Date(Date.UTC(ct.date.year, (ct.date.month || 1) - 1, ct.date.day || 1)) + : undefined; + +const extract = (dp) => { + if (dp.weight) { + const g = N(dp.weight.weightGrams); + return { + ct: dp.weight.sampleTime && dp.weight.sampleTime.civilTime, + value: g === undefined ? undefined : Math.round((g / 1000) * 10) / 10, + unit: "kg", + }; + } + if (dp.bodyFat) { + const p = N(dp.bodyFat.percentage); + return { + ct: dp.bodyFat.sampleTime && dp.bodyFat.sampleTime.civilTime, + value: p === undefined ? undefined : Math.round(p * 10) / 10, + unit: "%", + }; + } + return { ct: undefined, value: undefined, unit: "" }; +}; + +result = pts + .map((dp) => { + const { ct, value, unit } = extract(dp); + return { date: civilToDate(ct), value, unit, metric: metricName }; + }) + .filter((r) => r.date && r.value !== undefined); diff --git a/plugins/GoogleHealth/v1/dataStreams/scripts/dailyHealthMetric.js b/plugins/GoogleHealth/v1/dataStreams/scripts/dailyHealthMetric.js new file mode 100644 index 00000000..94bf7575 --- /dev/null +++ b/plugins/GoogleHealth/v1/dataStreams/scripts/dailyHealthMetric.js @@ -0,0 +1,39 @@ +// list of daily-summary dataPoints. Each DataPoint has one populated field with a +// nested civil `date` {year,month,day} and a single value: +// dailyRestingHeartRate.beatsPerMinute (bpm) +// dailyHeartRateVariability.averageHeartRateVariabilityMilliseconds (ms) +// dailyRespiratoryRate.breathsPerMinute +// dailyOxygenSaturation.averagePercentage (%) +const pts = data?.dataPoints || []; +const metricName = context?.config?.metric || ""; + +const N = (v) => { + const n = Number(v); + return Number.isFinite(n) ? n : undefined; +}; +const civilToDate = (c) => + c && c.year ? new Date(Date.UTC(c.year, (c.month || 1) - 1, c.day || 1)) : undefined; + +const extract = (dp) => { + if (dp.dailyRestingHeartRate) + return { date: dp.dailyRestingHeartRate.date, value: N(dp.dailyRestingHeartRate.beatsPerMinute), unit: "bpm" }; + if (dp.dailyHeartRateVariability) + return { + date: dp.dailyHeartRateVariability.date, + value: N(dp.dailyHeartRateVariability.averageHeartRateVariabilityMilliseconds), + unit: "ms", + }; + if (dp.dailyRespiratoryRate) + return { date: dp.dailyRespiratoryRate.date, value: N(dp.dailyRespiratoryRate.breathsPerMinute), unit: "breaths/min" }; + if (dp.dailyOxygenSaturation) + return { date: dp.dailyOxygenSaturation.date, value: N(dp.dailyOxygenSaturation.averagePercentage), unit: "%" }; + return { date: undefined, value: undefined, unit: "" }; +}; + +result = pts + .map((dp) => { + const { date, value, unit } = extract(dp); + const v = value === undefined ? undefined : Math.round(value * 10) / 10; + return { date: civilToDate(date), value: v, unit, metric: metricName }; + }) + .filter((r) => r.date && r.value !== undefined); diff --git a/plugins/GoogleHealth/v1/dataStreams/scripts/dailyMetric.js b/plugins/GoogleHealth/v1/dataStreams/scripts/dailyMetric.js new file mode 100644 index 00000000..fd4b41ec --- /dev/null +++ b/plugins/GoogleHealth/v1/dataStreams/scripts/dailyMetric.js @@ -0,0 +1,124 @@ +// dailyRollUp response: { rollupDataPoints: [ { civilStartTime, civilEndTime, :{...} } ] } +// Each metric has a distinct value field; int64 sums arrive as strings. See the v4 discovery doc. +const points = data?.rollupDataPoints || []; +const metricName = context?.config?.metric || ""; + +const N = (v) => { + const n = Number(v); + return Number.isFinite(n) ? n : undefined; +}; +const sum = (...xs) => { + const vals = xs.map(N).filter((v) => v !== undefined); + return vals.length ? vals.reduce((a, b) => a + b, 0) : undefined; +}; +const mid = (a, b) => { + const s = sum(a, b); + return s === undefined ? undefined : s / 2; +}; + +const civilToDate = (c) => + c && c.date && c.date.year + ? new Date(Date.UTC(c.date.year, (c.date.month || 1) - 1, c.date.day || 1)) + : undefined; + +// Returns { value, unit } for whichever metric field is populated on the point. +const extract = (pt) => { + if (pt.steps) return { value: N(pt.steps.countSum), unit: "steps" }; + if (pt.distance) { + const mm = N(pt.distance.millimetersSum); + return { value: mm === undefined ? undefined : Math.round(mm / 1000) / 1000, unit: "km" }; + } + if (pt.floors) return { value: N(pt.floors.countSum), unit: "floors" }; + if (pt.activeZoneMinutes) { + const z = pt.activeZoneMinutes; + return { + value: sum(z.sumInFatBurnHeartZone, z.sumInCardioHeartZone, z.sumInPeakHeartZone), + unit: "min", + }; + } + if (pt.activeEnergyBurned) { + const k = N(pt.activeEnergyBurned.kcalSum); + return { value: k === undefined ? undefined : Math.round(k), unit: "kcal" }; + } + if (pt.weight) { + const g = N(pt.weight.weightGramsAvg); + return { value: g === undefined ? undefined : Math.round(g / 1000 * 10) / 10, unit: "kg" }; + } + if (pt.bodyFat) { + const b = N(pt.bodyFat.bodyFatPercentageAvg); + return { value: b === undefined ? undefined : Math.round(b * 10) / 10, unit: "%" }; + } + if (pt.restingHeartRatePersonalRange) { + const r = pt.restingHeartRatePersonalRange; + const v = mid(r.beatsPerMinuteMin, r.beatsPerMinuteMax); + return { value: v === undefined ? undefined : Math.round(v), unit: "bpm" }; + } + if (pt.runVo2Max) { + const v = N(pt.runVo2Max.rateAvg); + return { value: v === undefined ? undefined : Math.round(v * 10) / 10, unit: "mL/kg/min" }; + } + if (pt.heartRateVariabilityPersonalRange) { + const h = pt.heartRateVariabilityPersonalRange; + const v = mid( + h.averageHeartRateVariabilityMillisecondsMin, + h.averageHeartRateVariabilityMillisecondsMax + ); + return { value: v === undefined ? undefined : Math.round(v), unit: "ms" }; + } + if (pt.hydrationLog) { + const a = pt.hydrationLog.amountConsumed; + return { value: a ? N(a.millilitersSum) : undefined, unit: "mL" }; + } + return { value: undefined, unit: "" }; +}; + +// Goal from plugin config (settings screen); per-day pctOfGoal so a mean() monitor +// reflects the average daily attainment shown by the gauge's mean() value. +const cfg = (context && context.dataSources && context.dataSources[0]) || {}; +const goalNum = (k, d) => { + const n = Number(cfg[k]); + return Number.isFinite(n) && n > 0 ? n : d; +}; +const GOALMAP = { + steps: ["stepGoal", 10000], + distance: ["distanceGoal", 8], + "active-energy-burned": ["activeCalorieGoal", 600], + "active-zone-minutes": ["zoneMinutesGoal", 22], + "hydration-log": ["waterGoal", 2000], +}; +const gm = GOALMAP[metricName]; +const goal = gm ? goalNum(gm[0], gm[1]) : undefined; + +result = points + .map((pt) => { + const { value, unit } = extract(pt); + return { + date: civilToDate(pt.civilStartTime), + value, + unit, + metric: metricName, + goal: goal, + pctOfGoal: goal && value !== undefined ? Math.round((value / goal) * 100) : undefined, + }; + }) + .filter((r) => r.date && r.value !== undefined); + +// Snapshot fix: a snapped single-day window with no data should render as an +// explicit zero (0 of goal, red) rather than an empty gauge with no maximum. +if ( + result.length === 0 && + goal !== undefined && + new Date(context.timeframe.end).getTime() - new Date(context.timeframe.start).getTime() <= 90000000 +) { + const end = new Date(context.timeframe.end); + result = [ + { + date: new Date(Date.UTC(end.getUTCFullYear(), end.getUTCMonth(), end.getUTCDate())), + value: 0, + unit: ({steps:"steps",distance:"km",floors:"floors","active-zone-minutes":"min","active-energy-burned":"kcal","hydration-log":"mL"})[metricName] || "", + metric: metricName, + goal: goal, + pctOfGoal: 0, + }, + ]; +} diff --git a/plugins/GoogleHealth/v1/dataStreams/scripts/foodLog.js b/plugins/GoogleHealth/v1/dataStreams/scripts/foodLog.js new file mode 100644 index 00000000..846748de --- /dev/null +++ b/plugins/GoogleHealth/v1/dataStreams/scripts/foodLog.js @@ -0,0 +1,38 @@ +// list of nutrition-log dataPoints. Each: { nutritionLog: { +// interval:{startTime}, foodDisplayName, mealType, energy:{kcal}, +// totalCarbohydrate:{grams}, totalFat:{grams}, nutrients:[{nutrient, quantity:{grams}}] } } +const pts = data?.dataPoints || []; + +const N = (v) => { + const n = Number(v); + return Number.isFinite(n) ? n : undefined; +}; +const r1 = (v) => (v === undefined ? undefined : Math.round(v * 10) / 10); +const grams = (q) => (q ? N(q.grams) : undefined); +const titleCase = (t) => + typeof t === "string" && t !== "MEAL_TYPE_UNSPECIFIED" + ? t.toLowerCase().replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()) + : undefined; + +result = pts + .map((pt) => { + const n = pt.nutritionLog; + if (!n) return null; + const iv = n.interval || {}; + const byNutrient = {}; + for (const item of n.nutrients || []) { + if (item && item.nutrient) byNutrient[item.nutrient] = grams(item.quantity); + } + return { + time: iv.startTime ? new Date(iv.startTime) : undefined, + food: n.foodDisplayName || "Food", + meal: titleCase(n.mealType), + calories: n.energy ? Math.round(N(n.energy.kcal)) : undefined, + carbsG: r1(grams(n.totalCarbohydrate) ?? byNutrient.CARBOHYDRATES), + fatG: r1(grams(n.totalFat)), + proteinG: r1(byNutrient.PROTEIN), + sugarG: r1(byNutrient.SUGAR), + fibreG: r1(byNutrient.DIETARY_FIBER), + }; + }) + .filter((r) => r && r.time); diff --git a/plugins/GoogleHealth/v1/dataStreams/scripts/heartRateIntraday.js b/plugins/GoogleHealth/v1/dataStreams/scripts/heartRateIntraday.js new file mode 100644 index 00000000..9c6fa030 --- /dev/null +++ b/plugins/GoogleHealth/v1/dataStreams/scripts/heartRateIntraday.js @@ -0,0 +1,21 @@ +// rollUp of heart-rate. RollupDataPoint: { startTime, endTime, heartRate:{ beatsPerMinuteAvg, beatsPerMinuteMin, beatsPerMinuteMax } } +const points = data?.rollupDataPoints || []; + +const N = (v) => { + const n = Number(v); + return Number.isFinite(n) ? n : undefined; +}; +const round = (v) => (v === undefined ? undefined : Math.round(v)); + +result = points + .map((pt) => { + const hr = pt.heartRate; + if (!hr) return null; + return { + time: pt.startTime ? new Date(pt.startTime) : undefined, + avgBpm: round(N(hr.beatsPerMinuteAvg)), + minBpm: round(N(hr.beatsPerMinuteMin)), + maxBpm: round(N(hr.beatsPerMinuteMax)), + }; + }) + .filter((r) => r && r.time && r.avgBpm !== undefined); diff --git a/plugins/GoogleHealth/v1/dataStreams/scripts/heartRateZonesDaily.js b/plugins/GoogleHealth/v1/dataStreams/scripts/heartRateZonesDaily.js new file mode 100644 index 00000000..6d392304 --- /dev/null +++ b/plugins/GoogleHealth/v1/dataStreams/scripts/heartRateZonesDaily.js @@ -0,0 +1,39 @@ +// dailyRollUp of time-in-heart-rate-zone. +// timeInHeartRateZone.timeInHeartRateZones = [{ heartRateZone: LIGHT|MODERATE|VIGOROUS|PEAK, duration:"Ns" }] +const points = data?.rollupDataPoints || []; + +const durMin = (s) => { + if (typeof s !== "string") return undefined; + const n = Number(s.replace(/s$/, "")); + return Number.isFinite(n) ? Math.round(n / 60) : undefined; +}; + +result = points + .map((pt) => { + const z = pt.timeInHeartRateZone; + if (!z) return null; + const zones = { LIGHT: 0, MODERATE: 0, VIGOROUS: 0, PEAK: 0 }; + for (const item of z.timeInHeartRateZones || []) { + const m = durMin(item.duration); + if (item.heartRateZone in zones && m !== undefined) zones[item.heartRateZone] += m; + } + const total = zones.LIGHT + zones.MODERATE + zones.VIGOROUS + zones.PEAK; + return { + date: + pt.civilStartTime && pt.civilStartTime.date && pt.civilStartTime.date.year + ? new Date( + Date.UTC( + pt.civilStartTime.date.year, + (pt.civilStartTime.date.month || 1) - 1, + pt.civilStartTime.date.day || 1 + ) + ) + : undefined, + lightMin: zones.LIGHT, + moderateMin: zones.MODERATE, + vigorousMin: zones.VIGOROUS, + peakMin: zones.PEAK, + totalMin: total, + }; + }) + .filter((r) => r && r.date); diff --git a/plugins/GoogleHealth/v1/dataStreams/scripts/intradayMetric.js b/plugins/GoogleHealth/v1/dataStreams/scripts/intradayMetric.js new file mode 100644 index 00000000..4f237d62 --- /dev/null +++ b/plugins/GoogleHealth/v1/dataStreams/scripts/intradayMetric.js @@ -0,0 +1,66 @@ +// rollUp response: { rollupDataPoints: [ { startTime, endTime, :{...} } ] } +// Physical-time windows. int64 sums arrive as strings. +const points = data?.rollupDataPoints || []; +const metricName = context?.config?.metric || ""; + +const N = (v) => { + const n = Number(v); + return Number.isFinite(n) ? n : undefined; +}; +const sum = (...xs) => { + const vals = xs.map(N).filter((v) => v !== undefined); + return vals.length ? vals.reduce((a, b) => a + b, 0) : undefined; +}; + +const extract = (pt) => { + if (pt.steps) return { value: N(pt.steps.countSum), unit: "steps" }; + if (pt.distance) { + const mm = N(pt.distance.millimetersSum); + return { value: mm === undefined ? undefined : Math.round(mm / 1000) / 1000, unit: "km" }; + } + if (pt.floors) return { value: N(pt.floors.countSum), unit: "floors" }; + if (pt.activeZoneMinutes) { + const z = pt.activeZoneMinutes; + return { + value: sum(z.sumInFatBurnHeartZone, z.sumInCardioHeartZone, z.sumInPeakHeartZone), + unit: "min", + }; + } + if (pt.activeEnergyBurned) { + const k = N(pt.activeEnergyBurned.kcalSum); + return { value: k === undefined ? undefined : Math.round(k), unit: "kcal" }; + } + if (pt.heartRate) { + const v = N(pt.heartRate.beatsPerMinuteAvg); + return { value: v === undefined ? undefined : Math.round(v), unit: "bpm" }; + } + return { value: undefined, unit: "" }; +}; + +// Goal from plugin config (settings screen); falls back to a default if unset. +const cfg = (context && context.dataSources && context.dataSources[0]) || {}; +const goalNum = (k, d) => { + const n = Number(cfg[k]); + return Number.isFinite(n) && n > 0 ? n : d; +}; +const GOALMAP = { + steps: ["stepGoal", 10000], + distance: ["distanceGoal", 8], + "active-energy-burned": ["activeCalorieGoal", 600], + "active-zone-minutes": ["zoneMinutesGoal", 22], +}; +const gm = GOALMAP[metricName]; +const goal = gm ? goalNum(gm[0], gm[1]) : undefined; + +const rows = points + .map((pt) => { + const { value, unit } = extract(pt); + return { time: pt.startTime ? new Date(pt.startTime) : undefined, value, unit, metric: metricName }; + }) + .filter((r) => r.time && r.value !== undefined); + +// pctOfGoal compares the day's TOTAL to the goal, held constant on every row +// so a mean() monitor reads the same figure the summed gauge shows. +const total = rows.reduce((a, r) => a + (r.value || 0), 0); +const pct = goal ? Math.round((total / goal) * 100) : undefined; +result = rows.map((r) => ({ ...r, goal: goal, pctOfGoal: pct })); diff --git a/plugins/GoogleHealth/v1/dataStreams/scripts/macroBreakdown.js b/plugins/GoogleHealth/v1/dataStreams/scripts/macroBreakdown.js new file mode 100644 index 00000000..c602b9aa --- /dev/null +++ b/plugins/GoogleHealth/v1/dataStreams/scripts/macroBreakdown.js @@ -0,0 +1,45 @@ +// dailyRollUp of nutrition-log, summed across the period into one row per macro. +// NutritionLogRollupValue: { energy:{kcalSum}, totalFat:{gramsSum}, totalCarbohydrate:{gramsSum}, +// nutrients:[{ nutrient:"PROTEIN"|..., quantity:{gramsSum} }] } +const points = data?.rollupDataPoints || []; + +const N = (v) => { + const n = Number(v); + return Number.isFinite(n) ? n : 0; +}; +const grams = (q) => (q ? N(q.gramsSum) : 0); + +let carbs = 0; +let fat = 0; +let protein = 0; +let any = false; + +for (const pt of points) { + const n = pt.nutritionLog; + if (!n) continue; + any = true; + carbs += grams(n.totalCarbohydrate); + fat += grams(n.totalFat); + for (const item of n.nutrients || []) { + if (item && item.nutrient === "PROTEIN") protein += grams(item.quantity); + if (item && item.nutrient === "CARBOHYDRATES" && !n.totalCarbohydrate) carbs += grams(item.quantity); + } +} + +if (!any) { + result = []; +} else { + // Atwater factors: carbs & protein 4 kcal/g, fat 9 kcal/g. + const rows = [ + { macro: "Carbohydrate", grams: carbs, calories: carbs * 4 }, + { macro: "Fat", grams: fat, calories: fat * 9 }, + { macro: "Protein", grams: protein, calories: protein * 4 }, + ]; + const totalCal = rows.reduce((a, r) => a + r.calories, 0) || 1; + result = rows.map((r) => ({ + macro: r.macro, + grams: Math.round(r.grams), + calories: Math.round(r.calories), + percent: Math.round((r.calories / totalCal) * 100), + })); +} diff --git a/plugins/GoogleHealth/v1/dataStreams/scripts/nutritionDaily.js b/plugins/GoogleHealth/v1/dataStreams/scripts/nutritionDaily.js new file mode 100644 index 00000000..bb0f4062 --- /dev/null +++ b/plugins/GoogleHealth/v1/dataStreams/scripts/nutritionDaily.js @@ -0,0 +1,63 @@ +// dailyRollUp of nutrition-log. NutritionLogRollupValue: +// { energy:{kcalSum}, totalFat:{gramsSum}, totalCarbohydrate:{gramsSum}, +// nutrients:[{ nutrient:"PROTEIN"|"SUGAR"|"SODIUM"|..., quantity:{gramsSum} }] } +const points = data?.rollupDataPoints || []; + +const N = (v) => { + const n = Number(v); + return Number.isFinite(n) ? n : undefined; +}; +const round = (v) => (v === undefined ? undefined : Math.round(v)); + +const civilToDate = (c) => + c && c.date && c.date.year + ? new Date(Date.UTC(c.date.year, (c.date.month || 1) - 1, c.date.day || 1)) + : undefined; + +const grams = (q) => (q ? N(q.gramsSum) : undefined); + +// Goals from plugin config (settings screen), with defaults. +const cfg = (context && context.dataSources && context.dataSources[0]) || {}; +const goalNum = (k, d) => { + const n = Number(cfg[k]); + return Number.isFinite(n) && n > 0 ? n : d; +}; +const calorieGoal = goalNum("calorieBudget", 2500); +const proteinGoal = goalNum("proteinGoal", 130); +const carbGoal = goalNum("carbBudget", 250); +const fatGoal = goalNum("fatBudget", 70); +const pct = (v, g) => (g && v !== undefined ? Math.round((v / g) * 100) : undefined); + +result = points + .map((pt) => { + const n = pt.nutritionLog; + if (!n) return null; + const byNutrient = {}; + for (const item of n.nutrients || []) { + if (item && item.nutrient) byNutrient[item.nutrient] = grams(item.quantity); + } + const sodiumG = byNutrient.SODIUM; + const calories = round(n.energy ? N(n.energy.kcalSum) : undefined); + const carbsG = round(grams(n.totalCarbohydrate) ?? byNutrient.CARBOHYDRATES); + const fatG = round(grams(n.totalFat)); + const proteinG = round(byNutrient.PROTEIN); + return { + date: civilToDate(pt.civilStartTime), + calories, + carbsG, + fatG, + proteinG, + sugarG: round(byNutrient.SUGAR), + fibreG: round(byNutrient.DIETARY_FIBER), + sodiumMg: sodiumG === undefined ? undefined : round(sodiumG * 1000), + calorieGoalCol: calorieGoal, + proteinGoalCol: proteinGoal, + carbGoalCol: carbGoal, + fatGoalCol: fatGoal, + caloriesPct: pct(calories, calorieGoal), + proteinPct: pct(proteinG, proteinGoal), + carbsPct: pct(carbsG, carbGoal), + fatPct: pct(fatG, fatGoal), + }; + }) + .filter((r) => r && r.date); diff --git a/plugins/GoogleHealth/v1/dataStreams/scripts/pairedDevices.js b/plugins/GoogleHealth/v1/dataStreams/scripts/pairedDevices.js new file mode 100644 index 00000000..dce7d249 --- /dev/null +++ b/plugins/GoogleHealth/v1/dataStreams/scripts/pairedDevices.js @@ -0,0 +1,25 @@ +// GET users/me/pairedDevices -> { pairedDevices: [ PairedDevice ] } +// PairedDevice: { name:"users/{u}/pairedDevices/{id}", deviceType:TRACKER|SCALE, +// deviceVersion (product name), batteryLevel(int), batteryStatus, macAddress, lastSyncTime, features[] } +const list = data?.pairedDevices || []; + +const N = (v) => { + const n = Number(v); + return Number.isFinite(n) ? n : undefined; +}; +const idFromName = (n) => (typeof n === "string" && n.includes("/") ? n.split("/").pop() : n); +const titleCase = (t) => + typeof t === "string" + ? t.toLowerCase().replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()) + : undefined; + +result = list.map((d) => ({ + deviceId: idFromName(d.name), + deviceName: d.deviceVersion || titleCase(d.deviceType) || "Device", + deviceType: titleCase(d.deviceType), + batteryLevel: N(d.batteryLevel), + batteryStatus: d.batteryStatus, + lastSyncTime: d.lastSyncTime ? new Date(d.lastSyncTime) : undefined, + featureCount: Array.isArray(d.features) ? d.features.length : undefined, + macAddress: d.macAddress, +})); diff --git a/plugins/GoogleHealth/v1/dataStreams/scripts/sleepSessions.js b/plugins/GoogleHealth/v1/dataStreams/scripts/sleepSessions.js new file mode 100644 index 00000000..b8627840 --- /dev/null +++ b/plugins/GoogleHealth/v1/dataStreams/scripts/sleepSessions.js @@ -0,0 +1,58 @@ +// list of sleep dataPoints. sleep: { interval:{startTime,endTime,civilEndTime}, +// summary:{ minutesAsleep, minutesAwake, minutesInSleepPeriod, stagesSummary:[{type,minutes}] }, stages:[...] } +const pts = data?.dataPoints || []; + +const N = (v) => { + const n = Number(v); + return Number.isFinite(n) ? n : undefined; +}; + +const civilToDate = (c) => + c && c.date && c.date.year + ? new Date(Date.UTC(c.date.year, (c.date.month || 1) - 1, c.date.day || 1)) + : undefined; + +// Sleep goal (hours) from plugin config, with default. +const cfg = (context && context.dataSources && context.dataSources[0]) || {}; +const sleepGoalRaw = Number(cfg.sleepGoal); +const sleepGoal = Number.isFinite(sleepGoalRaw) && sleepGoalRaw > 0 ? sleepGoalRaw : 8; + +result = pts + .map((pt) => { + const s = pt.sleep; + if (!s) return null; + const iv = s.interval || {}; + const sum = s.summary || {}; + const asleep = N(sum.minutesAsleep); + const awake = N(sum.minutesAwake); + const inBed = N(sum.minutesInSleepPeriod); + + // Per-stage minutes from stagesSummary. + const stage = { DEEP: 0, REM: 0, LIGHT: 0, AWAKE: 0, ASLEEP: 0, RESTLESS: 0 }; + for (const st of sum.stagesSummary || []) { + const m = N(st.minutes); + if (st.type in stage && m !== undefined) stage[st.type] += m; + } + + const date = civilToDate(iv.civilEndTime) || (iv.endTime ? new Date(iv.endTime) : undefined); + const efficiency = + asleep !== undefined && inBed ? Math.round((asleep / inBed) * 100) : undefined; + + return { + date: date, + asleepMin: asleep, + asleepHours: asleep === undefined ? undefined : Math.round((asleep / 60) * 10) / 10, + sleepGoalCol: sleepGoal, + sleepPct: + asleep === undefined ? undefined : Math.round((asleep / 60 / sleepGoal) * 100), + efficiencyPct: efficiency, + deepMin: stage.DEEP || undefined, + remMin: stage.REM || undefined, + lightMin: stage.LIGHT || undefined, + awakeMin: awake ?? (stage.AWAKE || undefined), + inBedMin: inBed, + startTime: iv.startTime ? new Date(iv.startTime) : undefined, + endTime: iv.endTime ? new Date(iv.endTime) : undefined, + }; + }) + .filter((r) => r && r.date); diff --git a/plugins/GoogleHealth/v1/dataStreams/scripts/userProfile.js b/plugins/GoogleHealth/v1/dataStreams/scripts/userProfile.js new file mode 100644 index 00000000..a38c5e45 --- /dev/null +++ b/plugins/GoogleHealth/v1/dataStreams/scripts/userProfile.js @@ -0,0 +1,39 @@ +// GET users/me/profile returns a single, sparse Profile object, e.g. +// { name: "users/{id}/profile", age, membershipStartDate: {year,month,day}, +// userConfiguredWalkingStrideLengthMm, userConfiguredRunningStrideLengthMm, +// autoRunningStrideLengthMm } +const p = data || {}; + +const num = (v) => { + const n = Number(v); + return Number.isFinite(n) ? n : undefined; +}; + +const userId = typeof p.name === "string" ? p.name.split("/")[1] : p.userId || p.id; + +// membershipStartDate is a civil date {year, month, day}. +let memberSince; +let membershipYears; +const m = p.membershipStartDate; +if (m && m.year) { + memberSince = new Date(Date.UTC(m.year, (m.month || 1) - 1, m.day || 1)); + membershipYears = + Math.round(((Date.now() - memberSince.getTime()) / (365.25 * 24 * 3600 * 1000)) * 10) / 10; +} + +const mmToCm = (v) => { + const n = num(v); + return n === undefined ? undefined : Math.round(n / 10 * 10) / 10; +}; + +result = [ + { + userId: userId, + displayName: "Me", + age: num(p.age), + memberSince: memberSince, + membershipYears: membershipYears, + walkingStrideCm: mmToCm(p.userConfiguredWalkingStrideLengthMm), + runningStrideCm: mmToCm(p.userConfiguredRunningStrideLengthMm ?? p.autoRunningStrideLengthMm), + }, +]; diff --git a/plugins/GoogleHealth/v1/dataStreams/scripts/workouts.js b/plugins/GoogleHealth/v1/dataStreams/scripts/workouts.js new file mode 100644 index 00000000..3f880d7e --- /dev/null +++ b/plugins/GoogleHealth/v1/dataStreams/scripts/workouts.js @@ -0,0 +1,47 @@ +// list of exercise dataPoints. Each: { exercise: { interval:{startTime,endTime}, +// exerciseType, displayName, activeDuration:"Ns", metricsSummary:{...} }, dataSource:{device} } +const pts = data?.dataPoints || []; + +const N = (v) => { + const n = Number(v); + return Number.isFinite(n) ? n : undefined; +}; +const round = (v, dp) => { + if (v === undefined) return undefined; + const f = Math.pow(10, dp || 0); + return Math.round(v * f) / f; +}; +// google-duration is a string like "8224s" or "8224.5s" +const durationSeconds = (s) => (typeof s === "string" ? N(s.replace(/s$/, "")) : N(s)); + +const titleCase = (t) => + typeof t === "string" + ? t.toLowerCase().replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()) + : undefined; + +result = pts + .map((pt) => { + const ex = pt.exercise; + if (!ex) return null; + const iv = ex.interval || {}; + const ms = ex.metricsSummary || {}; + let durSec = durationSeconds(ex.activeDuration); + if (durSec === undefined && iv.startTime && iv.endTime) { + durSec = (new Date(iv.endTime).getTime() - new Date(iv.startTime).getTime()) / 1000; + } + const distMm = N(ms.distanceMillimeters); + const device = pt.dataSource && pt.dataSource.device; + return { + startTime: iv.startTime ? new Date(iv.startTime) : undefined, + endTime: iv.endTime ? new Date(iv.endTime) : undefined, + type: ex.displayName || titleCase(ex.exerciseType) || "Workout", + durationMin: durSec === undefined ? undefined : Math.round(durSec / 60), + distanceKm: distMm === undefined ? undefined : round(distMm / 1000000, 2), + calories: round(N(ms.caloriesKcal), 0), + avgHeartRate: N(ms.averageHeartRateBeatsPerMinute), + steps: N(ms.steps), + activeZoneMinutes: N(ms.activeZoneMinutes), + source: device ? device.displayName || device.formFactor : undefined, + }; + }) + .filter((r) => r && r.startTime); diff --git a/plugins/GoogleHealth/v1/dataStreams/sleepSessions.json b/plugins/GoogleHealth/v1/dataStreams/sleepSessions.json new file mode 100644 index 00000000..da6923ae --- /dev/null +++ b/plugins/GoogleHealth/v1/dataStreams/sleepSessions.json @@ -0,0 +1,197 @@ +{ + "name": "sleepSessions", + "displayName": "Sleep Sessions", + "description": "Sleep sessions with time asleep, time awake, time in bed, efficiency and minutes in each sleep stage", + "tags": [ + "Sleep", + "Health" + ], + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "get", + "endpointPath": "users/me/dataTypes/sleep/dataPoints", + "getArgs": [ + { + "key": "filter", + "value": "sleep.interval.civil_end_time >= \"{{new Date(new Date(timeframe.end).getTime()-new Date(timeframe.start).getTime()<=90000000 ? timeframe.end : timeframe.start).toISOString().slice(0,10)}}\" AND sleep.interval.civil_end_time < \"{{new Date(new Date(timeframe.end).getTime()+86400000).toISOString().slice(0,10)}}\"" + } + ], + "paging": { + "mode": "token", + "pageSize": { + "realm": "queryArg", + "path": "pageSize", + "value": "25" + }, + "in": { + "realm": "payload", + "path": "nextPageToken" + }, + "out": { + "realm": "queryArg", + "path": "pageToken" + } + }, + "postRequestScript": "sleepSessions.js" + }, + "matches": "none", + "metadata": [ + { + "name": "date", + "displayName": "Date", + "shape": [ + "date", + { + "format": "dd/MM" + } + ], + "role": "timestamp" + }, + { + "name": "asleepMin", + "displayName": "Asleep (min)", + "shape": [ + "number", + { + "decimalPlaces": 0 + } + ], + "role": "value" + }, + { + "name": "asleepHours", + "displayName": "Asleep (h)", + "shape": [ + "number", + { + "decimalPlaces": 1 + } + ] + }, + { + "name": "efficiencyPct", + "displayName": "Efficiency", + "shape": [ + "percent", + { + "decimalPlaces": 0 + } + ] + }, + { + "name": "deepMin", + "displayName": "Deep (min)", + "shape": [ + "number", + { + "decimalPlaces": 0 + } + ] + }, + { + "name": "remMin", + "displayName": "REM (min)", + "shape": [ + "number", + { + "decimalPlaces": 0 + } + ] + }, + { + "name": "lightMin", + "displayName": "Light (min)", + "shape": [ + "number", + { + "decimalPlaces": 0 + } + ] + }, + { + "name": "awakeMin", + "displayName": "Awake (min)", + "shape": [ + "number", + { + "decimalPlaces": 0 + } + ] + }, + { + "name": "inBedMin", + "displayName": "In Bed (min)", + "shape": [ + "number", + { + "decimalPlaces": 0 + } + ] + }, + { + "name": "startTime", + "displayName": "Start", + "shape": [ + "date", + { + "format": "dd MMM HH:mm" + } + ] + }, + { + "name": "endTime", + "displayName": "End", + "shape": [ + "date", + { + "format": "dd MMM HH:mm" + } + ] + }, + { + "name": "sleepGoalCol", + "displayName": "Sleep Goal (h)", + "shape": "number", + "visible": false + }, + { + "name": "sleepPct", + "displayName": "% of Sleep Goal", + "shape": [ + "percent", + { + "decimalPlaces": 0 + } + ], + "visible": false, + "formatExpression": "{{ $['asleepHours'] }}h" + }, + { + "name": "sleepStatus", + "displayName": "sleep Status", + "computed": true, + "valueExpression": "{{ $[\"sleepPct\"] === undefined || $[\"sleepPct\"] === null ? \"unknown\" : $[\"sleepPct\"] >= 100 ? \"success\" : $[\"sleepPct\"] >= 75 ? \"warning\" : \"error\" }}", + "shape": "state", + "visible": false + } + ], + "timeframes": [ + "last12hours", + "last24hours", + "last7days", + "last30days", + "thisMonth", + "thisQuarter", + "thisYear" + ], + "defaultShaping": { + "sort": { + "by": [ + [ + "date", + "asc" + ] + ] + } + } +} \ No newline at end of file diff --git a/plugins/GoogleHealth/v1/dataStreams/userProfile.json b/plugins/GoogleHealth/v1/dataStreams/userProfile.json new file mode 100644 index 00000000..1bcf7b2d --- /dev/null +++ b/plugins/GoogleHealth/v1/dataStreams/userProfile.json @@ -0,0 +1,25 @@ +{ + "name": "userProfile", + "displayName": "User Profile", + "description": "The authenticated user's Google Health profile, including age, membership date and configured stride lengths", + "tags": ["User", "Profile"], + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "get", + "endpointPath": "users/me/profile", + "paging": { "mode": "none" }, + "postRequestScript": "userProfile.js" + }, + "matches": "none", + "providesPluginDiagnostics": true, + "metadata": [ + { "name": "displayName", "displayName": "User", "shape": "string", "role": "label" }, + { "name": "age", "displayName": "Age", "shape": "number" }, + { "name": "memberSince", "displayName": "Member Since", "shape": ["date", { "format": "dd MMM yyyy" }] }, + { "name": "membershipYears", "displayName": "Membership (years)", "shape": ["number", { "decimalPlaces": 1 }] }, + { "name": "walkingStrideCm", "displayName": "Walking Stride (cm)", "shape": ["number", { "decimalPlaces": 1 }] }, + { "name": "runningStrideCm", "displayName": "Running Stride (cm)", "shape": ["number", { "decimalPlaces": 1 }] }, + { "name": "userId", "displayName": "User ID", "shape": "string", "role": "id", "visible": false } + ], + "timeframes": false +} diff --git a/plugins/GoogleHealth/v1/dataStreams/workouts.json b/plugins/GoogleHealth/v1/dataStreams/workouts.json new file mode 100644 index 00000000..c9229290 --- /dev/null +++ b/plugins/GoogleHealth/v1/dataStreams/workouts.json @@ -0,0 +1,153 @@ +{ + "name": "workouts", + "displayName": "Workouts", + "description": "Logged exercise sessions with type, duration, distance, calories, average heart rate and steps", + "tags": [ + "Activity", + "Exercise" + ], + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "get", + "endpointPath": "users/me/dataTypes/exercise/dataPoints", + "getArgs": [ + { + "key": "filter", + "value": "exercise.interval.civil_start_time >= \"{{new Date(new Date(timeframe.end).getTime()-new Date(timeframe.start).getTime()<=90000000 ? timeframe.end : timeframe.start).toISOString().slice(0,10)}}\" AND exercise.interval.civil_start_time < \"{{new Date(new Date(timeframe.end).getTime()+86400000).toISOString().slice(0,10)}}\"" + } + ], + "paging": { + "mode": "token", + "pageSize": { + "realm": "queryArg", + "path": "pageSize", + "value": "25" + }, + "in": { + "realm": "payload", + "path": "nextPageToken" + }, + "out": { + "realm": "queryArg", + "path": "pageToken" + } + }, + "postRequestScript": "workouts.js" + }, + "matches": "none", + "metadata": [ + { + "name": "startTime", + "displayName": "Start", + "shape": [ + "date", + { + "format": "dd MMM yyyy HH:mm" + } + ], + "role": "timestamp" + }, + { + "name": "type", + "displayName": "Workout", + "shape": "string", + "role": "label" + }, + { + "name": "durationMin", + "displayName": "Duration (min)", + "shape": [ + "number", + { + "decimalPlaces": 0 + } + ] + }, + { + "name": "distanceKm", + "displayName": "Distance (km)", + "shape": [ + "number", + { + "decimalPlaces": 2 + } + ] + }, + { + "name": "calories", + "displayName": "Calories", + "shape": [ + "number", + { + "decimalPlaces": 0 + } + ], + "role": "value" + }, + { + "name": "avgHeartRate", + "displayName": "Avg HR (bpm)", + "shape": [ + "number", + { + "decimalPlaces": 0 + } + ] + }, + { + "name": "steps", + "displayName": "Steps", + "shape": [ + "number", + { + "thousandsSeparator": true + } + ] + }, + { + "name": "activeZoneMinutes", + "displayName": "Active Zone Min", + "shape": [ + "number", + { + "decimalPlaces": 0 + } + ] + }, + { + "name": "endTime", + "displayName": "End", + "shape": [ + "date", + { + "format": "dd MMM yyyy HH:mm" + } + ], + "visible": false + }, + { + "name": "source", + "displayName": "Device", + "shape": "string" + } + ], + "timeframes": [ + "last12hours", + "last24hours", + "last7days", + "last30days", + "thisMonth", + "thisQuarter", + "thisYear" + ], + "defaultShaping": { + "sort": { + "by": [ + [ + "startTime", + "desc" + ] + ] + } + } +} \ No newline at end of file diff --git a/plugins/GoogleHealth/v1/defaultContent/activity.dash.json b/plugins/GoogleHealth/v1/defaultContent/activity.dash.json new file mode 100644 index 00000000..a20f2364 --- /dev/null +++ b/plugins/GoogleHealth/v1/defaultContent/activity.dash.json @@ -0,0 +1,595 @@ +{ + "name": "Activity", + "schemaVersion": "1.5", + "timeframe": "last7days", + "dashboard": { + "_type": "layout/grid", + "columns": 12, + "version": 1, + "contents": [ + { + "i": "c846bb71-0ec9-40da-905c-ab111a10c963", + "x": 0, + "y": 0, + "w": 3, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Avg Daily Steps", + "description": "", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.dailyMetric}}", + "name": "dailyMetric", + "pluginConfigId": "{{configId}}", + "dataSourceConfig": { + "metric": "steps" + } + }, + "monitor": { + "tileRollsUp": true, + "monitorType": "threshold", + "condition": { + "columns": [ + "pctOfGoal" + ], + "logic": { + "if": [ + { + ">=": [ + { + "var": "mean" + }, + 100 + ] + }, + "success", + { + ">=": [ + { + "var": "mean" + }, + 75 + ] + }, + "warning", + "error" + ] + } + }, + "_type": "simple", + "aggregation": "mean", + "groupBy": "__group_by_none__", + "frequency": 720 + }, + "visualisation": { + "type": "data-stream-gauge", + "config": { + "data-stream-gauge": { + "value": { + "type": "mean", + "columns": [ + "value" + ] + }, + "label": "Avg Daily Steps", + "minimum": 0, + "maximumColumn": "goal" + } + } + } + } + }, + { + "i": "ba18c224-fe62-4df1-96d3-fe0101cadaa1", + "x": 3, + "y": 0, + "w": 3, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Avg Distance (km)", + "description": "", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.dailyMetric}}", + "name": "dailyMetric", + "pluginConfigId": "{{configId}}", + "dataSourceConfig": { + "metric": "distance" + } + }, + "monitor": { + "tileRollsUp": true, + "monitorType": "threshold", + "condition": { + "columns": [ + "pctOfGoal" + ], + "logic": { + "if": [ + { + ">=": [ + { + "var": "mean" + }, + 100 + ] + }, + "success", + { + ">=": [ + { + "var": "mean" + }, + 75 + ] + }, + "warning", + "error" + ] + } + }, + "_type": "simple", + "aggregation": "mean", + "groupBy": "__group_by_none__", + "frequency": 720 + }, + "visualisation": { + "type": "data-stream-gauge", + "config": { + "data-stream-gauge": { + "value": { + "type": "mean", + "columns": [ + "value" + ] + }, + "label": "Avg Distance (km)", + "minimum": 0, + "maximumColumn": "goal" + } + } + } + } + }, + { + "i": "f0003a67-5ee2-448d-a124-5db96cd7c5be", + "x": 6, + "y": 0, + "w": 3, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Avg Active Calories", + "description": "", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.dailyMetric}}", + "name": "dailyMetric", + "pluginConfigId": "{{configId}}", + "dataSourceConfig": { + "metric": "active-energy-burned" + } + }, + "monitor": { + "tileRollsUp": true, + "monitorType": "threshold", + "condition": { + "columns": [ + "pctOfGoal" + ], + "logic": { + "if": [ + { + ">=": [ + { + "var": "mean" + }, + 100 + ] + }, + "success", + { + ">=": [ + { + "var": "mean" + }, + 75 + ] + }, + "warning", + "error" + ] + } + }, + "_type": "simple", + "aggregation": "mean", + "groupBy": "__group_by_none__", + "frequency": 720 + }, + "visualisation": { + "type": "data-stream-gauge", + "config": { + "data-stream-gauge": { + "value": { + "type": "mean", + "columns": [ + "value" + ] + }, + "label": "Avg Active Calories", + "minimum": 0, + "maximumColumn": "goal" + } + } + } + } + }, + { + "i": "f011eb39-a17f-448d-9d70-d5e18a3b9216", + "x": 9, + "y": 0, + "w": 3, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Avg Zone Minutes", + "description": "", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.dailyMetric}}", + "name": "dailyMetric", + "pluginConfigId": "{{configId}}", + "dataSourceConfig": { + "metric": "active-zone-minutes" + } + }, + "monitor": { + "tileRollsUp": true, + "monitorType": "threshold", + "condition": { + "columns": [ + "pctOfGoal" + ], + "logic": { + "if": [ + { + ">=": [ + { + "var": "mean" + }, + 100 + ] + }, + "success", + { + ">=": [ + { + "var": "mean" + }, + 75 + ] + }, + "warning", + "error" + ] + } + }, + "_type": "simple", + "aggregation": "mean", + "groupBy": "__group_by_none__", + "frequency": 720 + }, + "visualisation": { + "type": "data-stream-gauge", + "config": { + "data-stream-gauge": { + "value": { + "type": "mean", + "columns": [ + "value" + ] + }, + "label": "Avg Zone Minutes", + "minimum": 0, + "maximumColumn": "goal" + } + } + } + } + }, + { + "i": "4a18e892-4091-41b7-afa3-e76159558b02", + "x": 0, + "y": 2, + "w": 4, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Daily Steps", + "description": "", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.dailyMetric}}", + "name": "dailyMetric", + "pluginConfigId": "{{configId}}", + "dataSourceConfig": { + "metric": "steps" + } + }, + "visualisation": { + "type": "data-stream-bar-chart", + "config": { + "data-stream-bar-chart": { + "xAxisData": "date", + "yAxisData": [ + "pctOfGoal" + ], + "xAxisGroup": "none", + "xAxisLabel": "", + "yAxisLabel": "% of goal", + "showXAxisLabel": false, + "showYAxisLabel": true, + "showLegend": false, + "legendPosition": "bottom", + "showGrid": true, + "horizontalLayout": "vertical", + "displayMode": "actual", + "showTotals": false, + "showValue": true, + "grouping": false, + "range": { + "type": "auto" + }, + "color": { + "type": "custom", + "customColors": [ + { + "color": "#259A51", + "expression": "value >= 100" + }, + { + "color": "#E18700", + "expression": "value >= 75 && value < 100" + }, + { + "color": "#D0021B", + "expression": "value < 75" + } + ] + } + } + } + } + } + }, + { + "i": "d8127e9e-b1cf-4920-b522-dc123f7aef95", + "x": 4, + "y": 2, + "w": 4, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Active Zone Minutes", + "description": "", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.dailyMetric}}", + "name": "dailyMetric", + "pluginConfigId": "{{configId}}", + "dataSourceConfig": { + "metric": "active-zone-minutes" + } + }, + "visualisation": { + "type": "data-stream-bar-chart", + "config": { + "data-stream-bar-chart": { + "xAxisData": "date", + "yAxisData": [ + "pctOfGoal" + ], + "xAxisGroup": "none", + "xAxisLabel": "", + "yAxisLabel": "% of goal", + "showXAxisLabel": false, + "showYAxisLabel": true, + "showLegend": false, + "legendPosition": "bottom", + "showGrid": true, + "horizontalLayout": "vertical", + "displayMode": "actual", + "showTotals": false, + "showValue": true, + "grouping": false, + "range": { + "type": "auto" + }, + "color": { + "type": "custom", + "customColors": [ + { + "color": "#259A51", + "expression": "value >= 100" + }, + { + "color": "#E18700", + "expression": "value >= 75 && value < 100" + }, + { + "color": "#D0021B", + "expression": "value < 75" + } + ] + } + } + } + } + } + }, + { + "i": "424fb1b9-9cad-4242-9ec9-031703fe325e", + "x": 8, + "y": 2, + "w": 4, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Distance Over Time", + "description": "", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.dailyMetric}}", + "name": "dailyMetric", + "pluginConfigId": "{{configId}}", + "dataSourceConfig": { + "metric": "distance" + } + }, + "visualisation": { + "type": "data-stream-line-graph", + "config": { + "data-stream-line-graph": { + "xAxisColumn": "date", + "yAxisColumn": [ + "value" + ], + "seriesColumn": "none", + "showLegend": false, + "legendPosition": "bottom", + "yAxisLabel": "km", + "showYAxisLabel": true, + "showTrendLine": false + } + } + } + } + }, + { + "i": "a43bb42a-d747-4510-b063-b742876ac7fe", + "x": 0, + "y": 4, + "w": 6, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Active Calories Over Time", + "description": "", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.dailyMetric}}", + "name": "dailyMetric", + "pluginConfigId": "{{configId}}", + "dataSourceConfig": { + "metric": "active-energy-burned" + } + }, + "visualisation": { + "type": "data-stream-line-graph", + "config": { + "data-stream-line-graph": { + "xAxisColumn": "date", + "yAxisColumn": [ + "value" + ], + "seriesColumn": "none", + "showLegend": false, + "legendPosition": "bottom", + "yAxisLabel": "Calories", + "showYAxisLabel": true, + "showTrendLine": false + } + } + } + } + }, + { + "i": "d9b07d34-7082-4cb5-9691-e7927918aa39", + "x": 6, + "y": 4, + "w": 6, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Recent Workouts", + "description": "", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.workouts}}", + "name": "workouts", + "pluginConfigId": "{{configId}}" + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false, + "columnOrder": [ + "type", + "startTime", + "durationMin", + "distanceKm", + "calories", + "avgHeartRate", + "steps", + "activeZoneMinutes", + "source" + ], + "hiddenColumns": [ + "endTime" + ] + } + } + } + } + } + ] + } +} \ No newline at end of file diff --git a/plugins/GoogleHealth/v1/defaultContent/devicePerspective.dash.json b/plugins/GoogleHealth/v1/defaultContent/devicePerspective.dash.json new file mode 100644 index 00000000..a4a40f7e --- /dev/null +++ b/plugins/GoogleHealth/v1/defaultContent/devicePerspective.dash.json @@ -0,0 +1,85 @@ +{ + "name": "Device", + "schemaVersion": "1.5", + "timeframe": "none", + "variables": ["{{variables.[Google Health Device]}}"], + "dashboard": { + "_type": "layout/grid", + "columns": 12, + "version": 1, + "contents": [ + { + "i": "cab9c1b8-7e6c-411a-8460-53fa0667351c", + "x": 0, + "y": 0, + "w": 6, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Details", + "description": "", + "timeframe": "none", + "variables": ["{{variables.[Google Health Device]}}"], + "activePluginConfigIds": ["{{configId}}"], + "dataStream": { + "id": "datastream-properties", + "name": "properties" + }, + "scope": { + "scope": "{{scopes.[Google Health Devices]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Google Health Device]}}" + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { "transpose": true } + } + } + } + }, + { + "i": "df1ac843-ef7f-4f6d-b6c8-1c13b8853d8c", + "x": 6, + "y": 0, + "w": 6, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Paired Devices", + "description": "", + "timeframe": "none", + "activePluginConfigIds": ["{{configId}}"], + "dataStream": { + "id": "{{dataStreams.pairedDevices}}", + "name": "pairedDevices", + "pluginConfigId": "{{configId}}" + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false, + "columnOrder": [ + "deviceName", + "deviceType", + "batteryLevel", + "batteryStatus", + "lastSyncTime", + "featureCount" + ], + "hiddenColumns": [] + } + } + } + } + } + ] + } +} diff --git a/plugins/GoogleHealth/v1/defaultContent/dietNutrition.dash.json b/plugins/GoogleHealth/v1/defaultContent/dietNutrition.dash.json new file mode 100644 index 00000000..1db34578 --- /dev/null +++ b/plugins/GoogleHealth/v1/defaultContent/dietNutrition.dash.json @@ -0,0 +1,584 @@ +{ + "name": "Diet & Nutrition", + "schemaVersion": "1.5", + "timeframe": "last24hours", + "dashboard": { + "_type": "layout/grid", + "columns": 12, + "version": 1, + "contents": [ + { + "i": "fe622143-d7e3-4ea3-abd2-1a234cc2c2f0", + "x": 0, + "y": 0, + "w": 3, + "h": 6, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Food Log", + "description": "", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.foodLog}}", + "name": "foodLog", + "pluginConfigId": "{{configId}}" + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false, + "columnOrder": [ + "food", + "calories", + "proteinG", + "carbsG", + "fatG" + ], + "hiddenColumns": [ + "time", + "meal", + "sugarG", + "fibreG" + ] + } + } + } + } + }, + { + "i": "a0ac707d-e7a7-42ea-880a-95f21be7f221", + "x": 3, + "y": 0, + "w": 3, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Calories", + "description": "", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.nutritionDaily}}", + "name": "nutritionDaily", + "pluginConfigId": "{{configId}}" + }, + "monitor": { + "tileRollsUp": true, + "monitorType": "threshold", + "condition": { + "columns": [ + "caloriesPct" + ], + "logic": { + "if": [ + { + "<=": [ + { + "var": "mean" + }, + 100 + ] + }, + "success", + { + "<=": [ + { + "var": "mean" + }, + 110 + ] + }, + "warning", + "error" + ] + } + }, + "_type": "simple", + "aggregation": "mean", + "groupBy": "__group_by_none__", + "frequency": 720 + }, + "visualisation": { + "type": "data-stream-gauge", + "config": { + "data-stream-gauge": { + "value": { + "type": "mean", + "columns": [ + "calories" + ] + }, + "label": "Calories", + "minimum": 0, + "maximumColumn": "calorieGoalCol" + } + } + } + } + }, + { + "i": "9c7d5a2a-47e6-4e2f-8939-fe26fef74cd5", + "x": 3, + "y": 4, + "w": 3, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Protein (g)", + "description": "", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.nutritionDaily}}", + "name": "nutritionDaily", + "pluginConfigId": "{{configId}}" + }, + "monitor": { + "tileRollsUp": true, + "monitorType": "threshold", + "condition": { + "columns": [ + "proteinPct" + ], + "logic": { + "if": [ + { + ">=": [ + { + "var": "mean" + }, + 100 + ] + }, + "success", + { + ">=": [ + { + "var": "mean" + }, + 75 + ] + }, + "warning", + "error" + ] + } + }, + "_type": "simple", + "aggregation": "mean", + "groupBy": "__group_by_none__", + "frequency": 720 + }, + "visualisation": { + "type": "data-stream-gauge", + "config": { + "data-stream-gauge": { + "value": { + "type": "mean", + "columns": [ + "proteinG" + ] + }, + "label": "Protein (g)", + "minimum": 0, + "maximumColumn": "proteinGoalCol" + } + } + } + } + }, + { + "i": "03c554fd-1373-43da-a823-25c577107bfa", + "x": 3, + "y": 2, + "w": 3, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Carbs (g)", + "description": "", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.nutritionDaily}}", + "name": "nutritionDaily", + "pluginConfigId": "{{configId}}" + }, + "monitor": { + "tileRollsUp": true, + "monitorType": "threshold", + "condition": { + "columns": [ + "carbsPct" + ], + "logic": { + "if": [ + { + "<=": [ + { + "var": "mean" + }, + 100 + ] + }, + "success", + { + "<=": [ + { + "var": "mean" + }, + 110 + ] + }, + "warning", + "error" + ] + } + }, + "_type": "simple", + "aggregation": "mean", + "groupBy": "__group_by_none__", + "frequency": 720 + }, + "visualisation": { + "type": "data-stream-gauge", + "config": { + "data-stream-gauge": { + "value": { + "type": "mean", + "columns": [ + "carbsG" + ] + }, + "label": "Carbs (g)", + "minimum": 0, + "maximumColumn": "carbGoalCol" + } + } + } + } + }, + { + "i": "dce973b2-15fe-46a3-aaf7-06e9adc586f7", + "x": 6, + "y": 2, + "w": 3, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Fat (g)", + "description": "", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.nutritionDaily}}", + "name": "nutritionDaily", + "pluginConfigId": "{{configId}}" + }, + "monitor": { + "tileRollsUp": true, + "monitorType": "threshold", + "condition": { + "columns": [ + "fatPct" + ], + "logic": { + "if": [ + { + "<=": [ + { + "var": "mean" + }, + 100 + ] + }, + "success", + { + "<=": [ + { + "var": "mean" + }, + 110 + ] + }, + "warning", + "error" + ] + } + }, + "_type": "simple", + "aggregation": "mean", + "groupBy": "__group_by_none__", + "frequency": 720 + }, + "visualisation": { + "type": "data-stream-gauge", + "config": { + "data-stream-gauge": { + "value": { + "type": "mean", + "columns": [ + "fatG" + ] + }, + "label": "Fat (g)", + "minimum": 0, + "maximumColumn": "fatGoalCol" + } + } + } + } + }, + { + "i": "b0d921d2-a561-49ef-8868-2611fe89ba4a", + "x": 6, + "y": 4, + "w": 3, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Hydration", + "description": "", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.dailyMetric}}", + "name": "dailyMetric", + "pluginConfigId": "{{configId}}", + "dataSourceConfig": { + "metric": "hydration-log" + } + }, + "monitor": { + "tileRollsUp": true, + "monitorType": "threshold", + "condition": { + "columns": [ + "pctOfGoal" + ], + "logic": { + "if": [ + { + ">=": [ + { + "var": "mean" + }, + 100 + ] + }, + "success", + { + ">=": [ + { + "var": "mean" + }, + 75 + ] + }, + "warning", + "error" + ] + } + }, + "_type": "simple", + "aggregation": "mean", + "groupBy": "__group_by_none__", + "frequency": 720 + }, + "visualisation": { + "type": "data-stream-gauge", + "config": { + "data-stream-gauge": { + "value": { + "type": "mean", + "columns": [ + "value" + ] + }, + "label": "Water (mL)", + "minimum": 0, + "maximumColumn": "goal" + } + } + } + } + }, + { + "i": "004b4c49-87cc-44da-adb1-ea9c05490e28", + "x": 9, + "y": 0, + "w": 3, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Macro Split (calories)", + "description": "", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.macroBreakdown}}", + "name": "macroBreakdown", + "pluginConfigId": "{{configId}}" + }, + "visualisation": { + "type": "data-stream-donut-chart", + "config": { + "data-stream-donut-chart": { + "valueColumn": "calories", + "labelColumn": "macro", + "hideCenterValue": false, + "showValuesAsPercentage": true, + "legendPosition": "right", + "legendMode": "table", + "palette": { + "Protein": "#3B82F6", + "Carbohydrate": "#10B981", + "Fat": "#F97316", + "Remaining": "#D1D5DB" + } + } + } + } + } + }, + { + "i": "e8e95b0f-1095-4aaa-85c2-24acae13f484", + "x": 6, + "y": 0, + "w": 3, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Macros (grams)", + "description": "", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.macroBreakdown}}", + "name": "macroBreakdown", + "pluginConfigId": "{{configId}}" + }, + "visualisation": { + "type": "data-stream-donut-chart", + "config": { + "data-stream-donut-chart": { + "valueColumn": "grams", + "labelColumn": "macro", + "hideCenterValue": false, + "showValuesAsPercentage": false, + "legendPosition": "right", + "legendMode": "table", + "palette": { + "Protein": "#3B82F6", + "Carbohydrate": "#10B981", + "Fat": "#F97316", + "Remaining": "#D1D5DB" + } + } + } + } + } + }, + { + "i": "9bca8fbe-c344-4bc4-8d7a-47d478d69b12", + "x": 9, + "y": 2, + "w": 3, + "h": 4, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Calories by Meal", + "description": "", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.foodLog}}", + "name": "foodLog", + "pluginConfigId": "{{configId}}", + "group": { + "by": [ + [ + "meal", + "uniqueValues" + ] + ], + "aggregate": [ + { + "type": "sum", + "names": [ + "calories" + ] + } + ] + } + }, + "visualisation": { + "type": "data-stream-bar-chart", + "config": { + "data-stream-bar-chart": { + "xAxisData": "meal_uniqueValues", + "yAxisData": [ + "calories_sum" + ], + "xAxisGroup": "none", + "xAxisLabel": "", + "yAxisLabel": "Calories", + "showXAxisLabel": true, + "showYAxisLabel": true, + "showLegend": false, + "legendPosition": "bottom", + "showGrid": true, + "horizontalLayout": "horizontal", + "displayMode": "actual", + "showTotals": false, + "showValue": false, + "grouping": false, + "range": { + "type": "auto" + } + } + } + } + } + } + ] + } +} \ No newline at end of file diff --git a/plugins/GoogleHealth/v1/defaultContent/manifest.json b/plugins/GoogleHealth/v1/defaultContent/manifest.json new file mode 100644 index 00000000..d31b3af4 --- /dev/null +++ b/plugins/GoogleHealth/v1/defaultContent/manifest.json @@ -0,0 +1,32 @@ +{ + "items": [ + { + "name": "today", + "type": "dashboard" + }, + { + "name": "activity", + "type": "dashboard" + }, + { + "name": "dietNutrition", + "type": "dashboard" + }, + { + "name": "trends", + "type": "dashboard" + }, + { + "name": "sleep", + "type": "dashboard" + }, + { + "name": "userPerspective", + "type": "dashboard" + }, + { + "name": "devicePerspective", + "type": "dashboard" + } + ] +} \ No newline at end of file diff --git a/plugins/GoogleHealth/v1/defaultContent/scopes.json b/plugins/GoogleHealth/v1/defaultContent/scopes.json new file mode 100644 index 00000000..405c1b8a --- /dev/null +++ b/plugins/GoogleHealth/v1/defaultContent/scopes.json @@ -0,0 +1,32 @@ +[ + { + "name": "Google Health Users", + "matches": { + "sourceType": { + "type": "oneOf", + "values": ["Google Health User"] + } + }, + "variable": { + "name": "Google Health User", + "allowMultipleSelection": false, + "default": "none", + "type": "object" + } + }, + { + "name": "Google Health Devices", + "matches": { + "sourceType": { + "type": "oneOf", + "values": ["Google Health Device"] + } + }, + "variable": { + "name": "Google Health Device", + "allowMultipleSelection": false, + "default": "none", + "type": "object" + } + } +] diff --git a/plugins/GoogleHealth/v1/defaultContent/sleep.dash.json b/plugins/GoogleHealth/v1/defaultContent/sleep.dash.json new file mode 100644 index 00000000..6e0686b1 --- /dev/null +++ b/plugins/GoogleHealth/v1/defaultContent/sleep.dash.json @@ -0,0 +1,293 @@ +{ + "name": "Sleep", + "schemaVersion": "1.5", + "timeframe": "last7days", + "dashboard": { + "_type": "layout/grid", + "columns": 12, + "version": 1, + "contents": [ + { + "i": "8ae7ed39-75f9-477a-a1e1-9170a935c685", + "x": 0, + "y": 0, + "w": 3, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Avg Sleep (h)", + "description": "", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.sleepSessions}}", + "name": "sleepSessions", + "pluginConfigId": "{{configId}}" + }, + "monitor": { + "tileRollsUp": true, + "monitorType": "threshold", + "condition": { + "columns": [ + "sleepPct" + ], + "logic": { + "if": [ + { + ">=": [ + { + "var": "mean" + }, + 100 + ] + }, + "success", + { + ">=": [ + { + "var": "mean" + }, + 75 + ] + }, + "warning", + "error" + ] + } + }, + "_type": "simple", + "aggregation": "mean", + "groupBy": "__group_by_none__", + "frequency": 720 + }, + "visualisation": { + "type": "data-stream-gauge", + "config": { + "data-stream-gauge": { + "value": { + "type": "mean", + "columns": [ + "asleepHours" + ] + }, + "label": "Avg Sleep (h)", + "minimum": 0, + "maximumColumn": "sleepGoalCol" + } + } + } + } + }, + { + "i": "a8670004-fe48-40f4-8e48-98af68239e1d", + "x": 3, + "y": 0, + "w": 9, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Sleep Duration", + "description": "", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.sleepSessions}}", + "name": "sleepSessions", + "pluginConfigId": "{{configId}}" + }, + "visualisation": { + "type": "data-stream-bar-chart", + "config": { + "data-stream-bar-chart": { + "xAxisData": "date", + "yAxisData": [ + "sleepPct" + ], + "xAxisGroup": "none", + "xAxisLabel": "", + "yAxisLabel": "% of goal", + "showXAxisLabel": false, + "showYAxisLabel": true, + "showLegend": false, + "legendPosition": "bottom", + "showGrid": true, + "horizontalLayout": "vertical", + "displayMode": "actual", + "showTotals": false, + "showValue": true, + "grouping": false, + "range": { + "type": "auto" + }, + "color": { + "type": "custom", + "customColors": [ + { + "color": "#259A51", + "expression": "value >= 100" + }, + { + "color": "#E18700", + "expression": "value >= 75 && value < 100" + }, + { + "color": "#D0021B", + "expression": "value < 75" + } + ] + } + } + } + } + } + }, + { + "i": "9e52a934-b507-4bc6-a0dd-1ddd6665913d", + "x": 0, + "y": 2, + "w": 6, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Sleep Efficiency", + "description": "", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.sleepSessions}}", + "name": "sleepSessions", + "pluginConfigId": "{{configId}}" + }, + "visualisation": { + "type": "data-stream-line-graph", + "config": { + "data-stream-line-graph": { + "xAxisColumn": "date", + "yAxisColumn": [ + "efficiencyPct" + ], + "seriesColumn": "none", + "showLegend": false, + "legendPosition": "bottom", + "yAxisLabel": "Efficiency", + "showYAxisLabel": true, + "showTrendLine": false + } + } + } + } + }, + { + "i": "9bc17a6c-43cb-4bac-8060-c4421bf22d92", + "x": 6, + "y": 2, + "w": 6, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Sleep Stages Breakdown", + "description": "", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.sleepSessions}}", + "name": "sleepSessions", + "pluginConfigId": "{{configId}}" + }, + "visualisation": { + "type": "data-stream-bar-chart", + "config": { + "data-stream-bar-chart": { + "xAxisData": "date", + "yAxisData": [ + "deepMin", + "remMin", + "lightMin", + "awakeMin" + ], + "xAxisGroup": "none", + "xAxisLabel": "", + "yAxisLabel": "Minutes", + "showXAxisLabel": false, + "showYAxisLabel": true, + "showLegend": true, + "legendPosition": "bottom", + "showGrid": true, + "horizontalLayout": "vertical", + "displayMode": "actual", + "showTotals": false, + "showValue": false, + "grouping": false, + "range": { + "type": "auto" + } + } + } + } + } + }, + { + "i": "4e863400-df49-44aa-a139-8869769e23c3", + "x": 0, + "y": 4, + "w": 12, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Sleep Sessions", + "description": "", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.sleepSessions}}", + "name": "sleepSessions", + "pluginConfigId": "{{configId}}" + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false, + "columnOrder": [ + "date", + "asleepHours", + "efficiencyPct", + "deepMin", + "remMin", + "lightMin", + "awakeMin", + "inBedMin", + "startTime", + "endTime" + ], + "hiddenColumns": [ + "asleepMin" + ] + } + } + } + } + } + ] + } +} \ No newline at end of file diff --git a/plugins/GoogleHealth/v1/defaultContent/today.dash.json b/plugins/GoogleHealth/v1/defaultContent/today.dash.json new file mode 100644 index 00000000..49cfda54 --- /dev/null +++ b/plugins/GoogleHealth/v1/defaultContent/today.dash.json @@ -0,0 +1,527 @@ +{ + "name": "Today", + "schemaVersion": "1.5", + "timeframe": "last24hours", + "dashboard": { + "_type": "layout/grid", + "columns": 12, + "version": 1, + "contents": [ + { + "i": "298e41df-695b-4edd-9c10-ab3cd66e3141", + "x": 0, + "y": 0, + "w": 3, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Steps", + "description": "", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.dailyMetric}}", + "name": "dailyMetric", + "pluginConfigId": "{{configId}}", + "dataSourceConfig": { + "metric": "steps" + } + }, + "monitor": { + "tileRollsUp": true, + "monitorType": "threshold", + "condition": { + "columns": [ + "pctOfGoal" + ], + "logic": { + "if": [ + { + ">=": [ + { + "var": "mean" + }, + 100 + ] + }, + "success", + { + ">=": [ + { + "var": "mean" + }, + 75 + ] + }, + "warning", + "error" + ] + } + }, + "_type": "simple", + "aggregation": "mean", + "groupBy": "__group_by_none__", + "frequency": 720 + }, + "visualisation": { + "type": "data-stream-gauge", + "config": { + "data-stream-gauge": { + "value": { + "type": "sum", + "columns": [ + "value" + ] + }, + "label": "Steps", + "minimum": 0, + "maximumColumn": "goal" + } + } + }, + "timeframe": "last24hours" + } + }, + { + "i": "a9269ec9-da14-4f14-bc7c-239a43d1566d", + "x": 3, + "y": 0, + "w": 3, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Active Calories", + "description": "", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.dailyMetric}}", + "name": "dailyMetric", + "pluginConfigId": "{{configId}}", + "dataSourceConfig": { + "metric": "active-energy-burned" + } + }, + "monitor": { + "tileRollsUp": true, + "monitorType": "threshold", + "condition": { + "columns": [ + "pctOfGoal" + ], + "logic": { + "if": [ + { + ">=": [ + { + "var": "mean" + }, + 100 + ] + }, + "success", + { + ">=": [ + { + "var": "mean" + }, + 75 + ] + }, + "warning", + "error" + ] + } + }, + "_type": "simple", + "aggregation": "mean", + "groupBy": "__group_by_none__", + "frequency": 720 + }, + "visualisation": { + "type": "data-stream-gauge", + "config": { + "data-stream-gauge": { + "value": { + "type": "sum", + "columns": [ + "value" + ] + }, + "label": "Active Calories", + "minimum": 0, + "maximumColumn": "goal" + } + } + }, + "timeframe": "last24hours" + } + }, + { + "i": "b7aac9ab-4428-4b26-ad2d-981b8c27b183", + "x": 6, + "y": 0, + "w": 3, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Distance (km)", + "description": "", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.dailyMetric}}", + "name": "dailyMetric", + "pluginConfigId": "{{configId}}", + "dataSourceConfig": { + "metric": "distance" + } + }, + "monitor": { + "tileRollsUp": true, + "monitorType": "threshold", + "condition": { + "columns": [ + "pctOfGoal" + ], + "logic": { + "if": [ + { + ">=": [ + { + "var": "mean" + }, + 100 + ] + }, + "success", + { + ">=": [ + { + "var": "mean" + }, + 75 + ] + }, + "warning", + "error" + ] + } + }, + "_type": "simple", + "aggregation": "mean", + "groupBy": "__group_by_none__", + "frequency": 720 + }, + "visualisation": { + "type": "data-stream-gauge", + "config": { + "data-stream-gauge": { + "value": { + "type": "sum", + "columns": [ + "value" + ] + }, + "label": "Distance (km)", + "minimum": 0, + "maximumColumn": "goal" + } + } + }, + "timeframe": "last24hours" + } + }, + { + "i": "45e785dd-f09f-4b8b-a442-8f59753c0e79", + "x": 9, + "y": 0, + "w": 3, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Active Zone Minutes", + "description": "", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.dailyMetric}}", + "name": "dailyMetric", + "pluginConfigId": "{{configId}}", + "dataSourceConfig": { + "metric": "active-zone-minutes" + } + }, + "monitor": { + "tileRollsUp": true, + "monitorType": "threshold", + "condition": { + "columns": [ + "pctOfGoal" + ], + "logic": { + "if": [ + { + ">=": [ + { + "var": "mean" + }, + 100 + ] + }, + "success", + { + ">=": [ + { + "var": "mean" + }, + 75 + ] + }, + "warning", + "error" + ] + } + }, + "_type": "simple", + "aggregation": "mean", + "groupBy": "__group_by_none__", + "frequency": 720 + }, + "visualisation": { + "type": "data-stream-gauge", + "config": { + "data-stream-gauge": { + "value": { + "type": "sum", + "columns": [ + "value" + ] + }, + "label": "Active Zone Minutes", + "minimum": 0, + "maximumColumn": "goal" + } + } + }, + "timeframe": "last24hours" + } + }, + { + "i": "a4f6c70e-e2c3-4ffd-82f3-25923e5973d2", + "x": 0, + "y": 2, + "w": 6, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Steps by Hour (last 24h)", + "description": "", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.intradayMetric}}", + "name": "intradayMetric", + "pluginConfigId": "{{configId}}", + "dataSourceConfig": { + "metric": "steps", + "window": "3600s" + } + }, + "visualisation": { + "type": "data-stream-bar-chart", + "config": { + "data-stream-bar-chart": { + "xAxisData": "time", + "yAxisData": [ + "value" + ], + "xAxisGroup": "none", + "xAxisLabel": "", + "yAxisLabel": "Steps", + "showXAxisLabel": false, + "showYAxisLabel": true, + "showLegend": false, + "legendPosition": "bottom", + "showGrid": true, + "horizontalLayout": "vertical", + "displayMode": "actual", + "showTotals": false, + "showValue": false, + "grouping": false, + "range": { + "type": "auto" + } + } + } + }, + "timeframe": "last24hours" + } + }, + { + "i": "da12acb2-cfb0-4572-b450-7f4e35844f75", + "x": 6, + "y": 2, + "w": 6, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Active Calories by Hour (last 24h)", + "description": "", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.intradayMetric}}", + "name": "intradayMetric", + "pluginConfigId": "{{configId}}", + "dataSourceConfig": { + "metric": "active-energy-burned", + "window": "3600s" + } + }, + "visualisation": { + "type": "data-stream-bar-chart", + "config": { + "data-stream-bar-chart": { + "xAxisData": "time", + "yAxisData": [ + "value" + ], + "xAxisGroup": "none", + "xAxisLabel": "", + "yAxisLabel": "Calories", + "showXAxisLabel": false, + "showYAxisLabel": true, + "showLegend": false, + "legendPosition": "bottom", + "showGrid": true, + "horizontalLayout": "vertical", + "displayMode": "actual", + "showTotals": false, + "showValue": false, + "grouping": false, + "range": { + "type": "auto" + } + } + } + }, + "timeframe": "last24hours" + } + }, + { + "i": "50226ef7-24bb-4612-9b8f-6fb5bfa88598", + "x": 0, + "y": 4, + "w": 9, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Heart Rate", + "description": "", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.heartRateIntraday}}", + "name": "heartRateIntraday", + "pluginConfigId": "{{configId}}" + }, + "visualisation": { + "type": "data-stream-line-graph", + "config": { + "data-stream-line-graph": { + "xAxisColumn": "time", + "yAxisColumn": [ + "avgBpm", + "minBpm", + "maxBpm" + ], + "seriesColumn": "none", + "showLegend": true, + "legendPosition": "bottom", + "yAxisLabel": "BPM", + "showYAxisLabel": true, + "showTrendLine": false + } + } + }, + "timeframe": "last24hours" + } + }, + { + "i": "7f3f7a2e-9d41-4c6b-8b2a-c55e0d9a1c33", + "x": 9, + "y": 4, + "w": 3, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Current Weight", + "description": "", + "timeframe": "thisYear", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.bodyMetric}}", + "name": "bodyMetric", + "pluginConfigId": "{{configId}}", + "dataSourceConfig": { + "metric": "weight" + }, + "sort": { + "by": [ + [ + "date", + "desc" + ] + ], + "top": 1 + } + }, + "visualisation": { + "type": "data-stream-scalar", + "config": { + "data-stream-scalar": { + "value": "value", + "comparisonColumn": "none", + "label": "kg (latest weigh-in)" + } + } + } + } + } + ] + } +} \ No newline at end of file diff --git a/plugins/GoogleHealth/v1/defaultContent/trends.dash.json b/plugins/GoogleHealth/v1/defaultContent/trends.dash.json new file mode 100644 index 00000000..f636f1a7 --- /dev/null +++ b/plugins/GoogleHealth/v1/defaultContent/trends.dash.json @@ -0,0 +1,266 @@ +{ + "name": "Trends Over Time", + "schemaVersion": "1.5", + "timeframe": "thisYear", + "dashboard": { + "_type": "layout/grid", + "columns": 12, + "version": 1, + "contents": [ + { + "i": "43cd4a46-d69e-418a-8287-48d7441eeb56", + "x": 0, + "y": 0, + "w": 6, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Weight (kg)", + "description": "", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.bodyMetric}}", + "name": "bodyMetric", + "pluginConfigId": "{{configId}}", + "dataSourceConfig": { + "metric": "weight" + } + }, + "visualisation": { + "type": "data-stream-line-graph", + "config": { + "data-stream-line-graph": { + "xAxisColumn": "date", + "yAxisColumn": [ + "value" + ], + "seriesColumn": "none", + "showLegend": false, + "legendPosition": "bottom", + "yAxisLabel": "kg", + "showYAxisLabel": true, + "showTrendLine": true + } + } + } + } + }, + { + "i": "3a891ba2-d902-49e0-a2ac-65f9cd19b572", + "x": 6, + "y": 0, + "w": 6, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Body Fat", + "description": "", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.bodyMetric}}", + "name": "bodyMetric", + "pluginConfigId": "{{configId}}", + "dataSourceConfig": { + "metric": "body-fat" + } + }, + "visualisation": { + "type": "data-stream-line-graph", + "config": { + "data-stream-line-graph": { + "xAxisColumn": "date", + "yAxisColumn": [ + "value" + ], + "seriesColumn": "none", + "showLegend": false, + "legendPosition": "bottom", + "yAxisLabel": "", + "showYAxisLabel": false, + "showTrendLine": true + } + } + } + } + }, + { + "i": "f87c6f9e-4683-435a-84df-7fad8d9f137b", + "x": 0, + "y": 2, + "w": 6, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Resting Heart Rate", + "description": "", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.dailyHealthMetric}}", + "name": "dailyHealthMetric", + "pluginConfigId": "{{configId}}", + "dataSourceConfig": { + "metric": "daily-resting-heart-rate" + } + }, + "visualisation": { + "type": "data-stream-line-graph", + "config": { + "data-stream-line-graph": { + "xAxisColumn": "date", + "yAxisColumn": [ + "value" + ], + "seriesColumn": "none", + "showLegend": false, + "legendPosition": "bottom", + "yAxisLabel": "bpm", + "showYAxisLabel": true, + "showTrendLine": true + } + } + } + } + }, + { + "i": "bc527aad-33a7-4857-9c11-a88734e730d4", + "x": 6, + "y": 2, + "w": 6, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Sleep Duration", + "description": "", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.sleepSessions}}", + "name": "sleepSessions", + "pluginConfigId": "{{configId}}" + }, + "visualisation": { + "type": "data-stream-line-graph", + "config": { + "data-stream-line-graph": { + "xAxisColumn": "date", + "yAxisColumn": [ + "asleepHours" + ], + "seriesColumn": "none", + "showLegend": false, + "legendPosition": "bottom", + "yAxisLabel": "Hours", + "showYAxisLabel": true, + "showTrendLine": true + } + } + } + } + }, + { + "i": "ca148149-76a8-4fa3-b673-d692567d867b", + "x": 0, + "y": 4, + "w": 6, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Calories Consumed (30 days)", + "description": "", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.nutritionDaily}}", + "name": "nutritionDaily", + "pluginConfigId": "{{configId}}" + }, + "visualisation": { + "type": "data-stream-line-graph", + "config": { + "data-stream-line-graph": { + "xAxisColumn": "date", + "yAxisColumn": [ + "calories" + ], + "seriesColumn": "none", + "showLegend": false, + "legendPosition": "bottom", + "yAxisLabel": "Calories", + "showYAxisLabel": true, + "showTrendLine": false + } + } + }, + "timeframe": "last30days" + } + }, + { + "i": "02053e84-e218-4a80-981f-07057184ab58", + "x": 6, + "y": 4, + "w": 6, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Active Calories Burned (30 days)", + "description": "", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.dailyMetric}}", + "name": "dailyMetric", + "pluginConfigId": "{{configId}}", + "dataSourceConfig": { + "metric": "active-energy-burned" + } + }, + "visualisation": { + "type": "data-stream-line-graph", + "config": { + "data-stream-line-graph": { + "xAxisColumn": "date", + "yAxisColumn": [ + "value" + ], + "seriesColumn": "none", + "showLegend": false, + "legendPosition": "bottom", + "yAxisLabel": "Calories", + "showYAxisLabel": true, + "showTrendLine": false + } + } + }, + "timeframe": "last30days" + } + } + ] + } +} \ No newline at end of file diff --git a/plugins/GoogleHealth/v1/defaultContent/userPerspective.dash.json b/plugins/GoogleHealth/v1/defaultContent/userPerspective.dash.json new file mode 100644 index 00000000..265c9fe2 --- /dev/null +++ b/plugins/GoogleHealth/v1/defaultContent/userPerspective.dash.json @@ -0,0 +1,174 @@ +{ + "name": "User", + "schemaVersion": "1.5", + "timeframe": "last30days", + "variables": [ + "{{variables.[Google Health User]}}" + ], + "dashboard": { + "_type": "layout/grid", + "columns": 12, + "version": 1, + "contents": [ + { + "i": "687e130a-0133-4e3a-b4c0-bd58b066ba48", + "x": 0, + "y": 0, + "w": 4, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Details", + "description": "", + "timeframe": "none", + "variables": [ + "{{variables.[Google Health User]}}" + ], + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "datastream-properties", + "name": "properties" + }, + "scope": { + "scope": "{{scopes.[Google Health Users]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Google Health User]}}" + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": true + } + } + } + } + }, + { + "i": "db9756b9-7391-44a4-ae4a-fead00725a44", + "x": 4, + "y": 0, + "w": 4, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "User Profile", + "description": "", + "timeframe": "none", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.userProfile}}", + "name": "userProfile", + "pluginConfigId": "{{configId}}" + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": true + } + } + } + } + }, + { + "i": "6aa4a260-85fc-4a77-8328-2cc3d49fcd67", + "x": 8, + "y": 0, + "w": 4, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Daily Steps", + "description": "", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.dailyMetric}}", + "name": "dailyMetric", + "pluginConfigId": "{{configId}}", + "dataSourceConfig": { + "metric": "steps" + } + }, + "visualisation": { + "type": "data-stream-line-graph", + "config": { + "data-stream-line-graph": { + "xAxisColumn": "date", + "yAxisColumn": [ + "value" + ], + "seriesColumn": "none", + "showLegend": false, + "legendPosition": "bottom", + "yAxisLabel": "Steps", + "showYAxisLabel": true, + "showTrendLine": false + } + } + } + } + }, + { + "i": "947dc9c0-dec7-4c59-b4cd-9c8c6134f219", + "x": 0, + "y": 3, + "w": 12, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Recent Workouts", + "description": "", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.workouts}}", + "name": "workouts", + "pluginConfigId": "{{configId}}" + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false, + "columnOrder": [ + "type", + "startTime", + "durationMin", + "distanceKm", + "calories", + "avgHeartRate", + "steps", + "activeZoneMinutes", + "source" + ], + "hiddenColumns": [ + "endTime" + ] + } + } + } + } + } + ] + } +} \ No newline at end of file diff --git a/plugins/GoogleHealth/v1/docs/README.md b/plugins/GoogleHealth/v1/docs/README.md new file mode 100644 index 00000000..02686ee8 --- /dev/null +++ b/plugins/GoogleHealth/v1/docs/README.md @@ -0,0 +1,79 @@ +# Google Health + +Bring your personal fitness, activity, sleep, body, heart-rate and nutrition data from the **Google Health API** into SquaredUp. The Google Health API is the next-generation replacement for the legacy Fitbit Web API and serves data from Fitbit devices, Google Pixel watches and other connected sources, all through Google's OAuth 2.0 platform. + +This plugin turns that data into dashboards you can explore — daily activity and calorie totals, weight and resting-heart-rate trends over time, sleep sessions, workouts and daily nutrition — and lets you drill down into the underlying figures. + +## What this plugin monitors + +The plugin imports two object types into SquaredUp: + +| Object type | What it represents | +| ----------- | ------------------ | +| **Google Health User** | The authenticated account holder — profile, goals, timezone and identity. Everything you see is that one person's data. | +| **Google Health Device** | Each paired Fitbit tracker or Pixel watch, with its battery level, last sync time and firmware version. | + +From those objects the plugin exposes data streams covering: + +- **Daily metrics** (steps, distance, floors, active zone minutes, calories, resting heart rate, weight, body fat, VO₂ max, SpO₂, respiratory rate, HRV, hydration) — one configurable stream that powers every "over time" trend chart. +- **Nutrition** — daily calories in, carbohydrates, fat, protein and water. +- **Heart-rate zones** — minutes and calories spent in each zone per day. +- **Sleep sessions** — start, end, duration, efficiency and time in each sleep stage. +- **Workouts** — exercise sessions with type, duration, distance, calories and heart rate. +- **Intraday heart rate** — heart rate through the day at a chosen resolution. +- **Profile & devices** — current profile details and paired-device status. + +The plugin ships ready-made **Activity**, **Diet & Nutrition**, **Trends Over Time** and **Sleep** dashboards, plus per-user and per-device perspectives. + +## Prerequisites — getting your credentials + +The Google Health API uses Google OAuth 2.0. You need a Google Cloud project with the API enabled and an OAuth client, and your Google account must have health data (from a Fitbit or Pixel device). + +1. **Create / choose a Google Cloud project** at . +2. **Enable the Google Health API** — in the Cloud console go to *APIs & Services → Library*, search for **Google Health API**, and click **Enable**. +3. **Configure the OAuth consent screen** — *APIs & Services → OAuth consent screen*: + - User type **External**. + - Add the scopes this plugin uses (all read-only): + - `https://www.googleapis.com/auth/googlehealth.profile.readonly` + - `https://www.googleapis.com/auth/googlehealth.settings.readonly` + - `https://www.googleapis.com/auth/googlehealth.activity_and_fitness.readonly` + - `https://www.googleapis.com/auth/googlehealth.health_metrics_and_measurements.readonly` + - `https://www.googleapis.com/auth/googlehealth.sleep.readonly` + - `https://www.googleapis.com/auth/googlehealth.nutrition.readonly` + - While the consent screen is in **Testing** mode, add your own Google account under **Test users** — this lets you authenticate without full Google verification. (Publishing to production for other users requires Google's sensitive-scope verification.) +4. **Create an OAuth client ID** — *APIs & Services → Credentials → Create credentials → OAuth client ID*: + - Application type **Web application**. + - Add SquaredUp's OAuth redirect URL as an **Authorized redirect URI**. SquaredUp shows this URL on the plugin's sign-in step when you add the plugin — copy it from there into the Cloud console. + - Copy the generated **Client ID** and **Client secret**. + +## Configuration fields + +When you add the plugin in SquaredUp you'll be asked for: + +| Field | What it is | Where to find it | Required | +| ----- | ---------- | ---------------- | -------- | +| **Google OAuth client ID** | The OAuth 2.0 client ID for your Google Cloud project | Cloud console → *Credentials* → your OAuth client | Yes | +| **Google OAuth client secret** | The matching client secret | Cloud console → *Credentials* → your OAuth client | Yes | +| **Sign in with Google Health** | Launches Google's sign-in so you can grant access | — click it and complete Google sign-in | Yes | + +After entering the client ID and secret, click **Sign in with Google Health**, choose your Google account and approve the requested health permissions. SquaredUp stores a refresh token and renews access automatically. + +## What gets indexed + +- **Google Health User** — one object representing you, with your display name, timezone and average daily step goal. +- **Google Health Device** — one object per paired tracker/watch, with battery, sync and version details. + +## Goals and monitoring + +The plugin's gauges measure progress against the targets you set in the **Daily goals** section of the plugin configuration (steps, calories, protein, water and so on) — edit the configuration and save to change them. + +Every gauge also ships with a pre-configured health monitor (green when you hit your goal, amber when slightly off, red when well off). **Monitors are dormant on the built-in dashboards** — this is standard SquaredUp behaviour for out-of-the-box content, so a plugin never generates monitoring load or notifications you didn't ask for. To activate them, open a dashboard and use **Copy dashboard**: your copy's gauges will show live health states and can raise notifications. + +## Known limitations + +- **One account per configuration.** The Google Health API is strictly single-user (`users/me`), so each plugin configuration surfaces exactly one person's data. Add a separate configuration per account. +- **Read-only.** The plugin only reads data; it never writes, edits or deletes health data. +- **Aggregation range caps.** The API caps daily roll-up requests at **90 days** for most metrics and **14 days** for heart-rate–derived metrics (heart rate, active minutes, heart-rate zones). Daily streams therefore offer timeframes up to those limits — very long ranges (e.g. a full year) aren't available in a single query. Longer-term trends are best viewed month-by-month. +- **Rate limits.** Google enforces roughly **300 requests per user per minute**; exceeding it returns HTTP 429. The plugin's dashboards stay well within this, but very frequent manual refreshes across many tiles can hit it briefly. +- **Data depends on the device.** Metrics only appear if your device records them — e.g. SpO₂, VO₂ max, HRV and sleep stages require a compatible Fitbit/Pixel device and recent sync. +- **Sensitive scopes.** Sharing this plugin with other users in production requires completing Google's OAuth verification for the health scopes. In Testing mode it works immediately for accounts added as test users. diff --git a/plugins/GoogleHealth/v1/icon.svg b/plugins/GoogleHealth/v1/icon.svg new file mode 100644 index 00000000..d59d5209 --- /dev/null +++ b/plugins/GoogleHealth/v1/icon.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/plugins/GoogleHealth/v1/indexDefinitions/default.json b/plugins/GoogleHealth/v1/indexDefinitions/default.json new file mode 100644 index 00000000..87a8e9a3 --- /dev/null +++ b/plugins/GoogleHealth/v1/indexDefinitions/default.json @@ -0,0 +1,33 @@ +{ + "steps": [ + { + "name": "user", + "dataStream": { "name": "userProfile" }, + "timeframe": "none", + "objectMapping": { + "id": "userId", + "name": "displayName", + "type": { "value": "Google Health User" }, + "properties": [ + "age", + "walkingStrideCm", + "runningStrideCm" + ] + } + }, + { + "name": "pairedDevices", + "dataStream": { "name": "pairedDevices" }, + "timeframe": "none", + "objectMapping": { + "id": "deviceId", + "name": "deviceName", + "type": { "value": "Google Health Device" }, + "properties": [ + "deviceType", + "batteryStatus" + ] + } + } + ] +} diff --git a/plugins/GoogleHealth/v1/metadata.json b/plugins/GoogleHealth/v1/metadata.json new file mode 100644 index 00000000..d6a71439 --- /dev/null +++ b/plugins/GoogleHealth/v1/metadata.json @@ -0,0 +1,63 @@ +{ + "name": "google-health", + "displayName": "Google Health", + "version": "1.0.0", + "author": { + "name": "@Daniel-Hodgson-SquaredUp", + "type": "community" + }, + "description": "Track personal fitness, activity, sleep, body metrics, heart rate and nutrition from the Google Health API (the successor to the Fitbit Web API).", + "category": "Monitoring", + "type": "cloud", + "schemaVersion": "2.1", + "importNotSupported": false, + "restrictedToPlatforms": [], + "keywords": [ + "google health", + "fitbit", + "pixel", + "fitness", + "activity", + "sleep", + "heart rate", + "nutrition", + "steps", + "wearable" + ], + "objectTypes": ["Google Health User", "Google Health Device"], + "links": [ + { + "category": "documentation", + "url": "https://github.com/squaredup/plugins/blob/main/plugins/GoogleHealth/v1/docs/README.md", + "label": "Help adding this plugin" + }, + { + "category": "source", + "url": "https://github.com/squaredup/plugins/tree/main/plugins/GoogleHealth/v1", + "label": "Repository" + } + ], + "base": { + "plugin": "WebAPI", + "majorVersion": "1", + "config": { + "baseUrl": "https://health.googleapis.com/v4", + "authMode": "oauth2", + "oauth2GrantType": "authCode", + "oauth2AuthUrl": "https://accounts.google.com/o/oauth2/v2/auth", + "oauth2TokenUrl": "https://oauth2.googleapis.com/token", + "oauth2ClientId": "{{oauth2ClientId}}", + "oauth2ClientSecret": "{{oauth2ClientSecret}}", + "oauth2ClientSecretLocationDuringAuth": "body", + "oauth2Scope": "https://www.googleapis.com/auth/googlehealth.profile.readonly https://www.googleapis.com/auth/googlehealth.settings.readonly https://www.googleapis.com/auth/googlehealth.activity_and_fitness.readonly https://www.googleapis.com/auth/googlehealth.health_metrics_and_measurements.readonly https://www.googleapis.com/auth/googlehealth.sleep.readonly https://www.googleapis.com/auth/googlehealth.nutrition.readonly", + "oauth2AuthExtraArgs": [ + { "key": "access_type", "value": "offline" }, + { "key": "prompt", "value": "consent" } + ], + "oauth2TokenExtraArgs": [], + "oauth2TokenExtraHeaders": [], + "queryArgs": [], + "headers": [] + } + } +} diff --git a/plugins/GoogleHealth/v1/ui.json b/plugins/GoogleHealth/v1/ui.json new file mode 100644 index 00000000..348e3013 --- /dev/null +++ b/plugins/GoogleHealth/v1/ui.json @@ -0,0 +1,49 @@ +[ + { + "type": "markdown", + "name": "info", + "content": "Connect your Google Health account (Fitbit / Pixel data). You'll need a Google Cloud project with the **Google Health API** enabled and an OAuth client. See the plugin help for step-by-step setup." + }, + { + "name": "oauth2ClientId", + "type": "text", + "label": "Google OAuth client ID", + "help": "The OAuth 2.0 client ID from your Google Cloud project (APIs & Services → Credentials).", + "placeholder": "1234567890-example.apps.googleusercontent.com", + "validation": { + "required": true + } + }, + { + "name": "oauth2ClientSecret", + "type": "password", + "label": "Google OAuth client secret", + "help": "The OAuth 2.0 client secret that pairs with the client ID above.", + "validation": { + "required": true + } + }, + { + "type": "oAuth2", + "name": "oauth2AuthCodeSignIn", + "label": "Sign in with Google Health", + "validation": { + "required": true + } + }, + { + "type": "markdown", + "name": "goalsHeader", + "content": "---\n### Daily goals\nSet your personal targets. Gauges fill towards these and their health status (green / amber / red) is measured against them. Leave blank to use the defaults shown." + }, + { "type": "number", "name": "stepGoal", "label": "Steps goal", "defaultValue": 10000, "help": "Target steps per day." }, + { "type": "number", "name": "distanceGoal", "label": "Distance goal (km)", "defaultValue": 8, "help": "Target distance per day, in kilometres." }, + { "type": "number", "name": "activeCalorieGoal", "label": "Active calories goal", "defaultValue": 600, "help": "Target active calories burned per day." }, + { "type": "number", "name": "zoneMinutesGoal", "label": "Active zone minutes goal", "defaultValue": 22, "help": "Target active zone minutes per day." }, + { "type": "number", "name": "sleepGoal", "label": "Sleep goal (hours)", "defaultValue": 8, "help": "Target hours asleep per night." }, + { "type": "number", "name": "waterGoal", "label": "Water goal (mL)", "defaultValue": 2000, "help": "Target water intake per day, in millilitres." }, + { "type": "number", "name": "calorieBudget", "label": "Calorie budget (kcal)", "defaultValue": 2500, "help": "Daily calorie intake budget. Gauge is green while at or under this." }, + { "type": "number", "name": "proteinGoal", "label": "Protein goal (g)", "defaultValue": 130, "help": "Target protein per day, in grams." }, + { "type": "number", "name": "carbBudget", "label": "Carbohydrate budget (g)", "defaultValue": 250, "help": "Daily carbohydrate budget, in grams." }, + { "type": "number", "name": "fatBudget", "label": "Fat budget (g)", "defaultValue": 70, "help": "Daily fat budget, in grams." } +] From c4580746fc2a033bd556b5bd41e02286f4d3a656 Mon Sep 17 00:00:00 2001 From: daniel hodgson <45772010+DanielCHodgson@users.noreply.github.com> Date: Sat, 18 Jul 2026 13:19:17 +0100 Subject: [PATCH 2/6] Support custom timeframes and polish dashboard visuals in Google Health - Allow all timeframes (including custom day ranges like Yesterday) on every range-driven data stream; day-length timeframes resolve to the calendar day they end in, handling exclusive-midnight ends correctly - Clamp intraday streams to midnight-to-midnight of the target day so the Today dashboard covers the calendar day, with hour-aligned windows - Plot actual values instead of % of goal on the Sleep Duration, Daily Steps and Active Zone Minutes bar charts, keeping goal-completion colours via hidden computed goal-band columns, with bar labels off - Move the Sleep Stages Breakdown legend to the right - Show time-only x-axis labels on the by-hour Today tiles via a computed HH:mm label column (charts ignore date column formats) Co-Authored-By: Claude Fable 5 --- .../v1/dataStreams/bodyMetric.json | 12 ++----- .../v1/dataStreams/dailyHealthMetric.json | 12 ++----- .../v1/dataStreams/dailyMetric.json | 28 ++++++++------- .../GoogleHealth/v1/dataStreams/foodLog.json | 12 ++----- .../v1/dataStreams/heartRateIntraday.json | 8 ++--- .../v1/dataStreams/heartRateZonesDaily.json | 18 ++++------ .../v1/dataStreams/intradayMetric.json | 20 ++++++----- .../v1/dataStreams/macroBreakdown.json | 20 ++++------- .../v1/dataStreams/nutritionDaily.json | 20 ++++------- .../v1/dataStreams/sleepSessions.json | 20 +++++------ .../GoogleHealth/v1/dataStreams/workouts.json | 12 ++----- .../v1/defaultContent/activity.dash.json | 36 +++++++++++-------- .../v1/defaultContent/sleep.dash.json | 20 ++++++----- .../v1/defaultContent/today.dash.json | 8 ++--- 14 files changed, 108 insertions(+), 138 deletions(-) diff --git a/plugins/GoogleHealth/v1/dataStreams/bodyMetric.json b/plugins/GoogleHealth/v1/dataStreams/bodyMetric.json index eb8a3e00..c61b32c2 100644 --- a/plugins/GoogleHealth/v1/dataStreams/bodyMetric.json +++ b/plugins/GoogleHealth/v1/dataStreams/bodyMetric.json @@ -13,7 +13,7 @@ "getArgs": [ { "key": "filter", - "value": "{{metric.replace(/-/g,'_')}}.sample_time.civil_time >= \"{{new Date(new Date(timeframe.end).getTime()-new Date(timeframe.start).getTime()<=90000000 ? timeframe.end : timeframe.start).toISOString().slice(0,10)}}\" AND {{metric.replace(/-/g,'_')}}.sample_time.civil_time < \"{{new Date(new Date(timeframe.end).getTime()+86400000).toISOString().slice(0,10)}}\"" + "value": "{{metric.replace(/-/g,'_')}}.sample_time.civil_time >= \"{{new Date(new Date(timeframe.end).getTime()-new Date(timeframe.start).getTime()<=90000000 ? new Date(timeframe.end).getTime()-1 : timeframe.start).toISOString().slice(0,10)}}\" AND {{metric.replace(/-/g,'_')}}.sample_time.civil_time < \"{{new Date(new Date(timeframe.end).getTime()+86399999).toISOString().slice(0,10)}}\"" } ], "paging": { @@ -96,15 +96,7 @@ "role": "unitLabel" } ], - "timeframes": [ - "last12hours", - "last24hours", - "last7days", - "last30days", - "thisMonth", - "thisQuarter", - "thisYear" - ], + "timeframes": true, "defaultShaping": { "sort": { "by": [ diff --git a/plugins/GoogleHealth/v1/dataStreams/dailyHealthMetric.json b/plugins/GoogleHealth/v1/dataStreams/dailyHealthMetric.json index ab6291fc..914dac10 100644 --- a/plugins/GoogleHealth/v1/dataStreams/dailyHealthMetric.json +++ b/plugins/GoogleHealth/v1/dataStreams/dailyHealthMetric.json @@ -13,7 +13,7 @@ "getArgs": [ { "key": "filter", - "value": "{{metric.replace(/-/g,'_')}}.date >= \"{{new Date(new Date(timeframe.end).getTime()-new Date(timeframe.start).getTime()<=90000000 ? timeframe.end : timeframe.start).toISOString().slice(0,10)}}\" AND {{metric.replace(/-/g,'_')}}.date < \"{{new Date(new Date(timeframe.end).getTime()+86400000).toISOString().slice(0,10)}}\"" + "value": "{{metric.replace(/-/g,'_')}}.date >= \"{{new Date(new Date(timeframe.end).getTime()-new Date(timeframe.start).getTime()<=90000000 ? new Date(timeframe.end).getTime()-1 : timeframe.start).toISOString().slice(0,10)}}\" AND {{metric.replace(/-/g,'_')}}.date < \"{{new Date(new Date(timeframe.end).getTime()+86399999).toISOString().slice(0,10)}}\"" } ], "paging": { @@ -104,15 +104,7 @@ "role": "unitLabel" } ], - "timeframes": [ - "last12hours", - "last24hours", - "last7days", - "last30days", - "thisMonth", - "thisQuarter", - "thisYear" - ], + "timeframes": true, "defaultShaping": { "sort": { "by": [ diff --git a/plugins/GoogleHealth/v1/dataStreams/dailyMetric.json b/plugins/GoogleHealth/v1/dataStreams/dailyMetric.json index 0d3a94c9..36afab36 100644 --- a/plugins/GoogleHealth/v1/dataStreams/dailyMetric.json +++ b/plugins/GoogleHealth/v1/dataStreams/dailyMetric.json @@ -14,16 +14,16 @@ "range": { "start": { "date": { - "year": "{{new Date(new Date(timeframe.end).getTime()-new Date(timeframe.start).getTime()<=90000000 ? timeframe.end : timeframe.start).getUTCFullYear()}}", - "month": "{{new Date(new Date(timeframe.end).getTime()-new Date(timeframe.start).getTime()<=90000000 ? timeframe.end : timeframe.start).getUTCMonth()+1}}", - "day": "{{new Date(new Date(timeframe.end).getTime()-new Date(timeframe.start).getTime()<=90000000 ? timeframe.end : timeframe.start).getUTCDate()}}" + "year": "{{new Date(new Date(timeframe.end).getTime()-new Date(timeframe.start).getTime()<=90000000 ? new Date(timeframe.end).getTime()-1 : timeframe.start).getUTCFullYear()}}", + "month": "{{new Date(new Date(timeframe.end).getTime()-new Date(timeframe.start).getTime()<=90000000 ? new Date(timeframe.end).getTime()-1 : timeframe.start).getUTCMonth()+1}}", + "day": "{{new Date(new Date(timeframe.end).getTime()-new Date(timeframe.start).getTime()<=90000000 ? new Date(timeframe.end).getTime()-1 : timeframe.start).getUTCDate()}}" } }, "end": { "date": { - "year": "{{new Date(new Date(timeframe.end).getTime()+86400000).getUTCFullYear()}}", - "month": "{{new Date(new Date(timeframe.end).getTime()+86400000).getUTCMonth()+1}}", - "day": "{{new Date(new Date(timeframe.end).getTime()+86400000).getUTCDate()}}" + "year": "{{new Date(new Date(timeframe.end).getTime()+86399999).getUTCFullYear()}}", + "month": "{{new Date(new Date(timeframe.end).getTime()+86399999).getUTCMonth()+1}}", + "day": "{{new Date(new Date(timeframe.end).getTime()+86399999).getUTCDate()}}" } } }, @@ -131,15 +131,17 @@ "valueExpression": "{{ $[\"pctOfGoal\"] === undefined || $[\"pctOfGoal\"] === null ? \"unknown\" : $[\"pctOfGoal\"] >= 100 ? \"success\" : $[\"pctOfGoal\"] >= 75 ? \"warning\" : \"error\" }}", "shape": "state", "visible": false + }, + { + "name": "goalBand", + "displayName": "Goal Progress", + "computed": true, + "valueExpression": "{{ $[\"pctOfGoal\"] === undefined || $[\"pctOfGoal\"] === null ? \"No goal\" : $[\"pctOfGoal\"] >= 100 ? \"Goal met\" : $[\"pctOfGoal\"] >= 75 ? \"Near goal\" : \"Below goal\" }}", + "shape": "string", + "visible": false } ], - "timeframes": [ - "last12hours", - "last24hours", - "last7days", - "last30days", - "thisMonth" - ], + "timeframes": true, "defaultShaping": { "sort": { "by": [ diff --git a/plugins/GoogleHealth/v1/dataStreams/foodLog.json b/plugins/GoogleHealth/v1/dataStreams/foodLog.json index 37c2745d..ecc243d2 100644 --- a/plugins/GoogleHealth/v1/dataStreams/foodLog.json +++ b/plugins/GoogleHealth/v1/dataStreams/foodLog.json @@ -13,7 +13,7 @@ "getArgs": [ { "key": "filter", - "value": "nutrition_log.interval.civil_start_time >= \"{{new Date(new Date(timeframe.end).getTime()-new Date(timeframe.start).getTime()<=90000000 ? timeframe.end : timeframe.start).toISOString().slice(0,10)}}\" AND nutrition_log.interval.civil_start_time < \"{{new Date(new Date(timeframe.end).getTime()+86400000).toISOString().slice(0,10)}}\"" + "value": "nutrition_log.interval.civil_start_time >= \"{{new Date(new Date(timeframe.end).getTime()-new Date(timeframe.start).getTime()<=90000000 ? new Date(timeframe.end).getTime()-1 : timeframe.start).toISOString().slice(0,10)}}\" AND nutrition_log.interval.civil_start_time < \"{{new Date(new Date(timeframe.end).getTime()+86399999).toISOString().slice(0,10)}}\"" } ], "paging": { @@ -128,15 +128,7 @@ "visible": false } ], - "timeframes": [ - "last12hours", - "last24hours", - "last7days", - "last30days", - "thisMonth", - "thisQuarter", - "thisYear" - ], + "timeframes": true, "defaultShaping": { "sort": { "by": [ diff --git a/plugins/GoogleHealth/v1/dataStreams/heartRateIntraday.json b/plugins/GoogleHealth/v1/dataStreams/heartRateIntraday.json index 274d3890..57a71a1d 100644 --- a/plugins/GoogleHealth/v1/dataStreams/heartRateIntraday.json +++ b/plugins/GoogleHealth/v1/dataStreams/heartRateIntraday.json @@ -9,8 +9,8 @@ "endpointPath": "users/me/dataTypes/heart-rate/dataPoints:rollUp", "postBody": { "range": { - "startTime": "{{new Date(timeframe.start).toISOString()}}", - "endTime": "{{new Date(timeframe.end).toISOString()}}" + "startTime": "{{new Date(new Date(timeframe.end).getTime()-new Date(timeframe.start).getTime()<=90000000 ? new Date(new Date(timeframe.end).getTime()-1).toISOString().slice(0,10) : timeframe.start).toISOString()}}", + "endTime": "{{new Date(new Date(timeframe.end).getTime()-new Date(timeframe.start).getTime()<=90000000 ? new Date(new Date(new Date(timeframe.end).getTime()-1).toISOString().slice(0,10)).getTime()+86400000 : timeframe.end).toISOString()}}" }, "windowSize": "900s" }, @@ -23,11 +23,11 @@ }, "matches": "none", "metadata": [ - { "name": "time", "displayName": "Time", "shape": ["date", { "format": "dd MMM HH:mm" }], "role": "timestamp" }, + { "name": "time", "displayName": "Time", "shape": ["date", { "format": "HH:mm" }], "role": "timestamp" }, { "name": "avgBpm", "displayName": "Avg (bpm)", "shape": ["number", { "decimalPlaces": 0 }], "role": "value" }, { "name": "minBpm", "displayName": "Min (bpm)", "shape": ["number", { "decimalPlaces": 0 }] }, { "name": "maxBpm", "displayName": "Max (bpm)", "shape": ["number", { "decimalPlaces": 0 }] } ], - "timeframes": ["last12hours", "last24hours", "last7days"], + "timeframes": true, "defaultShaping": { "sort": { "by": [["time", "asc"]] } } } diff --git a/plugins/GoogleHealth/v1/dataStreams/heartRateZonesDaily.json b/plugins/GoogleHealth/v1/dataStreams/heartRateZonesDaily.json index 84662cc0..0e6373c9 100644 --- a/plugins/GoogleHealth/v1/dataStreams/heartRateZonesDaily.json +++ b/plugins/GoogleHealth/v1/dataStreams/heartRateZonesDaily.json @@ -14,16 +14,16 @@ "range": { "start": { "date": { - "year": "{{new Date(new Date(timeframe.end).getTime()-new Date(timeframe.start).getTime()<=90000000 ? timeframe.end : timeframe.start).getUTCFullYear()}}", - "month": "{{new Date(new Date(timeframe.end).getTime()-new Date(timeframe.start).getTime()<=90000000 ? timeframe.end : timeframe.start).getUTCMonth()+1}}", - "day": "{{new Date(new Date(timeframe.end).getTime()-new Date(timeframe.start).getTime()<=90000000 ? timeframe.end : timeframe.start).getUTCDate()}}" + "year": "{{new Date(new Date(timeframe.end).getTime()-new Date(timeframe.start).getTime()<=90000000 ? new Date(timeframe.end).getTime()-1 : timeframe.start).getUTCFullYear()}}", + "month": "{{new Date(new Date(timeframe.end).getTime()-new Date(timeframe.start).getTime()<=90000000 ? new Date(timeframe.end).getTime()-1 : timeframe.start).getUTCMonth()+1}}", + "day": "{{new Date(new Date(timeframe.end).getTime()-new Date(timeframe.start).getTime()<=90000000 ? new Date(timeframe.end).getTime()-1 : timeframe.start).getUTCDate()}}" } }, "end": { "date": { - "year": "{{new Date(new Date(timeframe.end).getTime()+86400000).getUTCFullYear()}}", - "month": "{{new Date(new Date(timeframe.end).getTime()+86400000).getUTCMonth()+1}}", - "day": "{{new Date(new Date(timeframe.end).getTime()+86400000).getUTCDate()}}" + "year": "{{new Date(new Date(timeframe.end).getTime()+86399999).getUTCFullYear()}}", + "month": "{{new Date(new Date(timeframe.end).getTime()+86399999).getUTCMonth()+1}}", + "day": "{{new Date(new Date(timeframe.end).getTime()+86399999).getUTCDate()}}" } } }, @@ -99,11 +99,7 @@ "role": "value" } ], - "timeframes": [ - "last12hours", - "last24hours", - "last7days" - ], + "timeframes": true, "defaultShaping": { "sort": { "by": [ diff --git a/plugins/GoogleHealth/v1/dataStreams/intradayMetric.json b/plugins/GoogleHealth/v1/dataStreams/intradayMetric.json index 5faa5743..839892a0 100644 --- a/plugins/GoogleHealth/v1/dataStreams/intradayMetric.json +++ b/plugins/GoogleHealth/v1/dataStreams/intradayMetric.json @@ -12,8 +12,8 @@ "endpointPath": "users/me/dataTypes/{{metric}}/dataPoints:rollUp", "postBody": { "range": { - "startTime": "{{new Date(timeframe.start).toISOString()}}", - "endTime": "{{new Date(timeframe.end).toISOString()}}" + "startTime": "{{new Date(new Date(timeframe.end).getTime()-new Date(timeframe.start).getTime()<=90000000 ? new Date(new Date(timeframe.end).getTime()-1).toISOString().slice(0,10) : timeframe.start).toISOString()}}", + "endTime": "{{new Date(new Date(timeframe.end).getTime()-new Date(timeframe.start).getTime()<=90000000 ? new Date(new Date(new Date(timeframe.end).getTime()-1).toISOString().slice(0,10)).getTime()+86400000 : timeframe.end).toISOString()}}" }, "windowSize": "{{window || '3600s'}}" }, @@ -99,7 +99,7 @@ "shape": [ "date", { - "format": "dd MMM HH:mm" + "format": "HH:mm" } ], "role": "timestamp" @@ -145,13 +145,17 @@ "valueExpression": "{{ $[\"pctOfGoal\"] === undefined || $[\"pctOfGoal\"] === null ? \"unknown\" : $[\"pctOfGoal\"] >= 100 ? \"success\" : $[\"pctOfGoal\"] >= 75 ? \"warning\" : \"error\" }}", "shape": "state", "visible": false + }, + { + "name": "timeLabel", + "displayName": "Time", + "computed": true, + "valueExpression": "{{ new Date($[\"time\"]).toISOString().slice(11,16) }}", + "shape": "string", + "visible": false } ], - "timeframes": [ - "last12hours", - "last24hours", - "last7days" - ], + "timeframes": true, "defaultShaping": { "sort": { "by": [ diff --git a/plugins/GoogleHealth/v1/dataStreams/macroBreakdown.json b/plugins/GoogleHealth/v1/dataStreams/macroBreakdown.json index 65a91be8..4b4d7378 100644 --- a/plugins/GoogleHealth/v1/dataStreams/macroBreakdown.json +++ b/plugins/GoogleHealth/v1/dataStreams/macroBreakdown.json @@ -14,16 +14,16 @@ "range": { "start": { "date": { - "year": "{{new Date(new Date(timeframe.end).getTime()-new Date(timeframe.start).getTime()<=90000000 ? timeframe.end : timeframe.start).getUTCFullYear()}}", - "month": "{{new Date(new Date(timeframe.end).getTime()-new Date(timeframe.start).getTime()<=90000000 ? timeframe.end : timeframe.start).getUTCMonth()+1}}", - "day": "{{new Date(new Date(timeframe.end).getTime()-new Date(timeframe.start).getTime()<=90000000 ? timeframe.end : timeframe.start).getUTCDate()}}" + "year": "{{new Date(new Date(timeframe.end).getTime()-new Date(timeframe.start).getTime()<=90000000 ? new Date(timeframe.end).getTime()-1 : timeframe.start).getUTCFullYear()}}", + "month": "{{new Date(new Date(timeframe.end).getTime()-new Date(timeframe.start).getTime()<=90000000 ? new Date(timeframe.end).getTime()-1 : timeframe.start).getUTCMonth()+1}}", + "day": "{{new Date(new Date(timeframe.end).getTime()-new Date(timeframe.start).getTime()<=90000000 ? new Date(timeframe.end).getTime()-1 : timeframe.start).getUTCDate()}}" } }, "end": { "date": { - "year": "{{new Date(new Date(timeframe.end).getTime()+86400000).getUTCFullYear()}}", - "month": "{{new Date(new Date(timeframe.end).getTime()+86400000).getUTCMonth()+1}}", - "day": "{{new Date(new Date(timeframe.end).getTime()+86400000).getUTCDate()}}" + "year": "{{new Date(new Date(timeframe.end).getTime()+86399999).getUTCFullYear()}}", + "month": "{{new Date(new Date(timeframe.end).getTime()+86399999).getUTCMonth()+1}}", + "day": "{{new Date(new Date(timeframe.end).getTime()+86399999).getUTCDate()}}" } } }, @@ -74,13 +74,7 @@ ] } ], - "timeframes": [ - "last12hours", - "last24hours", - "last7days", - "last30days", - "thisMonth" - ], + "timeframes": true, "defaultShaping": { "sort": { "by": [ diff --git a/plugins/GoogleHealth/v1/dataStreams/nutritionDaily.json b/plugins/GoogleHealth/v1/dataStreams/nutritionDaily.json index 0be680e2..5bc384af 100644 --- a/plugins/GoogleHealth/v1/dataStreams/nutritionDaily.json +++ b/plugins/GoogleHealth/v1/dataStreams/nutritionDaily.json @@ -14,16 +14,16 @@ "range": { "start": { "date": { - "year": "{{new Date(new Date(timeframe.end).getTime()-new Date(timeframe.start).getTime()<=90000000 ? timeframe.end : timeframe.start).getUTCFullYear()}}", - "month": "{{new Date(new Date(timeframe.end).getTime()-new Date(timeframe.start).getTime()<=90000000 ? timeframe.end : timeframe.start).getUTCMonth()+1}}", - "day": "{{new Date(new Date(timeframe.end).getTime()-new Date(timeframe.start).getTime()<=90000000 ? timeframe.end : timeframe.start).getUTCDate()}}" + "year": "{{new Date(new Date(timeframe.end).getTime()-new Date(timeframe.start).getTime()<=90000000 ? new Date(timeframe.end).getTime()-1 : timeframe.start).getUTCFullYear()}}", + "month": "{{new Date(new Date(timeframe.end).getTime()-new Date(timeframe.start).getTime()<=90000000 ? new Date(timeframe.end).getTime()-1 : timeframe.start).getUTCMonth()+1}}", + "day": "{{new Date(new Date(timeframe.end).getTime()-new Date(timeframe.start).getTime()<=90000000 ? new Date(timeframe.end).getTime()-1 : timeframe.start).getUTCDate()}}" } }, "end": { "date": { - "year": "{{new Date(new Date(timeframe.end).getTime()+86400000).getUTCFullYear()}}", - "month": "{{new Date(new Date(timeframe.end).getTime()+86400000).getUTCMonth()+1}}", - "day": "{{new Date(new Date(timeframe.end).getTime()+86400000).getUTCDate()}}" + "year": "{{new Date(new Date(timeframe.end).getTime()+86399999).getUTCFullYear()}}", + "month": "{{new Date(new Date(timeframe.end).getTime()+86399999).getUTCMonth()+1}}", + "day": "{{new Date(new Date(timeframe.end).getTime()+86399999).getUTCDate()}}" } } }, @@ -219,13 +219,7 @@ "visible": false } ], - "timeframes": [ - "last12hours", - "last24hours", - "last7days", - "last30days", - "thisMonth" - ], + "timeframes": true, "defaultShaping": { "sort": { "by": [ diff --git a/plugins/GoogleHealth/v1/dataStreams/sleepSessions.json b/plugins/GoogleHealth/v1/dataStreams/sleepSessions.json index da6923ae..908baa9a 100644 --- a/plugins/GoogleHealth/v1/dataStreams/sleepSessions.json +++ b/plugins/GoogleHealth/v1/dataStreams/sleepSessions.json @@ -13,7 +13,7 @@ "getArgs": [ { "key": "filter", - "value": "sleep.interval.civil_end_time >= \"{{new Date(new Date(timeframe.end).getTime()-new Date(timeframe.start).getTime()<=90000000 ? timeframe.end : timeframe.start).toISOString().slice(0,10)}}\" AND sleep.interval.civil_end_time < \"{{new Date(new Date(timeframe.end).getTime()+86400000).toISOString().slice(0,10)}}\"" + "value": "sleep.interval.civil_end_time >= \"{{new Date(new Date(timeframe.end).getTime()-new Date(timeframe.start).getTime()<=90000000 ? new Date(timeframe.end).getTime()-1 : timeframe.start).toISOString().slice(0,10)}}\" AND sleep.interval.civil_end_time < \"{{new Date(new Date(timeframe.end).getTime()+86399999).toISOString().slice(0,10)}}\"" } ], "paging": { @@ -173,17 +173,17 @@ "valueExpression": "{{ $[\"sleepPct\"] === undefined || $[\"sleepPct\"] === null ? \"unknown\" : $[\"sleepPct\"] >= 100 ? \"success\" : $[\"sleepPct\"] >= 75 ? \"warning\" : \"error\" }}", "shape": "state", "visible": false + }, + { + "name": "sleepGoalBand", + "displayName": "Goal Progress", + "computed": true, + "valueExpression": "{{ $[\"sleepPct\"] === undefined || $[\"sleepPct\"] === null ? \"No goal\" : $[\"sleepPct\"] >= 100 ? \"Goal met\" : $[\"sleepPct\"] >= 75 ? \"Near goal\" : \"Below goal\" }}", + "shape": "string", + "visible": false } ], - "timeframes": [ - "last12hours", - "last24hours", - "last7days", - "last30days", - "thisMonth", - "thisQuarter", - "thisYear" - ], + "timeframes": true, "defaultShaping": { "sort": { "by": [ diff --git a/plugins/GoogleHealth/v1/dataStreams/workouts.json b/plugins/GoogleHealth/v1/dataStreams/workouts.json index c9229290..c82bbe59 100644 --- a/plugins/GoogleHealth/v1/dataStreams/workouts.json +++ b/plugins/GoogleHealth/v1/dataStreams/workouts.json @@ -13,7 +13,7 @@ "getArgs": [ { "key": "filter", - "value": "exercise.interval.civil_start_time >= \"{{new Date(new Date(timeframe.end).getTime()-new Date(timeframe.start).getTime()<=90000000 ? timeframe.end : timeframe.start).toISOString().slice(0,10)}}\" AND exercise.interval.civil_start_time < \"{{new Date(new Date(timeframe.end).getTime()+86400000).toISOString().slice(0,10)}}\"" + "value": "exercise.interval.civil_start_time >= \"{{new Date(new Date(timeframe.end).getTime()-new Date(timeframe.start).getTime()<=90000000 ? new Date(timeframe.end).getTime()-1 : timeframe.start).toISOString().slice(0,10)}}\" AND exercise.interval.civil_start_time < \"{{new Date(new Date(timeframe.end).getTime()+86399999).toISOString().slice(0,10)}}\"" } ], "paging": { @@ -131,15 +131,7 @@ "shape": "string" } ], - "timeframes": [ - "last12hours", - "last24hours", - "last7days", - "last30days", - "thisMonth", - "thisQuarter", - "thisYear" - ], + "timeframes": true, "defaultShaping": { "sort": { "by": [ diff --git a/plugins/GoogleHealth/v1/defaultContent/activity.dash.json b/plugins/GoogleHealth/v1/defaultContent/activity.dash.json index a20f2364..f775a369 100644 --- a/plugins/GoogleHealth/v1/defaultContent/activity.dash.json +++ b/plugins/GoogleHealth/v1/defaultContent/activity.dash.json @@ -349,11 +349,11 @@ "data-stream-bar-chart": { "xAxisData": "date", "yAxisData": [ - "pctOfGoal" + "value" ], - "xAxisGroup": "none", + "xAxisGroup": "goalBand", "xAxisLabel": "", - "yAxisLabel": "% of goal", + "yAxisLabel": "Steps", "showXAxisLabel": false, "showYAxisLabel": true, "showLegend": false, @@ -362,7 +362,7 @@ "horizontalLayout": "vertical", "displayMode": "actual", "showTotals": false, - "showValue": true, + "showValue": false, "grouping": false, "range": { "type": "auto" @@ -372,15 +372,19 @@ "customColors": [ { "color": "#259A51", - "expression": "value >= 100" + "expression": "series == \"Goal met\"" }, { "color": "#E18700", - "expression": "value >= 75 && value < 100" + "expression": "series == \"Near goal\"" }, { "color": "#D0021B", - "expression": "value < 75" + "expression": "series == \"Below goal\"" + }, + { + "color": "#88898C", + "expression": "series == \"No goal\"" } ] } @@ -419,11 +423,11 @@ "data-stream-bar-chart": { "xAxisData": "date", "yAxisData": [ - "pctOfGoal" + "value" ], - "xAxisGroup": "none", + "xAxisGroup": "goalBand", "xAxisLabel": "", - "yAxisLabel": "% of goal", + "yAxisLabel": "Minutes", "showXAxisLabel": false, "showYAxisLabel": true, "showLegend": false, @@ -432,7 +436,7 @@ "horizontalLayout": "vertical", "displayMode": "actual", "showTotals": false, - "showValue": true, + "showValue": false, "grouping": false, "range": { "type": "auto" @@ -442,15 +446,19 @@ "customColors": [ { "color": "#259A51", - "expression": "value >= 100" + "expression": "series == \"Goal met\"" }, { "color": "#E18700", - "expression": "value >= 75 && value < 100" + "expression": "series == \"Near goal\"" }, { "color": "#D0021B", - "expression": "value < 75" + "expression": "series == \"Below goal\"" + }, + { + "color": "#88898C", + "expression": "series == \"No goal\"" } ] } diff --git a/plugins/GoogleHealth/v1/defaultContent/sleep.dash.json b/plugins/GoogleHealth/v1/defaultContent/sleep.dash.json index 6e0686b1..83daa112 100644 --- a/plugins/GoogleHealth/v1/defaultContent/sleep.dash.json +++ b/plugins/GoogleHealth/v1/defaultContent/sleep.dash.json @@ -109,11 +109,11 @@ "data-stream-bar-chart": { "xAxisData": "date", "yAxisData": [ - "sleepPct" + "asleepHours" ], - "xAxisGroup": "none", + "xAxisGroup": "sleepGoalBand", "xAxisLabel": "", - "yAxisLabel": "% of goal", + "yAxisLabel": "Hours", "showXAxisLabel": false, "showYAxisLabel": true, "showLegend": false, @@ -122,7 +122,7 @@ "horizontalLayout": "vertical", "displayMode": "actual", "showTotals": false, - "showValue": true, + "showValue": false, "grouping": false, "range": { "type": "auto" @@ -132,15 +132,19 @@ "customColors": [ { "color": "#259A51", - "expression": "value >= 100" + "expression": "series == \"Goal met\"" }, { "color": "#E18700", - "expression": "value >= 75 && value < 100" + "expression": "series == \"Near goal\"" }, { "color": "#D0021B", - "expression": "value < 75" + "expression": "series == \"Below goal\"" + }, + { + "color": "#88898C", + "expression": "series == \"No goal\"" } ] } @@ -227,7 +231,7 @@ "showXAxisLabel": false, "showYAxisLabel": true, "showLegend": true, - "legendPosition": "bottom", + "legendPosition": "right", "showGrid": true, "horizontalLayout": "vertical", "displayMode": "actual", diff --git a/plugins/GoogleHealth/v1/defaultContent/today.dash.json b/plugins/GoogleHealth/v1/defaultContent/today.dash.json index 49cfda54..bbb48b84 100644 --- a/plugins/GoogleHealth/v1/defaultContent/today.dash.json +++ b/plugins/GoogleHealth/v1/defaultContent/today.dash.json @@ -334,7 +334,7 @@ "z": 0, "config": { "_type": "tile/data-stream", - "title": "Steps by Hour (last 24h)", + "title": "Steps by Hour", "description": "", "activePluginConfigIds": [ "{{configId}}" @@ -352,7 +352,7 @@ "type": "data-stream-bar-chart", "config": { "data-stream-bar-chart": { - "xAxisData": "time", + "xAxisData": "timeLabel", "yAxisData": [ "value" ], @@ -389,7 +389,7 @@ "z": 0, "config": { "_type": "tile/data-stream", - "title": "Active Calories by Hour (last 24h)", + "title": "Active Calories by Hour", "description": "", "activePluginConfigIds": [ "{{configId}}" @@ -407,7 +407,7 @@ "type": "data-stream-bar-chart", "config": { "data-stream-bar-chart": { - "xAxisData": "time", + "xAxisData": "timeLabel", "yAxisData": [ "value" ], From 91460b119cb68a5bca76568b229a4f491297dee7 Mon Sep 17 00:00:00 2001 From: daniel hodgson <45772010+DanielCHodgson@users.noreply.github.com> Date: Sat, 18 Jul 2026 13:40:26 +0100 Subject: [PATCH 3/6] Order meals, keep Activity picker live, expose goal columns in drilldowns - Order Calories by Meal top-to-bottom as Anytime, Breakfast, Lunch, Dinner by re-ranking the foodLog mealOrder column and sorting the tile by it - Keep Activity tiles tied to the dashboard timeframe (defaulting to last7days) rather than pinning per tile - Unhide Goal, % of Goal and Goal Status columns in dailyMetric and sleepSessions so tile drilldowns show goal context, and drop the stale formatExpressions that rendered the % columns as raw values Co-Authored-By: Claude Fable 5 --- .../v1/dataStreams/dailyMetric.json | 10 +++---- .../GoogleHealth/v1/dataStreams/foodLog.json | 2 +- .../v1/dataStreams/sleepSessions.json | 10 +++---- .../v1/defaultContent/activity.dash.json | 27 +++++++------------ .../v1/defaultContent/dietNutrition.dash.json | 14 ++++++++++ 5 files changed, 30 insertions(+), 33 deletions(-) diff --git a/plugins/GoogleHealth/v1/dataStreams/dailyMetric.json b/plugins/GoogleHealth/v1/dataStreams/dailyMetric.json index 36afab36..c490d708 100644 --- a/plugins/GoogleHealth/v1/dataStreams/dailyMetric.json +++ b/plugins/GoogleHealth/v1/dataStreams/dailyMetric.json @@ -109,8 +109,7 @@ { "name": "goal", "displayName": "Goal", - "shape": "number", - "visible": false + "shape": "number" }, { "name": "pctOfGoal", @@ -120,17 +119,14 @@ { "decimalPlaces": 0 } - ], - "visible": false, - "formatExpression": "{{ $['value'] }} {{ $['unit'] }}" + ] }, { "name": "status", "displayName": "Goal Status", "computed": true, "valueExpression": "{{ $[\"pctOfGoal\"] === undefined || $[\"pctOfGoal\"] === null ? \"unknown\" : $[\"pctOfGoal\"] >= 100 ? \"success\" : $[\"pctOfGoal\"] >= 75 ? \"warning\" : \"error\" }}", - "shape": "state", - "visible": false + "shape": "state" }, { "name": "goalBand", diff --git a/plugins/GoogleHealth/v1/dataStreams/foodLog.json b/plugins/GoogleHealth/v1/dataStreams/foodLog.json index ecc243d2..ac0a2300 100644 --- a/plugins/GoogleHealth/v1/dataStreams/foodLog.json +++ b/plugins/GoogleHealth/v1/dataStreams/foodLog.json @@ -123,7 +123,7 @@ "name": "mealOrder", "displayName": "Meal Order", "computed": true, - "valueExpression": "{{ ({ 'Before Breakfast': 1, 'Breakfast': 2, 'Before Lunch': 3, 'Lunch': 4, 'Before Dinner': 5, 'Dinner': 6, 'After Dinner': 7, 'Snack': 8, 'Anytime': 9 })[$['meal']] || 10 }}", + "valueExpression": "{{ ({ 'Anytime': 1, 'Before Breakfast': 2, 'Breakfast': 3, 'Before Lunch': 4, 'Lunch': 5, 'Before Dinner': 6, 'Dinner': 7, 'After Dinner': 8, 'Snack': 9 })[$['meal']] || 10 }}", "shape": "number", "visible": false } diff --git a/plugins/GoogleHealth/v1/dataStreams/sleepSessions.json b/plugins/GoogleHealth/v1/dataStreams/sleepSessions.json index 908baa9a..c0209425 100644 --- a/plugins/GoogleHealth/v1/dataStreams/sleepSessions.json +++ b/plugins/GoogleHealth/v1/dataStreams/sleepSessions.json @@ -151,8 +151,7 @@ { "name": "sleepGoalCol", "displayName": "Sleep Goal (h)", - "shape": "number", - "visible": false + "shape": "number" }, { "name": "sleepPct", @@ -162,17 +161,14 @@ { "decimalPlaces": 0 } - ], - "visible": false, - "formatExpression": "{{ $['asleepHours'] }}h" + ] }, { "name": "sleepStatus", "displayName": "sleep Status", "computed": true, "valueExpression": "{{ $[\"sleepPct\"] === undefined || $[\"sleepPct\"] === null ? \"unknown\" : $[\"sleepPct\"] >= 100 ? \"success\" : $[\"sleepPct\"] >= 75 ? \"warning\" : \"error\" }}", - "shape": "state", - "visible": false + "shape": "state" }, { "name": "sleepGoalBand", diff --git a/plugins/GoogleHealth/v1/defaultContent/activity.dash.json b/plugins/GoogleHealth/v1/defaultContent/activity.dash.json index f775a369..7e45f73a 100644 --- a/plugins/GoogleHealth/v1/defaultContent/activity.dash.json +++ b/plugins/GoogleHealth/v1/defaultContent/activity.dash.json @@ -18,8 +18,7 @@ "z": 0, "config": { "_type": "tile/data-stream", - "title": "Avg Daily Steps", - "description": "", + "title": "Avg Daily Steps", "description": "", "activePluginConfigIds": [ "{{configId}}" ], @@ -96,8 +95,7 @@ "z": 0, "config": { "_type": "tile/data-stream", - "title": "Avg Distance (km)", - "description": "", + "title": "Avg Distance (km)", "description": "", "activePluginConfigIds": [ "{{configId}}" ], @@ -174,8 +172,7 @@ "z": 0, "config": { "_type": "tile/data-stream", - "title": "Avg Active Calories", - "description": "", + "title": "Avg Active Calories", "description": "", "activePluginConfigIds": [ "{{configId}}" ], @@ -252,8 +249,7 @@ "z": 0, "config": { "_type": "tile/data-stream", - "title": "Avg Zone Minutes", - "description": "", + "title": "Avg Zone Minutes", "description": "", "activePluginConfigIds": [ "{{configId}}" ], @@ -330,8 +326,7 @@ "z": 0, "config": { "_type": "tile/data-stream", - "title": "Daily Steps", - "description": "", + "title": "Daily Steps", "description": "", "activePluginConfigIds": [ "{{configId}}" ], @@ -404,8 +399,7 @@ "z": 0, "config": { "_type": "tile/data-stream", - "title": "Active Zone Minutes", - "description": "", + "title": "Active Zone Minutes", "description": "", "activePluginConfigIds": [ "{{configId}}" ], @@ -478,8 +472,7 @@ "z": 0, "config": { "_type": "tile/data-stream", - "title": "Distance Over Time", - "description": "", + "title": "Distance Over Time", "description": "", "activePluginConfigIds": [ "{{configId}}" ], @@ -521,8 +514,7 @@ "z": 0, "config": { "_type": "tile/data-stream", - "title": "Active Calories Over Time", - "description": "", + "title": "Active Calories Over Time", "description": "", "activePluginConfigIds": [ "{{configId}}" ], @@ -564,8 +556,7 @@ "z": 0, "config": { "_type": "tile/data-stream", - "title": "Recent Workouts", - "description": "", + "title": "Recent Workouts", "description": "", "activePluginConfigIds": [ "{{configId}}" ], diff --git a/plugins/GoogleHealth/v1/defaultContent/dietNutrition.dash.json b/plugins/GoogleHealth/v1/defaultContent/dietNutrition.dash.json index 1db34578..cea99634 100644 --- a/plugins/GoogleHealth/v1/defaultContent/dietNutrition.dash.json +++ b/plugins/GoogleHealth/v1/defaultContent/dietNutrition.dash.json @@ -546,8 +546,22 @@ "names": [ "calories" ] + }, + { + "type": "min", + "names": [ + "mealOrder" + ] } ] + }, + "sort": { + "by": [ + [ + "mealOrder_min", + "asc" + ] + ] } }, "visualisation": { From 747ed173aeec16c58577a69c7f9e2747f01e486e Mon Sep 17 00:00:00 2001 From: daniel hodgson <45772010+DanielCHodgson@users.noreply.github.com> Date: Sat, 18 Jul 2026 13:49:20 +0100 Subject: [PATCH 4/6] Restructure Google Health README to match Google plugin docs format Follows the Before you start / OAuth credentials / Configuring the plugin / Troubleshooting structure used by the Google Search Console and Google Sheets plugins, and documents that daily goals cannot be imported from the API and must be entered manually in the config. Co-Authored-By: Claude Fable 5 --- plugins/GoogleHealth/v1/docs/README.md | 171 ++++++++++++++++--------- 1 file changed, 114 insertions(+), 57 deletions(-) diff --git a/plugins/GoogleHealth/v1/docs/README.md b/plugins/GoogleHealth/v1/docs/README.md index 02686ee8..232c820c 100644 --- a/plugins/GoogleHealth/v1/docs/README.md +++ b/plugins/GoogleHealth/v1/docs/README.md @@ -1,79 +1,136 @@ -# Google Health +# Before you start -Bring your personal fitness, activity, sleep, body, heart-rate and nutrition data from the **Google Health API** into SquaredUp. The Google Health API is the next-generation replacement for the legacy Fitbit Web API and serves data from Fitbit devices, Google Pixel watches and other connected sources, all through Google's OAuth 2.0 platform. +## Prerequisites -This plugin turns that data into dashboards you can explore — daily activity and calorie totals, weight and resting-heart-rate trends over time, sleep sessions, workouts and daily nutrition — and lets you drill down into the underlying figures. +- A Google account with health data from a Fitbit tracker or Pixel watch +- A Google Cloud Project -## What this plugin monitors +## Enabling the Google Health API -The plugin imports two object types into SquaredUp: +1. Go to the [Google Cloud Console](https://console.cloud.google.com/) -| Object type | What it represents | -| ----------- | ------------------ | -| **Google Health User** | The authenticated account holder — profile, goals, timezone and identity. Everything you see is that one person's data. | -| **Google Health Device** | Each paired Fitbit tracker or Pixel watch, with its battery level, last sync time and firmware version. | +2. Select the project you wish to use -From those objects the plugin exposes data streams covering: +3. Navigate to **APIs & Services** > **Library** -- **Daily metrics** (steps, distance, floors, active zone minutes, calories, resting heart rate, weight, body fat, VO₂ max, SpO₂, respiratory rate, HRV, hydration) — one configurable stream that powers every "over time" trend chart. -- **Nutrition** — daily calories in, carbohydrates, fat, protein and water. -- **Heart-rate zones** — minutes and calories spent in each zone per day. -- **Sleep sessions** — start, end, duration, efficiency and time in each sleep stage. -- **Workouts** — exercise sessions with type, duration, distance, calories and heart rate. -- **Intraday heart rate** — heart rate through the day at a chosen resolution. -- **Profile & devices** — current profile details and paired-device status. +4. Search for **Google Health API** -The plugin ships ready-made **Activity**, **Diet & Nutrition**, **Trends Over Time** and **Sleep** dashboards, plus per-user and per-device perspectives. +5. Select it and click **Enable** -## Prerequisites — getting your credentials +## Creating OAuth 2.0 Credentials -The Google Health API uses Google OAuth 2.0. You need a Google Cloud project with the API enabled and an OAuth client, and your Google account must have health data (from a Fitbit or Pixel device). +1. From the [Google Cloud Console](https://console.cloud.google.com/), navigate to **APIs & Services** > **Credentials** -1. **Create / choose a Google Cloud project** at . -2. **Enable the Google Health API** — in the Cloud console go to *APIs & Services → Library*, search for **Google Health API**, and click **Enable**. -3. **Configure the OAuth consent screen** — *APIs & Services → OAuth consent screen*: - - User type **External**. - - Add the scopes this plugin uses (all read-only): - - `https://www.googleapis.com/auth/googlehealth.profile.readonly` - - `https://www.googleapis.com/auth/googlehealth.settings.readonly` - - `https://www.googleapis.com/auth/googlehealth.activity_and_fitness.readonly` - - `https://www.googleapis.com/auth/googlehealth.health_metrics_and_measurements.readonly` - - `https://www.googleapis.com/auth/googlehealth.sleep.readonly` - - `https://www.googleapis.com/auth/googlehealth.nutrition.readonly` - - While the consent screen is in **Testing** mode, add your own Google account under **Test users** — this lets you authenticate without full Google verification. (Publishing to production for other users requires Google's sensitive-scope verification.) -4. **Create an OAuth client ID** — *APIs & Services → Credentials → Create credentials → OAuth client ID*: - - Application type **Web application**. - - Add SquaredUp's OAuth redirect URL as an **Authorized redirect URI**. SquaredUp shows this URL on the plugin's sign-in step when you add the plugin — copy it from there into the Cloud console. - - Copy the generated **Client ID** and **Client secret**. +2. Click **+ Create credentials** and select **OAuth client ID** -## Configuration fields +3. If prompted, configure the OAuth consent screen: -When you add the plugin in SquaredUp you'll be asked for: + - Choose **External** + - Enter the required application details + - Add the following read-only scopes: -| Field | What it is | Where to find it | Required | -| ----- | ---------- | ---------------- | -------- | -| **Google OAuth client ID** | The OAuth 2.0 client ID for your Google Cloud project | Cloud console → *Credentials* → your OAuth client | Yes | -| **Google OAuth client secret** | The matching client secret | Cloud console → *Credentials* → your OAuth client | Yes | -| **Sign in with Google Health** | Launches Google's sign-in so you can grant access | — click it and complete Google sign-in | Yes | + ```text + https://www.googleapis.com/auth/googlehealth.profile.readonly + https://www.googleapis.com/auth/googlehealth.settings.readonly + https://www.googleapis.com/auth/googlehealth.activity_and_fitness.readonly + https://www.googleapis.com/auth/googlehealth.health_metrics_and_measurements.readonly + https://www.googleapis.com/auth/googlehealth.sleep.readonly + https://www.googleapis.com/auth/googlehealth.nutrition.readonly + ``` -After entering the client ID and secret, click **Sign in with Google Health**, choose your Google account and approve the requested health permissions. SquaredUp stores a refresh token and renews access automatically. + - While the consent screen is in **Testing** mode, add your own Google account under **Test users** — this lets you authenticate without completing Google's sensitive-scope verification -## What gets indexed +4. On the **Create OAuth client ID** page: -- **Google Health User** — one object representing you, with your display name, timezone and average daily step goal. -- **Google Health Device** — one object per paired tracker/watch, with battery, sync and version details. + - Select **Web application** as the application type + - Enter a name for your OAuth client -## Goals and monitoring +5. Add authorized JavaScript origins: -The plugin's gauges measure progress against the targets you set in the **Daily goals** section of the plugin configuration (steps, calories, protein, water and so on) — edit the configuration and save to change them. + - Click **+ Add URI** under **Authorized JavaScript origins** + - Enter: -Every gauge also ships with a pre-configured health monitor (green when you hit your goal, amber when slightly off, red when well off). **Monitors are dormant on the built-in dashboards** — this is standard SquaredUp behaviour for out-of-the-box content, so a plugin never generates monitoring load or notifications you didn't ask for. To activate them, open a dashboard and use **Copy dashboard**: your copy's gauges will show live health states and can raise notifications. + ```text + https://app.squaredup.com + ``` -## Known limitations +6. Add authorized redirect URIs: -- **One account per configuration.** The Google Health API is strictly single-user (`users/me`), so each plugin configuration surfaces exactly one person's data. Add a separate configuration per account. -- **Read-only.** The plugin only reads data; it never writes, edits or deletes health data. -- **Aggregation range caps.** The API caps daily roll-up requests at **90 days** for most metrics and **14 days** for heart-rate–derived metrics (heart rate, active minutes, heart-rate zones). Daily streams therefore offer timeframes up to those limits — very long ranges (e.g. a full year) aren't available in a single query. Longer-term trends are best viewed month-by-month. -- **Rate limits.** Google enforces roughly **300 requests per user per minute**; exceeding it returns HTTP 429. The plugin's dashboards stay well within this, but very frequent manual refreshes across many tiles can hit it briefly. -- **Data depends on the device.** Metrics only appear if your device records them — e.g. SpO₂, VO₂ max, HRV and sleep stages require a compatible Fitbit/Pixel device and recent sync. -- **Sensitive scopes.** Sharing this plugin with other users in production requires completing Google's OAuth verification for the health scopes. In Testing mode it works immediately for accounts added as test users. + - Click **+ Add URI** under **Authorized redirect URIs** + - Enter: + + ```text + https://app.squaredup.com/settings/pluginsoauth2 + ``` + +7. Click **Create** + +8. Copy the **Client ID** and **Client Secret** that are displayed, save these somewhere secure as you won't be able to view the client secret again + +## Configuring the plugin + +Populate the following fields when configuring the plugin: + +| Field | Description | +|---------|---------| +| Google OAuth client ID | The Client ID created in Google Cloud | +| Google OAuth client secret | The Client Secret created in Google Cloud | +| Sign in with Google Health | Click to authenticate using the Google account whose health data you want to monitor | + +### Daily goals + +The Google Health API does not expose the goals you have set in the Fitbit app, so they cannot be imported — enter them manually in the **Daily goals** section of the plugin configuration. Gauges fill towards these targets and their health status (green / amber / red) is measured against them. Leave any field blank to use the default shown. + +| Goal | Default | +|---------|---------| +| Steps goal | 10000 | +| Distance goal (km) | 8 | +| Active calories goal | 600 | +| Active zone minutes goal | 22 | +| Sleep goal (hours) | 8 | +| Water goal (mL) | 2000 | +| Calorie budget (kcal) | 2500 | +| Protein goal (g) | 130 | +| Carbohydrate budget (g) | 250 | +| Fat budget (g) | 70 | + +To change a goal later, edit the data source configuration and save — dashboards pick up the new targets on their next refresh. + +## Example configuration + +| Setting | Example | +|---------|---------| +| Google OAuth client ID | `1234567890-example.apps.googleusercontent.com` | +| Google OAuth client secret | `GOCSPX-xxxxxxxxxxxxxxxxxxxx` | +| Steps goal | `10000` | + +## Troubleshooting + +### Authentication fails + +Verify that: + +- The Google Health API is enabled +- The Client ID and Client Secret are correct +- The redirect URI exactly matches: + + ```text + https://app.squaredup.com/settings/pluginsoauth2 + ``` + +- If the OAuth consent screen is in **Testing** mode, the signed-in Google account is listed as a test user + +### Some metrics show no data + +Verify that: + +- Your device records the metric — SpO₂, VO₂ max, HRV and sleep stages require a compatible Fitbit or Pixel device +- The device has synced recently via the Fitbit app + +### Goals look wrong + +Goals are not imported from your Google account — set them manually in the **Daily goals** section of the plugin configuration. + +### Long timeframes return errors + +The API caps daily roll-up requests at 90 days for most metrics and 14 days for heart-rate–derived metrics, and enforces a rate limit of roughly 300 requests per user per minute (HTTP 429 when exceeded). Use shorter timeframes and avoid rapidly refreshing many tiles. From 71cff6acc5974059139e54109df45122cda5a2ca Mon Sep 17 00:00:00 2001 From: daniel hodgson <45772010+DanielCHodgson@users.noreply.github.com> Date: Sat, 18 Jul 2026 13:51:01 +0100 Subject: [PATCH 5/6] Add note on enabling the Health API via the Google setup page Co-Authored-By: Claude Fable 5 --- plugins/GoogleHealth/v1/docs/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/plugins/GoogleHealth/v1/docs/README.md b/plugins/GoogleHealth/v1/docs/README.md index 232c820c..955882f7 100644 --- a/plugins/GoogleHealth/v1/docs/README.md +++ b/plugins/GoogleHealth/v1/docs/README.md @@ -17,6 +17,8 @@ 5. Select it and click **Enable** +> **Note:** If you cannot see the Google Health API in the library, it can also be enabled by following the steps at + ## Creating OAuth 2.0 Credentials 1. From the [Google Cloud Console](https://console.cloud.google.com/), navigate to **APIs & Services** > **Credentials** From 32e76647f1dd03052b8fd7e85ca848c2dd24a188 Mon Sep 17 00:00:00 2001 From: daniel hodgson <45772010+DanielCHodgson@users.noreply.github.com> Date: Sat, 18 Jul 2026 14:42:36 +0100 Subject: [PATCH 6/6] Address CodeRabbit review feedback - Limit the dailyMetric description to the metrics its selector offers - Attribute the empty single-day snapshot to the snapped day (timeframe.end - 1ms) rather than the day containing the raw end - Guard foodLog calories against NaN when energy.kcal is absent - Preserve fractional minutes in heart-rate zone durations so per-zone and total minutes sum accurately (display still rounds) - Keep legitimate zero-minute sleep stages while leaving stages blank for sessions with no stage data at all - Fall back to the auto-calculated walking stride when no manual value is configured, matching the running stride - Capitalize status column display names and tile title case Co-Authored-By: Claude Fable 5 --- plugins/GoogleHealth/v1/dataStreams/dailyMetric.json | 2 +- .../GoogleHealth/v1/dataStreams/nutritionDaily.json | 8 ++++---- .../v1/dataStreams/scripts/dailyMetric.js | 6 ++++-- .../GoogleHealth/v1/dataStreams/scripts/foodLog.js | 2 +- .../v1/dataStreams/scripts/heartRateZonesDaily.js | 2 +- .../v1/dataStreams/scripts/sleepSessions.js | 12 +++++++----- .../v1/dataStreams/scripts/userProfile.js | 2 +- .../GoogleHealth/v1/dataStreams/sleepSessions.json | 2 +- .../v1/defaultContent/dietNutrition.dash.json | 4 ++-- .../GoogleHealth/v1/defaultContent/trends.dash.json | 4 ++-- 10 files changed, 24 insertions(+), 20 deletions(-) diff --git a/plugins/GoogleHealth/v1/dataStreams/dailyMetric.json b/plugins/GoogleHealth/v1/dataStreams/dailyMetric.json index c490d708..5dfa5c1c 100644 --- a/plugins/GoogleHealth/v1/dataStreams/dailyMetric.json +++ b/plugins/GoogleHealth/v1/dataStreams/dailyMetric.json @@ -1,7 +1,7 @@ { "name": "dailyMetric", "displayName": "Daily Metric", - "description": "Daily total or average for a chosen health metric such as steps, distance, calories, weight, body fat, resting heart rate or VO2 max", + "description": "Daily total for a chosen activity metric such as steps, distance, floors, active zone minutes, active calories or hydration", "tags": [ "Activity", "Health" diff --git a/plugins/GoogleHealth/v1/dataStreams/nutritionDaily.json b/plugins/GoogleHealth/v1/dataStreams/nutritionDaily.json index 5bc384af..7d65ae03 100644 --- a/plugins/GoogleHealth/v1/dataStreams/nutritionDaily.json +++ b/plugins/GoogleHealth/v1/dataStreams/nutritionDaily.json @@ -188,7 +188,7 @@ }, { "name": "caloriesStatus", - "displayName": "calories Status", + "displayName": "Calories Status", "computed": true, "valueExpression": "{{ $[\"caloriesPct\"] === undefined || $[\"caloriesPct\"] === null ? \"unknown\" : $[\"caloriesPct\"] <= 100 ? \"success\" : $[\"caloriesPct\"] <= 110 ? \"warning\" : \"error\" }}", "shape": "state", @@ -196,7 +196,7 @@ }, { "name": "proteinStatus", - "displayName": "protein Status", + "displayName": "Protein Status", "computed": true, "valueExpression": "{{ $[\"proteinPct\"] === undefined || $[\"proteinPct\"] === null ? \"unknown\" : $[\"proteinPct\"] >= 100 ? \"success\" : $[\"proteinPct\"] >= 75 ? \"warning\" : \"error\" }}", "shape": "state", @@ -204,7 +204,7 @@ }, { "name": "carbsStatus", - "displayName": "carbs Status", + "displayName": "Carbs Status", "computed": true, "valueExpression": "{{ $[\"carbsPct\"] === undefined || $[\"carbsPct\"] === null ? \"unknown\" : $[\"carbsPct\"] <= 100 ? \"success\" : $[\"carbsPct\"] <= 110 ? \"warning\" : \"error\" }}", "shape": "state", @@ -212,7 +212,7 @@ }, { "name": "fatStatus", - "displayName": "fat Status", + "displayName": "Fat Status", "computed": true, "valueExpression": "{{ $[\"fatPct\"] === undefined || $[\"fatPct\"] === null ? \"unknown\" : $[\"fatPct\"] <= 100 ? \"success\" : $[\"fatPct\"] <= 110 ? \"warning\" : \"error\" }}", "shape": "state", diff --git a/plugins/GoogleHealth/v1/dataStreams/scripts/dailyMetric.js b/plugins/GoogleHealth/v1/dataStreams/scripts/dailyMetric.js index fd4b41ec..c7c55985 100644 --- a/plugins/GoogleHealth/v1/dataStreams/scripts/dailyMetric.js +++ b/plugins/GoogleHealth/v1/dataStreams/scripts/dailyMetric.js @@ -110,10 +110,12 @@ if ( goal !== undefined && new Date(context.timeframe.end).getTime() - new Date(context.timeframe.start).getTime() <= 90000000 ) { - const end = new Date(context.timeframe.end); + // Match the request's snapped day (the day containing timeframe.end - 1ms) + // so a window ending exactly at midnight doesn't attribute the zero to the next day. + const snapshotDay = new Date(new Date(context.timeframe.end).getTime() - 1); result = [ { - date: new Date(Date.UTC(end.getUTCFullYear(), end.getUTCMonth(), end.getUTCDate())), + date: new Date(Date.UTC(snapshotDay.getUTCFullYear(), snapshotDay.getUTCMonth(), snapshotDay.getUTCDate())), value: 0, unit: ({steps:"steps",distance:"km",floors:"floors","active-zone-minutes":"min","active-energy-burned":"kcal","hydration-log":"mL"})[metricName] || "", metric: metricName, diff --git a/plugins/GoogleHealth/v1/dataStreams/scripts/foodLog.js b/plugins/GoogleHealth/v1/dataStreams/scripts/foodLog.js index 846748de..7ab14756 100644 --- a/plugins/GoogleHealth/v1/dataStreams/scripts/foodLog.js +++ b/plugins/GoogleHealth/v1/dataStreams/scripts/foodLog.js @@ -27,7 +27,7 @@ result = pts time: iv.startTime ? new Date(iv.startTime) : undefined, food: n.foodDisplayName || "Food", meal: titleCase(n.mealType), - calories: n.energy ? Math.round(N(n.energy.kcal)) : undefined, + calories: n.energy ? r1(N(n.energy.kcal)) : undefined, carbsG: r1(grams(n.totalCarbohydrate) ?? byNutrient.CARBOHYDRATES), fatG: r1(grams(n.totalFat)), proteinG: r1(byNutrient.PROTEIN), diff --git a/plugins/GoogleHealth/v1/dataStreams/scripts/heartRateZonesDaily.js b/plugins/GoogleHealth/v1/dataStreams/scripts/heartRateZonesDaily.js index 6d392304..893ced77 100644 --- a/plugins/GoogleHealth/v1/dataStreams/scripts/heartRateZonesDaily.js +++ b/plugins/GoogleHealth/v1/dataStreams/scripts/heartRateZonesDaily.js @@ -5,7 +5,7 @@ const points = data?.rollupDataPoints || []; const durMin = (s) => { if (typeof s !== "string") return undefined; const n = Number(s.replace(/s$/, "")); - return Number.isFinite(n) ? Math.round(n / 60) : undefined; + return Number.isFinite(n) ? n / 60 : undefined; }; result = points diff --git a/plugins/GoogleHealth/v1/dataStreams/scripts/sleepSessions.js b/plugins/GoogleHealth/v1/dataStreams/scripts/sleepSessions.js index b8627840..a025a06f 100644 --- a/plugins/GoogleHealth/v1/dataStreams/scripts/sleepSessions.js +++ b/plugins/GoogleHealth/v1/dataStreams/scripts/sleepSessions.js @@ -27,8 +27,10 @@ result = pts const awake = N(sum.minutesAwake); const inBed = N(sum.minutesInSleepPeriod); - // Per-stage minutes from stagesSummary. + // Per-stage minutes from stagesSummary. Sessions without stage data + // (hasStages false) leave the stage columns blank rather than zero. const stage = { DEEP: 0, REM: 0, LIGHT: 0, AWAKE: 0, ASLEEP: 0, RESTLESS: 0 }; + const hasStages = (sum.stagesSummary || []).length > 0; for (const st of sum.stagesSummary || []) { const m = N(st.minutes); if (st.type in stage && m !== undefined) stage[st.type] += m; @@ -46,10 +48,10 @@ result = pts sleepPct: asleep === undefined ? undefined : Math.round((asleep / 60 / sleepGoal) * 100), efficiencyPct: efficiency, - deepMin: stage.DEEP || undefined, - remMin: stage.REM || undefined, - lightMin: stage.LIGHT || undefined, - awakeMin: awake ?? (stage.AWAKE || undefined), + deepMin: hasStages ? stage.DEEP : undefined, + remMin: hasStages ? stage.REM : undefined, + lightMin: hasStages ? stage.LIGHT : undefined, + awakeMin: awake ?? (hasStages ? stage.AWAKE : undefined), inBedMin: inBed, startTime: iv.startTime ? new Date(iv.startTime) : undefined, endTime: iv.endTime ? new Date(iv.endTime) : undefined, diff --git a/plugins/GoogleHealth/v1/dataStreams/scripts/userProfile.js b/plugins/GoogleHealth/v1/dataStreams/scripts/userProfile.js index a38c5e45..9f080907 100644 --- a/plugins/GoogleHealth/v1/dataStreams/scripts/userProfile.js +++ b/plugins/GoogleHealth/v1/dataStreams/scripts/userProfile.js @@ -33,7 +33,7 @@ result = [ age: num(p.age), memberSince: memberSince, membershipYears: membershipYears, - walkingStrideCm: mmToCm(p.userConfiguredWalkingStrideLengthMm), + walkingStrideCm: mmToCm(p.userConfiguredWalkingStrideLengthMm ?? p.autoWalkingStrideLengthMm), runningStrideCm: mmToCm(p.userConfiguredRunningStrideLengthMm ?? p.autoRunningStrideLengthMm), }, ]; diff --git a/plugins/GoogleHealth/v1/dataStreams/sleepSessions.json b/plugins/GoogleHealth/v1/dataStreams/sleepSessions.json index c0209425..9248447f 100644 --- a/plugins/GoogleHealth/v1/dataStreams/sleepSessions.json +++ b/plugins/GoogleHealth/v1/dataStreams/sleepSessions.json @@ -165,7 +165,7 @@ }, { "name": "sleepStatus", - "displayName": "sleep Status", + "displayName": "Sleep Status", "computed": true, "valueExpression": "{{ $[\"sleepPct\"] === undefined || $[\"sleepPct\"] === null ? \"unknown\" : $[\"sleepPct\"] >= 100 ? \"success\" : $[\"sleepPct\"] >= 75 ? \"warning\" : \"error\" }}", "shape": "state" diff --git a/plugins/GoogleHealth/v1/defaultContent/dietNutrition.dash.json b/plugins/GoogleHealth/v1/defaultContent/dietNutrition.dash.json index cea99634..8be68f4d 100644 --- a/plugins/GoogleHealth/v1/defaultContent/dietNutrition.dash.json +++ b/plugins/GoogleHealth/v1/defaultContent/dietNutrition.dash.json @@ -440,7 +440,7 @@ "z": 0, "config": { "_type": "tile/data-stream", - "title": "Macro Split (calories)", + "title": "Macro Split (Calories)", "description": "", "activePluginConfigIds": [ "{{configId}}" @@ -482,7 +482,7 @@ "z": 0, "config": { "_type": "tile/data-stream", - "title": "Macros (grams)", + "title": "Macros (Grams)", "description": "", "activePluginConfigIds": [ "{{configId}}" diff --git a/plugins/GoogleHealth/v1/defaultContent/trends.dash.json b/plugins/GoogleHealth/v1/defaultContent/trends.dash.json index f636f1a7..a091e8c5 100644 --- a/plugins/GoogleHealth/v1/defaultContent/trends.dash.json +++ b/plugins/GoogleHealth/v1/defaultContent/trends.dash.json @@ -187,7 +187,7 @@ "z": 0, "config": { "_type": "tile/data-stream", - "title": "Calories Consumed (30 days)", + "title": "Calories Consumed (30 Days)", "description": "", "activePluginConfigIds": [ "{{configId}}" @@ -228,7 +228,7 @@ "z": 0, "config": { "_type": "tile/data-stream", - "title": "Active Calories Burned (30 days)", + "title": "Active Calories Burned (30 Days)", "description": "", "activePluginConfigIds": [ "{{configId}}"