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..c61b32c2 --- /dev/null +++ b/plugins/GoogleHealth/v1/dataStreams/bodyMetric.json @@ -0,0 +1,110 @@ +{ + "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 ? 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": { + "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": true, + "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..914dac10 --- /dev/null +++ b/plugins/GoogleHealth/v1/dataStreams/dailyHealthMetric.json @@ -0,0 +1,118 @@ +{ + "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 ? 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": { + "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": true, + "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..5dfa5c1c --- /dev/null +++ b/plugins/GoogleHealth/v1/dataStreams/dailyMetric.json @@ -0,0 +1,151 @@ +{ + "name": "dailyMetric", + "displayName": "Daily Metric", + "description": "Daily total for a chosen activity metric such as steps, distance, floors, active zone minutes, active calories or hydration", + "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 ? 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()+86399999).getUTCFullYear()}}", + "month": "{{new Date(new Date(timeframe.end).getTime()+86399999).getUTCMonth()+1}}", + "day": "{{new Date(new Date(timeframe.end).getTime()+86399999).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" + }, + { + "name": "pctOfGoal", + "displayName": "% of Goal", + "shape": [ + "percent", + { + "decimalPlaces": 0 + } + ] + }, + { + "name": "status", + "displayName": "Goal Status", + "computed": true, + "valueExpression": "{{ $[\"pctOfGoal\"] === undefined || $[\"pctOfGoal\"] === null ? \"unknown\" : $[\"pctOfGoal\"] >= 100 ? \"success\" : $[\"pctOfGoal\"] >= 75 ? \"warning\" : \"error\" }}", + "shape": "state" + }, + { + "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": true, + "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..ac0a2300 --- /dev/null +++ b/plugins/GoogleHealth/v1/dataStreams/foodLog.json @@ -0,0 +1,146 @@ +{ + "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 ? 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": { + "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": "{{ ({ '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 + } + ], + "timeframes": true, + "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..57a71a1d --- /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(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" + }, + "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": "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": true, + "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..0e6373c9 --- /dev/null +++ b/plugins/GoogleHealth/v1/dataStreams/heartRateZonesDaily.json @@ -0,0 +1,113 @@ +{ + "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 ? 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()+86399999).getUTCFullYear()}}", + "month": "{{new Date(new Date(timeframe.end).getTime()+86399999).getUTCMonth()+1}}", + "day": "{{new Date(new Date(timeframe.end).getTime()+86399999).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": true, + "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..839892a0 --- /dev/null +++ b/plugins/GoogleHealth/v1/dataStreams/intradayMetric.json @@ -0,0 +1,169 @@ +{ + "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(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'}}" + }, + "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": "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 + }, + { + "name": "timeLabel", + "displayName": "Time", + "computed": true, + "valueExpression": "{{ new Date($[\"time\"]).toISOString().slice(11,16) }}", + "shape": "string", + "visible": false + } + ], + "timeframes": true, + "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..4b4d7378 --- /dev/null +++ b/plugins/GoogleHealth/v1/dataStreams/macroBreakdown.json @@ -0,0 +1,88 @@ +{ + "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 ? 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()+86399999).getUTCFullYear()}}", + "month": "{{new Date(new Date(timeframe.end).getTime()+86399999).getUTCMonth()+1}}", + "day": "{{new Date(new Date(timeframe.end).getTime()+86399999).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": true, + "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..7d65ae03 --- /dev/null +++ b/plugins/GoogleHealth/v1/dataStreams/nutritionDaily.json @@ -0,0 +1,233 @@ +{ + "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 ? 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()+86399999).getUTCFullYear()}}", + "month": "{{new Date(new Date(timeframe.end).getTime()+86399999).getUTCMonth()+1}}", + "day": "{{new Date(new Date(timeframe.end).getTime()+86399999).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": true, + "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..c7c55985 --- /dev/null +++ b/plugins/GoogleHealth/v1/dataStreams/scripts/dailyMetric.js @@ -0,0 +1,126 @@ +// 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 +) { + // 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(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, + 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..7ab14756 --- /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 ? r1(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..893ced77 --- /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) ? 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..a025a06f --- /dev/null +++ b/plugins/GoogleHealth/v1/dataStreams/scripts/sleepSessions.js @@ -0,0 +1,60 @@ +// 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. 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; + } + + 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: 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, + }; + }) + .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..9f080907 --- /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 ?? p.autoWalkingStrideLengthMm), + 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..9248447f --- /dev/null +++ b/plugins/GoogleHealth/v1/dataStreams/sleepSessions.json @@ -0,0 +1,193 @@ +{ + "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 ? 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": { + "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" + }, + { + "name": "sleepPct", + "displayName": "% of Sleep Goal", + "shape": [ + "percent", + { + "decimalPlaces": 0 + } + ] + }, + { + "name": "sleepStatus", + "displayName": "Sleep Status", + "computed": true, + "valueExpression": "{{ $[\"sleepPct\"] === undefined || $[\"sleepPct\"] === null ? \"unknown\" : $[\"sleepPct\"] >= 100 ? \"success\" : $[\"sleepPct\"] >= 75 ? \"warning\" : \"error\" }}", + "shape": "state" + }, + { + "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": true, + "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..c82bbe59 --- /dev/null +++ b/plugins/GoogleHealth/v1/dataStreams/workouts.json @@ -0,0 +1,145 @@ +{ + "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 ? 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": { + "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": true, + "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..7e45f73a --- /dev/null +++ b/plugins/GoogleHealth/v1/defaultContent/activity.dash.json @@ -0,0 +1,594 @@ +{ + "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": [ + "value" + ], + "xAxisGroup": "goalBand", + "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" + }, + "color": { + "type": "custom", + "customColors": [ + { + "color": "#259A51", + "expression": "series == \"Goal met\"" + }, + { + "color": "#E18700", + "expression": "series == \"Near goal\"" + }, + { + "color": "#D0021B", + "expression": "series == \"Below goal\"" + }, + { + "color": "#88898C", + "expression": "series == \"No goal\"" + } + ] + } + } + } + } + } + }, + { + "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": [ + "value" + ], + "xAxisGroup": "goalBand", + "xAxisLabel": "", + "yAxisLabel": "Minutes", + "showXAxisLabel": false, + "showYAxisLabel": true, + "showLegend": false, + "legendPosition": "bottom", + "showGrid": true, + "horizontalLayout": "vertical", + "displayMode": "actual", + "showTotals": false, + "showValue": false, + "grouping": false, + "range": { + "type": "auto" + }, + "color": { + "type": "custom", + "customColors": [ + { + "color": "#259A51", + "expression": "series == \"Goal met\"" + }, + { + "color": "#E18700", + "expression": "series == \"Near goal\"" + }, + { + "color": "#D0021B", + "expression": "series == \"Below goal\"" + }, + { + "color": "#88898C", + "expression": "series == \"No goal\"" + } + ] + } + } + } + } + } + }, + { + "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..8be68f4d --- /dev/null +++ b/plugins/GoogleHealth/v1/defaultContent/dietNutrition.dash.json @@ -0,0 +1,598 @@ +{ + "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" + ] + }, + { + "type": "min", + "names": [ + "mealOrder" + ] + } + ] + }, + "sort": { + "by": [ + [ + "mealOrder_min", + "asc" + ] + ] + } + }, + "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..83daa112 --- /dev/null +++ b/plugins/GoogleHealth/v1/defaultContent/sleep.dash.json @@ -0,0 +1,297 @@ +{ + "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": [ + "asleepHours" + ], + "xAxisGroup": "sleepGoalBand", + "xAxisLabel": "", + "yAxisLabel": "Hours", + "showXAxisLabel": false, + "showYAxisLabel": true, + "showLegend": false, + "legendPosition": "bottom", + "showGrid": true, + "horizontalLayout": "vertical", + "displayMode": "actual", + "showTotals": false, + "showValue": false, + "grouping": false, + "range": { + "type": "auto" + }, + "color": { + "type": "custom", + "customColors": [ + { + "color": "#259A51", + "expression": "series == \"Goal met\"" + }, + { + "color": "#E18700", + "expression": "series == \"Near goal\"" + }, + { + "color": "#D0021B", + "expression": "series == \"Below goal\"" + }, + { + "color": "#88898C", + "expression": "series == \"No goal\"" + } + ] + } + } + } + } + } + }, + { + "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": "right", + "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..bbb48b84 --- /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", + "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": "timeLabel", + "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", + "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": "timeLabel", + "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..a091e8c5 --- /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..955882f7 --- /dev/null +++ b/plugins/GoogleHealth/v1/docs/README.md @@ -0,0 +1,138 @@ +# Before you start + +## Prerequisites + +- A Google account with health data from a Fitbit tracker or Pixel watch +- A Google Cloud Project + +## Enabling the Google Health API + +1. Go to the [Google Cloud Console](https://console.cloud.google.com/) + +2. Select the project you wish to use + +3. Navigate to **APIs & Services** > **Library** + +4. Search for **Google Health API** + +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** + +2. Click **+ Create credentials** and select **OAuth client ID** + +3. If prompted, configure the OAuth consent screen: + + - Choose **External** + - Enter the required application details + - Add the following read-only scopes: + + ```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 + ``` + + - 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 + +4. On the **Create OAuth client ID** page: + + - Select **Web application** as the application type + - Enter a name for your OAuth client + +5. Add authorized JavaScript origins: + + - Click **+ Add URI** under **Authorized JavaScript origins** + - Enter: + + ```text + https://app.squaredup.com + ``` + +6. Add authorized redirect URIs: + + - 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. 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." } +]