# Google Health Integration (Former Fitbit)

**URL:** <https://community.homey.app/t/google-health-integration-former-fitbit/155332>\
**Category:** Devices\
**Tags:** homeyscript\
**Created:** [May 25, 2026, 8:26pm UTC](https://community.homey.app/t/google-health-integration-former-fitbit/155332 "2026-05-25T20:26:13Z")\
**Posts on this page:** 4\
**Page:** 1

<div class="post-metadata">

**Author:** ![Arcondor](https://sea1.discourse-cdn.com/flex025/user_avatar/community.homey.app/arcondor/32/123744_2.png) [@Arcondor](https://community.homey.app/u/Arcondor)\
**Post date:** [May 25, 2026, 8:26pm UTC](https://community.homey.app/t/google-health-integration-former-fitbit/155332/1 "2026-05-25T20:26:13Z")

</div>

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.

1. Go to the [Google Cloud Console](https://console.cloud.google.com/) and create a new Project (e.g., “Homey Health API”).
2. Navigate to **APIs & Services \> Library** , search for the **Fitness API** , and click _Enable_.
3. 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).

1. 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!

1. **Get your Refresh Token:**

- Go to the [Google OAuth 2.0 Playground](https://developers.google.com/oauthplayground/).
- 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_token` that appears in the response box.

### Step 2: Configure Homey Logic & HomeyScript

Now we need to tell Homey how to pull and handle the data.

1. Go to Homey **Logic Variables** and create two new Text variables:

- `GoogleAccess` (Leave this blank for now)
- `GoogleRefresh` (Paste your `refresh_token` from the Playground here)

1. Open **HomeyScript** and create a new script (e.g., `GoogleHealth.js`).
2. Paste the following code. **Important:** Replace the `clientId` and `clientSecret` variables at the very top with your own credentials from Step 1!

Homeyscript

```auto
==========================================
// 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.

1. Install the [Device Capabilities App](https://www.google.com/search?q=https://homey.app/en-us/app/com.arjankranenburg.virtual/Device-Capabilities/) on your Homey Pro.
2. Add a new device → Choose **Device Capabilities** → **Advanced Virtual Device (AVD)**. Name it something like “Google Health”.
3. 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)

1. **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!

 ![30152](https://us1.discourse-cdn.com/flex025/uploads/athom/original/3X/2/8/2882f325d05849bc1733903caed86ecc1dc37a0b.jpeg)

---

<div class="post-metadata">

**Author:** ![Peter\_Kawa](https://sea1.discourse-cdn.com/flex025/user_avatar/community.homey.app/peter_kawa/32/173848_2.png) [@Peter\_Kawa](https://community.homey.app/u/Peter_Kawa)\
**Post date:** [May 25, 2026, 11:30pm UTC](https://community.homey.app/t/google-health-integration-former-fitbit/155332/2 "2026-05-25T23:30:02Z")

</div>

Hi,

Your code looks very messed up, please follow advice:

> [@\[Tip\] Please use Preformatted Text for Logs and Code Blocks for cleaner posts](https://community.homey.app/t/tip-please-use-preformatted-text-for-logs-and-code-blocks-for-cleaner-posts/149488):
>
> To ensure our community can provide the best help, please use Preformatted Text for all technical content. Why Use Preformatted Text? When you paste logs, configuration files, or code snippets as plain text, Discourse often alters the formatting. This makes it difficult to read and can strip away crucial characters needed to diagnose your issue. If you didn’t create it yourself, like logs or JSONs (interviews) paste it as Preformatted Text! For smaller Code, Logs etc. in context of a topic, b…

---

<div class="post-metadata">

**Author:** ![Arcondor](https://sea1.discourse-cdn.com/flex025/user_avatar/community.homey.app/arcondor/32/123744_2.png) [@Arcondor](https://community.homey.app/u/Arcondor)\
**Post date:** [May 26, 2026, 5:12am UTC](https://community.homey.app/t/google-health-integration-former-fitbit/155332/3 "2026-05-26T05:12:16Z")

</div>

Hi Peter,

Thank you for your advice. I preformatted the script.

Best regards

---

<div class="post-metadata">

**Author:** ![Peter\_Kawa](https://sea1.discourse-cdn.com/flex025/user_avatar/community.homey.app/peter_kawa/32/173848_2.png) [@Peter\_Kawa](https://community.homey.app/u/Peter_Kawa)\
**Post date:** [May 26, 2026, 9:08pm UTC](https://community.homey.app/t/google-health-integration-former-fitbit/155332/4 "2026-05-26T21:08:38Z")

</div>

Fantastic! Thx
