<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Kristian Partl</title>
    <description>The latest articles on DEV Community by Kristian Partl (@kristianp26).</description>
    <link>https://dev.to/kristianp26</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3938647%2Fcfc434d1-dd60-44a3-a912-3b485eceff6f.png</url>
      <title>DEV Community: Kristian Partl</title>
      <link>https://dev.to/kristianp26</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/kristianp26"/>
    <language>en</language>
    <item>
      <title>I built a tool that syncs 25+ BLE smart scales to InfluxDB, Home Assistant, Garmin, Strava, and more, no phone app needed</title>
      <dc:creator>Kristian Partl</dc:creator>
      <pubDate>Mon, 18 May 2026 17:26:36 +0000</pubDate>
      <link>https://dev.to/kristianp26/i-built-a-tool-that-syncs-25-ble-smart-scales-to-garmin-connect-no-phone-app-needed-aij</link>
      <guid>https://dev.to/kristianp26/i-built-a-tool-that-syncs-25-ble-smart-scales-to-garmin-connect-no-phone-app-needed-aij</guid>
      <description>&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;I own a cheap BLE smart scale. It measures weight &lt;em&gt;and&lt;/em&gt; body composition (fat %, muscle, water, bone mass…) over Bluetooth. Solid hardware. The catch: the only way to get those numbers into &lt;strong&gt;Garmin Connect&lt;/strong&gt; was this ritual:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Step on the scale.&lt;/li&gt;
&lt;li&gt;Open the vendor's phone app.&lt;/li&gt;
&lt;li&gt;Wait for it to connect and sync.&lt;/li&gt;
&lt;li&gt;Read the numbers off the app.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Manually type them into Garmin Connect.&lt;/strong&gt; Every. Single. Time.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If I forgot my phone, or the app's cloud was down, or I just couldn't be bothered — no data point. For something that's only useful as a &lt;em&gt;trend&lt;/em&gt;, missing days quietly kills the whole point.&lt;/p&gt;

&lt;p&gt;I didn't want to depend on a phone app that talks to a vendor cloud I don't control. So I built &lt;strong&gt;&lt;a href="https://github.com/KristianP26/ble-scale-sync" rel="noopener noreferrer"&gt;BLE Scale Sync&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it does
&lt;/h2&gt;

&lt;p&gt;A small always-on device (a &lt;strong&gt;Raspberry Pi Zero 2 W&lt;/strong&gt; in my case) sits next to the scale and listens over Bluetooth. You step on, wait a few seconds, step off — and the reading lands in Garmin Connect. No phone. No app. No manual entry. No vendor cloud. Your data stays on your hardware.&lt;/p&gt;

&lt;p&gt;Garmin is just one target. The same measurement fans out to whatever you configure:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Garmin Connect&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Strava&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MQTT&lt;/strong&gt; → Home Assistant (auto-discovery, sensors appear automatically)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;InfluxDB&lt;/strong&gt; → Grafana dashboards&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Webhook&lt;/strong&gt; → anything&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ntfy&lt;/strong&gt; → push notification on your phone&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;File&lt;/strong&gt; → CSV / JSONL for your own scripts&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And it speaks &lt;strong&gt;25+ scale brands&lt;/strong&gt; — Xiaomi (Mi Scale 2 passive broadcast), Renpho / FITINDEX / QN-Scale, Eufy, Yunmai, Beurer, Sanitas, Medisana, and more. Many protocols were ported from the excellent &lt;a href="https://github.com/oliexdev/openScale" rel="noopener noreferrer"&gt;openScale&lt;/a&gt; project; a handful I reverse-engineered for this one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setup is one command
&lt;/h2&gt;

&lt;p&gt;There's an interactive wizard that does BLE scale auto-discovery, user profiles, exporter selection, and connectivity tests for you:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;-it&lt;/span&gt; &lt;span class="nt"&gt;--network&lt;/span&gt; host &lt;span class="nt"&gt;--cap-add&lt;/span&gt; NET_ADMIN &lt;span class="nt"&gt;--cap-add&lt;/span&gt; NET_RAW &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--group-add&lt;/span&gt; 112 &lt;span class="nt"&gt;-v&lt;/span&gt; /var/run/dbus:/var/run/dbus:ro &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-v&lt;/span&gt; ./config.yaml:/app/config.yaml &lt;span class="se"&gt;\&lt;/span&gt;
  ghcr.io/kristianp26/ble-scale-sync:latest setup
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It generates a &lt;code&gt;config.yaml&lt;/code&gt; you never &lt;em&gt;have&lt;/em&gt; to touch by hand, but it's plain and readable if you want to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;ble&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;scale_mac&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;FF:03:00:13:A1:04'&lt;/span&gt;   &lt;span class="c1"&gt;# so you don't grab a neighbour's scale&lt;/span&gt;

