Withings Health Binding [5.0.0.0;6.0.0.0)

Key Value
uid withings
type binding
author Nanna Agesen
version range [5.0.0;6.0.0)
download org.openhab.binding.withings-5.2.0-SNAPSHOT.jar

Withings Health Binding

A comprehensive openHAB binding for Withings health devices — smart scales, blood pressure monitors, activity trackers, sleep monitors, and thermometers. Retrieves body measurements, cardiovascular data, activity metrics, and sleep analysis via the Withings Cloud API v2 with full OAuth2 authorization.

Features

  • Body Composition — Weight, fat ratio, fat mass, fat-free mass, muscle mass, bone mass, hydration from WiFi smart scales
  • Cardiovascular Monitoring — Heart rate, systolic/diastolic blood pressure, SpO2, pulse wave velocity, VO2 max, vascular age, body temperature
  • Activity Tracking — Steps, distance, calories, elevation, light/moderate/intense activity durations, heart rate zones
  • Sleep Analysis — Total sleep, deep/light/REM duration, wakeup count, sleep score, snoring, heart rate and respiration during sleep
  • OAuth2 Web Authorization — Built-in servlet at /withings for browser-based authorization (modeled after HomeConnect binding)
  • Automatic Token Refresh — Tokens refreshed transparently before expiry, persisted via StorageService (survives reboots — even with .things file configuration)
  • Multi-Person Support — Multiple person things per account bridge, filtered by Withings user ID
  • Configurable Polling — Separate intervals for body (15 min), activity (30 min), and sleep (60 min)
  • User Filtering — Measurements filtered by user ID to avoid mixing data from shared scales

Supported Things

Thing Type Description
account (Bridge) Withings API account — manages OAuth2 tokens
person Individual user — polls measurements by user ID

Channels

Body Measurements

Channel Type Description
body#weight Number:Mass Body weight (kg)
body#fatRatio Number Body fat percentage (%)
body#fatMass Number:Mass Fat mass (kg)
body#fatFreeMass Number:Mass Fat-free / lean body mass (kg)
body#muscleMass Number:Mass Muscle mass (kg)
body#boneMass Number:Mass Bone mass (kg)
body#hydration Number:Mass Body hydration (kg)
body#lastMeasurement DateTime Timestamp of most recent measurement

Devices: Body, Body+, Body Comp, Body Scan

Cardiovascular

Channel Type Description
cardiovascular#heartPulse Number Heart rate (bpm)
cardiovascular#systolicBP Number:Pressure Systolic blood pressure (mmHg)
cardiovascular#diastolicBP Number:Pressure Diastolic blood pressure (mmHg)
cardiovascular#pulseWaveVelocity Number Pulse wave velocity (m/s)
cardiovascular#vo2Max Number VO2 Max (ml/min/kg)
cardiovascular#vascularAge Number Estimated vascular age (years)
cardiovascular#spo2 Number Blood oxygen saturation (%)
cardiovascular#temperature Number:Temperature Body temperature (°C)

Devices: BPM Connect, BPM Core, Body Scan, ScanWatch, Thermo

Activity

Channel Type Description
activity#steps Number Steps taken today
activity#distance Number:Length Distance travelled (m)
activity#calories Number:Energy Active calories burned (J)
activity#totalCalories Number:Energy Total calories — active + passive (J)
activity#elevation Number Floors climbed
activity#softActivity Number:Time Light activity duration (s)
activity#moderateActivity Number:Time Moderate activity duration (s)
activity#intenseActivity Number:Time Intense activity duration (s)
activity#hrAverage Number Average heart rate (bpm)
activity#hrMin Number Minimum heart rate (bpm)
activity#hrMax Number Maximum heart rate (bpm)

Devices: ScanWatch, Steel HR, Move, Go

Sleep

Channel Type Description
sleep#totalSleepTime Number:Time Total time asleep (s)
sleep#deepSleepDuration Number:Time Deep sleep duration (s)
sleep#lightSleepDuration Number:Time Light sleep duration (s)
sleep#remSleepDuration Number:Time REM sleep duration (s)
sleep#wakeupCount Number Wakeup count
sleep#wakeupDuration Number:Time Time awake during night (s)
sleep#timeToSleep Number:Time Time to fall asleep (s)
sleep#timeToWakeup Number:Time Time in bed after waking (s)
sleep#sleepScore Number Sleep quality score (0–100)
sleep#snoring Number:Time Snoring duration (s)
sleep#sleepHrAverage Number Average HR during sleep (bpm)
sleep#sleepHrMin Number Min HR during sleep (bpm)
sleep#sleepHrMax Number Max HR during sleep (bpm)
sleep#sleepRrAverage Number Average respiration rate (brpm)
sleep#sleepRrMin Number Min respiration rate (brpm)
sleep#sleepRrMax Number Max respiration rate (brpm)

