Skip to content

Changelog ​

All notable changes to this project will be documented in this file.

The format is based on Keep a Changelog.

1.40.0 (2026-11-09) ​

Added ​

  • ble: log a scale connection that BlueZ before 5.83 ends without a reason, in debug (b99c18e), closes #497
  • qn: decode the extended dialect's one-part 0xB1 result (#331) (1b7e114)
  • qn: export the scale's skeletal muscle on the extended dialect (#331) (cb72f53)
  • qn: set the algorithm byte of the profile frame with ble.qn_algorithm (#331) (18638b8)
  • qn: use the scale's own body composition on the extended dialect (#331) (94cea96)

Fixed ​

  • addon: stop the ESPHome-key shell test and the config watcher tests from flaking (#504) (b829290)
  • firmware: forward every notification of a burst, in order (cb0127b), closes #501
  • qn: point at the ESP32 firmware when a 0xb1 arrives without its first part (119acdf), closes #501
  • qn: read the scale's 7-byte answer to the profile frame (#331) (630c154)
  • qn: read the weigh-in of a 20-byte scale without the dynamic-package bit (c7fea17), closes #530
  • renpho: send a time zone west of UTC to the R-MSC04 as the Renpho app does (658cc16), closes #497
  • runtime: attribute live readings by the scale's user slot (#529) (4de9340)
  • runtime: warn when two users claim a live weigh-in's scale slot (1c7c29a)
  • scales: do not pass the fixed consent slot of the SBF72 and standard GATT adapters as the user slot (aff5c7b), closes #529

Docs ​

  • add a made in Europe line with the EU flag to the footer (d71b030)
  • add a See all exporters button to the landing hero (b8881c3)
  • align the docs pages with the landing page (56f4f9f)
  • group the landing page's export targets and show where it runs (bdd6adc)
  • list every export target on the landing page and fix its dark mode (bb6631c)
  • multi-user: say the scale's user slot assigns a weigh-in first (2a4d269), closes #529
  • put the landing platform line on its own row (67eb37c)
  • put trailing pointer sentences on their own line (dc47598)
  • qn: say the extended dialect's live weight frames are not acknowledged (c7996ca), closes #331
  • redesign the landing page around a live log and the data flow (9c9ca3f)
  • renpho: explain why the R-MSC04 hangs up 5.5 seconds in on Linux and how to stop it (830e682), closes #497
  • show that the landing install cards open a guide (bf2596d)
  • show the footer on the docs pages too (ba118b9)
  • stack the landing transports, enlarge the hub and copy the Docker setup run (c0b7ad9)
  • stream the landing hero log, draw the data flow in and show the setup wizard (bb82acf)
  • tighten the landing data flow band (6e77985)
  • zoom the landing Home Assistant badge on hover (ddd0d48)

1.39.1 (2026-11-04) ​

Fixed ​

  • config: say a reload waits for the next weigh-in on the proxy transports (57be4ee)
  • mi-scale: ignore a stable Mi Scale 2 frame of 10 kg or less (85f66b2), closes #526
  • mi-scale: ignore a stable XMTZC04HM frame of 10 kg or less (140466f), closes #526
  • qn: read the 20-byte dialect's 0xB1 result when it comes in one part (d96b1cb), closes #331

1.39.0 (2026-11-02) ​

Added ​

  • robi-s9: read the Healthkeep S11 A7 result record (6be979e)

Fixed ​

  • ble: hand over the live reading of a capture run before stored records (d8790ed), closes #511
  • ble: round the weight in the reading log lines (7fea3d6)
  • medisana: read the BS444 weight and composition from their own characteristics (2bde007), closes #511
  • renpho: keep the link open while the scale is still measuring (f460712), closes #507
  • renpho: read the scale clock as UTC when its live frames say so (d4d6f36), closes #510

Docs ​

  • config: say a reload on the proxy transports waits for the next weigh-in (cab707c)
  • renpho: tip for the R-MSC04 reconnecting after a weigh-in (0468bfa)
  • site: link the copyright name to partl.dev (8786abf)

1.38.0 (2026-10-29) ​

Added ​

  • addon: expose qn_user_slot as an add-on option (af9cfef), closes #501
  • qn: ble.qn_user_slot sets the user slot of the profile frame (732b0d6), closes #501
  • qn: send the vendor app's profile frame on the 20-byte dialect with ble.qn_user_key (8b040f4), closes #501
  • renpho: recognise the A-R003 advertisement before connecting (bf1da60), closes #507
  • renpho: recognise the nameless Renpho A-R003 by its characteristics (e74ddea), closes #507
  • scales: add the Huawei CH100 / AH100 adapter (9f1b9b7), closes #503

Fixed ​

  • deps: bump garminconnect from 0.3.19 to 0.3.20 (1afee2f)
  • qn: keep a session without a 0x12 off slot 1 with ble.qn_user_slot (b68ecc2), closes #501
  • renpho: stop computing ES-WBE28 body fat from a misread impedance (252af9c), closes #507

Docs ​

  • exporters: note that a Strava API app needs a subscription (3fadce5), closes #498
  • exporters: use a bind mount owned by UID 1000 for the File exporter in Docker (01bc715)
  • interfaces: deviceAddress is the CoreBluetooth UUID on macOS, not empty (eafe8e9), closes #503
  • scales: list the Renpho A-R003 (cb2e4e5), closes #507

1.37.0 (2026-10-25) ​

Added ​

  • addon: user_estimate_body_composition option (1d42686)
  • ble: log why BlueZ ended a scale connection, in debug (5a5dc59)
  • config: users[].estimate_body_composition turns the BMI estimate off (9475789)
  • scales: export the R-MSC04's own skeletal muscle percentage (a2968ca)
  • scales: export the R-MSC04's ten segment impedances as diagnostic values (de3e6f6)

Fixed ​

  • ble: log a dropped BlueZ device once instead of on every broadcast poll (158d8a2)

Docs ​

  • HACS cannot distribute Supervisor add-ons (43eee54)
  • on Linux, noble_driver does switch to a noble handler (3202780)
  • rewrite the docs site, README and add-on help to be shorter (2b632c3)
  • say the R-MSC04 can still lose the connection on some weigh-ins (31c33c0)
  • say the R-MSC04 profile byte is confirmed on one unit (11259ab)

1.36.0 (2026-10-22) ​

Added ​

  • addon: expose rmsc04_profile_byte as an add-on option (dd9a52f)
  • ble: clear a stale bond on BlueZ before 5.83 too (#471) (5d16b89)
  • scales: export the R-MSC04 body fat with ble.rmsc04_profile_byte (1076ea9)
  • warn at startup when the running Node.js is not supported (eedf00a)

Fixed ​

  • ble: tell when a failed connection ended on BlueZ before 5.79 (3521649)
  • docker: move the base image to Node.js 22.23.4 (b2beab0)
  • require a Node.js release with the October 2026 security fixes (9a66e9f)

Docs ​

  • configuration, supported-scales, body-composition, multi-user. (1076ea9)
  • troubleshooting. (eedf00a)

1.35.0 (2026-10-21) ​

Added ​

  • mqtt: publish the measurement time and a Home Assistant timestamp sensor (60ab09e)
  • qn: use the scale's own BMR and body age on the 19-byte dialect (e5aff44)

Fixed ​

  • ble: count node-ble's waitDevice timeout as the MAC no-show (3074b26)
  • ble: give a noble session the manufacturer data its scale advertised earlier in the run (41d7fbb)
  • ble: give a node-ble session the manufacturer data its scale advertised earlier in the run (3deb5d7)
  • ble: treat the reconnects to a scale that stays on as one window (4372ee4)
  • config: keep the startup noble driver across a config reload (66acf20)
  • config: report ble.mqtt_proxy.auto_connect as restart-required (c8dacfe)
  • config: warn that a runtime.watch_config edit needs a restart (8ffcdd2)
  • qn: export a stored weigh-in the 18-byte scale kept for no user (7f50115)
  • qn: read an 18-byte impedance only a scrambled body gives unscrambled when no advertisement came (3558a06)
  • runtime: keep the dedup marks file when it cannot be read (3eaa407)
  • runtime: keep the exporter slot across a config reload (988adcc)
  • yunmai: stop the reply to the unlock from holding a session open (c12706a)

Docs ​

  • addon: say the MQTT sensors are always in kg and where the payload goes (d72acfa)
  • config: say a single-shot mqtt-proxy run honours session_timeout_sec (b958c53)
  • scales: say the Yunmai Mini 2 impedance and body fat are confirmed on hardware (24c59f6)

1.34.0 (2026-10-20) ​

Added ​

  • addon: expose no_reading_rescan_delay (a64ad37)
  • addon: expose pairing_retry_max_min (9b708b4)
  • addon: expose qn_result_ack as an add-on option (d71898e)
  • ble: hold back an unpaired scale whose pairing nobody confirmed (54aeaaf)
  • ble: log how each failed connect to a scale ended (1ba2164)
  • qn: send the end-of-measurement frame after a 19-byte result with ble.qn_result_ack (0ced062)
  • runtime: wait at least a set time after a session that sent no reading (9ba6f31)
  • scales: let an adapter claim a MiBeacon product id like a name (05a11d9)
  • scales: pair a BF915 at once when it advertises a stored weigh-in (6c1f98b)
  • yunmai: recognise the Mini 2 (M1690) by its MiBeacon product id (8ddede0)

Fixed ​

  • ble: bound RemoveDevice and StopDiscovery on the node-ble connect and teardown path (788134d)
  • ble: clear a stale bond only when BlueZ reports that the scale refused the key (#471) (40d4da3)
  • ble: clear the reason probe deadline when a shutdown ends the probe (16d4a1e)
  • ble: do not log a write refused by a closed D-Bus stream as BlueZ's answer (08d1289)
  • ble: do not say a connection never came up when BlueZ gave a reason for ending it (86486e8)
  • ble: do not throw on a stored reading whose time cannot be read (adfaf7d)
  • ble: end a session for a shutdown without settling it on its held reading (e8db55c)
  • ble: end an ESPHome scan's GATT session at once on a shutdown (9e04efd)
  • ble: fail calls left on a D-Bus connection we replaced instead of parking them (6803d05)
  • ble: give every node-ble Disconnect a deadline and stop retrying on a stale link (f174029)
  • ble: keep a GATT-only scale's BlueZ object between cycles (a46cecd)
  • ble: keep a pairing agent call on a replaced connection off the new one's state (3a80853)
  • ble: keep the stale-link brake per scale (19fce15)
  • ble: keep the stale-link recovery short on shutdown, bounded, and from piling up calls (31b2ed4)
  • ble: let a pairing hold count the scale's reports as hearing the radio (8825d1b)
  • ble: let a shutdown end the check after a stale-link power-cycle (cb5c0dd)
  • ble: log a Disconnect written after the D-Bus connection closed as not sent (752ece3)
  • ble: name the host reboot, not a Home Assistant restart, in the stale-link warning (7145f75)
  • ble: power-cycle the adapter when BlueZ keeps a dead link marked as connected (31570ff)
  • ble: say how a held pairing goes on once its wait is over (1eeb186)
  • ble: stop warning when a proxy reconnects to a scale still on after its reading (f5b9241)
  • ble: tear a session down through the adapter the connect retry moved to (3439fe3)
  • esphome-proxy: key a stored-only GATT reading by its record time in the dedup window (a6ccae2)
  • mqtt-proxy: mark a known broadcast scale passive on its first advert (#470) (1d013f5)
  • qn: blame the scale, not the option, for a stored weigh-in it gave no user (775d7d2)
  • qn: bound a stored record's stamp in a session without a scale-info frame (4f07222)
  • qn: do not take a long-dialect record stamped after its receipt as the live weigh-in (ed6592f)
  • qn: drop a 0x15 left over from an earlier 19-byte result block (a1a4382)
  • qn: export a weigh-in the 18-byte scale kept from before the connection (1a80ab4)
  • qn: export an 18-byte stored record only after the first user's profile was accepted (252459e)
  • qn: log the scale's muscle and bone as percentages with their kg (fe85d6d)
  • qn: log why a stored weigh-in is not read (024337a)
  • qn: never read a classic scale's 0x23 through the long-frame layout (cfba76f)
  • qn: pick the ES-30M live layout by the frame's shape, not by the divisor (7cf45e5)
  • qn: say why a session ended with an 18-byte weigh-in still held for the stored ones (1fc7a7e)
  • qn: take the long-frame divisor from the scale-info flags too (706af1f)
  • qn: take the weight divisor from bit 0 of the classic scale-info flags (e35b085)
  • qn: use the scale's composition when its weight settled before it accepted the profile (49f15c0)
  • qn: wait for the stored weigh-ins before ending an 18-byte session (fd0c608), closes #463
  • runtime: date the replay anchor on the scale's clock when the live weigh-in has a stamp (0ed2fb7)
  • runtime: do not announce a pairing wait a single-shot run never keeps (0f00b54)
  • runtime: keep a stored weigh-in measured after the last export from being dropped as a replay (671439f)
  • runtime: keep the last live weigh-in across a restart for the replay check (51e5c2c)
  • runtime: keep the later saved weigh-in of another user when saving replay anchors (911bb62)
  • runtime: treat a replay anchor time before 2024 as unknown (8288807)
  • runtime: warn about a failed replay anchor save once per process (cfd5b6b)
  • scales: use the R-MSC04 record when the weight settled before the connection (ffae70a)
  • wizard: say the scan cooldown only applies to the built-in Bluetooth handlers (deda875)
  • yunmai: read resistance and body fat from the frame, not the name (d6bf620)
  • yunmai: use the BMI estimate, not generic BIA, when the Yunmai formula gives up (3dd5835)

Docs ​

  • ble: correct the cooldown and post-reading comments (b574b27)
  • ble: correct what the stale-link comments say (4a1179f)
  • ble: describe the pairing wait as it doubles up to its cap (c64dda8)
  • ble: name every path where the freshness tracker's first check proves nothing (0f1ff1a)
  • ble: name the RSSI re-discovery among the paths where the first freshness check proves nothing (1429ec4)
  • ble: say a call through a replaced connection fails, not that it hangs (3622ec5)
  • ble: say a connection over a refused key outlives the error by one interval minus the 100 ms channel timer (e6b222a)
  • ble: say where the freshness tracker's first check is not evidence (fe557c3)
  • ble: stop the pairing wait example at 32 minutes in the backoff header too (09a665e)
  • correct what the QN stored weigh-in and history pages say (bc01a64)
  • describe the 19-byte QN results confirmed on two units (5d3db6a)
  • drop the 64-minute example from the pairing retry schedule (be5416f)
  • ha-addon: list the Strava token directory in the persistence table (34e5386)
  • mark the Runstar FG2431WB as confirmed on hardware (d88826f)
  • mount config.yaml writable in the docker run examples (73f7098)
  • name the real ble.handler value the scan waits apply to (509695d)
  • qn: place the qn_result_ack write line where it is logged (6d3e175)
  • qn: say where the classic fixture values come from and which ES-CS20M reads 0x23 (ffab5de)
  • say a QN stored weigh-in needs the first user's profile accepted (7ea115b)
  • say a saved weigh-in far from last_known_weight is dropped only at the next start (a89a632)
  • say a saved weigh-in far from last_known_weight is dropped only at the next start (3e01758)
  • say adapter test frames have to come from the real scale (36595f7)
  • say auto_clear_stale_bond clears a pairing only when BlueZ reports a refused key (79cce31)
  • say only the RemoveDevice after a failed connection triggers the stale-link power-cycle (18861b9)
  • say only what the logs show about the two early 18-byte QN weigh-ins (c712c26)
  • say retry_failed_exports false keeps no queue, not nothing on disk (6d8a33c)
  • say scan_cooldown only applies to the built-in Bluetooth handlers (570558d)
  • say SIGHUP cannot reload a config replaced under a single-file mount (362dcc3)
  • say the pairing retry wait keeps doubling up to pairing_retry_max_min (509f162)
  • say the stale-link power-cycle follows the RemoveDevice after any connect or GATT session without a reading (f986199)
  • say the stored weigh-in stamp bound also covers sessions without a scale-info frame (dee4a1d)
  • say the weigh-in signal is seen on a BF915 only (8836ffe)
  • say what the stale-link brake holds back, and how to restart Bluetooth on HA OS (99ba2f1)
  • say what the two early 18-byte QN weigh-ins had in common (aa99e6f)
  • say when a weigh-in does not ask for the pairing again (5a4f1cd)
  • say when auto_clear_stale_bond clears a pairing and why a weak link does not (#471) (c241aa0)
  • say which failed connect attempts the debug log describes, and from when it gives BlueZ's reason (dd32de2)
  • say which QN stored weigh-ins are from memory and keep a dedup mark (519fc38)
  • scales: describe the R-MSC04 record gates as the code has them (2847e28)
  • scales: say the version 2 MiBeacon frame is the one of the Mini 2 in #474 (6e5562d)
  • tell the two Beurer display problems apart and say what the wait holds up (0eb7722)
  • yunmai: say only SIGNAL resistances from 150 up pass the impedance gate (67d7f95)
  • yunmai: say that byte 4 was 0x01 once and what that may mean (f2fcc43)

1.33.0 (2026-10-13) ​

Added ​

  • ble: let an adapter turn a notify binding on after its own writes (361c767), closes #408
  • es-cs20m: compute body fat from the 0x18 resistance (ee1d762), closes #436
  • es-cs20m: take the weight and resistance from the scale's 0x18 result (6dce383), closes #436
  • qn: send the vendor app's profile frame when ble.qn_user_key is set (6f1cdd0), closes #463
  • qn: use the scale's own body composition on the 19-byte dialect (4b37ef5), closes #331
  • scales: add the A&D UC-352BLE adapter (887e68f), closes #408
  • scales: add the Etekcity Fit 8S broadcast adapter (0c21023), closes #408
  • scales: add the Eufy C20 / P3 broadcast adapter (cc50075), closes #408
  • scales: add the PICOOC Mini Lite broadcast adapter (66378c1), closes #408
  • scales: compute AC 27 body composition with the vendor's WLA07 formula (6fbdab1)
  • scales: send the FitDays profile handshake to AC 27 scales (788d0ab)

Fixed ​

  • ble: hand over the stored records of a session a timeout ends (5634e05), closes #331
  • ble: keep the held reading when the scale disconnects after stored records (ec88e4f), closes #331
  • es-cs20m: stop taking a stored record's resistance for the live weigh-in (34a036e), closes #436
  • esphome: log a notify handle mismatch only for a handle nobody subscribed to (5f16659), closes #331
  • qn: read the 19-byte stored records as one record each and report them with their time (d3abdff), closes #331
  • qn: send the configured sex in the 19-byte profile frame (32c12f4), closes #331
  • qn: send the host's UTC offset in the 9-byte time sync (df3e53f), closes #331
  • runtime: give every proxy GATT session the current user profile (2f53dc8), closes #331
  • scales: complete AC 27 weigh-ins on a locked or steady weight (9d61d93)
  • scales: give WLA07 the same whole-cm height the AC 27 scale gets (5728af6)
  • scales: pair the Active Era impedance with this session's weight (3372c69)
  • scales: pick AC 27 scales by their 0x27AC advertisement before MGB (f041880)
  • scales: stop the Inlife adapter from claiming PICOOC scales (9972b10), closes #408
  • silvergear: read the weight from the low 18 bits, not a fixed bias (#467) (37453d8)

Docs ​

  • describe the 19-byte QN body composition and stored weigh-ins (e4a2fcc), closes #331
  • describe the AC 27 family on the supported scales page (1e8b3b2)
  • list the Etekcity Fit 8S (cdb381d), closes #408
  • point source comments at public evidence, not private notes (c9dcf9a)
  • qn: describe the 0x20 extra byte as the UTC offset in the option comments (68010c5), closes #331
  • readme: credit etekcity_esf551_ble as the source of the Fit 8S layout (adbafd3), closes #408
  • readme: drop the sentence above the contributors grid (5012cce)
  • say the scale most likely, not certainly, computed with the male profile (ededb48), closes #331
  • say when a 19-byte QN stored weigh-in gives way to a live one (6f19e1e), closes #331
  • scales: describe the ES-CS20M result frame and confirm the AE01/AE02 sequence (2bb2f96), closes #436
  • scales: drop the address requirement the Fit 8S does not have on macOS (d701113), closes #408
  • scales: list the Foryond FG263LB (AAA002) under the Silvergear adapter (6801ae7), closes #408
  • scales: list the INSMART AAA006 under the Silvergear adapter (f15417f), closes #467
  • scales: say the AC 27 display unit byte comes from the BS-06 handler (f5b572d)
  • scales: say the Eufy P2 / P2 Pro reads body composition over GATT (f4aefa1), closes #289
  • scales: state the 30 s bound on the ES-CS20M result hold (574659f), closes #436

1.32.0 (2026-10-11) ​

Added ​

  • addon: add Home Assistant Bluetooth as a ble_transport, no token needed (1ab2e39), closes #420
  • addon: add the ble_transport option with an ESPHome proxy transport (f6fba59), closes #420
  • addon: add the ESP32 MQTT proxy as a ble_transport (a82c025), closes #420
  • addon: stop when the ESP32 proxy cannot share the MQTT broker URL (fd93b9b), closes #420
  • addon: warn when custom_config ignores the transport options (0b21a42), closes #420
  • exporters: add a Dreeve exporter (#421) (9fa3534)
  • qn: read the weight from the 19-byte dialect's measurement stream (#331) (47514a3)
  • scales: add OKOK / Chipsea broadcast adapter (2.0 and C0) (f779c1f), closes #408
  • scales: choose the R-MSC04 connect writes with BLE_RMSC04_HANDSHAKE (5c565c6), closes #434
  • scales: log the R-MSC04 handshake replies, stored records and session length (1052ba8), closes #434
  • scales: send the R-MSC04 clock and a guest profile instead of the start command (74bb6e8), closes #434

Fixed ​

  • addon: create config.yaml as 0600 on the first start (0827149), closes #420
  • addon: write option values into config.yaml exactly as entered (297f5cf), closes #420
  • ble: do not count a session that heard the scale toward the watchdog (5cdeed9), closes #460
  • ble: report a session timeout as connected, not as no scale found (0d1981d), closes #460
  • garmin: keep an unquoted value out of setup_garmin's YAML error (37fa30c)
  • ha-bluetooth: fail a websocket that errors before opening at once (37d5e96), closes #420
  • ha-bluetooth: stop reading Home Assistant's subscribe snapshot as clock skew (227c264), closes #420
  • ha-bluetooth: use the newest packet's manufacturer data, not the merged history (f064baa), closes #408
  • qn: read the stored record's resistances big-endian at [12..13] and [14..15] (e18ec67), closes #461
  • qn: stop sending the delete-users frame by default (72b2dd5), closes #461
  • qn: unscramble the impedance of scales that advertise it scrambled (ffce022), closes #461
  • runtime: keep the retry queue when its file cannot be read (65270f7), closes #460
  • runtime: say how many queued exports wait and when the next is due (cb9cbe6), closes #460
  • scales: report an R-MSC04 write still waiting at session end as pending (785355b), closes #434
  • security: hide a fetch URL password that holds whitespace (4897bff)
  • security: keep secret redaction linear on long lines (6648f93)
  • security: keep the config line out of YAML warnings (5d99d59)
  • security: redact credentials from error messages and log lines (372e8c3)
  • security: redact export errors before they are queued or reported (b648352)
  • security: redact lastError of queue entries written before redaction (8ae956c)
  • security: redact query string secrets and Telegram bot tokens (d9af52f)
  • wger: type measurement category ids as string or number (#459) (a8cc0bd)

Docs ​

  • add rolandgeider to contributors (a3549ce)
  • addon: document the Bluetooth transport options (82d75e4), closes #420
  • addon: point the ESP32 at the broker it shares, not always this host (fcc072a), closes #420
  • body-composition: say a refused impedance is exported as 0 (28a7bd5), closes #461
  • config: finish the qn_report_byte warning in the example config (73238d8), closes #461
  • config: name the one handler that exempts an answered session (13881b4), closes #460
  • contributing: explain how the contributors grid is generated (2d7958c)
  • contributing: match the release notes and release-please files to the config (8bc2df4)
  • contributing: name the .env mapping and the count tool for a new exporter (03c3c42), closes #421
  • contributing: put co-author lines for a pull request in its description (d59c5f1)
  • contributing: say the exporter name lists go beyond README and the home page (99f97b7), closes #421
  • describe the 19-byte QN weigh-in stream and what is read from it (c2916d6), closes #331
  • exporters: show Wger and HealthLog as per-user exporters in config.yaml.example (9e9f1f8), closes #421
  • ha-bluetooth: explain the subscribe snapshot and the clock warning (2ee996c), closes #420
  • list OKOK / Chipsea broadcast scales (2081efe), closes #408
  • make the per-user exporter merge sentences read unambiguously (e50648d), closes #421
  • readme: add the missing code contributors to the grid (09bd430)
  • readme: show commit counts in the contributors grid, ordered by count (3c1c87b)
  • say per-user exporters are added to global_exporters (86ec90a), closes #421
  • scales: describe the R-MSC04 connect handshake (c9e0614), closes #434
  • the retry queue survives a container restart, not a re-create (3a4a526), closes #460
  • troubleshooting: which connected sessions the watchdog counts (f04515f), closes #460

1.31.0 (2026-10-06) ​

Added ​

  • addon: add the adapter_privacy option (37ad5f8)
  • ble: add a 'blocked' scan failure for a scale found but not connected (70bf715)
  • ble: extend the composition hold while a transfer is still arriving (5563ec6)
  • ble: restart a scan that stops delivering advertisements (fc465f0)
  • ble: turn on LE privacy so a Beurer scale keeps the bond (#417) (3b4dc23)
  • config: add ble.adapter_privacy and pass it to the node-ble scan (1b65828)
  • garmin: add skip_metabolic_age to leave metabolic age out of the upload (#455) (b2e5584)
  • scales: build the QN A00D profile frame from an age and a height (760fee4)
  • wizard: ask for units and authorize Strava during setup (2a39fbb)
  • wizard: section menu, edit-aware BLE and secrets kept in .env (bd0bb53)
  • wizard: show what an edit changes before saving it (116cee6)

Fixed ​

  • ble: count a no-show as idle when the scan watchdog heard the room (7f0877b)
  • ble: log when the scan watchdog cannot list devices (605f6b1)
  • ble: read the adapter state back after a btmgmt power cycle (72d9c47)
  • ble: take over a Discovering state that no client owns (c2960aa)
  • scales: hold the Beurer/Sanitas link while the 0x59 stream is arriving (#211) (3cd85e6)
  • scales: send the Arboleaf app's profile sequence on the 19-byte QN dialect (#331) (6cb5ad1)
  • scales: send the Renpho app's start sequence to ES-CS20M units with AE01/AE02 (#436) (269c504)
  • wizard: keep comments on edit, test every exporter, mask the HA token (8c99de6)

Docs ​

  • ble: stop citing burst silences as proof of a stalled scan (c9bd5fb)
  • describe the 19-byte QN profile frame sent under qn_weight_ack (a724152)
  • describe the athlete metabolic age rule as a clamp above actual age (6692acf)
  • describe the ES-CS20M start sequence and the profile it writes to the scale (c093733)
  • describe the wizard section menu and secrets kept in .env (8cfb02d)
  • describe units, comment-preserving edits and Strava in the wizard (c40b052)
  • explain ble.adapter_privacy and the Beurer bond it keeps (37f77b6)
  • explain how a stuck BlueZ scan is now healed (1a4377d)
  • garmin: limit 'depends on age alone' to profiles without is_athlete (52f3fb0)

1.30.1 (2026-10-04) ​

Fixed ​

  • addon: declare the MQTT service and give btmgmt its capability (80cb57d)
  • addon: restart the app after it exits and guard the Garmin token (26be7ba)
  • ble: harden the BLE transports, reloads and the run path (1cf535f)
  • ble: honour shutdown mid-session and tidy the transport edges (90572d6)
  • ble: keep the pairing agent scoped to the scale being paired (7dde25d)
  • ble: stop abandoned poll cycles, drop the SIG 0xFFFF weight, remember Salter exports (08dacba)
  • ci: repair GHCR cleanup, pin actions and images, harden add-on and worker (e61d823)
  • config: honour env overrides, legacy .env exporters and wizard edits (9f1fd7b)
  • config: keep edits, read .env beside --config and follow symlinks (5f5d394)
  • config: keep secrets out of reload, YAML and header log lines (3483bf5)
  • config: refuse two accounts sharing one Garmin or Strava token dir (ac8e5fa)
  • config: reload .env, escape ${ and tidy runtime and exporter edges (2bff4cd)
  • config: stop .env leftovers overriding config.yaml, load scan config properly (242b605)
  • exporters: keep Garmin and Strava token directories owner-only (b494e84)
  • file: create the export file owner-only (891733e)
  • firmware: give host connects ownership, a time budget and TLS (21a3bc7)
  • firmware: keep the ESP32 proxy scanning and connected (1152b4c)
  • firmware: publish the newest advertisement of each device (ae14129)
  • firmware: validate config, bound MQTT inputs and pin aioble (a50398c)
  • garmin: keep results across SIGTERM, treat 401 as permanent, check Python (09af3e3)
  • mqtt-proxy: require a password for a LAN-exposed embedded broker (1f4057d)
  • runtime: give stored records their own time and owner, validate composition (0e7d395)
  • runtime: keep scanning while the retry queue drains and exit cleanly (40cbdb7)
  • scales: apply the record-time and composition rules to the adapters (38f1b9b)
  • scales: bind QN timers to their session and check ESF-551 checksums (40920fe)
  • wizard: stop suggesting a public MQTT broker for health data (a405cf8)

Docs ​

  • bring the guides back in line with the code (c48cbc7)
  • configuration: overrides come from the real environment, not .env (9bf6778)
  • cover --config with .env, ESP32 TLS, structured proxy errors and live reload limits (94709e1)
  • describe the retry schedule, add-on MQTT and liveness fixes and wizard user edits (835d37e)
  • exporters: do not ship a guessable ntfy topic in the examples (879d78b)
  • Garmin 401 and Python version messages, Salter dedup mark file (a18e293)
  • multi-user: give each Garmin and Strava account its own token_dir (5150f07)
  • per-user exporters merge, stored weigh-ins, composition without impedance (7b0e5e9)

1.30.0 (2026-10-02) ​

Added ​

  • addon: expose preemptive_adapter_reset as an add-on option (182dc7b), closes #417
  • ble: let ble.preemptive_adapter_reset skip the post-GATT power-cycle (c8c2263), closes #417
  • ble: say once at info when the post-GATT power-cycle is off (b9f18eb), closes #417
  • exporters: add a HealthLog exporter (#443) (6c1535d)
  • garmin: upload BMR as basal_met (#432) (14d79e8)
  • qn: support an independent physical display unit (#429) (ced93a0)
  • scales: acknowledge R-MSC04 status frames like the Renpho app (563b9f5), closes #434
  • scales: add Senssun IF_B7 broadcast adapter (Grifema GA2001) (eaec55a), closes #423
  • scales: read a Senssun IF_B7 that displays lb (c81d7ff), closes #423
  • scales: wait for the R-MSC04 body composition record (b8861eb), closes #434
  • silvergear: hold a settled weigh-in for its own post-weigh-in frame (33a8e9d), closes #357

Fixed ​

  • addon: actually pass --config-path to setup_garmin.py (22cbffb), closes #435
  • addon: don't offer to re-import a /share token that was already imported (2571cb2)
  • addon: import a Garmin token that is newer than the cached one (#428) (7cd76a9)
  • addon: pass the config path to setup_garmin.py as --config-path (b18342b), closes #435
  • addon: report a skipped /share Garmin token only when it was skipped (aea66aa)
  • ble: keep a composition hold from being cut short by the idle timeout (bd22b7c), closes #434
  • ble: let a composition hold outlast the session cap (b45ad0f), closes #434
  • ble: queue GATT writes per characteristic so BlueZ stops rejecting them as In Progress (#211) (b3c81d2)
  • ble: resolve a held broadcast reading at the scan deadline instead of dropping it (2d3c12b), closes #357
  • ble: stop pointing a one-time pairing passkey at beurer_pin (#430) (2a925c0)
  • body-comp: use the user's sex constant in the metabolic age reference (7c9f9a5), closes #433
  • config: make validate build the exporters it reports on (ecaf281)
  • config: stop atomicWrite destroying the file it cannot replace (efb7c8b)
  • config: validate what the schema and the env path both let through (91b000b)
  • es-cs20m: read 0x14 stability from the status low nibble (91cba08), closes #376
  • exporters,config: parse what the config says, and stop the watcher eating edits (8a27c8b)
  • exporters: own the MQTT client, and bound every step of an export (ed94dca)
  • firmware: build the config print as one f-string (7feb850), closes #422
  • firmware: skip auto-connect for advertisement-read scales (4c2b819), closes #422
  • garmin: redact query values and respect suppressed context in error chains (8d2ae43)
  • garmin: say the token file is missing instead of asking for a password (2e3a304), closes #435
  • garmin: stop setup reusing a stale token (#427) (3699443)
  • mi-scale: export muscle mass, not skeletal muscle, for Xiaomi scales (#431) (6e9ce36)
  • mqtt-proxy: never drive a GATT session for an advertisement-read scale (b074fa5), closes #422
  • mqtt-proxy: refuse an advertisement-read scale on the single-shot GATT path too (24d3b95), closes #422
  • mqtt-proxy: tell the ESP32 which scales are read from advertisements (10dcb40), closes #422
  • qn: send the weight anchor twice after START, where both Arboleaf captures put it (513c027), closes #331 #75
  • qn: stop answering 20-byte live frames with a 0.17 kg A2 (9a9025a), closes #331 #235
  • qn: stop reading a 20-byte live frame through the original layout (f33618e), closes #331
  • runtime: deliver a queued export to its own user, with its own timestamp (4db38bf)
  • scales: log Senssun IF_B7 frames in an unknown state (cafa1d7), closes #423
  • scales: stop QN claiming a 0x1A10 scale on its AE00 service alone (909c062), closes #436
  • silvergear: export a weigh-in that ends before its post-weigh-in frame (ad49bb3), closes #357
  • silvergear: time the hold on a monotonic clock (7470ac0), closes #357
  • utils,exporters: give retries a delay, and make the HA metric check check (985e86a)

Docs ​

  • addon: say where the QN weight anchor is sent (b707812), closes #331
  • ble: count the composition hold in the session and cycle ceilings (c042d64)
  • describe muscle mass as fat-free mass minus bone (f8a9e89)
  • describe the R-MSC04 composition wait (66f524f), closes #434
  • document ble.preemptive_adapter_reset (4c1e414), closes #417
  • esp32-proxy: explain how advertisement-read scales are handled (686f30b), closes #422
  • esp32-proxy: say a restart can bring back one dropped connect (e9d6f70), closes #422
  • faq: describe the retry backoff and queue as they ship now (0cf84de)
  • fix two figures from today's follow-ups (07d9726)
  • garmin: list BMR among the fields weight_only leaves unset (1c5f7e5)
  • interfaces: say the completion hold is GATT-only (094e719), closes #357
  • list the Grifema GA2001 / Senssun IF_B7 as a broadcast scale (bde2812), closes #423
  • qn: document the post-START weight anchor (f57e283), closes #331
  • qn: record that 0x08 switches a QN-S500 to stone (dd6ca9d)
  • qn: say display_unit only reaches QN scales, and how sure 0x08 is (e2b0449)
  • qn: stop telling users to try qn_time_sync_long apart from qn_weight_ack (e49fbeb), closes #331
  • qualify where preemptive_adapter_reset applies (2cc878c), closes #417
  • record the env-override and webhook header behaviour that changed (7a2dbc4)
  • say the Grifema GA2001 / Senssun IF_B7 reads in kg and lb (08737bb), closes #423
  • say the Senssun IF_B7 lb reading rests on one weigh-in (0f7e5f8), closes #423
  • say which broadcast paths hold a partial reading (289424b), closes #357
  • scales: note the Silvergear wait for its post-weigh-in frame (15b4f29), closes #357
  • scales: say the R-MSC04 profile is the one the app writes (4bd5d16), closes #434
  • scales: say when a Silvergear step-off sends the weight at once (fdf1df9), closes #357
  • troubleshooting: cover scales that show a passkey to type, not to confirm (#430) (6e2b0f6)

1.29.0 (2026-09-09) ​

Added ​

  • active-era: log the raw impedance, which decided nothing until now (e5525e4), closes #386
  • addon: expose out_of_range as an add-on option (83e2345), closes #395
  • addon: expose qn_config_long as an add-on option (3a4922e)
  • addon: expose the Garmin upload timeout (7dd7123), closes #399
  • addon: expose the idle rescan delay, and name three untranslated options (ac083b2), closes #398
  • ble: add onSessionStart, the hook adapters can actually reset state in (68d411c), closes #394
  • ble: hand onSessionStart the device address (a66f5dd), closes #406
  • ble: release the session device and report what BlueZ can see (ea45116)
  • config: add ble.qn_config_long (24d4dce)
  • config: add out_of_range for readings no weight_range covers (30cab53), closes #395
  • config: add runtime.idle_rescan_delay (fcb753f), closes #398
  • config: add runtime.retry_failed_exports (975baca), closes #412
  • garmin: make the uploader timeout configurable, and raise the default (88f18c2), closes #399
  • inlife: make the undecoded impedance field answerable from one log (5a7de85), closes #405
  • qn: send the vendor app's 10-byte 0x13 config frame behind a switch (5172d7f)
  • runtime: add the failed-export queue (fe9b81c), closes #412
  • runtime: enqueue a failed export and flush it on the next cycle (77b8755), closes #412
  • runtime: stop exporting a reading nobody's range vouches for (2aa7628), closes #395
  • runtime: stop idle scan cycles using the error backoff (7796c6d), closes #398
  • scales: claim the FitTrack Dara under the Lefu AC02 adapter (#408) (823b1f3)

Fixed ​

  • beurer-bf720: apply the sentinel rule to muscle, not only to fat (83a21e5), closes #405
  • beurer: clear the stability buffer and partial composition per session (0a6e5f4), closes #394
  • ble: close the four gaps the review found in the cleanup work (831a086), closes #404
  • ble: correct dbus-next match-rule refcounting at runtime (12c222e), closes #396
  • ble: decide dbus-next is broken by behaviour, not by reading its source (5b533de), closes #396
  • ble: hand back the BlueZ device proxies the liveness probe creates (80681ea)
  • ble: keep the duplicate-advertisement filter node-ble was overwriting (2f0e476)
  • ble: key the filtered-scan claim on the adapter, not on module state (acb7ba5)
  • ble: let a reading session its caller gave up on clean itself up (9adf018), closes #404
  • ble: release the device proxies the connect and scan paths drop (01a1612), closes #404
  • ble: release the device proxies the connect retry loop replaces (db3f092)
  • ble: stop the detached broadcast poll rebuilding a released proxy (0d7d815)
  • close the gaps the review found in the two new settings (e214da5)
  • close the review's findings on the per-device variant work (75ab1c4), closes #406
  • config: honour BLE_HANDLER=esphome-proxy, and make the liveness timeout reloadable (3b3f187), closes #407
  • config: warn about the five proxy keys that also need a restart (f7327c8), closes #407
  • config: warn that force_scale_adapter needs a restart (29e1f21), closes #407
  • diagnose: decode QN broadcasts with the real decoder, not a copy (a7cf7ab), closes #406
  • es-cs20m: clear session state even when the scale sends no START frame (14ef35b)
  • exporters: parse numeric fields instead of casting them (06c2da5), closes #399
  • hoffen: stop decoding our own command echo as a weight (40c4feb), closes #405
  • inlife: log the frames this parser rejects, not only the ones it accepts (492731d), closes #405
  • inlife: wait for the impedance frame instead of resolving on the first weight (1f3390e), closes #413
  • mqtt-proxy: undo a partial watcher start instead of leaking its listeners (b8bb494), closes #404
  • qn: reset the session state before the notify bindings go live (093dbae)
  • repair what the review found in the tech-debt sweep (31e87d3), closes #406
  • runtime: close two ways the retry queue could lose or duplicate a reading (bd28382), closes #412
  • runtime: make out_of_range warn actually warn (b370434), closes #395
  • scales: finish the #394 session-state rollout across eight adapters (155f39d)
  • scales: finish the sentinel rule, and correct the cross-check test (2bd04a0), closes #405
  • scales: guard the SIG body-composition sentinels in one decoder (706ccf6), closes #405
  • scales: pin body composition to the reading it was measured for (dff4019)
  • scales: reject SIG date-time values the specification forbids (74dce86)
  • scales: reset per-session state in onSessionStart, not onConnected (d3cc7db)
  • scales: route the last BIA callers through the plausibility guard (e50c9f2)
  • scales: stop six adapters exporting the previous person's weigh-in (26f6a47), closes #394
  • scales: widen five name gates to scales whose protocol we already ship (fce8d7b), closes #409
  • secret handling, file permissions, and log injection (12d9acf)
  • three defects that crash the process or silently lose a weigh-in (55050e6)
  • yunmai: resolve the Mini variant per device, at session start (3271173), closes #406
  • yunmai: stop a later advertisement clearing the Mini variant flag (c4b3559), closes #406

Docs ​

  • correct five claims in the new documentation (aeecb0a), closes #407
  • document idle_rescan_delay, and correct what scan_cooldown governs (1d968d6), closes #398
  • document out_of_range, and correct what weight_range actually does (4e16e88), closes #395
  • document qn_config_long, and when to stop testing options one at a time (d62f15c)
  • document the export retry queue, and correct the FAQ (a26949a), closes #412
  • document what the code does but the docs never said (c4c69ed), closes #407
  • fix the config reference so a hand-written config actually loads (f92db73)
  • readme: add gvarph and chrisauer to the contributors grid (2ebb697)
  • say plainly that docker.mode is accepted and unused (e609b29), closes #407
  • scales: correct the exclusion notes on the SIG WSS decoder (74b6575)
  • scales: repair the onSessionStart doc comments (4d3a1ab)
  • stop naming a version out_of_range has not shipped in yet (0264bfe)
  • switch the star history chart back to star-history.com (15ac7bc)

1.28.0 (2026-09-07) ​

Added ​

  • ble: carry the device address, and auto-detect the anonymous ES-CS20M (e5b34c1), closes #376
  • qn: add ble.qn_time_sync_long for the vendor app's 9-byte 0x20 frame (4c8432c), closes #331
  • scales: add a shared impedance plausibility guard for BIA (58127d7), closes #386
  • tools: derive the published adapter and exporter counts from the code (770fd03), closes #366

Fixed ​

  • addon: emit the ble block when qn_a4_prelude is the only option set (8da7b8d), closes #331
  • addon: make the custom_config warning loop a real line continuation (25f151c)
  • beurer: a zeroed composition field is not a measurement (79b509b), closes #386
  • ble: cancel a pairing with the method BlueZ actually has (4ce82af), closes #335
  • ble: set the BlueZ duplicate filter before starting discovery, not after (4b37f20), closes #372
  • ble: stop cleanly while a BLE pairing is still in flight (4451122), closes #335
  • exporters: read every boolean field the safe way, not just garmin.weight_only (a5183cb)
  • qn: send the weight anchor before START, where the vendor app sends it (f18aa93), closes #331
  • qn: send the weight anchor once, not at ready time and again before START (66aae34)
  • runtime: force-exit with 0 when the stop was one you asked for (8c7779a), closes #335
  • scales: use the impedance these four adapters already parse (cc04ec4), closes #386

Docs ​

  • addon: say that a custom config edit needs a restart (6054a08)
  • body-composition: explain when an impedance is refused (dbf9f98), closes #386
  • qn: reunite two docblocks with the code they document (46bf2aa), closes #367
  • robi: the S9 does not read an impedance, so it is not on the #386 list (411acef)
  • supported-scales: two rows claimed body composition they do not produce (adcf4a0), closes #386

1.27.0 (2026-09-05) ​

Added ​

  • scales: add the Speediance Smart Scale FG2211WBF adapter (#383) (e1284ad)
  • ble: add the Home Assistant Bluetooth transport (#375) (56489ca)
  • ble: name the missing npm package when a BLE transport is not installed (1d1f448), closes #364
  • cli: dispatch subcommands from a single entry point (a8e0f3e), closes #365
  • cli: run the Garmin auth helper as a subcommand (fc93208), closes #365
  • exporters: let the Garmin exporter upload the weight alone (#373) (d270901)
  • qn: add ble.qn_a4_prelude, the two 0xA4 frames the Arboleaf app sends after START (dbb39dd), closes #331
  • scales: add the Etekcity ESF-551 Smart Fitness Scale (80769d4), closes #385
  • scales: add the Xiaomi Body Composition Scale S400 adapter (#374) (26b08e8)

Fixed ​

  • beurer: latch the SBF70 frame layout so a forced adapter stops reading 12.80 kg (f89561a), closes #384
  • ble: make the missing-transport message actionable where the app runs (db7edd2), closes #364
  • cli: answer --version, and stop promising flags a command ignores (2b1cfd9), closes #365
  • cli: print the command the reader can actually type (2f9c3d9), closes #365
  • cli: translate the Garmin arguments the way docker-entrypoint does (d16a36b), closes #365
  • config: look for config.yaml and .env in the working directory first (de98a29), closes #365
  • config: read config.yaml and .env from one directory, not two (6fddf9d), closes #365
  • diagnose: say when the configured transport is a proxy, instead of failing to load Noble (a4dd6aa), closes #376
  • influxdb: authenticate the healthcheck so InfluxDB v3 passes (4e4c846)
  • qn: hold the connection context across the FFE3 fallback so the 0x1F ack reaches the scale (#371) (28d8397), closes #370
  • runtime: fail fast when a BLE stack is missing, instead of retrying forever (cdba062), closes #364
  • salter: read both ends of a backlogged slot, not just the oldest (#360) (8c66101)
  • silvergear: log a settling weight once per value, not once per re-read (c6926ef), closes #372
  • speediance: run the BIA estimator, gate the impedance, and update the counts (580b18a)

Docs ​

  • cli: name the ble-scale-sync command in the remaining usage strings (6a8466b), closes #365
  • correct the source deployment guides for tsx as a devDependency (84529bf)
  • count 35 protocol adapters after the Xiaomi S400 landed (dae289d)
  • document the npm install path, the subcommands and where config is read from (420096b)
  • index: link the clone path instead of repeating it (f09a68d), closes #365
  • supported-scales: cut protocol detail and unpack the limitations (20c8c06)

1.26.0 (2026-08-26) ​

Added ​

  • ble: add a provisional weight type that cannot reach an exporter (3d8e7f1), closes #356
  • ble: expose transport liveness from the proxy watchers (5753c66), closes #281
  • ble: let removeDevice delete a bond when the caller asks (c2ddca9), closes #335
  • ble: opt-in recovery from a bond the scale has forgotten (d8f8a92), closes #335
  • ble: show settling weights on the live status line (817e952), closes #356
  • config: resolve a per-user weight anchor into the user profile (7ca681f)
  • qn: let the pre-weigh-in A2 carry a real weight anchor (a9aa0ae)
  • qn: make the live weight acknowledgement configurable (e25fc4e)
  • qn: report a device whose layout is 1byone, not QN (48d03be), closes #320
  • runtime: restart a proxy transport that has gone silent (a99bcdc), closes #281
  • silvergear: report the settling stream as a live weight (5d8ebd1), closes #356
  • update-check: add a persisted cooldown state module (d5aab9d)

Fixed ​

  • addon: use a plain string for qn_live_weight_ack and normalise it (4a995e2)
  • beurer: decode impedance instead of skipping it as unused (b3239f8), closes #354
  • beurer: say why the provisioning commit reports 0 from config (c3a469b), closes #335
  • beurer: stop warning about consent silence on a successful session (3eb96ca)
  • qn: acknowledge each live weight frame with its own weight (99486ab)
  • qn: say when the weight anchor falls back to the capture value (98677e4)
  • qn: send the configured weight anchor, not the captured 77.15 kg (7bed196)
  • salter: set the scale clock when unset so weigh-ins are stored (#355) (d30bec0)
  • update-check: make the once-per-day cooldown survive restarts (937916d)
  • worker: label the stats dashboard units honestly (22a3592)
  • worker: say the inflated update-check count is still growing (b6c1034)

Docs ​

  • beurer: correct the menu-profile claim and the battery-wipe scope (e0d406d), closes #335
  • ble: point the stale-bond message at the issue it describes (70e35ba), closes #335
  • correct adapter priority and exporter list claims (0712e8d)
  • document qn_live_weight_ack and expose it in the add-on (dd48faa), closes #75
  • document the persisted update-check cooldown (bf15cbb)
  • document the proxy liveness timeout and expose it in the add-on (eba1ea9), closes #281
  • document the stale-bond recovery option and expose it in the add-on (b44d0fa), closes #335
  • drop the unusable --config workaround for the cooldown state file (ac47111)
  • readme: add JamieSBenson to the contributors grid (9660aa5)
  • switch the star history chart to star-history.dera.page (e0dfe2b)
  • widen qn_weight_ack to cover the pre-weigh-in anchor (5c5c119)

1.25.0 (2026-08-25) ​

Added ​

  • beurer: register a SIG user record when consent has nothing to consent to (a517737), closes #335
  • ble: env-gated raw BLE frame capture for protocol debugging (#211) (9102b59)
  • notifications: report the other exporters' results with report_exports (#350) (3a237f6)
  • qn: add ble.qn_report_byte so the history-response byte can be tested (74bd6cd)
  • scales: add Salter SA00656/SA00432 (#341) (7586389)
  • scales: add the Silvergear Smart Scale 108 broadcast adapter (53ed2d2), closes #297
  • scales: add Xiaomi Mi Smart Scale 2 XMTZC04HM adapter (#353) (e8bc91a)

Fixed ​

  • addon: normalise the QN byte options and stop ignoring them in silence (a1b7b41)
  • beurer: provision the date of birth in UTC so the day does not shift west of Greenwich (#348) (cb5af45)
  • beurer: say so when the scale never answers the consent (aadfaf0)
  • ble: measure the reading deadline as scale silence, not time since connect (#351) (c50fb7c)
  • ble: reach broadcast-only and nameless scales on every transport (0c0d8bb)
  • ble: release the notify subscription when adapter init loses the startup race (6b2138e), closes #338
  • ble: write the CCCD directly when the driver's subscribe fails (e47e779)
  • config: ignore watcher events that leave the config content unchanged (#349) (c66b6e7)
  • docker: install Python 3.12 for garminconnect 0.3.11 and keep the arm/v7 base (#352) (6eeecb3)
  • hutbit: claim a named Lefu FFB0 advert that omits the D618 service (4413599), closes #322
  • mqtt-proxy: stop leaking message listeners from timed-out and failed GATT sessions (#339) (cc2a050)
  • notifications: format weight, muscle and bone in the configured weight unit (#347) (013810e)
  • qn: apply the protocol byte override to the classic dialect and the pre-0x12 phases (#337) (86352a7)
  • qn: extend the 0xFC history-response byte to the es26m dialect (be9bc13)
  • qn: send 0xFC in the history response on the extended dialect (26553d2), closes #235
  • scan: match on the advertisement in the node-ble discovery tool (13e2a2e), closes #280
  • silvergear: read only the low five bits of the last byte as a checksum (7b30776), closes #297

Docs ​

  • correct the adapter file map, the MGB routing note and the ble example (fc100ed)
  • readme: add the contributors whose code has been merged (8fe22c8)

1.24.0 (2026-08-21) ​

Added ​

  • ble: log every advertisement on the proxy transports (857ff2c), closes #322
  • hutbit: decode the raw impedance frame and feed it to body composition (479ac02), closes #322
  • qn: decode extended-dialect b4/b1 result frames (#235) (129aa21)

Fixed ​

  • beurer: report the change-increment outcome in the profile commit line (34e92dc), closes #229
  • beurer: say when the consent was accepted but the vendor user table is empty (3de33c3), closes #229
  • ble: plumb the BlueZ advertisement into adapter matching on Linux (4a212e4)
  • hutbit: do not pair an impedance with a weight from an earlier weigh-in (ff8d0eb)
  • qn: echo the protocol byte on 19-byte scale info, with ble.qn_protocol_byte to override it (9401627), closes #75 #331
  • qn: take the extended-dialect weight from the live 0xB1, not the stored 0xB4 (0c0b03f), closes #235

Changed ​

  • ble: only build the advertisement line when debug logging is on (2095316)

Docs ​

  • scales: the Hutbit family now reports its raw impedance (74c9a08), closes #322

1.23.0 (2026-08-19) ​

Added ​

  • ble: answer the QN AE00 authentication challenge (#235, #75) (93bc9d3)
  • ble: honour ble.session_timeout_sec on the native read path (#83) (840cbb1)
  • config: add ble.force_scale_adapter to override protocol detection (#318, #319) (9c67525)
  • config: add users[].beurer_provision and ble.session_timeout_sec (#229, #83) (1d70cbc)
  • docker: report the image channel and commit at startup (#318) (7759fde)
  • scales: decode Eufy P2 Pro impedance from bytes [4..5] (#289) (416a038)

Fixed ​

  • ble: resolve the right adapter on both mqtt-proxy GATT paths (#317, #319) (044d72b)
  • ble: stop a BlueZ write rejection from killing the process (#138) (f198fd3)
  • config: warn instead of silently dropping unknown config keys (#318) (1bb67a3)
  • firmware: use esptool v5 command names and require esptool 5 or newer (49ccecd)
  • qn-scale: echo the protocol type from the twenty byte scale info frame (#235) (03fd3d7)
  • qn-scale: send the measurement trigger the extended dialect waits for (#235) (6083307)
  • runtime: survive a stray promise rejection in continuous mode (#138) (ba8dd63)
  • scales: accept the six-byte Lefu OEM advertisement (#318) (cc20fe7)
  • scales: correct defects found reviewing the issue sweep (#229, #235, #318) (f102a46)
  • scales: make an unprovisioned Beurer scale diagnosable (#229) (92cb8aa)

Docs ​

  • addon: point Home Assistant OS testers at the dev add-on channel (8399415)
  • explain dev builds, unknown config keys and the Beurer battery wipe (eeeeba0)
  • scales: stop claiming the Hutbit family has no usable impedance sensor (#322) (1873569)
  • scope session_timeout_sec honestly and stop publishing planning notes (758b53d)

1.22.1 (2026-08-02) ​

Fixed ​

  • ble: tear down the replacement client when an ESPHome rebuild fails (#303) (be875dd)

1.22.0 (2026-08-02) ​

Added ​

  • ble: add an opt-in ESPHome advertisement liveness watchdog (#303, #281) (2d0a8bc)

Fixed ​

  • ble: handle D-Bus transport errors and bound the poll cycle (#290) (8915190)
  • ble: log the ESPHome proxy client lifecycle and guarantee teardown (#303, #281) (8a4b5d5)
  • ble: name the stale bond behind le-connection-abort-by-local (#290, #229) (04e7012)
  • ble: re-send a send-once unlock after notifications are enabled (#283) (78d2cbb)
  • ble: register the BlueZ pairing agent for already-bonded scales (#83) (fc2a568)
  • ble: stop retrying the BlueZ default-agent role once it is unavailable (#83) (baa83ec)
  • ci: check out the release branch without tracking so the add-on changelog sync runs (#301) (eaa2002)
  • scales: dispatch QN AE02 frames separately and subscribe to AE02 once (#75, #235) (c3c5fb0)
  • scales: log the QN frame that the weight parser discards (#75, #235) (6fcff59)

Docs ​

  • resolve the Bluetooth group GID instead of hardcoding 112 (#311) (734e5d7)
  • scales: correct the Eufy P2 impedance claim and the R-MSC04 blocker (#289, #117) (179d9b0)

1.21.2 (2026-07-27) ​

Fixed ​

  • addon: generate the add-on changelog from the project changelog (#294) (249197e)
  • ble: also treat a bluetoothd UnknownObject on a device path as a dropped peer (#297) (943bc81)
  • ble: connect with discovery active when BlueZ drops the peer object (#297) (86f3b87)
  • ble: correct defects found reviewing the issue sweep (#296, #252, #229) (74ad3a6)
  • ble: decode esphome GATT payloads from the base64 data field (#291) (2c868dc)
  • ble: write the CCCD over the ESPHome proxy so scales actually notify (#252) (32e5af1)
  • ci: fetch the release branch with a refspec so the add-on changelog sync can check it out (#300) (5569e96)
  • firmware: end a GATT session the host never finishes so scanning resumes (#296) (73ded71)
  • firmware: read indicate-only chars via indicated() so Robi S9 FFB3 result reaches the adapter (#248) (9036d00)
  • scales: commit the Beurer user profile after consent so the scale finishes (#229) (fae6326)

Docs ​

  • add bondesen and junaidk to contributors (e26419a)
  • esphome-proxy: document the CCCD fix and the unbonded-link limit (#252) (1bc623f)

1.21.1 (2026-07-22) ​

Fixed ​

  • ble: bond and retry GATT discovery for consent scales on timeout (a3a828e)
  • scales: treat Eufy P2/P2 Pro GATT reading as weight-only (76ca675)

Docs ​

  • correct adapter and exporter counts, add Robi S9 row (87b6bbd)

1.21.0 (2026-07-20) ​

Added ​

Fixed ​

  • body-comp: report muscle mass as fat-free mass minus bone (#253) (7efa781)
  • runtime: tick the healthcheck heartbeat on a timer so idle is not unhealthy (#277) (2fca68f)
  • scales: claim OEM/rebranded Hutbit ("SWAN") via the 0x02AC manufacturer signature (#279) (eb3b669)
  • scales: gate Eufy P2 and 1byone completion on a settled weight (#284) (#285) (7edfe6c)
  • scales: narrow the Hutbit OEM claim and pass mfg data on noble target-MAC (#278) (9d17405)
  • scales: route Beurer BF788 and BF950 to the SIG consent adapter (#229, #255) (5cd3f92)
  • scales: stop the Beurer adapter hijacking its siblings, and honour lb frames (c5362a2)

Docs ​

  • document the shipped Renpho R-MSC04 adapter and its weight-only limitation (#117) (8ece358)

1.20.0 (2026-07-13) ​

Added ​

Fixed ​

  • addon: run the Home Assistant add-on unconfined so it can reach BlueZ (cac714b), closes #271
  • ble: keep the ESPHome proxy connection alive on unknown API messages (#252) (e561b91)
  • ble: re-resolve adapter after GATT discovery on proxy paths (#258, #251, #255) (9aca212)
  • ble: wrap dynamic-path device.gatt() in withTimeout (#273) (0531e35)
  • firmware: enable BLE CCCD notifications in start_notify (#274) (368353e)
  • renpho: only require the UCP char, guard the vendor 0xFFE2 writes (#267) (f4d7d5c)
  • renpho: perform SIG consent handshake so the ES-WBE28 streams readings (#267) (65ed721)
  • scales: complete Yunmai weight-only reading instead of timing out (a86ffe8)
  • scales: honour configured weight_unit in the QN display command (#269) (d188970)
  • scales: identify Type-1 QN scales by char pair over the ESP32 proxy (#272) (76737ed)
  • scales: route Beurer BF500 to the SIG consent adapter (#83) (25bbc20)
  • scales: write the Hutbit FFB1 handshake without response (#268) (de12300)

1.19.0 (2026-06-19) ​

Added ​

  • config: add ble.bind_key and adapter configure() hook (#232) (b319e69)
  • scales: add declarative MatchDescriptor and matchesDescriptor helper (#245) (7e63799)
  • scales: add Xiaomi Mijia S800 broadcast adapter (#232) (5ee2c2e)
  • scales: central resolveAdapter with priority-based precedence and startup overlap detection (#245) (dcc2bdd)
  • scales: register Xiaomi S800 and inject bind key at startup (#232) (d3fdb65)

Fixed ​

  • ble: advertise lazy_notify capability to ESP32 proxy (#231) (9004d31)
  • ble: command host-ordered proxy notify enable after subscribe (#231) (d33e3aa)
  • ble: correct Robi S9 weight scaling to 3-byte grams (#248) (21b3afa)
  • firmware: add per-board IDF-heap guard tunables defaulting to 0 (#139) (59bd65f)
  • firmware: drain aioble services before characteristic discovery (#231) (c584098)
  • firmware: drive aioble GATT discovery with async for (#231) (c943e11)
  • firmware: enable proxy notify lazily on per-char subscribe command (#231) (f4a4492)
  • firmware: parse lazy_notify capability flag from config (#231) (b24292a)
  • firmware: refuse GATT connect on near-empty IDF heap instead of crashing (#139) (1beac82)
  • firmware: retry opposite BLE address type on any connect failure (#231) (7a958ab)
  • firmware: trust controller-reported BLE address type on autonomous connect (#231) (4ae2662)
  • scales: preserve exact/startsWith claim semantics in generic-adapter exclusion (#245) (99a5a10)

Docs ​

  • align scale-type vs board support table cells for #139 (e301da7)
  • document no-PSRAM broadcast-only vs PSRAM GATT support for #139 (fa63930)
  • document Xiaomi S800 support and ble.bind_key (#232) (6ccccb0)
  • firmware: correct gc-vs-IDF-heap comment around the connect guard (#139) (e576e8e)
  • fix stale counts, logo easter egg, and consolidate changelog menu (cf85243)
  • unify historical changelog entries to release-please format (6d29c7e)

1.18.0 (2026-06-16) ​

Added ​

Fixed ​

  • ble: bootstrap ESP32 autonomous GATT connect for GATT-only scales (#231) (2266086)
  • ble: connect before publishing scan results on autonomous path (#231) (793fdb2)
  • ble: connect random-address GATT scales via ESP32 proxy (#231) (989ace6)
  • ble: correct misreported static-random addr_type at scan source (#231) (0a2097e)
  • ble: derive connect addr_type from MAC bits for misreported random scales (#231) (4909038)
  • ble: preserve bond by not RemoveDevice'ing paired scales (#168) (d662199)
  • ble: register BlueZ pairing agent for BF720 encrypted bonding (#168) (d2a1aaf)
  • esp32: restore aioble IRQ before GATT connect (#231) (c29dc1c)

Changed ​

  • ble: single pre-connect GC pass on PSRAM boards (#231) (62e134b)

Docs ​

  • note minimal-delay autonomous connect for GATT scales (#231) (b679550)
  • recommend scale_mac for GATT-only scales over the ESP32 proxy (#231) (8b670c1)

1.17.0 (2026-06-15) ​

Added ​

  • ble: add Robi S9 adapter (Lefu/Fitdays FFB0-new protocol) (#228) (4a8fda5)

Fixed ​

  • ble: attempt bonding and name failed CCCD subscribe for Beurer BF720 (#168) (45bfd87)
  • ble: match QN scales advertising AE00 alongside fff0 (#235) (6a12687)
  • firmware: ease GATT connect on no-PSRAM ESP32 + heap diagnostic (#139) (7bef0f6)

1.16.1 (2026-06-15) ​

Fixed ​

  • ble: decode Sanitas SBF70/BF710 body composition via 0x59 ACK stream (#211) (c3beb00)
  • ble: parse QN 0x23 stored record for V10 Renpho/ES-CS20M (#213) (5617f28)
  • ble: read ESPHome proxy pre-decoded GATT uuid (#229) (#234) (c1079c7)

1.16.0 (2026-06-04) ​

Added ​

  • ble: add adapter liveness probe for watchdog classification (#213) (e50ce5b)
  • ble: add poll-failure classification helpers (#213) (484e16a)
  • mqtt-proxy: ESP32 autonomous GATT connect for fast-sleeping scales (#214) (c4a3b33)

Fixed ​

  • ble: keep a permanent error listener on the ESPHome proxy client (#210) (ca7eb7d)
  • ble: reset gattInProgress on failed GATT connect + clearer ESP32 errors (#201) (dcd1ac6)
  • ble: send BLE address type on ESPHome proxy GATT connect (#215) (#223) (71e735b)
  • ble: tag idle vs wedge poll failures in scanAndReadRaw (#213) (407753e)
  • config: accept bare 32-hex CoreBluetooth UUID for scale_mac (#212) (#224) (47d5033)
  • firmware: flush scan batch early on a known scale MAC (#201) (c692b42)
  • runtime: watchdog ignores idle no-shows (#213) (9e1588f)

Docs ​

1.15.0 (2026-05-21) ​

Added ​

  • exporter: add Intervals.icu exporter (#203) (60af3d0)
  • exporter: add Telegram exporter (#207) (0b73e0f)
  • firmware: support generic ESP-WROOM-32 boards (487e482)

Fixed ​

  • ble: route dual-mode adapters to GATT when no broadcast data (#201) (9de2dee)
  • exporters: fail fast on non-retryable HTTP errors + validate required config (6ebea77)
  • firmware: harden constrained ESP32 boards against scan-buffer OOM (c99605e)
  • firmware: parse 128-bit / 32-bit service UUIDs and service data (bf918ad)
  • firmware: publish service-UUID-only devices in scan results (c1f9723), closes #201

Docs ​

  • bump adapter and exporter counts to 25 and 8 (0e21abe)
  • scales: note Mi Scale 2 works on all BLE transports (e126848)

1.14.1 (2026-05-19) ​

Fixed ​

  • firmware: correct mip ref position for async primitives install (8974caa), closes #198
  • force-exit on hung shutdown so the watchdog actually restarts the container (#194) (#196) (3b60393)

1.14.0 (2026-05-18) ​

Added ​

  • ble: registry self-check to prevent adapter matches() collisions (#182) (663f55a)
  • ble: ESPHome proxy Phase 2 - GATT + multi-proxy (#116) (#187) (c850aa3)
  • scales: Beurer BF720 / BF105 SIG-standard adapter (#168) (#180) (3f52568)
  • scales: add Eufy T9120 (A1) support (#178) (3f5ecd4)

Fixed ​

  • scales: defer Renpho ES-WBE28 from the QN adapter (#191) (27a58d5)
  • ble: characteristic-aware adapter matching for 0xFFF0 collision (#177) (#179) (6cb7992)

Docs ​

  • add boildead and alexw23 to contributors (bf15b87)

1.13.1 (2026-05-18) ​

Fixed ​

  • docker: add missing setup-strava command to entrypoint (#188) (#189) (af47e3a)

1.13.0 (2026-05-13) ​

Added ​

  • drop Node 20 support, require Node 22+ (25712d1)
  • replay cached offline frames with timestamps (#164) (6bae585)
  • scale: support Renpho ES-32MD via ES-CS20M adapter (#172) (068c14f)

Fixed ​

  • deps: regen package-lock.json with emnapi entries (CI fix) (40b3d9d)
  • runtime: use raw weight for single-user mqtt display (b8d0b90)

1.12.1 (2026-05-12) ​

Fixed ​

  • ble: RSSI freshness no longer treats absent prop as stale (#167) (8340caa), closes #169

1.12.0 (2026-05-06) ​

Added ​

  • config: hot-reload config.yaml without restart (#123) (d213d5c)
  • runtime: systemd Type=notify watchdog integration (#144) (68ee5bf)
  • scales: add Xiaomi Mi Scale 2 broadcast adapter (#134) (6e5ffb4)
  • scales: scaffold experimental ADE A2 adapter (#159) (#160) (11aec6f)
  • trisa: implement ADE BA 1600 challenge response (#138) (#158) (91aa28e)

Fixed ​

  • ble: guard against dying-peer connect stall (#143) (abd3c22)
  • ble: per-address grace state in esphome-proxy scanAndReadRaw (#161) (875cb28)
  • review: address v1.12.0 review findings (ed90fbf)
  • runtime: cache systemd-notify ENOENT to avoid spawn-storm on hosts without it (#144) (8959287)
  • scales: ack offline frames on Renpho ES-26BB-B (#157) (70f2066)
  • scales: warn instead of debug when ES-26BB-B offline ack write fails (4e265e1)

Docs ​

  • deep consistency sweep across guide and reference pages (e979b0c)
  • readme: bump scale count, note Linux stability features (5a47c06)

1.11.0 (2026-05-01) ​

Added ​

  • ble: consecutive-failure watchdog for BlueZ stuck-state recovery (#154) (dcc822d)
  • trisa: support ADE BA 1600 (fitvigo) firmware variant — weight only (#153) (515d1a7)

Fixed ​

  • firmware: match scan_duration_ms to GATT connect timeout (#141) (54dcc3b)
  • node-ble: add timeout to device.gatt() and waitForRawReading (#142) (2e45366), closes #140
  • prevent orphaned BlueZ discovery sessions in continuous mode (#81) (e6ee513)
  • trisa: fail fast when no measurement char and harden ADE parser boundary (74a34a6)

Docs ​

  • addon: backfill CHANGELOG and refresh DOCS for v1.8.2 through v1.10.2 (d918a1d)
  • document Pi 3/4 BlueZ stuck-state limitation + watchdog mitigation (a039b4b)
  • esp32-proxy: add Windows flashing guidance (#152) (2796d7b)
  • trisa: clarify optional-binding + variant-detection semantics (ae87d84)

Thanks ​

  • @fromport for the device.gatt() / waitForRawReading GATT-acquisition timeout fix that prevents indefinite hangs on stalled BlueZ adapters (#142)

1.10.2 (2026-04-24) ​

Fixed ​

  • Renpho ES-26M: the 18-byte 0x12 scale-info frame (where byte[1] is the packet length and bytes [2-7] carry the device MAC) was being misread as "byte[2] is the protocol type", yielding proto=0xFF and causing every subsequent handshake command to be rejected by the scale. No 0x10 weight frames were ever returned. The QN-Scale adapter now detects the long-frame format (data.length >= 18 && data[1] === length) and falls through to the ES-30M code path with weightScaleFactor=10, so the existing heuristic auto-corrects the weight divisor. The unconditional skip of R1=R2=0 stable frames in ES-30M mode is also lifted: the ES-26M never reports impedance when the user is wearing socks, and those frames are the only stable reading available in that scenario. Contributed by @fromport (#128)

Changed ​

  • ESPHome proxy: the handler now logs a one-time Phase 1 capability summary on connect, listing which configured scale adapters are broadcast-capable (produce readings on this transport) and which are GATT-only (will time out until Phase 2 / #116 ships). Users who were only seeing the generic Timed out waiting for any recognized scale broadcast via ESPHome proxy line now immediately see whether their scale brand is in the broadcast-capable set, instead of having to reproduce the failure twice to catch the per-MAC warn. Surfaces the Yunmai / Beurer / Mi Scale 2 / etc. mismatch reported by @geniusliang in #133

Docs ​

  • CONTRIBUTING.md: project structure tree refreshed to match the current layout (HA add-on, ESPHome proxy handler, updated scale/test files)
  • README: contributors migrated to a GitHub-style table with inline avatars, @fromport added for the ES-26M contribution

Thanks ​

  • @fromport for diagnosing and fixing the ES-26M handshake end to end, including the heuristic weight-divisor path
  • @geniusliang for the detailed ESPHome proxy repro that led to the capability-summary UX improvement

1.10.1 (2026-04-22) ​

Fixed ​

  • ESPHome proxy: the ReadingWatcher silently dropped advertisements from dual-mode scale adapters (parseBroadcast defined and charNotifyUuid set) when the broadcast frame was not weight-bearing. Reported by @deadhurricane on an Elis 1 / ES-30M, which matches by name as a QN-Scale but only beacons its MAC in manufacturer data and carries weight over GATT. The handler now warns once per MAC that the scale needs a GATT connection (Phase 2), pointing the user at the native BLE handler or the ESP32 MQTT proxy as workarounds instead of leaving them staring at a silent log (#116, #75)
  • Logger: runtime.debug: true in config.yaml did not switch the log level to DEBUG, only the DEBUG=true env var did. The app now honors the config value on startup, so HA Add-on users (who pass debug as an option, not an env var) and anyone driving the runtime from config.yaml get the verbose BLE logs they expect

1.10.0 (2026-04-22) ​

Added ​

  • Embedded MQTT broker for the ESP32 proxy: zero-config setup, no Mosquitto required. When ble.mqtt_proxy.broker_url is omitted, BLE Scale Sync now starts an embedded aedes broker on 0.0.0.0:1883 by default; the internal client connects over loopback, and the ESP32 firmware just points at the host machine's LAN IP. Port and bind interface are configurable via embedded_broker_port and embedded_broker_bind, optional username/password are enforced when set. Existing broker_url setups are untouched (#54)
  • ESPHome Bluetooth proxy transport (experimental, phase 1 / broadcast-only): third BLE handler option ble.handler: esphome-proxy. If you already run an ESPHome Bluetooth proxy mesh for Home Assistant, BLE Scale Sync can connect to it over Native API (port 6053, optional Noise encryption or legacy password) and reuse it as its BLE radio, so no dedicated ESP32 with custom firmware and no MQTT broker are required. Phase 1 handles broadcast scales only; GATT scales log a warning and are skipped until phase 2 lands. New docs/guide/esphome-proxy.md covers setup, encryption key handling and limitations (#116)
  • Setup wizard: new "Use built-in embedded broker" option for the mqtt-proxy handler, so new installs skip the external broker prompt by default. Handler selection now also includes "Via ESPHome Bluetooth proxy" with prompts for host, port and authentication mode

Security ​

  • Embedded MQTT broker: configs that bind the embedded broker to a non-loopback interface (default 0.0.0.0) now require username + password and are rejected at schema validation time. The wizard defaults to requiring auth and falls back to 127.0.0.1 when the user declines, so misconfiguration cannot silently expose a LAN-reachable broker without credentials.
  • ESPHome proxy: configs that set both encryption_key and password are rejected at schema validation time. Pick Noise (encryption_key, recommended) or legacy password, not both.

1.9.0 (2026-04-21) ​

Added ​

  • Eufy Smart Scale P2 (T9148) and P2 Pro (T9149): new dedicated adapter with the AES-128-CBC C0/C1/C2/C3 handshake required by these models. Weight + impedance over GATT FFF2 after authentication, passive weight reading from the 19-byte advertisement without connecting. Prevents the prior false match as a QN scale that crashed with Operation is not supported on FFF1 (#98)

Fixed ​

  • Setup wizard: picking no exporter in the export-targets checkbox silently produced a config without any global_exporters block, so the first run emitted All exports failed and exited with code 1. The wizard now asks an explicit Continue without exporters? confirmation when the checkbox is submitted empty and re-prompts if the user declines (#98)
  • Orchestrator: dispatchExports([]) logged All exports failed because allFailed defaulted to true with zero iterations. Empty exporter lists now short-circuit with a clear warning (No exporters configured — measurement processed but not sent anywhere) and return success, so single-shot mode no longer exits with code 1 when the config has no exporters

Thanks ​

  • @mart1058 and @dbrb2 for diagnose output, HCI snoop logs, and testing the Eufy P2 Pro protocol reverse-engineering (#98)
  • bdr99/eufylife-ble-client for the reference Python implementation of the Eufy T9148/T9149 auth handshake and frame formats

1.8.2 (2026-04-20) ​

Fixed ​

  • Sanitas SBF70 / Beurer BF710 family: weight parsed as a stuck 12.80 kg regardless of the real reading on the scale. Root cause: the BF710 variant (start byte 0xE7) sends a compact 5-byte 0x58 frame with weight at bytes [3-4] BE, not the 6+ byte BF700/BF800 layout the adapter assumed. The adapter rejected every live weight frame as too short and then mis-parsed the 0x59 finalize frame. Now branches on isBf710Type and applies a 3-reading stability window (0.3 kg tolerance) so the scale's initial metadata frame does not trigger early completion (#112)

Thanks ​

  • @flow778 for capturing raw BLE frames that made the fix possible (#112)

1.8.1 (2026-04-20) ​

Fixed ​

  • Garmin: upload failed with 'Garmin' object has no attribute 'garth' after garminconnect released 0.3.0 on 2026-04-02, which dropped the garth dependency in favor of native authentication. The Python bridge accessed garmin.garth.sess.headers and garmin.garth.dump(), both removed in 0.3.x. Migrated to the new API: Garmin.login(tokenstore) auto-persists on successful credential login, and client.dump(token_dir) saves tokens after MFA. Custom User-Agent override is no longer needed because garminconnect now uses curl_cffi TLS impersonation and randomized browser fingerprints internally (#114)
  • Docker: added libcurl4-openssl-dev so curl_cffi (new transitive dep via garminconnect 0.3.x) builds from source on armv7, where PyPI has no prebuilt wheel

Breaking ​

  • Tokens from garminconnect 0.2.x (old garth OAuth1/OAuth2 files) are incompatible with 0.3.x. Existing installs must re-authenticate: npm run setup-garmin, or in the HA Add-on just restart the add-on so it re-runs setup from the credentials you entered. The setup script auto-removes leftover oauth*_token.json files before writing the new token format.

Thanks ​

1.8.0 (2026-04-17) ​

Added ​

  • HA Add-on: one-click install via a My Home Assistant badge in the README, landing page, getting-started guide, and HA Add-on guide. Manual steps remain as a fallback for users without My Home Assistant configured
  • HA Add-on: weight_unit and height_unit exposed as add-on options (kg/lbs, cm/in). The CLI and exporters display in the chosen unit while internal math stays in kg/cm
  • HA Add-on: last_known_weight persists across restarts. The runtime config lives at /data/config.yaml and a small Python helper (merge_last_weights.py) copies preserved per-user weights from the previous run into the freshly generated config on every startup, so multi-user identification by weight stays accurate after reboots and add-on updates
  • Docs: new Home Assistant Add-on guide covering install, full configuration reference, MQTT auto-detection, Garmin setup (including the MFA and IP-block workarounds), custom config mode, persistence semantics, and troubleshooting. Promoted to a first-class quick-start in the README and landing page

1.7.5 (2026-04-15) ​

Fixed ​

  • HA Add-on: Garmin Connect uploads now work out of the box. The add-on previously created an empty /data/garmin-tokens/ directory and never ran the authentication step, so the first upload always failed with No such file or directory: '/data/garmin-tokens/oauth1_token.json'. On first start the add-on now runs setup_garmin.py --from-config to generate OAuth tokens from the email and password you entered in the UI (#111)
  • Docker: armv7 image builds failed because cffi (transitive dep via garminconnect) had no pre-built wheel for armv7 + Python 3.11 and pip could not compile from source. Added python3-dev, libffi-dev, and libssl-dev to the image so cffi builds cleanly

Added ​

  • HA Add-on: MFA-friendly token import. If your Garmin account uses 2FA, drop pre-generated oauth1_token.json and oauth2_token.json files into /share/ble-scale-sync/garmin-tokens/ and the add-on imports them on startup, skipping the interactive auth that has no terminal inside an add-on container
  • HA Add-on: DOCS.md now explains the full Garmin setup flow including the MFA workaround and the IP-block workaround

Thanks ​

1.7.4 (2026-04-02) ​

Fixed ​

  • QN Scale: rewrote adapter as a notification-driven state machine for newer firmware (Renpho Elis 1, ES-CS20M) that requires an AE00 service handshake before measurement data flows (#75, #84)
  • QN Scale: added ES-30M weight frame format detection (different byte layout for weight and impedance)
  • QN Scale: 0x13 config byte now sends 0x01 (kg) instead of 0x08, which was switching the scale display to lb
  • QN Scale: 2-second fallback timer for Linux (BlueZ D-Bus) where the initial 0x12 frame may be lost due to a CCCD subscription race condition
  • QN Scale: skip impedance-less stable frames on ES-30M so the adapter waits for the full body composition reading

Thanks ​

  • @DJBenson for extensive macOS testing, packet capture analysis, and reverse-engineering the state machine flow (#84)
  • @ericandreani for persistent Linux/Docker testing across multiple iterations (#75)

1.7.3 (2026-04-02) ​

Fixed ​

  • Docker: diagnose command was missing from the entrypoint, causing "exec: diagnose: not found" when running docker run ... diagnose <MAC> (#98)

Thanks ​

  • @mart1058 for reporting the missing Docker diagnose command (#98)

1.7.2 (2026-04-01) ​

Fixed ​

  • QN Scale: UUID fallback (FFF0/FFE0) no longer matches named devices from other brands. Prevents Eufy, 1byone, and similar scales that share the FFF0 service from being incorrectly identified as QN Scale and failing with "Operation is not supported" (#98)

Thanks ​

  • @mart1058 for reporting the Eufy P2 Pro connection failure (#98)

1.7.1 (2026-03-30) ​

Fixed ​

  • Update check: replaced strict 24-hour cooldown with calendar-day (UTC) comparison. Users who weigh in slightly earlier each day (e.g. 7:00 AM, then 6:55 AM) were being skipped

1.7.0 (2026-03-29) ​

Added ​

  • Update check with anonymous usage statistics (#87). After each successful measurement (max once per 24h), the app checks api.blescalesync.dev for newer versions. Only the app version, OS, and architecture are sent via the User-Agent header. Disable with update_check: false in config.yaml. Automatically disabled in CI environments
  • Cloudflare Worker (worker/) serving the /version endpoint and a public stats dashboard at stats.blescalesync.dev with aggregated anonymous data (version distribution, OS/architecture breakdown)
  • Setup wizard shows an update notice before the first step if a newer version is available
  • CI: GitHub Actions workflow for automatic Cloudflare Worker deployment on push to main (worker.yml)

1.6.4 (2026-03-27) ​

Fixed ​

  • BLE: use ATT Write Request instead of Reliable Write in node-ble handler, fixing "Operation is not supported" errors on Medisana BS430 and similar scales that do not support reliable writes (#85)

Improved ​

  • BLE: GATT characteristic flags are now logged during discovery (DEBUG=true) for easier troubleshooting

Thanks ​

1.6.3 (2026-03-04) ​

Fixed ​

  • Docker: removed cleanup workflow that was deleting multi-arch platform manifests, making all Docker images unpullable (#74, #76)

Thanks ​

1.6.2 (2026-03-02) ​

Changed ​

  • CI: Docker latest tag now only applies to GitHub releases, not every push to main (#70)
  • CI: Removed push-to-main Docker build trigger (#71)
  • Docs: SEO meta keywords added to all documentation pages (#69)
  • Docs: Alternatives page updated with Strava, file export, and ESP32 proxy sections (#68)
  • Docs: ESP32 BLE proxy section added to getting started guide (#67)

1.6.1 (2026-03-01) ​

Fixed ​

  • BlueZ stale discovery recovery after Docker container restart. Adds kernel-level adapter reset via btmgmt as Tier 4 fallback when D-Bus recovery fails, plus proactive adapter reset in Docker entrypoint (#39, #43)

Changed ​

  • CI: Docker cleanup workflow removes PR images and untagged versions from GHCR (#58)
  • Docs: Contributors section added to README
  • Node.js: minimum version bumped to 20.19.0 (required by eslint 10.0.2)
  • Deps: @stoprocent/noble 2.3.16, eslint 10.0.2, typescript-eslint 8.56.1, @types/node 25.3.3, @inquirer/prompts 8.3.0

Thanks ​

1.6.0 (2026-02-28) ​

Added ​

  • ESP32 BLE proxy (experimental) for remote BLE scanning over MQTT. Use a cheap ESP32 board (~8€) as a wireless Bluetooth radio, enabling BLE Scale Sync on machines without local Bluetooth. Supports both broadcast and GATT scales
  • ESP32 display board (Guition ESP32-S3-4848S040) with LVGL UI showing scan status, user matches, and export results
  • Beep feedback on ESP32 boards with I2S buzzer (Atom Echo) when a known scale is detected
  • Streaming BLE scan for ESP32-S3 boards with hardware radio coexistence
  • Docker mqtt-proxy compose (docker-compose.mqtt-proxy.yml) requiring no BlueZ, D-Bus, or NET_ADMIN
  • Setup wizard includes interactive mqtt-proxy configuration
  • BLE_HANDLER=mqtt-proxy environment variable as alternative to config.yaml
  • ESP32 proxy documentation page with architecture diagram, flashing guide, and MQTT topics reference

Changed ​

  • Renpho broadcast parsing consolidated into QN scale adapter
  • Landing page updated with ESP32 proxy and Setup Wizard feature cards

Thanks ​

  • @APIUM for the ESP32 MQTT proxy implementation (#45)

1.5.0 (2026-02-24) ​

Added ​

  • File exporter (CSV/JSONL) for local measurement logging without external services. Auto-header CSV with proper escaping, JSONL format, per-user file paths, and directory writability healthcheck
  • Strava exporter with OAuth2 token management. Updates athlete weight via PUT /api/v3/athlete. Automatic token refresh, restricted file permissions (0o600), and interactive setup script (npm run setup-strava)
  • Strava API application setup guide in documentation with step-by-step instructions

1.4.0 (2026-02-24) ​

Added ​

  • BLE diagnostic tool (npm run diagnose) for detailed device analysis: advertisement data, service UUIDs, RSSI, connectable flag, and step-by-step GATT connection testing
  • Broadcast mode for non-connectable QN-protocol scales (#34). Reads weight directly from BLE advertisement data without requiring a GATT connection
  • Garmin 2FA/MFA support in setup_garmin.py. Prompts for authenticator code when Garmin requires multi-factor authentication

Fixed ​

  • QN broadcast parser: corrected byte layout (LE uint16 at bytes 17-18, stability flag at byte 15). Previous layout produced incorrect weight values
  • ES-CS20M: service UUID 0x1A10 fallback for unnamed Yunmai-protocol devices (#34)
  • ES-CS20M: 0x11 STOP frame support as stability signal (#34)

Changed ​

  • CI: Node.js 24 added to test matrix (required check)
  • CI: PR-triggered Docker image builds with pr-{id} tags (#44)
  • Deps: ESLint v10, typescript-eslint v8.56

Thanks ​

1.3.0 (2026-02-16) ​

Added ​

  • Garmin multi-user Docker authentication — setup-garmin --user <name> and --all-users commands
  • setup_garmin.py --from-config mode reads users and credentials from config.yaml
  • --token-dir argument for garmin_upload.py and setup_garmin.py — per-user token directories
  • Tilde expansion for token_dir in TypeScript exporter
  • 4 new Garmin exporter tests (token_dir passing, tilde expansion, backward compat)
  • pyyaml dependency for config.yaml parsing in Python scripts
  • Docker multi-user volume examples in docker-compose.example.yml and docs

Fixed ​

  • Friendly error message when D-Bus socket is not accessible (missing -v /var/run/dbus:/var/run/dbus:ro in Docker) instead of raw ENOENT crash (#25)

Changed ​

  • Wizard passes Garmin credentials via environment variables instead of CLI arguments (security)

Thanks ​

1.2.2 (2026-02-14) ​

Added ​

  • Annotated config.yaml.example with all sections and exporters
  • CONTRIBUTING.md — development guide, project structure, test coverage, adding adapters/exporters, PR guidelines
  • CHANGELOG.md
  • GitHub Release and TypeScript badges
  • Documentation split into docs/ — exporters, multi-user, body-composition, troubleshooting

Changed ​

  • Rewrite README (~220 lines, Docker-first quick start, simplified scales table)
  • Move dev content (project structure, test coverage, adding adapters/exporters) into CONTRIBUTING.md
  • .env.example now notes that config.yaml is the preferred configuration method

1.2.1 (2026-02-13) ​

Added ​

  • Docker support with multi-arch images (linux/amd64, linux/arm64, linux/arm/v7)
  • Dockerfile, docker-entrypoint.sh, docker-compose.example.yml
  • GitHub Actions workflow for automated GHCR builds on release
  • Docker health check via heartbeat file

1.2.0 (2026-02-13) ​

Added ​

  • Interactive setup wizard (npm run setup) — BLE discovery, user profiles, exporter configuration, connectivity tests
  • Edit mode — reconfigure any section without starting over
  • Non-interactive mode (--non-interactive) for CI/automation
  • Schema-driven exporter prompts — new exporters auto-appear in the wizard

1.1.0 (2026-02-13) ​

Added ​

  • Multi-user support — weight-based user matching (4-tier priority)
  • Per-user exporters (override global for specific users)
  • config.yaml as primary configuration format (.env fallback preserved)
  • Automatic last_known_weight tracking (debounced, atomic YAML writes)
  • Drift detection — warns when weight approaches range boundaries
  • unknown_user strategy (nearest, log, ignore)
  • SIGHUP config reload (Linux/macOS)
  • Exporter registry with self-describing schemas
  • Multi-user context propagation to all 5 exporters (MQTT topic routing, InfluxDB tags, Webhook fields, Ntfy prefix)

1.0.1 (2026-02-13) ​

Changed ​

  • Configuration is now config.yaml-first with .env as legacy fallback
  • README rewritten for config.yaml workflow

1.0.0 (2026-02-12) ​

Added ​

  • Initial release
  • 23 BLE scale adapters (QN-Scale, Xiaomi Mi Scale 2, Yunmai, Beurer, Sanitas, Medisana, and more)
  • 5 export targets: Garmin Connect, MQTT (Home Assistant), Webhook, InfluxDB, Ntfy
  • BIA body composition calculation (10 metrics)
  • Cross-platform BLE support (Linux/node-ble, Windows/@abandonware/noble, macOS/@stoprocent/noble)
  • Continuous mode with auto-reconnect
  • Auto-discovery (no MAC address required)
  • Exporter healthchecks at startup
  • 894 unit tests across 49 test files

Released under the GPL-3.0 License.
Software made in Europe