&lt;span class="na"&gt;exporters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;garmin&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;mqtt&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;broker_url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;mqtt://homeassistant.local:1883'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then run it continuously and forget it exists:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--restart&lt;/span&gt; unless-stopped &lt;span class="nt"&gt;--network&lt;/span&gt; host &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--cap-add&lt;/span&gt; NET_ADMIN &lt;span class="nt"&gt;--cap-add&lt;/span&gt; NET_RAW &lt;span class="nt"&gt;--group-add&lt;/span&gt; 112 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--device&lt;/span&gt; /dev/rfkill &lt;span class="nt"&gt;-v&lt;/span&gt; /var/run/dbus:/var/run/dbus:ro &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-v&lt;/span&gt; ./config.yaml:/app/config.yaml:ro &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;CONTINUOUS_MODE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  ghcr.io/kristianp26/ble-scale-sync:latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you run &lt;strong&gt;Home Assistant OS/Supervised&lt;/strong&gt;, it's a one-click add-on instead — UI config, Mosquitto auto-detected, Garmin tokens bootstrapped on first start.&lt;/p&gt;

&lt;h2&gt;
  
  
  A few things I'm proud of
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;No Pi next to the scale? Use a $5 ESP32.&lt;/strong&gt; Bathrooms are often out of Bluetooth range of wherever your server lives. So there's an ESP32 BLE proxy: it sits by the scale and relays BLE over WiFi/MQTT to the container running anywhere on your network. It even ships an embedded MQTT broker so there's zero broker setup. If you already run an &lt;strong&gt;ESPHome Bluetooth proxy mesh&lt;/strong&gt; for Home Assistant, it can reuse that instead (multi-proxy with automatic RSSI-based pick).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Multi-user.&lt;/strong&gt; Two people, one scale. It identifies who stepped on by weight range and routes to per-user exporters (my Garmin, your Garmin).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Historical backfill.&lt;/strong&gt; Some scales cache offline measurements. On reconnect it replays them &lt;em&gt;with their original timestamps&lt;/em&gt; to targets that support back-dating (Garmin, InfluxDB, File), so a week away from the Pi doesn't leave a hole in your chart.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Linux BLE is… a journey.&lt;/strong&gt; BlueZ has a notorious "stuck discovery" state where scanning silently dies after hours/days. There's a consecutive-failure watchdog that auto-recovers it, plus optional systemd &lt;code&gt;Type=notify&lt;/code&gt; integration for whole-loop freezes. Headless reliability was honestly the hardest part — way harder than parsing the scale protocols.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Body composition is computed locally.&lt;/strong&gt; Scales send weight + raw bioelectrical impedance; the 10 body-composition metrics are derived on-device via BIA formulas. Nothing leaves your network.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stack
&lt;/h2&gt;

&lt;p&gt;TypeScript / Node 22, &lt;code&gt;zod&lt;/code&gt; for config validation, &lt;code&gt;node-ble&lt;/code&gt; (Linux) / noble (macOS/Windows) for BLE, &lt;code&gt;mqtt&lt;/code&gt; + an embedded &lt;code&gt;aedes&lt;/code&gt; broker, Vitest. Adapters and exporters are both pluggable registries — adding a new scale or export target is a single self-contained module, which is documented in &lt;code&gt;CONTRIBUTING.md&lt;/code&gt; if you want to add yours.&lt;/p&gt;

&lt;p&gt;Cross-platform: Linux (Docker + native), macOS, Windows. GPL-3.0.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it / links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Repo:&lt;/strong&gt; &lt;a href="https://github.com/KristianP26/ble-scale-sync" rel="noopener noreferrer"&gt;https://github.com/KristianP26/ble-scale-sync&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Docs:&lt;/strong&gt; &lt;a href="https://blescalesync.dev" rel="noopener noreferrer"&gt;https://blescalesync.dev&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Getting started:&lt;/strong&gt; &lt;a href="https://blescalesync.dev/guide/getting-started" rel="noopener noreferrer"&gt;https://blescalesync.dev/guide/getting-started&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Supported scales:&lt;/strong&gt; &lt;a href="https://blescalesync.dev/guide/supported-scales" rel="noopener noreferrer"&gt;https://blescalesync.dev/guide/supported-scales&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your scale isn't on the list, open an issue with a BLE capture — &lt;code&gt;npm run diagnose&lt;/code&gt; collects everything needed. PRs for new adapters very welcome.&lt;/p&gt;

&lt;p&gt;Happy to answer anything in the comments — especially curious whether other people hit the same Garmin-sync wall, or solved it differently.&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>iot</category>
      <category>typescript</category>
      <category>selfhosting</category>
    </item>
  </channel>
</rss>