Devices: Sleep Analyzer, ScanWatch, Steel HR

Quick Start

withings.things

Bridge withings:account:home "Withings Account" [
    clientId="YOUR_CLIENT_ID",
    clientSecret="YOUR_CLIENT_SECRET",
    redirectUri="https://your-openhab.example.com/callback"
] {
    Thing person john "John" [userId=12345678, pollingIntervalBody=15, pollingIntervalActivity=30, pollingIntervalSleep=60]
    Thing person jane "Jane" [userId=87654321]
}

withings.items

Group gWithings "Withings Health" <body>

Number:Mass   Withings_Weight         "Weight [%.1f kg]"           <body>  (gWithings) { channel="withings:person:home:john:body#weight" }
Number        Withings_Fat_Ratio      "Fat Ratio [%.1f %%]"        <body>  (gWithings) { channel="withings:person:home:john:body#fatRatio" }
Number:Mass   Withings_Fat_Mass       "Fat Mass [%.1f kg]"         <body>  (gWithings) { channel="withings:person:home:john:body#fatMass" }
Number:Mass   Withings_Fat_Free_Mass  "Fat Free Mass [%.1f kg]"    <body>  (gWithings) { channel="withings:person:home:john:body#fatFreeMass" }
Number:Mass   Withings_Muscle_Mass    "Muscle Mass [%.1f kg]"      <body>  (gWithings) { channel="withings:person:home:john:body#muscleMass" }
Number:Mass   Withings_Bone_Mass      "Bone Mass [%.2f kg]"        <body>  (gWithings) { channel="withings:person:home:john:body#boneMass" }
Number:Mass   Withings_Hydration      "Hydration [%.1f kg]"        <body>  (gWithings) { channel="withings:person:home:john:body#hydration" }
DateTime      Withings_Last           "Last Measurement [%1$td-%1$tm-%1$tY %1$tH:%1$tM]" <time> (gWithings) { channel="withings:person:home:john:body#lastMeasurement" }

OAuth2 Authorization

  1. Configure bridge with clientId, clientSecret, and redirectUri
  2. Open http://your-openhab:8080/withings in a browser
  3. Click “Authorize with Withings” → log in → grant access
  4. Note the User ID shown on success page → use for person thing userId

If behind a reverse proxy (nginx):

location /callback {
    proxy_pass http://127.0.0.1:8080/withings;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-Proto $scheme;
}

Weight Alert Rule

rules.when()
    .item("Withings_Weight").changed()
    .then(event => {
        const weight = items.getItem("Withings_Weight").numericState;
        const previous = event.oldState;
        if (previous !== null) {
            const diff = weight - parseFloat(previous);
            if (Math.abs(diff) > 1.0) {
                actions.NotificationAction.sendBroadcastNotification(
                    `Weight change: ${diff > 0 ? '+' : ''}${diff.toFixed(1)} kg (now ${weight.toFixed(1)} kg)`
                );
            }
        }
    })
    .build("Withings Weight Change Alert");

Prerequisites

  • Withings Developer Account with registered application
  • At least one Withings health device linked to your account
  • Public callback URL for OAuth2 (or use pre-configured tokens)
  • Scopes: user.metrics, user.activity

Resources

5 Likes

Excellent - I have been waiting for this. Currently I have an Home Assistent instance running just for Withings, and polled from OpenHAB via http the Withings data. So this binding would be way more simple for me - thanks!

Only I am having OAuth issues… My OpenHAB is not accessible from the internet, so I am having issues with Redirect URI. I then thought, that the ‘OpenHAB Cloud’ could help: ‘https://home.myopenhab.org/config’. When using '../withings’ I landed with the right parameters in the Withings Authentication page, but when I allowed access I got ‘An error occurred while getting authorization: Invalid state’. In the OpenHAB logs the following was listed:

  • Couldn’t find a user with the provided authentication code pending
  • Token issuing failed: invalid_grant

I then tried entering ‘www.google.com’ for Redirect URI in both, the bridge-binding and the Withings Dev Account. As a result I then received an code, but only one… so this was not sufficient for the bridge binding.

