Hey everyone,
Having all my metrics in one central dashboard is incredibly useful, especially for tracking outdoor activities or indoor workouts. I wanted to bring my daily Google Health (formerly Fitbit) data directly into my Homey Pro to display it on a custom dashboard.
The Result: A fully automated Advanced Virtual Device (AVD) that updates throughout the day. It perfectly extracts and calculates real-time data from the Google Health v4 API, including Steps, Calories, Heart Rate, HRV, VO2Max, and actual Cardio Load (accurately combining the Cardio and Peak heart rate zones, just like the Fitbit app) as well as daily Distance in kilometers.
Here is the step-by-step guide on how to set this up. Grab a coffee, sit back, and let’s get this running!
Step 1: Set up the Google Cloud Console
To access your personal health data, you need to create an API project in Google Cloud to generate the necessary OAuth tokens.
- Go to the Google Cloud Console and create a new Project (e.g., “Homey Health API”).
- Navigate to APIs & Services > Library, search for the Fitness API, and click Enable.
- Go to OAuth consent screen:
- Choose External and create.
- Fill in the mandatory app details (just a name and your email address).
- Under Test users, make sure to add your own Google email address (the one you use for your health data).
- Go to Credentials:
- Click Create Credentials → OAuth client ID.
- Application type: Web application.
- Under Authorized redirect URIs, add exactly this URL:
https://developers.google.com/oauthplayground - Save it. You will now see your Client ID and Client Secret. Keep these handy!
- Get your Refresh Token:
- Go to the Google OAuth 2.0 Playground.
- Click the gear icon (Settings) in the top right, check “Use your own OAuth credentials”, and paste your Client ID and Client Secret.
- On the left, input these scopes in the text field at the bottom of the list and click Authorize APIs:
https://www.googleapis.com/auth/fitness.activity.read, https://www.googleapis.com/auth/fitness.body.read, https://www.googleapis.com/auth/fitness.heart_rate.read - Log into your Google account and grant permission.
- Click Exchange authorization code for tokens. Save the
refresh_tokenthat appears in the response box.
Step 2: Configure Homey Logic & HomeyScript
Now we need to tell Homey how to pull and handle the data.
- Go to Homey Logic Variables and create two new Text variables:
GoogleAccess(Leave this blank for now)GoogleRefresh(Paste yourrefresh_tokenfrom the Playground here)
- Open HomeyScript and create a new script (e.g.,
GoogleHealth.js). - Paste the following code. Important: Replace the
clientIdandclientSecretvariables at the very top with your own credentials from Step 1!
Homeyscript
==========================================
// Google Health API v4: Master Dashboard
// ==========================================
const clientId = 'YOUR_CLIENT_ID_HERE';
const clientSecret = 'YOUR_CLIENT_SECRET_HERE';
// 1. Load Tokens
const vars = await Homey.logic.getVariables();
const accessVar = Object.values(vars).find(v => v.name === 'GoogleAccess');
const refreshVar = Object.values(vars).find(v => v.name === 'GoogleRefresh');
if (!refreshVar || !accessVar) return { error: "Logic variables missing!" };
let accessToken = accessVar.value || '';
// Token Refresh Logic with Lock to prevent Race Conditions
let refreshPromise = null;
async function safeTokenRefresh() {
if (refreshPromise) {
log('Waiting for running token refresh...');
return refreshPromise;
}
refreshPromise = refreshGoogleToken();
try {
await refreshPromise;
} finally {
refreshPromise = null;
}
return accessToken;
}
async function refreshGoogleToken() {
log('Refreshing OAuth Token...');
const response = await fetch('https://oauth2.googleapis.com/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: `client_id=${clientId}&client_secret=${clientSecret}&refresh_token=${refreshVar.value}&grant_type=refresh_token`
});
const data = await response.json();
if (data.error) throw new Error('Refresh failed: ' + data.error_description);
accessToken = data.access_token;
await Homey.logic.updateVariable({ id: accessVar.id, variable: { value: accessToken } });
return accessToken;
}
// RollUp Fetch (for daily sums like steps/calories/distance)
async function getDailyRollUp(dataType) {
const url = `https://health.googleapis.com/v4/users/me/dataTypes/${dataType}/dataPoints:dailyRollUp`;
const now = new Date();
const tomorrow = new Date(now);
tomorrow.setDate(now.getDate() + 1);
const body = {
range: {
start: {
date: { year: now.getFullYear(), month: now.getMonth() + 1, day: now.getDate() },
time: { hours: 0, minutes: 0, seconds: 0, nanos: 0 }
},
end: {
date: { year: tomorrow.getFullYear(), month: tomorrow.getMonth() + 1, day: tomorrow.getDate() },
time: { hours: 0, minutes: 0, seconds: 0, nanos: 0 }
}
},
windowSizeDays: 1
};
let response = await fetch(url, {
method: 'POST',
headers: { 'Authorization': 'Bearer ' + accessToken, 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
if (response.status === 401) {
await safeTokenRefresh();
response = await fetch(url, {
method: 'POST',
headers: { 'Authorization': 'Bearer ' + accessToken, 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
}
return await response.json();
}
// Standard Fetch (for snapshots like Pulse/HRV/VO2max)
async function getHealthData(dataType) {
const url = `https://health.googleapis.com/v4/users/me/dataTypes/${dataType}/dataPoints`;
let response = await fetch(url, { headers: { 'Authorization': 'Bearer ' + accessToken } });
if (response.status === 401) {
await safeTokenRefresh();
response = await fetch(url, { headers: { 'Authorization': 'Bearer ' + accessToken } });
}
return await response.json();
}
try {
log('--- START GOOGLE HEALTH V4 MASTER FETCH ---');
const [steps, azm, cal, hr, hrv, vo2, dist] = await Promise.all([
getDailyRollUp('steps'),
getDailyRollUp('active-zone-minutes'),
getDailyRollUp('total-calories'),
getHealthData('heart-rate'),
getHealthData('heart-rate-variability'),
getHealthData('vo2-max'),
getDailyRollUp('distance')
]);
const getRollUpValue = (data, unionField, possibleKeys) => {
if (data.rollupDataPoints && data.rollupDataPoints.length > 0) {
const dp = data.rollupDataPoints[0];
if (dp[unionField]) {
for (const key of possibleKeys) {
if (dp[unionField][key] !== undefined) return Number(dp[unionField][key]);
}
}
}
return 0;
};
const getDataPointValue = (data, unionField, possibleKeys) => {
if (data.dataPoints && data.dataPoints.length > 0) {
const dp = data.dataPoints[0];
if (dp[unionField]) {
const keysArray = Array.isArray(possibleKeys) ? possibleKeys : [possibleKeys];
for (const key of keysArray) {
if (dp[unionField][key] !== undefined) return Number(dp[unionField][key]);
}
}
}
return 0;
};
const stepsToday = getRollUpValue(steps, 'steps', ['countSum', 'count']);
const calories = Math.round(getRollUpValue(cal, 'totalCalories', ['kcalSum', 'caloriesKcal', 'calories', 'energy', 'value']));
const pulseCurrent = getDataPointValue(hr, 'heartRate', ['beatsPerMinute', 'value']);
const stressHrv = getDataPointValue(hrv, 'heartRateVariability', ['rootMeanSquareOfSuccessiveDifferencesMilliseconds', 'value']);
let vo2MaxCurrent = getDataPointValue(vo2, 'vo2Max', ['vo2MaxValue', 'vo2Max', 'value']);
vo2MaxCurrent = Math.round(vo2MaxCurrent * 10) / 10;
// Distance is provided in millimeters by the API, converting to kilometers
const distanceMillimeters = getRollUpValue(dist, 'distance', ['millimetersSum', 'meters', 'value']);
const distanceKm = Math.round((distanceMillimeters / 1000000) * 100) / 100;
// Specific extraction to calculate true Cardio Load (Cardio Zone + Peak Zone)
let cardioMinutes = 0;
if (azm.rollupDataPoints && azm.rollupDataPoints.length > 0 && azm.rollupDataPoints[0].activeZoneMinutes) {
const zones = azm.rollupDataPoints[0].activeZoneMinutes;
const cardio = Number(zones.sumInCardioHeartZone || 0);
const peak = Number(zones.sumInPeakHeartZone || 0);
cardioMinutes = cardio + peak;
}
await tag('Steps', stepsToday);
await tag('Cardio', cardioMinutes);
await tag('Calories', calories);
await tag('Pulse', pulseCurrent);
await tag('HRV', stressHrv);
await tag('VO2Max', vo2MaxCurrent);
await tag('Distance', distanceKm);
log(`RESULT -> Steps: ${stepsToday}, Cardio: ${cardioMinutes}, Cal: ${calories}, Pulse: ${pulseCurrent}, HRV: ${stressHrv}, VO2Max: ${vo2MaxCurrent}, Distance: ${distanceKm} km`);
return true;
} catch (e) {
log('Critical Error:', e.message);
return false;
}`
Step 3: Create the Virtual Device (Device Capabilities)
To display the data beautifully and use it in your automations, we will use the Device Capabilities app.
- Install the Device Capabilities App on your Homey Pro.
- Add a new device → Choose Device Capabilities → Advanced Virtual Device (AVD). Name it something like “Google Health”.
- Edit the device settings and create Number Fields for your metrics. I recommend adding these custom fields:
- Steps
- Cardio Load (Unit: min)
- Calories (Unit: kcal)
- Pulse (Unit: bpm)
- HRV (Unit: ms)
- VO2Max
- Distance (Unit: km)
- Create a Flow to link them up:
- WHEN: Every 15 Minutes (or choose your preferred interval).
- THEN: Run the HomeyScript
GoogleHealth.js. - AND THEN: (Set a 2-second delay to ensure the script finishes processing) → Update the AVD fields using the Local Tokens/Tags generated by the HomeyScript (e.g., map the AVD “Distance” field to the script tag
Distance).
And that’s it! The script automatically handles token refreshes and edge-case payload parsing in the background, making it a true “set and forget” integration.
If you have any questions or want to extract even more metrics, feel free to ask!
