├── .gitignore ├── src ├── Entities │ ├── MonthlyMaxHr.ts │ ├── MonthlySteps.ts │ ├── MonthlyRestHr.ts │ ├── MonthlyDistance.ts │ ├── MonthlyDuration.ts │ └── Wellness.ts ├── ImportStepsHandler.ts ├── ImportDistanceHandler.ts ├── ImportDurationHandler.ts ├── ImportMaxHrHandler.ts ├── ImportRestHrHandler.ts ├── ImportSleepDurationHandler.ts ├── ImportStressLevelHandler.ts └── GConnect │ └── client.ts ├── .editorconfig ├── package.json ├── webpack.config.js ├── tslint.json ├── config.ts ├── tsconfig.json ├── serverless ├── api.ts └── handler.ts └── serverless.yml /.gitignore: -------------------------------------------------------------------------------- 1 | # package directories 2 | node_modules 3 | jspm_packages 4 | 5 | # Serverless directories 6 | .serverless 7 | 8 | # Webpack directories 9 | .webpack 10 | 11 | .idea 12 | .DS_Store 13 | 14 | -------------------------------------------------------------------------------- /src/Entities/MonthlyMaxHr.ts: -------------------------------------------------------------------------------- 1 | import { List } from 'immutable'; 2 | 3 | export interface MonthlyMaxHrInterface { 4 | date: string; 5 | maxHr: number; 6 | } 7 | 8 | export type MonthlyMaxHrCollection = List; 9 | -------------------------------------------------------------------------------- /src/Entities/MonthlySteps.ts: -------------------------------------------------------------------------------- 1 | import { List } from 'immutable'; 2 | 3 | export interface MonthlyStepsInterface { 4 | date: string; 5 | steps: number; 6 | } 7 | 8 | export type MonthlyStepsCollection = List; 9 | -------------------------------------------------------------------------------- /src/Entities/MonthlyRestHr.ts: -------------------------------------------------------------------------------- 1 | import { List } from 'immutable'; 2 | 3 | export interface MonthlyRestHrInterface { 4 | date: string; 5 | restHr: number; 6 | } 7 | 8 | export type MonthlyRestHrCollection = List; 9 | -------------------------------------------------------------------------------- /src/Entities/MonthlyDistance.ts: -------------------------------------------------------------------------------- 1 | import { List } from 'immutable'; 2 | 3 | export interface MonthlyDistanceInterface { 4 | date: string; 5 | distance: number; 6 | activities: number; 7 | } 8 | 9 | export type MonthlyDistanceCollection = List; 10 | -------------------------------------------------------------------------------- /src/Entities/MonthlyDuration.ts: -------------------------------------------------------------------------------- 1 | import { List } from 'immutable'; 2 | 3 | export interface MonthlyDurationInterface { 4 | date: string; 5 | duration: number; 6 | activities: number; 7 | } 8 | 9 | export type MonthlyDurationCollection = List; 10 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: https://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Unix-style newlines with a newline ending every file 7 | [*] 8 | end_of_line = lf 9 | insert_final_newline = true 10 | indent_style = space 11 | indent_size = 2 12 | charset = utf-8 13 | -------------------------------------------------------------------------------- /src/Entities/Wellness.ts: -------------------------------------------------------------------------------- 1 | import { List } from 'immutable'; 2 | 3 | export interface WeeklySleepDuration { 4 | date: string; 5 | sleep: number; 6 | } 7 | 8 | export interface WeeklyRestHeartRate { 9 | date: string; 10 | restHr: number; 11 | } 12 | 13 | export interface WeeklyStressLevel { 14 | date: string; 15 | stressLevel: number; 16 | } 17 | 18 | export type WeeklyRestHeartRateCollection = List; 19 | export type WeeklySleepDurationCollection = List; 20 | export type WeeklyStressLevelCollection = List; 21 | -------------------------------------------------------------------------------- /src/ImportStepsHandler.ts: -------------------------------------------------------------------------------- 1 | import GConnect from './GConnect/client'; 2 | import Axios from 'axios'; 3 | import { DynamoDB } from 'aws-sdk'; 4 | import { MonthlyStepsCollection } from './Entities/MonthlySteps'; 5 | 6 | export const importSteps = async () => { 7 | const client = new GConnect(Axios); 8 | const docClient = new DynamoDB.DocumentClient(); 9 | const stepsCollection: MonthlyStepsCollection = await client.getMonthlySteps(); 10 | for (const item of stepsCollection.toArray()) { 11 | const param = { 12 | TableName: 'steps', 13 | Item: { 14 | date: item.date, 15 | steps: item.steps, 16 | }, 17 | }; 18 | await docClient.put(param).promise(); 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /src/ImportDistanceHandler.ts: -------------------------------------------------------------------------------- 1 | import GConnect from './GConnect/client'; 2 | import Axios from 'axios'; 3 | import { DynamoDB } from 'aws-sdk'; 4 | import { MonthlyDistanceCollection } from './Entities/MonthlyDistance'; 5 | 6 | export const importDistance = async () => { 7 | const client = new GConnect(Axios); 8 | const docClient = new DynamoDB.DocumentClient(); 9 | const distanceCollection: MonthlyDistanceCollection = await client.getMonthlyDistance(); 10 | for (const item of distanceCollection.toArray()) { 11 | const param = { 12 | TableName: 'distance', 13 | Item: { 14 | date: item.date, 15 | distance: item.distance, 16 | activities: item.activities, 17 | }, 18 | }; 19 | await docClient.put(param).promise(); 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /src/ImportDurationHandler.ts: -------------------------------------------------------------------------------- 1 | import GConnect from './GConnect/client'; 2 | import Axios from 'axios'; 3 | import { DynamoDB } from 'aws-sdk'; 4 | import { MonthlyDurationCollection } from './Entities/MonthlyDuration'; 5 | 6 | export const importDuration = async () => { 7 | const client = new GConnect(Axios); 8 | const docClient = new DynamoDB.DocumentClient(); 9 | const durationCollection: MonthlyDurationCollection = await client.getMonthlyDuration(); 10 | for (const item of durationCollection.toArray()) { 11 | const param = { 12 | TableName: 'duration', 13 | Item: { 14 | date: item.date, 15 | duration: item.duration, 16 | activities: item.activities, 17 | }, 18 | }; 19 | await docClient.put(param).promise(); 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /src/ImportMaxHrHandler.ts: -------------------------------------------------------------------------------- 1 | import GConnect from './GConnect/client'; 2 | import Axios from 'axios'; 3 | import { DynamoDB } from 'aws-sdk'; 4 | import { MonthlyMaxHrCollection } from './Entities/MonthlyMaxHr'; 5 | import { DocumentClient } from 'aws-sdk/lib/dynamodb/document_client'; 6 | import UpdateItemInput = DocumentClient.UpdateItemInput; 7 | 8 | export const importMaxHr = async () => { 9 | const client = new GConnect(Axios); 10 | const docClient = new DynamoDB.DocumentClient(); 11 | const maxHrData: MonthlyMaxHrCollection = await client.getMaxHrData(); 12 | for (const item of maxHrData.toArray()) { 13 | const param: UpdateItemInput = { 14 | TableName: 'maxhr', 15 | Key: { 16 | date: item.date, 17 | }, 18 | UpdateExpression: 'SET maxHr = :maxHr', 19 | ExpressionAttributeValues: { 20 | ':maxHr': item.maxHr, 21 | }, 22 | }; 23 | 24 | await docClient.update(param).promise(); 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /src/ImportRestHrHandler.ts: -------------------------------------------------------------------------------- 1 | import GConnect from './GConnect/client'; 2 | import Axios from 'axios'; 3 | import { DynamoDB } from 'aws-sdk'; 4 | import { DocumentClient } from 'aws-sdk/lib/dynamodb/document_client'; 5 | import { WeeklyRestHeartRateCollection } from './Entities/Wellness'; 6 | import UpdateItemInput = DocumentClient.UpdateItemInput; 7 | 8 | export const importRestHr = async () => { 9 | const client = new GConnect(Axios); 10 | const docClient = new DynamoDB.DocumentClient(); 11 | const restHrData: WeeklyRestHeartRateCollection = await client.getWeeklyRestHr(); 12 | for (const item of restHrData.toArray()) { 13 | const param: UpdateItemInput = { 14 | TableName: 'wellness', 15 | Key: { 16 | date: item.date, 17 | }, 18 | UpdateExpression: 'SET restHr = :restHr', 19 | ExpressionAttributeValues: { 20 | ':restHr': item.restHr, 21 | }, 22 | }; 23 | 24 | await docClient.update(param).promise(); 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /src/ImportSleepDurationHandler.ts: -------------------------------------------------------------------------------- 1 | import GConnect from './GConnect/client'; 2 | import Axios from 'axios'; 3 | import { DynamoDB } from 'aws-sdk'; 4 | import { DocumentClient } from 'aws-sdk/lib/dynamodb/document_client'; 5 | import { WeeklySleepDurationCollection } from './Entities/Wellness'; 6 | import UpdateItemInput = DocumentClient.UpdateItemInput; 7 | 8 | export const importSleepDuration = async () => { 9 | const client = new GConnect(Axios); 10 | const docClient = new DynamoDB.DocumentClient(); 11 | const sleepDurationData: WeeklySleepDurationCollection = await client.getWeeklySleepDuration(); 12 | for (const item of sleepDurationData.toArray()) { 13 | const param: UpdateItemInput = { 14 | TableName: 'wellness', 15 | Key: { 16 | date: item.date, 17 | }, 18 | UpdateExpression: 'SET sleep = :sleep', 19 | ExpressionAttributeValues: { 20 | ':sleep': item.sleep, 21 | }, 22 | }; 23 | 24 | await docClient.update(param).promise(); 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /src/ImportStressLevelHandler.ts: -------------------------------------------------------------------------------- 1 | import GConnect from './GConnect/client'; 2 | import Axios from 'axios'; 3 | import { DynamoDB } from 'aws-sdk'; 4 | import { DocumentClient } from 'aws-sdk/lib/dynamodb/document_client'; 5 | import { WeeklyStressLevelCollection } from './Entities/Wellness'; 6 | import UpdateItemInput = DocumentClient.UpdateItemInput; 7 | 8 | export const importStressLevel = async () => { 9 | const client = new GConnect(Axios); 10 | const docClient = new DynamoDB.DocumentClient(); 11 | const stressLevelData: WeeklyStressLevelCollection = await client.getWeeklyStressLevel(); 12 | for (const item of stressLevelData.toArray()) { 13 | const param: UpdateItemInput = { 14 | TableName: 'wellness', 15 | Key: { 16 | date: item.date, 17 | }, 18 | UpdateExpression: 'SET stressLevel = :stressLevel', 19 | ExpressionAttributeValues: { 20 | ':stressLevel': item.stressLevel, 21 | }, 22 | }; 23 | 24 | await docClient.update(param).promise(); 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Garmin.MaxHR", 3 | "version": "1.0.0", 4 | "description": "Garmin MaxHr", 5 | "main": "handler.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "dependencies": { 10 | "@babel/polyfill": "^7.2.5", 11 | "@types/moment": "^2.13.0", 12 | "axios": "^0.18.0", 13 | "axios-cookiejar-support": "^0.4.2", 14 | "immutable": "^4.0.0-rc.12", 15 | "moment": "^2.23.0", 16 | "serverless": "^1.35.1", 17 | "uuid": "^3.3.2" 18 | }, 19 | "devDependencies": { 20 | "@types/aws-lambda": "^8.10.1", 21 | "@types/immutable": "^3.8.7", 22 | "@types/node": "^8.10.38", 23 | "aws-sdk": "^2.381.0", 24 | "serverless-offline": "^3.31.3", 25 | "serverless-webpack": "^5.2.0", 26 | "source-map-support": "^0.5.6", 27 | "ts-loader": "^4.5.0", 28 | "tslint": "^5.12.0", 29 | "tslint-config-airbnb": "^5.11.1", 30 | "tslint-react": "^3.6.0", 31 | "typescript": "^3.2.2", 32 | "webpack": "^4.27.1", 33 | "webpack-node-externals": "^1.7.2" 34 | }, 35 | "author": "Martin Roest", 36 | "license": "MIT" 37 | } 38 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const slsw = require("serverless-webpack"); 3 | const nodeExternals = require("webpack-node-externals"); 4 | 5 | const lambdaFunctions = { 6 | mode: slsw.lib.webpack.isLocal ? "development" : "production", 7 | entry: slsw.lib.entries, 8 | devtool: "source-map", 9 | externals: [nodeExternals()], 10 | resolve: { 11 | extensions: [".js", ".jsx", ".json", ".ts", ".tsx"], 12 | }, 13 | optimization: { 14 | // We no not want to minimize our code. 15 | minimize: false, 16 | }, 17 | performance: { 18 | // Turn off size warnings for entry points 19 | hints: false, 20 | }, 21 | output: { 22 | libraryTarget: "commonjs", 23 | path: path.resolve(__dirname, ".webpack"), 24 | filename: "[name].js", 25 | }, 26 | target: "node", 27 | module: { 28 | rules: [ 29 | // all files with a `.ts` or `.tsx` extension will be handled by `ts-loader` 30 | { 31 | test: /\.tsx?$/, 32 | loader: "ts-loader", 33 | include: __dirname, 34 | exclude: /node_modules/, 35 | }, 36 | ], 37 | }, 38 | }; 39 | 40 | module.exports = lambdaFunctions; 41 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "tslint:recommended", 4 | "tslint-config-airbnb", 5 | "tslint-react" 6 | ], 7 | "jsRules": { 8 | "object-literal-sort-keys": false 9 | }, 10 | "rules": { 11 | "variable-name": [ 12 | true, 13 | "allow-leading-underscore", 14 | "ban-keywords" 15 | ], 16 | "function-name": [ 17 | true, 18 | { 19 | "method-regex": "^[a-z][\\w\\d]+$", 20 | "private-method-regex": "^[a-z][\\w\\d]+$", 21 | "protected-method-regex": "^[a-z][\\w\\d]+$", 22 | "static-method-regex": "^[a-zA-Z_\\d]+$", 23 | "function-regex": "^[a-zA-Z][\\w\\d]+$" 24 | } 25 | ], 26 | "semicolon": [true, "always", "ignore-bound-class-methods"], 27 | "import-name": false, 28 | "ordered-imports": false, 29 | "ter-arrow-parens": false, 30 | "object-literal-sort-keys": false, 31 | "object-shorthand-properties-first": false, 32 | "interface-name": ["never-prefix"], 33 | "max-line-length": [150], 34 | "no-duplicate-imports": false, 35 | "jsx-no-multiline-js": false, 36 | "prefer-array-literal": false, 37 | "cyclomatic-complexity": [ 38 | true, 39 | 12 40 | ], 41 | "jsx-boolean-value": false 42 | }, 43 | "rulesDirectory": [] 44 | } 45 | -------------------------------------------------------------------------------- /config.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | GARMIN_CONNECT: { 3 | EMAIL: 'roest.martin%40gmail.com', 4 | SSO_URL: 'https://sso.garmin.com/sso/signin?service=https%3A%2F%2Fconnect.garmin.com%2Fmodern%2F&webhost=https%3A%2F%2Fconnect.garmin.com&source=https%3A%2F%2Fconnect.garmin.com%2Fnl-NL%2Fsignin&redirectAfterAccountLoginUrl=https%3A%2F%2Fconnect.garmin.com%2Fmodern%2F&redirectAfterAccountCreationUrl=https%3A%2F%2Fconnect.garmin.com%2Fmodern%2F&gauthHost=https%3A%2F%2Fsso.garmin.com%2Fsso&locale=nl_NL&id=gauth-widget&cssUrl=https%3A%2F%2Fstatic.garmincdn.com%2Fcom.garmin.connect%2Fui%2Fcss%2Fgauth-custom-v1.2-min.css&privacyStatementUrl=%2F%2Fconnect.garmin.com%2Fnl-NL%2Fprivacy%2F&clientId=GarminConnect&rememberMeShown=true&rememberMeChecked=false&createAccountShown=true&openCreateAccount=false&displayNameShown=false&consumeServiceTicket=false&initialFocus=true&embedWidget=false&generateExtraServiceTicket=true&generateTwoExtraServiceTickets=false&generateNoServiceTicket=false&globalOptInShown=true&globalOptInChecked=false&mobile=false&connectLegalTerms=true&locationPromptShown=true', 5 | WELLNESS_MONTHLY_URL: 'https://connect.garmin.com/modern/proxy/userstats-service/wellness/monthly/martinroest', 6 | WELLNESS_WEEKLY_URL: 'https://connect.garmin.com/modern/proxy/userstats-service/wellness/weekly/martinroest', 7 | ACTIVITY_URL: 'https://connect.garmin.com/modern/proxy/fitnessstats-service/activity', 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "src/**/*" 4 | ], 5 | "compilerOptions": { 6 | "removeComments": false, 7 | "skipLibCheck": true, 8 | "esModuleInterop": true, 9 | "noLib": false, 10 | "strict": true, /* Enable all strict type-checking options. */ 11 | "noImplicitAny": false, /* Raise error on expressions and declarations with an implied 'any' type. */ 12 | "strictNullChecks": true, /* Enable strict null checks. */ 13 | "strictFunctionTypes": true, /* Enable strict checking of function types. */ 14 | "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 15 | "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 16 | "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 17 | "downlevelIteration": true, 18 | 19 | /* Additional Checks */ 20 | "noUnusedLocals": true, /* Report errors on unused locals. */ 21 | "noUnusedParameters": false, /* Report errors on unused parameters. */ 22 | "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 23 | "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 24 | "forceConsistentCasingInFileNames": true, 25 | "allowSyntheticDefaultImports": true, 26 | 27 | "noEmit": false, 28 | "sourceMap": true, 29 | "target": "es6", 30 | "lib": ["es6", "dom", "es2017.string"], 31 | "moduleResolution": "node", 32 | "allowJs": true, 33 | "jsx": "react" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /serverless/api.ts: -------------------------------------------------------------------------------- 1 | import { DynamoDB } from 'aws-sdk'; 2 | 3 | export function heartrate(event, context, callback) { 4 | handler(getMaxHr, callback); 5 | } 6 | 7 | export function distance(event, context, callback) { 8 | handler(getDistance, callback); 9 | } 10 | 11 | export function duration(event, context, callback) { 12 | handler(getDuration, callback); 13 | } 14 | 15 | export function steps(event, context, callback) { 16 | handler(getSteps, callback); 17 | } 18 | 19 | export function wellness(event, context, callback) { 20 | handler(getWellness, callback); 21 | } 22 | 23 | const handler = async (itemCaller: () => Promise, callbackHandler) => { 24 | try { 25 | const items = await itemCaller(); 26 | callbackHandler(null, { 27 | statusCode: 200, 28 | body: JSON.stringify(items), 29 | headers: { 30 | 'Access-Control-Allow-Credentials': true, 31 | 'Access-Control-Allow-Origin': '*', 32 | }, 33 | }); 34 | } catch (error) { 35 | callbackHandler(error.message, { 36 | statusCode: 500, 37 | body: JSON.stringify(error.message), 38 | headers: { 39 | 'Access-Control-Allow-Credentials': true, 40 | 'Access-Control-Allow-Origin': '*', 41 | }, 42 | }); 43 | } 44 | }; 45 | 46 | const getMaxHr = async () => { 47 | const dbClient = new DynamoDB.DocumentClient(); 48 | return dbClient.scan({ TableName: 'maxhr' }).promise(); 49 | }; 50 | 51 | const getDistance = async () => { 52 | const dbClient = new DynamoDB.DocumentClient(); 53 | return dbClient.scan({ TableName: 'distance' }).promise(); 54 | }; 55 | 56 | const getDuration = async () => { 57 | const dbClient = new DynamoDB.DocumentClient(); 58 | return dbClient.scan({ TableName: 'duration' }).promise(); 59 | }; 60 | 61 | const getSteps = async () => { 62 | const dbClient = new DynamoDB.DocumentClient(); 63 | return dbClient.scan({ TableName: 'steps' }).promise(); 64 | }; 65 | 66 | const getWellness = async () => { 67 | const dbClient = new DynamoDB.DocumentClient(); 68 | return dbClient.scan({ TableName: 'wellness' }).promise(); 69 | }; 70 | -------------------------------------------------------------------------------- /serverless/handler.ts: -------------------------------------------------------------------------------- 1 | import { importMaxHr } from '../src/ImportMaxHrHandler'; 2 | import { importDistance } from '../src/ImportDistanceHandler'; 3 | import { importDuration } from '../src/ImportDurationHandler'; 4 | import { importSteps } from '../src/ImportStepsHandler'; 5 | import { importRestHr } from '../src/ImportRestHrHandler'; 6 | import { importSleepDuration } from '../src/ImportSleepDurationHandler'; 7 | import { importStressLevel } from '../src/ImportStressLevelHandler'; 8 | 9 | export const heartRate = async (event, context, callback) => { 10 | try { 11 | await importMaxHr(); 12 | callback(null, { statusCode: 200, body: 'OK' }); 13 | } catch (error) { 14 | callback(error.message); 15 | } 16 | }; 17 | 18 | export const restHr = async (event, context, callback) => { 19 | try { 20 | await importRestHr(); 21 | callback(null, { statusCode: 200, body: 'OK' }); 22 | } catch (error) { 23 | callback(error.message); 24 | } 25 | }; 26 | 27 | export const distance = async (event, context, callback) => { 28 | try { 29 | await importDistance(); 30 | callback(null, { statusCode: 200, body: 'OK' }); 31 | } catch (error) { 32 | callback(error.message); 33 | } 34 | }; 35 | 36 | export const duration = async (event, context, callback) => { 37 | try { 38 | await importDuration(); 39 | callback(null, { statusCode: 200, body: 'OK' }); 40 | } catch (error) { 41 | callback(error.message); 42 | } 43 | }; 44 | 45 | export const steps = async (event, context, callback) => { 46 | try { 47 | await importSteps(); 48 | callback(null, { statusCode: 200, body: 'OK' }); 49 | } catch (error) { 50 | callback(error.message); 51 | } 52 | }; 53 | 54 | export const sleep = async (event, context, callback) => { 55 | try { 56 | await importSleepDuration(); 57 | callback(null, { statusCode: 200, body: 'OK' }); 58 | } catch (error) { 59 | callback(error.message); 60 | } 61 | }; 62 | 63 | export const stress = async (event, context, callback) => { 64 | try { 65 | await importStressLevel(); 66 | callback(null, { statusCode: 200, body: 'OK' }); 67 | } catch (error) { 68 | callback(error.message); 69 | } 70 | }; 71 | -------------------------------------------------------------------------------- /serverless.yml: -------------------------------------------------------------------------------- 1 | service: 2 | name: PersonalHealthDashboard 3 | 4 | # Add the serverless-webpack plugin 5 | plugins: 6 | - serverless-webpack 7 | - serverless-offline 8 | 9 | # Pool Id eu-west-2_hwda35xPV 10 | # Pool ARN arn:aws:cognito-idp:eu-west-2:552281511161:userpool/eu-west-2_hwda35xPV 11 | # App client ID 6b85ma6aotsan5p8l8g8evsi3 12 | # Domain https://phd.auth.eu-west-2.amazoncognito.com 13 | # Identity Pool ID eu-west-2:3bad1577-1fcd-47bb-83d5-419fd552d7d7 14 | # API client ID 15 | 16 | provider: 17 | name: aws 18 | region: eu-west-2 19 | stage: prod 20 | runtime: nodejs8.10 21 | iamRoleStatements: 22 | - Effect: Allow 23 | Action: 24 | - dynamodb:DescribeTable 25 | - dynamodb:Query 26 | - dynamodb:Scan 27 | - dynamodb:GetItem 28 | - dynamodb:PutItem 29 | - dynamodb:UpdateItem 30 | - dynamodb:DeleteItem 31 | Resource: 32 | - "arn:aws:dynamodb:eu-west-2:*:*" 33 | - Effect: Allow 34 | Action: 35 | - ssm:GetParameter 36 | Resource: 37 | - 'Fn::Join': 38 | - ':' 39 | - - 'arn:aws:ssm' 40 | - Ref: 'AWS::Region' 41 | - Ref: 'AWS::AccountId' 42 | - 'parameter/gc_password' 43 | custom: 44 | webpack: 45 | webpackConfig: ./webpack.config.js 46 | includeModules: true 47 | 48 | resources: 49 | Resources: 50 | maxhr: 51 | Type: AWS::DynamoDB::Table 52 | Properties: 53 | TableName: "maxhr" 54 | AttributeDefinitions: 55 | - AttributeName: "date" 56 | AttributeType: "S" 57 | KeySchema: 58 | - AttributeName: "date" 59 | KeyType: "HASH" 60 | ProvisionedThroughput: 61 | ReadCapacityUnits: 5 62 | WriteCapacityUnits: 5 63 | distance: 64 | Type: AWS::DynamoDB::Table 65 | Properties: 66 | TableName: "distance" 67 | AttributeDefinitions: 68 | - AttributeName: "date" 69 | AttributeType: "S" 70 | KeySchema: 71 | - AttributeName: "date" 72 | KeyType: "HASH" 73 | ProvisionedThroughput: 74 | ReadCapacityUnits: 5 75 | WriteCapacityUnits: 5 76 | duration: 77 | Type: AWS::DynamoDB::Table 78 | Properties: 79 | TableName: "duration" 80 | AttributeDefinitions: 81 | - AttributeName: "date" 82 | AttributeType: "S" 83 | KeySchema: 84 | - AttributeName: "date" 85 | KeyType: "HASH" 86 | ProvisionedThroughput: 87 | ReadCapacityUnits: 5 88 | WriteCapacityUnits: 5 89 | steps: 90 | Type: AWS::DynamoDB::Table 91 | Properties: 92 | TableName: "steps" 93 | AttributeDefinitions: 94 | - AttributeName: "date" 95 | AttributeType: "S" 96 | KeySchema: 97 | - AttributeName: "date" 98 | KeyType: "HASH" 99 | ProvisionedThroughput: 100 | ReadCapacityUnits: 5 101 | WriteCapacityUnits: 5 102 | wellness: 103 | Type: AWS::DynamoDB::Table 104 | Properties: 105 | TableName: "wellness" 106 | AttributeDefinitions: 107 | - AttributeName: "date" 108 | AttributeType: "S" 109 | KeySchema: 110 | - AttributeName: "date" 111 | KeyType: "HASH" 112 | ProvisionedThroughput: 113 | ReadCapacityUnits: 5 114 | WriteCapacityUnits: 5 115 | 116 | functions: 117 | heartrate: 118 | handler: serverless/handler.heartRate 119 | events: 120 | - schedule: rate(30 minutes) 121 | resthr: 122 | handler: serverless/handler.restHr 123 | events: 124 | - schedule: rate(30 minutes) 125 | distance: 126 | handler: serverless/handler.distance 127 | events: 128 | - schedule: rate(30 minutes) 129 | duration: 130 | handler: serverless/handler.duration 131 | events: 132 | - schedule: rate(30 minutes) 133 | steps: 134 | handler: serverless/handler.steps 135 | events: 136 | - schedule: rate(30 minutes) 137 | sleep: 138 | handler: serverless/handler.sleep 139 | events: 140 | - schedule: rate(30 minutes) 141 | stress: 142 | handler: serverless/handler.stress 143 | events: 144 | - schedule: rate(30 minutes) 145 | api-heartrate: 146 | handler: serverless/api.heartrate 147 | events: 148 | - http: 149 | path: heartrate 150 | method: get 151 | cors: true 152 | authorizer: aws_iam 153 | api-distance: 154 | handler: serverless/api.distance 155 | events: 156 | - http: 157 | path: distance 158 | method: get 159 | cors: true 160 | authorizer: aws_iam 161 | api-duration: 162 | handler: serverless/api.duration 163 | events: 164 | - http: 165 | path: duration 166 | method: get 167 | cors: true 168 | authorizer: aws_iam 169 | api-steps: 170 | handler: serverless/api.steps 171 | events: 172 | - http: 173 | path: steps 174 | method: get 175 | cors: true 176 | authorizer: aws_iam 177 | api-wellness: 178 | handler: serverless/api.wellness 179 | events: 180 | - http: 181 | path: wellness 182 | method: get 183 | cors: true 184 | authorizer: aws_iam 185 | -------------------------------------------------------------------------------- /src/GConnect/client.ts: -------------------------------------------------------------------------------- 1 | import Axios, { AxiosInstance, AxiosRequestConfig } from 'axios'; 2 | import axiosCookieJarSupport from 'axios-cookiejar-support'; 3 | import tough, { CookieJar } from 'tough-cookie'; 4 | import { MonthlyMaxHrCollection, MonthlyMaxHrInterface } from '../Entities/MonthlyMaxHr'; 5 | import { List } from 'immutable'; 6 | import { MonthlyDistanceCollection, MonthlyDistanceInterface } from '../Entities/MonthlyDistance'; 7 | import { MonthlyDurationCollection, MonthlyDurationInterface } from '../Entities/MonthlyDuration'; 8 | import { SSM } from 'aws-sdk'; 9 | import { MonthlyStepsCollection, MonthlyStepsInterface } from '../Entities/MonthlySteps'; 10 | import moment from 'moment'; 11 | import config from '../../config'; 12 | import { 13 | WeeklyRestHeartRateCollection, 14 | WeeklySleepDuration, 15 | WeeklySleepDurationCollection, WeeklyStressLevelCollection, 16 | } from '../Entities/Wellness'; 17 | 18 | export type GConnectTicket = string; 19 | 20 | interface ActivityParams { 21 | aggregation: 'monthly' | 'weekly'; 22 | startDate: string; 23 | userFirstDay: string; 24 | groupByParentActivityType: boolean; 25 | endDate: string; 26 | metric: string; 27 | } 28 | 29 | class GConnect { 30 | private static getEndDate = (): string => { 31 | return moment().format('YYYY-MM-') + moment().endOf('month').format('DD'); 32 | }; 33 | 34 | private static getActivityParams(metric: string): ActivityParams { 35 | return { 36 | aggregation: 'monthly', 37 | userFirstDay: 'monday', 38 | groupByParentActivityType: false, 39 | startDate: moment().subtract(3, 'months').format('YYYY-MM-01'), 40 | endDate: this.getEndDate(), 41 | metric, 42 | }; 43 | } 44 | 45 | private readonly axios: AxiosInstance; 46 | private readonly cookieJar: CookieJar; 47 | private readonly requestConfig: AxiosRequestConfig; 48 | 49 | constructor(axios: AxiosInstance) { 50 | this.axios = axios; 51 | this.cookieJar = new tough.CookieJar(); 52 | axiosCookieJarSupport(this.axios); 53 | 54 | this.requestConfig = { 55 | withCredentials: true, 56 | jar: this.cookieJar, 57 | validateStatus: (status: number) => { 58 | return status >= 200 && status <= 302; 59 | }, 60 | }; 61 | 62 | } 63 | 64 | public async getWeeklySleepDuration(): Promise { 65 | await this.login(); 66 | const wellnessParams = { 67 | fromWeekStartDate: moment().subtract(3, 'months').format('YYYY-MM-01'), 68 | metricId: 26, 69 | }; 70 | 71 | const response = await Axios.get( 72 | config.GARMIN_CONNECT.WELLNESS_WEEKLY_URL, 73 | { ...this.requestConfig, ...{ params: wellnessParams } }, 74 | ); 75 | if (!response.data.allMetrics.metricsMap.SLEEP_SLEEP_DURATION) { 76 | throw new Error('Invalid response'); 77 | } 78 | return List(response.data.allMetrics.metricsMap.SLEEP_SLEEP_DURATION.map((item: any): WeeklySleepDuration => { 79 | return { 80 | date: item.startDateOfWeek, 81 | sleep: item.value, 82 | }; 83 | })); 84 | } 85 | 86 | public async getMaxHrData(): Promise { 87 | await this.login(); 88 | 89 | const response = await Axios.get( 90 | config.GARMIN_CONNECT.ACTIVITY_URL, 91 | { ...this.requestConfig, ...{ params: GConnect.getActivityParams('maxHr') } }, 92 | ); 93 | 94 | return List(response.data.map((item: any): MonthlyMaxHrInterface => { 95 | return { 96 | date: item.date, 97 | maxHr: item.stats.all.maxHr.max, 98 | }; 99 | })); 100 | } 101 | 102 | public async getMonthlySteps(): Promise { 103 | await this.login(); 104 | const wellnessParams = { 105 | fromMonthStartDate: moment().subtract(3, 'months').format('YYYY-MM-01'), 106 | metricId: 29, 107 | }; 108 | 109 | const response = await Axios.get( 110 | config.GARMIN_CONNECT.WELLNESS_MONTHLY_URL, 111 | { ...this.requestConfig, ...{ params: wellnessParams } }, 112 | ); 113 | 114 | if (!response.data.allMetrics.metricsMap.WELLNESS_TOTAL_STEPS) { 115 | throw new Error('Invalid response'); 116 | } 117 | return List(response.data.allMetrics.metricsMap.WELLNESS_TOTAL_STEPS.map((item: any): MonthlyStepsInterface => { 118 | return { 119 | date: `${item.month.year}-${String(item.month.monthId).padStart(2, '0')}-01`, 120 | steps: item.value, 121 | }; 122 | })); 123 | } 124 | 125 | public async getWeeklyRestHr(): Promise { 126 | await this.login(); 127 | 128 | const wellnessParams = { 129 | fromWeekStartDate: moment().subtract(3, 'months').format('YYYY-MM-01'), 130 | metricId: 60, 131 | }; 132 | 133 | const response = await Axios.get( 134 | config.GARMIN_CONNECT.WELLNESS_WEEKLY_URL, 135 | { ...this.requestConfig, ...{ params: wellnessParams } }, 136 | ); 137 | if (!response.data.allMetrics.metricsMap.WELLNESS_RESTING_HEART_RATE) { 138 | throw new Error('Invalid response'); 139 | } 140 | return List(response.data.allMetrics.metricsMap.WELLNESS_RESTING_HEART_RATE.map((item: any) => { 141 | return { 142 | date: item.startDateOfWeek, 143 | restHr: item.value, 144 | }; 145 | })); 146 | } 147 | 148 | public async getWeeklyStressLevel(): Promise { 149 | await this.login(); 150 | 151 | const wellnessParams = { 152 | fromWeekStartDate: moment().subtract(3, 'months').format('YYYY-MM-01'), 153 | metricId: 63, 154 | }; 155 | 156 | const response = await Axios.get( 157 | config.GARMIN_CONNECT.WELLNESS_WEEKLY_URL, 158 | { ...this.requestConfig, ...{ params: wellnessParams } }, 159 | ); 160 | if (!response.data.allMetrics.metricsMap.WELLNESS_AVERAGE_STRESS) { 161 | throw new Error('Invalid response'); 162 | } 163 | return List(response.data.allMetrics.metricsMap.WELLNESS_AVERAGE_STRESS.map((item: any) => { 164 | return { 165 | date: item.startDateOfWeek, 166 | stressLevel: item.value, 167 | }; 168 | })); 169 | } 170 | 171 | public async getMonthlyDistance(): Promise { 172 | await this.login(); 173 | 174 | const response = await Axios.get( 175 | config.GARMIN_CONNECT.ACTIVITY_URL, 176 | { ...this.requestConfig, ...{ params: GConnect.getActivityParams('distance') } }, 177 | ); 178 | 179 | return List(response.data.map((item: any): MonthlyDistanceInterface => { 180 | return { 181 | date: item.date, 182 | distance: Math.round(item.stats.all.distance.sum / 100000), 183 | activities: item.stats.all.distance.count, 184 | }; 185 | })); 186 | } 187 | 188 | public async getMonthlyDuration(): Promise { 189 | await this.login(); 190 | 191 | const response = await Axios.get( 192 | config.GARMIN_CONNECT.ACTIVITY_URL, 193 | { ...this.requestConfig, ...{ params: GConnect.getActivityParams('duration') } }, 194 | ); 195 | 196 | return List(response.data.map((item: any): MonthlyDurationInterface => { 197 | return { 198 | date: item.date, 199 | duration: Math.round(item.stats.all.duration.sum), 200 | activities: item.stats.all.duration.count, 201 | }; 202 | })); 203 | } 204 | 205 | private async login(): Promise { 206 | const loginForm = await Axios.get(config.GARMIN_CONNECT.SSO_URL, this.requestConfig); 207 | const csrfMatch = loginForm.data.match(/_csrf" value="([0-9A-Z]+)"/); 208 | if (!csrfMatch) { 209 | throw Error('Unable to login. Can not find CSRF token'); 210 | } 211 | const token = csrfMatch.pop(); 212 | const paramStore = new SSM(); 213 | const getParameterResult = await paramStore.getParameter({ Name: 'gc_password' }).promise(); 214 | if (!getParameterResult.Parameter) { 215 | throw new Error('Unable to get parameter store value'); 216 | } 217 | const response = await Axios.post( 218 | config.GARMIN_CONNECT.SSO_URL, 219 | `username=${config.GARMIN_CONNECT.EMAIL}&embed=false&password=${getParameterResult.Parameter.Value}&_csrf=${token}`, 220 | { 221 | ...this.requestConfig, ...{ 222 | headers: { 223 | Referer: config.GARMIN_CONNECT.SSO_URL, 224 | 'Content-Type': 'application/x-www-form-urlencoded', 225 | }, 226 | }, 227 | }, 228 | ); 229 | const matched: string[] | null = response.data.match(/(ST-[-0-9a-zA-Z]+-cas)/gm); 230 | 231 | if (matched == null) { 232 | throw new Error('Unable to login'); 233 | } 234 | const ticket = matched.pop(); 235 | await Axios.get(`https://connect.garmin.com/modern/?ticket=${ticket}`, this.requestConfig); 236 | return ticket || ''; 237 | } 238 | } 239 | 240 | export default GConnect; 241 | --------------------------------------------------------------------------------