My OpenHAB 5.1.3 runs on RPI on Docker.

~~~~~~~~~~~

Update: Solved :slight_smile:

I then tried as callback URL ‘http://192.168.178.135:8083/withings‘ . So it seems the ‘withings’ in the end made the difference. Then the flow ended with Success!!!
Small observation: in the success window the ‘User ID’ is mentioned, it is required to add an OpenHAB Thing for the User. I have ScanWatch and Body+, waiting now for the values to arrive :-). Thanks again, great binding!

1 Like

Now that the authorization is working fine the binding starts polling. Here is what I found, seems format mismatch:

2026-04-10 12:11:34.931 [ERROR] [g.withings.handler.WithingsApiClient] - Error fetching sleep summary: java.lang.NumberFormatException: Expected an int but was 0.96 at line 1 column 357 path $.body.series[0].data.sleep_efficiency
com.google.gson.JsonSyntaxException: java.lang.NumberFormatException: Expected an int but was 0.96 at line 1 column 357 path $.body.series[0].data.sleep_efficiency
        at com.google.gson.internal.bind.TypeAdapters$7.read(TypeAdapters.java:267) ~[?:?]
        at com.google.gson.internal.bind.TypeAdapters$7.read(TypeAdapters.java:257) ~[?:?]
        at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$2.readIntoField(ReflectiveTypeAdapterFactory.java:271) ~[?:?]
        at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$FieldReflectionAdapter.readField(ReflectiveTypeAdapterFactory.java:561) ~[?:?]
        at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$Adapter.read(ReflectiveTypeAdapterFactory.java:519) ~[?:?]
        at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$2.readIntoField(ReflectiveTypeAdapterFactory.java:271) ~[?:?]
        at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$FieldReflectionAdapter.readField(ReflectiveTypeAdapterFactory.java:561) ~[?:?]
        at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$Adapter.read(ReflectiveTypeAdapterFactory.java:519) ~[?:?]
        at com.google.gson.internal.bind.TypeAdapterRuntimeTypeWrapper.read(TypeAdapterRuntimeTypeWrapper.java:40) ~[?:?]
        at com.google.gson.internal.bind.CollectionTypeAdapterFactory$Adapter.read(CollectionTypeAdapterFactory.java:84) ~[?:?]
        at com.google.gson.internal.bind.CollectionTypeAdapterFactory$Adapter.read(CollectionTypeAdapterFactory.java:64) ~[?:?]
        at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$2.readIntoField(ReflectiveTypeAdapterFactory.java:271) ~[?:?]
        at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$FieldReflectionAdapter.readField(ReflectiveTypeAdapterFactory.java:561) ~[?:?]
        at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$Adapter.read(ReflectiveTypeAdapterFactory.java:519) ~[?:?]
        at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$2.readIntoField(ReflectiveTypeAdapterFactory.java:271) ~[?:?]
        at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$FieldReflectionAdapter.readField(ReflectiveTypeAdapterFactory.java:561) ~[?:?]
        at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$Adapter.read(ReflectiveTypeAdapterFactory.java:519) ~[?:?]
        at com.google.gson.Gson.fromJson(Gson.java:1359) ~[?:?]
        at com.google.gson.Gson.fromJson(Gson.java:1260) ~[?:?]
        at com.google.gson.Gson.fromJson(Gson.java:1170) ~[?:?]
        at com.google.gson.Gson.fromJson(Gson.java:1107) ~[?:?]
        at org.openhab.binding.withings.handler.WithingsApiClient.getSleepSummary(WithingsApiClient.java:254) ~[?:?]
        at org.openhab.binding.withings.handler.WithingsPersonHandler.pollSleep(WithingsPersonHandler.java:409) ~[?:?]
        at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:572) ~[?:?]
        at java.util.concurrent.FutureTask.run(FutureTask.java:317) ~[?:?]
        at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:304) ~[?:?]
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144) ~[?:?]
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642) ~[?:?]
        at java.lang.Thread.run(Thread.java:1583) [?:?]
Caused by: java.lang.NumberFormatException: Expected an int but was 0.96 at line 1 column 357 path $.body.series[0].data.sleep_efficiency
        at com.google.gson.stream.JsonReader.nextInt(JsonReader.java:1353) ~[?:?]
        at com.google.gson.internal.bind.TypeAdapters$7.read(TypeAdapters.java:265) ~[?:?]
        ... 28 more

Yes, same format error here as well.