chore: meege upstream branch - #2
Open
AlphaKR93 wants to merge 80 commits into
Open
Conversation
* Translated using Weblate (Japanese) Currently translated at 97.2% (779 of 801 strings) Translation: openScale/Strings Translate-URL: https://hosted.weblate.org/projects/openscale/strings/ja/ * Translated using Weblate (Spanish) Currently translated at 100.0% (801 of 801 strings) Translation: openScale/Strings Translate-URL: https://hosted.weblate.org/projects/openscale/strings/es/ * Translated using Weblate (Russian) Currently translated at 92.1% (738 of 801 strings) Translation: openScale/Strings Translate-URL: https://hosted.weblate.org/projects/openscale/strings/ru/ * Translated using Weblate (German) Currently translated at 100.0% (801 of 801 strings) Translation: openScale/Strings Translate-URL: https://hosted.weblate.org/projects/openscale/strings/de/ * Added translation using Weblate (Catalan) * Translated using Weblate (Swedish) Currently translated at 88.7% (711 of 801 strings) Translation: openScale/Strings Translate-URL: https://hosted.weblate.org/projects/openscale/strings/sv/ * Translated using Weblate (Catalan) Currently translated at 8.8% (71 of 801 strings) Translation: openScale/Strings Translate-URL: https://hosted.weblate.org/projects/openscale/strings/ca/ * Translated using Weblate (Swedish) Currently translated at 89.1% (714 of 801 strings) Translation: openScale/Strings Translate-URL: https://hosted.weblate.org/projects/openscale/strings/sv/ * Translated using Weblate (Portuguese (Brazil)) Currently translated at 100.0% (801 of 801 strings) Translation: openScale/Strings Translate-URL: https://hosted.weblate.org/projects/openscale/strings/pt_BR/ * Translated using Weblate (Bulgarian) Currently translated at 100.0% (801 of 801 strings) Translation: openScale/Strings Translate-URL: https://hosted.weblate.org/projects/openscale/strings/bg/ * Translated using Weblate (French) Currently translated at 87.6% (702 of 801 strings) Translation: openScale/Strings Translate-URL: https://hosted.weblate.org/projects/openscale/strings/fr/ * Translated using Weblate (Bulgarian) Currently translated at 100.0% (801 of 801 strings) Translation: openScale/Strings Translate-URL: https://hosted.weblate.org/projects/openscale/strings/bg/ * Translated using Weblate (Swedish) Currently translated at 93.0% (745 of 801 strings) Translation: openScale/Strings Translate-URL: https://hosted.weblate.org/projects/openscale/strings/sv/ * Translated using Weblate (Swedish) Currently translated at 96.0% (769 of 801 strings) Translation: openScale/Strings Translate-URL: https://hosted.weblate.org/projects/openscale/strings/sv/ --------- Co-authored-by: apemay <apemay.dev@gmail.com> Co-authored-by: Isaac Clark <pkv.ko@yandex.ru> Co-authored-by: Holger Eisold <github@eisold.family> Co-authored-by: Eduard Ereza Martínez <eduard@ereza.cat> Co-authored-by: Joel A <joeax910@student.liu.se> Co-authored-by: Luan M. D. Lima <weblate.capitol237@slmail.me> Co-authored-by: Viktoria PETROVA <victoriaapetrova@gmail.com> Co-authored-by: François Kabiry <francoiskabiry@yahoo.fr>
Added values-hi/strings.xml to provide full Hindi localization support.
…st and update Hindi strings
* Add support for Yunmai X (YMBS-M268) broadcast-only scale The Yunmai X (YMBS-M268) advertises measurements as manufacturer-specific data without a local name and needs no GATT connection (the vendor app calls this device family "YUNMAI-ADV"). Frames echo the device MAC byte-reversed, carry a 0B 74 17 signature, a state byte (live/stable/ final), weight and impedance as u16 BE, and an XOR checksum. Identification uses the frame structure + MAC echo during scans and the advertised 16-bit service UUID 0x1320 for saved-device snapshots (the scale advertises no name to match on). Body composition is derived from weight + impedance via YunmaiLib, like the Yunmai SE/Mini handler. Protocol reverse-engineered from an advertisement capture and confirmed end-to-end against a live measurement driven through this handler (weight and full body composition derived and stored). Unit-test fixtures are synthetic frames built to the observed layout. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Show handler name for saved scales that advertise no name Broadcast-only scales such as the Yunmai X advertise no local name, so the saved-scale panel rendered a blank line above the MAC. Fall back to the resolved handler display name when the advertised name is blank. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* Added scale support for EEBBL scale. * Remove "DEBUG" from supported device names in EEBBLHandler --------- Co-authored-by: oliexdev <olie.xdev@googlemail.com>
The Vitafit "Body Fat" scale (VT701, Vitafit/Fitdays app) speaks a custom GATT protocol on service 0xFFF0 that no existing handler covers. The protocol was reverse-engineered from an HCI snoop of a Vitafit-app weigh-in and confirmed against the scale's displayed readings (85.00 kg / 393 Ohm, 84.95 kg / 389 Ohm). Frames are [hdr][len][0x26][type][data][chk][0xAA] with an XOR checksum. The handler streams live weight (type 0x10, stable at flag 0x02, u16 BE in 0.01 kg), acks the stable weight to trigger the impedance measurement, reads the whole-body impedance (type 0x11, u16 BE Ohm), and derives body composition via the shared StandardImpedanceLib. A 6-second fallback publishes weight-only if no impedance frame arrives. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: OliE <olie.xdev@googlemail.com>
* Add minimal Huawei Hagrid WSP support * moved HuaweiHagridSecretProvider into HuaweiHagridWspLib.kt --------- Co-authored-by: shisoratsu <277485761+shisoratsu@users.noreply.github.com> Co-authored-by: oliexdev <olie.xdev@googlemail.com>
- Allow external apps to enrich existing measurement entries with any metric (including custom types) via the `values_json` payload. - Skip types already handled by explicit columns to prevent redundant updates. - Add `GenericValueJsonTest` to verify round-trip parsing of numeric values across predefined and custom types.
Commit cd4d5e1 ("Fix: Use UTC for MiScale time synchronization") switched both the write of the BLE Current Time characteristic and the parsing of time read back from the scale from the device's default (local) timezone to hardcoded UTC. The scale itself has no timezone concept: it only ever receives and echoes back plain wall-clock digits (year/month/day/hour/minute/second), with no UTC/local distinction. It expects those digits to represent local wall-clock time. Sending it UTC digits instead - while still interpreting whatever it reports back as UTC - produces a timestamp permanently offset from the true measurement time by the device's local UTC offset (e.g. +2h during CEST, +1h during CET), since the round-trip no longer matches the convention the firmware itself assumes. This was found by cross-referencing years of MIBFS (Xiaomi Mi Body Composition Scale) measurements recorded by openScale against an independent BLE reader of the same physical scale: every measurement from before cd4d5e1 matches ground truth exactly; every measurement since is offset by exactly the local UTC delta at the time of measurement. No linked issue or repro was found for cd4d5e1's stated rationale ("previously the device's local timezone was used, which could lead to incorrect timestamps"), and it is contradicted by this data. This reverts the two timezone-related lines to their pre-cd4d5e16 behavior (local timezone for both write and parse), while keeping the unrelated day-of-week/adjust-reason payload byte correction from the same commit, which is a separate, valid protocol fix. Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
eLink/Icomon "MGB"-family scale on service 0xFFB0. Reads weight plus body fat, water, lean mass, bone, protein and visceral fat from the scale's data dump, with time/user/unit sync. Registered ahead of MGBHandler so its FitTrack name match wins the shared 0xFFB0 service. Includes unit tests for the frame decode.
) * Add selectable Scientific body-composition algorithm to Mi Scale Mi Scale v1/v2 previously derived body composition only from the reverse-engineered Mi Fit algorithm (MiScaleLib). This adds a per-scale setting letting users pick between that "Xiaomi (original app)" path and a new "Scientific" estimator. - BodyMiScaleLib: mono-frequency estimator ported from the bodymiscale Home Assistant integration (GPL-3.0). SCIENCE mode chains a hardware-calibrated LBM with peer-reviewed formulas (Siri, Pace, Wang, Schofield) and additionally reports protein and BMR. Locked to byte-for-byte parity with the reference project via a regression test. - MiScaleHandler: radio picker to choose the algorithm; new measurements use the selected path. - ScaleDeviceHandler: extract a reusable SettingRadioGroup composable (settings-backed radio group) for device configuration UIs. - CREDITS: add bodymiscale attribution. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Daniel Kukula <daniel.kuku@gmail.com> * Simplify BodyMiScaleLib to a scientific-only estimator The Mode enum threaded through every method was a vestigial artifact of the upstream bodymiscale port. openScale already selects the algorithm at the handler (MiScaleLib vs BodyMiScaleLib), so BodyMiScaleLib.Mode.XIAOMI was never used in production — only by a test comparing it to MiScaleLib. Drop Mode and its XIAOMI branches so the class is purely the scientific path. Method signatures shrink accordingly (getProtein no longer needs muscle/water, which existed only for the Xiaomi formula). Output is unchanged; the regression fixture still matches byte-for-byte. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Update CREDITS --------- Signed-off-by: Daniel Kukula <daniel.kuku@gmail.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: OliE <olie.xdev@googlemail.com>
* Add Runstar R6 scale support Adds RunstarR6Handler for the Runstar R6, whose FFB0 protocol is distinct from R5 despite the similar name. Publishes live weight, heart rate, impedance, and syncs offline-stored history entries. RunstarR5Handler now excludes "RUNSTAR-R6" explicitly so the two can't collide; R6 is registered ahead of R5 in ScaleFactory. Also corrects two stale comments in RobiS9Handler now that the shared FFB0 checksum and impedance/heart-rate offsets are known. * Tighten Runstar R5 name matching instead of excluding R6 R5 matched any "RUNSTAR-" prefix, which also caught R6 devices. Restrict it to the known "RUNSTAR-R5"/"RUNSTAR-RX" names instead of adding an R6-specific exclusion — removes the need for R6 to be registered ahead of R5, and the collision no longer needs explaining in RunstarR6Handler's doc. Also trims two RobiS9Handler comments down to the new finding (checksum formula, impedance/heart-rate offsets) without pointing at RunstarR6Handler for the backstory. --------- Co-authored-by: Leon Dönch <leon.doench@nelsen.de>
* Add Keep S3 scale support
* Revert addition of extended measurement metrics and database version 16
This change removes support for several recently added measurement types and reverts the database schema to version 15.
Key changes include:
* **Measurement Types:** Removed `PHASE_ANGLE`, `SKELETAL_MUSCLE`, `LEAN_SOFT_TISSUE`, `SUBCUTANEOUS_FAT`, `BODY_AGE`, `BMI_22_REFERENCE_WEIGHT`, and `DEVICE_IMPEDANCE` from `MeasurementTypeKey` and `ScaleMeasurement`.
* **Database:** Reverted `AppDatabase` version to 15, deleted `MIGRATION_15_16`, and removed the version 16 JSON schema.
* **Units & Strings:** Removed `UnitType.DEGREE` and associated string resources and localization logic.
* **Logic Cleanup:**
* Removed generalized percentage-to-mass unit conversion logic in `ConverterUtils` and `MeasurementTypeCrudUseCases`.
* Updated `KeepS3Handler` to stop publishing extended metrics.
* Removed `awaitPendingOperations` from the BLE transport interface.
* Add muscle percentage mapping in KeepS3Handler
* Fix Keep S3 runtime behavior
---------
Co-authored-by: oliexdev <olie.xdev@googlemail.com>
Shares service 0xFFB0 and the 20-byte frame family with RobiS9Handler and RunstarR6Handler, but differs in two ways. Frames fragment: byte 2 is a fragment index, not a constant zero, and the 0xA3 result spans two frames. Every fragment contributes bytes[3..18], so the byte after the fragment index is payload rather than a header byte to skip. Both existing handlers in this family reject such frames outright. The 0xBA profile is generated rather than replayed. RobiS9Handler replays a captured handshake because its timestamp and token could not be regenerated; this one is understood field by field, so it is built from the openScale user. flags1 carries sex and age and is the only place they are sent -- getting it wrong is silent, the scale simply computes for the wrong person. The generated frame is byte-identical to a capture the scale accepts. Body composition comes from Wla25BodyComposition, ported from ICBodyFatAlgorithmWLA25::calc in the vendor library: fat mass is a 13-term linear regression over height, weight, the rounded BMI and all ten impedances, and the rest follows from fat-free mass. Verified against the vendor library over 240 randomised inputs spanning both clamp boundaries, both impedance branches and both sexes -- exact on all ten fields. Three roundings are load-bearing and each is worth a tenth of a unit: the weight is rounded before anything else, the BMI fed into the regression is rounded, and fat mass is rounded before fat-free mass is derived from it. The rounding is half-up and computed in float32; everything else runs in double, matching the library. Doing it all in Float shifts BMR by 1 kcal in roughly one case in eighty. compute() takes no sex parameter: this algorithm's body composition does not depend on it, only metabolic age does. Known limitation: bone mass can read 0.1 kg below the scale's own display. The vendor app agrees with this code, and no algorithm the vendor library ships reproduces the scale's combination of values, so the scale's firmware appears to compute bone differently. Metabolic age is implemented but not published -- ScaleMeasurement has no field for it. EtekcityLib and HesleyHandler hit the same wall, one computing it and the other reading it off the wire, both discarding it. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Support 8-byte streaming frames in MGBHandler (Dr Trust Smart 505) MGBHandler previously assumed every notification was a 20-byte composite frame and dropped anything else, so scales like the Dr Trust Smart 505 (same SWAN name / 0xFFB0 service, but 8-byte streaming frames) connected and never produced a measurement. onNotification now dispatches on frame size: the existing 20-byte composite path is unchanged (only moved into onCompositeFrame), and a new 8-byte streaming path parses live/final weight and impedance frames via pure, unit-tested companion functions, latching both values before publishing one measurement and deriving body composition with StandardImpedanceLib (same approach as VitafitVT701Handler). * Inline onStreamingFrame into onNotification It had a single call site; folding it into the 8-byte branch matches VitafitVT701Handler's flatter onNotification style. No logic change. --------- Co-authored-by: Claude <noreply@anthropic.com>
* Add Keep S3 scale support
* Revert addition of extended measurement metrics and database version 16
This change removes support for several recently added measurement types and reverts the database schema to version 15.
Key changes include:
* **Measurement Types:** Removed `PHASE_ANGLE`, `SKELETAL_MUSCLE`, `LEAN_SOFT_TISSUE`, `SUBCUTANEOUS_FAT`, `BODY_AGE`, `BMI_22_REFERENCE_WEIGHT`, and `DEVICE_IMPEDANCE` from `MeasurementTypeKey` and `ScaleMeasurement`.
* **Database:** Reverted `AppDatabase` version to 15, deleted `MIGRATION_15_16`, and removed the version 16 JSON schema.
* **Units & Strings:** Removed `UnitType.DEGREE` and associated string resources and localization logic.
* **Logic Cleanup:**
* Removed generalized percentage-to-mass unit conversion logic in `ConverterUtils` and `MeasurementTypeCrudUseCases`.
* Updated `KeepS3Handler` to stop publishing extended metrics.
* Removed `awaitPendingOperations` from the BLE transport interface.
* Add muscle percentage mapping in KeepS3Handler
* Fix Keep S3 runtime behavior
* Complete Traditional Chinese translation
---------
Co-authored-by: oliexdev <olie.xdev@googlemail.com>
- Switched to `ExistingPeriodicWorkPolicy.UPDATE` to prevent backup postponement on reboots or settings changes - Added tracking and display for the next scheduled backup time in settings - Added detailed error reporting and specific failure states (location inaccessible, file creation, or write failures) - Removed flex interval from `PeriodicWorkRequest` to ensure more regular scheduling - Improved folder picker behavior by launching at the currently configured location - Added unit tests for backup error state persistence
* Added a "package" extra in `startSyncService()`, so all five triggers carry it and a future one cannot forget it.
Scales only report what they can weigh, so waist, hips, caliper and custom values were absent on every Bluetooth sync. applyValueInheritance fills those gaps from the last measurement *before* the incoming timestamp, so historic entries read from a scale's memory inherit the state of their own point in time, never of a later one. Only numeric (FLOAT/INT) values are inherited; empty, derived, internal and disabled types are skipped. Values the scale delivered always win. The add-measurement form now pre-fills through the same method, so a manual entry starts out exactly where a sync would.
Aggregated entries are synthetic and all carry Measurement.id == -1, so the LazyColumn key must not fall back to the id. It did whenever isAggregated was false while the list already held aggregated items — that flag comes from the settings flow, the items from the data flow, and the two disagree for a frame after an aggregation change or on the first composition, where the cached data flow already has the previous result. Every row then got the key -1 and Compose aborted the measure pass with "Key -1 was already used". Key by id + timestamp instead: unique for raw rows via the database id, unique for aggregated rows because their periods are disjoint.
* Complete the Turkish translation (154 -> 782 of 811 strings) Turkish was the least complete of the maintained locales at 19%, while German and Spanish sit at 95%. Four out of five strings a Turkish user saw were still English, which makes large parts of the app - Bluetooth setup, insights, backup and every error message - unreadable for users who do not speak English. This adds the 628 missing translatable strings. Existing translations are untouched, and their terminology is followed for consistency. Strings marked translatable=false are left out. Verified: gradlew :app:processDebugResources completes successfully; format specifiers match the source strings one for one. * Reword three Turkish strings that read awkwardly in context Found while walking the app on an emulator with the Turkish locale active - these were literal renderings that a native speaker would not say out loud. - permission prompt: 'erişimine gereksinimimiz var' -> 'erişimi gerekiyor' - generic confirm: the verb placeholder no longer has to fit a Turkish case ending, which it cannot do for an arbitrary verb - unit-change dialog: 'sürdürmek' -> 'devam etmek' * Shorten the plateau chip so it fits on screen in Turkish The insight chips sit in a horizontally scrolling Row, and my translation of the plateau chip was long enough that it was clipped at the screen edge on a 1080px-wide device: it rendered as '180 gunluk durag'. The English 'plateau %1$d days' fits, so this was the translation's problem, not the layout's. '%1$d gunluk duraganlik' -> '%1$d gun duragan', which is shorter than the English source and reads just as naturally. Verified on an Android 16 emulator with the app locale set to Turkish: the chip now renders in full as '180 gun duragan'.
Every other file in core/bluetooth/libs has a matching test - 17 libs, 16 tests. This adds the one that was missing. The class documents three rounding steps as load-bearing and pins the rounding mode with two exact values; both are asserted here through compute(), since round1 is private and rounds the incoming weight before anything else. The validity gate's asymmetry - slots 0 and 5 checked against 1.0, the other eight against 100.0 - is covered per slot, because that asymmetry is what fixes the ordering of the ten impedances. The remaining assertions are on invariants the source states outright (the 3-60% fat clamp, visceral fat 1-20, fat-free mass as weight minus fat, BMR from Katch-McArdle), so they hold independently of the regression constants and cannot go stale if those are ever retuned.
* Picooc: add a driver for the connection-oriented scale line Adds a handler for the Picooc scales that speak the 0xFFF0 GATT protocol (PICOOC-C1..CQ, PICOOC-14..25, PICOOC-IS, the S3 Lite V2.0 which advertises as PICOOC-CQ, and the Latin series, which advertises with no vendor prefix). The scale publishes weight, whole-body impedance and heart rate. It does not report body composition of its own, so fat/water/muscle/bone/LBM/BMR are derived from the impedance via StandardImpedanceLib, as the other impedance-only scales in this package do. The 0x32 composition frame is parsed and preferred when present. Protocol notes, verified against a PICOOC-CQ capture: - The scale opens with 0x3A (time request plus a BOM block) and expects a 10-byte reply; the length field counts the 0xF1 prefix. - It never sends the documented 0x30 profile request, so the sex/height/age frame is pushed unprompted after the handshake. Without it the scale has nothing to run its BIA engine on. - There is no live weight stream on this model: a single 0x39 arrives with the completion flag already set. - The pound field in 0x39 is 15 bits wide. 0x862C/10 reads 3434.8 lb, while (0x862C & 0x7FFF)/10 is 158.0 lb, which is exactly the 71.7 kg reported in the same frame. The top bit is a separate flag of unknown meaning. - 0x3C must be acknowledged. Unacknowledged, the scale resends a byte-identical "3C 05 00 00 01" ten times at 1 Hz and then drops the link, never reaching the reading its own display was showing. Acknowledged, it sends the in-progress frame once and then "3C 05 00 44 02" - 0x44 = 68 bpm, matching the display. Layout is [bpm BE 2B][status], status 1 measuring, 2 final. - Publication waits for the scale to hang up, which it does on its own once finished; a re-armed quiet period is the fallback. Heart rate lands 10 s after the weight, so publishing on the weight frame loses it. Opcodes 0x35, 0x36, 0x37, 0x3B, 0x3E and 0x3F are acknowledged and dumped to the log but not decoded: no capture has produced them yet. The metabolic body age in 0x32 and the phase angle in 0x39 are decoded and logged but not stored, as ScaleMeasurement has no field for either. PICOOC-L (Mini) is deliberately left unclaimed. It is broadcast-only and never accepts a GATT connection, so it needs a separate BROADCAST_ONLY handler. The handler is registered ahead of ScaleupHandler, which claims any device whose manufacturer data carries a 0xD0/0xE0 key regardless of name. https://garden.1900.live/22-knowledge/%E5%BC%80%E5%8F%91/picooc%E4%BD%93%E8%84%82%E7%A7%A4ble%E5%8D%8F%E8%AE%AE%E5%88%86%E6%9E%90 * Update ScaleFactory.kt --------- Co-authored-by: Каманцев Александр Андреевич <kamantsevaa@answer-42.ru> Co-authored-by: OliE <olie.xdev@googlemail.com>
* Add support for the Hume Health Dara 2.0 scale Fixes #1448. This device advertises literally as "Dara 2.0" and its BLE Device Information service reports manufacturer "LeFu Scale" — not FitTrack, despite sharing a model name with FitTrack's unrelated "Dara" scale (a different product on a different chip family, 0xFFB0/AC02 framing vs this device's 0xFFF0/0xCF framing). That name collision is why FitTrackDaraHandler's "FITTRACK"-prefixed name match never fired for this unit, even after 3.1.2 shipped named FitTrack Dara 2.0 support. New HumeDara2Handler matches on the device's actual advertised name and decodes the real 11-byte measurement frame (reverse-engineered from five real weigh-ins, ground truth documented in the class doc comment): weight and a raw BIA impedance reading, both cross-checked against the official Hume app's own display. Body composition (fat/water/muscle/ bone/BMR) is computed from that impedance via openScale's existing StandardImpedanceLib generic formula rather than guessed frame offsets, since the remaining frame bytes don't correlate with anything Hume's own app displays — this is an approximation of Hume's proprietary numbers, not a device reading, and the class doc comment states the measured accuracy against a real reading (~1-2% off on skeletal muscle %/BMR, ~15% on fat%/water%/lean mass, ~24% on bone). Kept as a sibling of ExcelvanCF36xHandler (same GATT layout and 0xCF frame header, evidently the same underlying chip family under a different rebrand) rather than merged into it, since the measurement frame shape differs and the device names don't collide — matching this codebase's existing one-handler-per-rebrand pattern. * Update ScaleFactory.kt --------- Co-authored-by: OliE <olie.xdev@googlemail.com>
Co-authored-by: danidemis <daniele.demicheli@outlook.it> Co-authored-by: OliE <olie.xdev@googlemail.com>
* Add selectable Scientific body-composition algorithm to Mi Scale Mi Scale v1/v2 previously derived body composition only from the reverse-engineered Mi Fit algorithm (MiScaleLib). This adds a per-scale setting letting users pick between that "Xiaomi (original app)" path and a new "Scientific" estimator. - BodyMiScaleLib: mono-frequency estimator ported from the bodymiscale Home Assistant integration (GPL-3.0). SCIENCE mode chains a hardware-calibrated LBM with peer-reviewed formulas (Siri, Pace, Wang, Schofield) and additionally reports protein and BMR. Locked to byte-for-byte parity with the reference project via a regression test. - MiScaleHandler: radio picker to choose the algorithm; new measurements use the selected path. - ScaleDeviceHandler: extract a reusable SettingRadioGroup composable (settings-backed radio group) for device configuration UIs. - CREDITS: add bodymiscale attribution. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Daniel Kukula <daniel.kuku@gmail.com> * Simplify BodyMiScaleLib to a scientific-only estimator The Mode enum threaded through every method was a vestigial artifact of the upstream bodymiscale port. openScale already selects the algorithm at the handler (MiScaleLib vs BodyMiScaleLib), so BodyMiScaleLib.Mode.XIAOMI was never used in production — only by a test comparing it to MiScaleLib. Drop Mode and its XIAOMI branches so the class is purely the scientific path. Method signatures shrink accordingly (getProtein no longer needs muscle/water, which existed only for the Xiaomi formula). Output is unchanged; the regression fixture still matches byte-for-byte. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Update CREDITS * Correct female body composition estimates --------- Signed-off-by: Daniel Kukula <daniel.kuku@gmail.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: OliE <olie.xdev@googlemail.com>
- Update aggregation logic to explicitly use `WeekFields` when calculating period bounds and keys - Pass `weekFields` into `periodBounds` and `periodKey` calls to ensure consistency across aggregation contexts - Update period label generation to consistently use `weekFields` derived from `LocaleUtils.systemWeekFields()` - Update date formatting in `MeasurementChartLayers` and `TableScreen` to correctly handle week labels based on shared `WeekFields` - Adjust `MeasurementAggregationUseCase` to accept and propagate `weekFields` for accurate aggregation key calculation
- Prevent processing stale advertisements after scan stops - Ensure handler cleanup when a weigh-in completes successfully - Explicitly call handler disconnection and detachment to reset per-session state
* Huawei Scale 3: add realtime body composition support * Huawei Scale 3: address review feedback * Adjust Scale 3 impedance handling for accurate BIA reporting - Correctly convert Scale 3 low-frequency impedance from 0.1-ohm units - Store the converted, model-used impedance value instead of the raw reading - Update tests to validate the correct impedance unit conversion for Scale 3 - Add necessary boilerplate documentation/licenses to Scale3BodyComposition - Update package documentation style across scale files --------- Co-authored-by: oliexdev <olie.xdev@googlemail.com>
* Never send a zero weight in the Huawei user record A Huawei CH100 capture showed the scale answering our USER_INFO with USER_CHANGED 127 times in 33 seconds, re-requesting the record after every reply. The record we sent carried weight 0: sendUserInfo falls back to user.initialWeight when no measurement has been taken yet in the session, and that field is 0 for a freshly created profile. Add ScaleDeviceHandler.fallbackWeightKg(), which walks last stored measurement -> profile initial weight -> BMI-22 estimate from body height, and never returns 0. On the same hardware this drops the USER_CHANGED storm from 127 cycles to 5. HuaweiCH100SHandler had the identical line and is fixed with it. Also pin two things the previous fix got wrong on paper: the encrypted USER_INFO payload is 16 bytes, not 14, and the unit tests do not remove the need to verify against real hardware. Most fixtures in HuaweiAhCh100HandlerTest were generated by a second implementation of the same assumed layout, so they can only show that two ports of one guess agree. Add a section with frames captured off a real scale and checked against its display, including the 16th byte the documented layout does not mention. Not addressed: the body fat percentage. It arrives fully computed in the frame, and re-measuring with a correct user record moved it from 33.8% to 33.9% -- i.e. not at all. The scale's own algorithm produces it, and at the reported 313 ohm StandardImpedanceLib explicitly declines to do better. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Do not claim the undocumented 16th byte is meaningless The two captures behind that claim were 0.1 kg apart, so a slowly-varying value like visceral fat level would look constant either way. #547 reports visceral fat 11.5 at BMI 24.8 and 14 at BMI 27.5 from the vendor app; these captures are BMI 37 and the byte reads 21. Pin the value, do not explain it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Declare 14, not 16, in the USER_INFO length byte The vendor app sends "DC 0E 09" followed by 16 encrypted bytes: the trailing 2-byte constant is transmitted but sits outside the declared length. v2.5.4's "Total = 14 bytes" note describes the same thing; the 3.x port pulled the trailer into the payload and moved the length byte to 16 with it. Measured on real hardware. Two idle sessions, connected, nobody standing on the scale, so the only variable is the length byte: 0x10 (16) 1026 USER_CHANGED polls over 274 s median 266 ms 0x0E (14) 533 USER_CHANGED polls over 532 s median 1001 ms The scale polls four times slower once the record is declared the way its own app declares it. Note what this does NOT show: the same comparison across the weight fix in the previous commit finds no difference (245 ms with weight 0 vs 266 ms with a correct weight, both at 0x10). That fix stands on sending correct data, not on this behaviour -- see the correction in the PR description. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Inline the user-record weight fallback into the Huawei handlers * Pin the user record with tests that can actually fail The USER_INFO test asserted a length byte it passed in itself: it built a frame with explicitLen = 14 and then checked the frame said 14. It would have stayed green if sendUserInfo went back to declaring 16 -- the same defect this PR is about, in the test that was meant to catch it. Replace it with HuaweiUserRecordWireTest, which attaches both handlers to a capturing transport, drives connect/wake/auth, and decrypts the record off the wire. Verified the tests can fail: reverting the weight fallback and the length byte turns 5 of the 7 red, and the 2 that stay green are the ones that do not depend on those lines. Fixtures are synthetic -- the tests derive every expected value from the protocol, so there is no reason to put a real device address or real body measurements in the repository. Also log both lengths for USER_INFO. A log line reading len=16 next to a frame reading 0x0E is how the length byte went unnoticed in the first place, and the payload comment claiming "Total = 16 bytes" over a frame that declares 14 was the same trap for the next reader. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FofPiyxen5QkNMQqk4gzA4 --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: oliexdev <olie.xdev@googlemail.com>
Replaces the MeasurementTypeKey enum with an identity-based registry (builtin./ble./user., schema 15->16). ScaleMeasurement becomes a typed value map; unit value classes (Kg, Percent, ...) enforce the delivery unit at compile time. CSV and sync lose their CUSTOM special cases; sync wire format is identity-only (API v3, needs matching openScale-sync). First users: BF1000 segmental values, S400 ECW/ICW/BCM.
The Beurer BF800-W sets bit 31 of the 4-byte timestamp on live CMD_MEASUREMENT frames while sending the same field with the bit cleared in CMD_SAVED_MEASUREMENT frames. Decoded unmasked, a live measurement lands roughly 68 years in the future, so measurements were saved dated 2094 (#1500). Unix timestamps do not need bit 31 until 2038, so the flag bit is safe to mask off. Adds decodeTimestampSeconds() and uses it for both the stored timestamp and the duplicate-suppression comparison.
* fix: use ScaleUser age in RyFit handler * fix: correct EEBBL capability status
Adds a handler for the AiLink/eLink broadcast body-fat scale family, driven
by the vendor "AiLink" app (com.pingwang.elink) and sold under many private
labels. The unit this was developed against advertises as "EL1" and is
retailed in Portugal as "BALANCA BODY SMART C/ APP".
These scales are non-connectable: they never accept a GATT link and instead
repeat the whole measurement inside an encrypted manufacturer record, so the
handler runs in BROADCAST_ONLY mode.
Advertisement layout (service 0xF0A0). AiLink abuses the manufacturer field:
the two bytes Android parses as a company id are really the vendor's CID and
VID, so ScanRecord hands the record back already stripped of them.
companyId = VID << 8 | CID
[0] PID
[1..6] MAC, little-endian
[7] checksum: sum of [8..17] & 0xFF
[8..17] 10-byte payload, TEA-encrypted
The payload is 32-round TEA over only the first 8 bytes, with a 128-bit key
built from the device ids (k0 = pid + 0x41493000, k1 = vid + 0x4C327900,
k2 = cid + 0x31783100, k3 = 0x306C6531). Decrypted it yields a rolling
sequence counter, a status byte, the weight with its unit and decimal count,
and the impedance.
Only the status 0xFF frame is published, and only once per session: the scale
streams live weight while the user settles, then latches the final frame and
repeats it for as long as it stays awake.
The impedance field is stale until the scale's BIA phase finishes, so it is
only ever read from the completed frame, and 0 is treated as "BIA failed" ->
publish weight without body composition. Verified on hardware: barefoot the
scale completes with 500 ohms, while standing on it through footwear it runs
the same BIA phase, detects the broken circuit and completes with status 3
and 0 ohms. Body composition itself is left to StandardImpedanceLib rather
than porting the vendor's formulas.
Protocol parsing lives in AiLinkLib, free of Android dependencies and covered
by unit tests. Its fixtures are synthetic - built by running the protocol's
own TEA encryption over invented readings - so they exercise the real cipher,
key derivation and checksum without committing anyone's health data.
Co-authored-by: oliexdev <olie.xdev@googlemail.com>
…he window
and new measurements dropped out of it. The end date is now optional and means
"always up to today".
- Resolve bounds in one place (TimeRangeFilter.resolveBounds); the UI and the
data pipeline had drifted apart.
- Snap bounds to local calendar days — the picker reports UTC midnight, and
rolling ranges shifted by an hour across a DST switch.
- Show the range picker as a Material 3 full-screen dialog, seeded from the
stored range.
- Optionally apply time range and aggregation to all screens, toggled per
section. Linking only swaps the settings prefix, so unlinking restores each
screen exactly.
#1507) * Fix crash on every successful weigh-in: unwrap Kg in saved-measurement snackbar 39a0c5c ("BLE handlers can contribute custom measurement types", #1481/#1484) changed BleConnector's "measurement saved" snackbar args from the old Float measurementData.weight to measurementData[MeasurementType.WEIGHT] ?: 0f. That subscript returns Kg? (the Key<Kg> type), not Float?, so whenever a measurement actually has a weight (i.e. on every successful weigh-in on every scale), the snackbar's format arg list carries a Kg object instead of a raw Float. Resources.getString then throws formatting it against the "%1$.1f kg" specifier in bluetooth_connector_measurement_saved: java.util.IllegalFormatConversionException: f != com.health.openscale.core.data.Kg at ... Resources.getString at ... AppNavigationKt$AppNavigation$2$1$1$2.emit Fix: unwrap .value to get the Float the format string expects. Added a regression test exercising the exact Resources.getString call that threw (constructing the full BleConnector needs a live ScaleCommunicator and DB facades, so the test mirrors just the argument-building step instead). Verified it fails against the pre-fix expression and passes against the fix. * Delete android_app/app/src/test/java/com/health/openscale/core/service/BleConnectorSnackbarTest.kt --------- Co-authored-by: OliE <olie.xdev@googlemail.com>
* Adds support for AnD UC352BLE * Delete android_app/gradle/gradle-daemon-jvm.properties * Update AndUC352BLEHandlerTest.kt * Update ScaleFactory.kt * Update AndUC352BLEHandler.kt --------- Co-authored-by: blowntobits <richardschneider@mail.de> Co-authored-by: OliE <olie.xdev@googlemail.com>
…nfig (#1502) * Fix QNHandler for GE CS 10 G "Fit Plus": pairing, weight parsing, config Three independent bugs, all confirmed against a BTSnoop capture of the official "Fit Profile" app driving a real Fit Plus unit (a GE-branded Qing Niu / QN-Scale device): 1. supportFor() never claimed the device. Its pre-connect advertisement carries only the FFE0 service UUID -- AE00 and FFF0 are real GATT services but only visible after connecting, so neither the QN-family name list nor the AE00 relaxation matched "Fit Plus". Added "fit plus" as a recognized name. 2. Live-weight (0x10) frames were never recognized as valid. This unit reports weightScaleFactor=100 via the 0x12 handshake frame but still uses the ES-30M byte layout ([4]=state, [5,6]=weight), which was previously gated on weightScaleFactor==10 only. Added isLongFrameVariant, derived from the 0x12 frame's own structure (length byte == frame length) rather than the scale factor, and used it as an additional trigger for ES-30M-style parsing. 3. State==1 ("stabilizing") was treated as stable alongside state==2, but state==1 frames always carry R1=R2=0 in the capture -- treating it as final would publish a measurement with fabricated impedance and then discard the real state==2 reading via the hasPublishedForThisSession guard. Tightened to state==2 only. Also, cross-referenced against the decompiled vendor Android SDK (CmdBuilder/QNDecoderImpl): - Added the 0x1F stable-reading acknowledgment ([0x1F, 0x05, protocolType, 0x10, checksum]), which the vendor app sends unconditionally and openScale never sent at all. - The 0x13 config command now carries the real user's height/age/gender instead of zeros, matching CmdBuilder.buildCmd(0x13, scaleType, [unit, lightInterval, height, age, gender]). QNHandlerProtocolTest.kt is new and includes a regression test built from byte-for-byte frames taken from the BTSnoop capture (device address anonymized) covering bugs 2 and 3 together. QNHandlerMatchTest gains a matching test for the FFE0-only advertisement and has its now-incorrect "Fit Plus should not match" negative case corrected to a generic device name. Verified end-to-end on the physical device: openScale now pairs and publishes real weight/impedance measurements. * Address review: drop broken length-based heuristic, scope fixes, fix impedance units - Remove isLongFrameVariant. byte[1] is the frame's own length for every well-formed frame in this protocol, so "byte[1] == frame length" is true almost always, not just for the 18-byte 0x12 variant -- it doesn't discriminate anything. Worse, it silently widened the ES-30M format gate for every classic factor-100 QN/Yolanda/Renpho device: any weigh-in whose low weight byte is <= 2 (~1/85 readings) would misparse the stable flag and impedance as weight. weightScaleFactor == 10.0f alone already selects the right layout for the captured Fit Plus family, since its 0x12 frame's byte[10] happens to fail the existing (pre-existing, unchanged) `== 1` check the same way a classic factor-10 device's would. - Scope the state==2-only tightening, the 0x1F ack, and the real height/age/gender in the 0x13 config to protocolType == 0xFF (the captured Fit Plus family). Every other protocolType keeps its original, longer-serving behavior (state 1 or 2 treated as stable, no 0x1F ack, zeroed config bytes) until a capture justifies extending these there too. - Fix impedance units for the same family: the vendor SDK's own two-byte combine (MeasureDecoder.h -> twoByte2Int) applies no scaling, so raw r1 (e.g. 4350) is left as reported. But TrisaBodyAnalyzeLib's formula is documented against impedance in the ~300-1000 Ohm range, matching r1/10, not raw r1 (which is landing ~10x above that and was being fed through an ad-hoc 0.3*(r1-400) transform whose constants only make sense against a ~300-1000 input to begin with). Stores and uses r1/10 for this family; every other protocolType keeps the existing raw-r1 + ad-hoc-transform behavior untouched. Flagged in comments as inferred, not confirmed against the vendor's own (unavailable) body-composition algorithm. - Merge master (typed MeasurementType/Kg/Ohm/Percent value map replacing direct ScaleMeasurement field access). Tests: two new companions per gated behavior proving classic protocolTypes keep their original behavior, plus a direct regression test for the low-weight-byte misparse the length heuristic would have caused. All QNHandler tests and the full suite pass.
Insights always computed over a user's whole history, so several years of
data drowned out anything recent. It was the only data screen without the
filter menu.
- Wire the filter action into InsightsScreen, as in StatisticsScreen.
- Extract timeRangeBoundsFlow out of buildScreenFlow so insightsFlow reuses
the same persisted-range resolution.
- insightsForUser takes optional start/end millis and filters before computing.
- Skip the type-filter-row toggle for Insights, as for Statistics — its row
always shows, so the entry would do nothing.
Signed-off-by: Fhatuwani Sikhwari <fhatu.sikhwari@gmail.com>
* feat: Add support for PICOOC Mini Lite broadcast scale Non-connectable PICOOC-L advertisements carry weight and impedance; body composition is a clean-room port of the vendor's 4.3.0 White/Caucasian BIA path, with an online learner for the vendor's anchor_weight/anchor_beta profile state. Plumbs the existing useAssistedWeighing flag (9436dcf) into ScaleUser so the handler can skip composition while assisted weighing is active. * Wire up metabolic age across handlers, trim PICOOC PR --------- Co-authored-by: dfadeev <1289810+dnsfdv@users.noreply.github.com> Co-authored-by: oliexdev <olie.xdev@googlemail.com>
* Translated using Weblate (Portuguese (Brazil)) Currently translated at 100.0% (811 of 811 strings) Translation: openScale/Strings Translate-URL: https://hosted.weblate.org/projects/openscale/strings/pt_BR/ * Translated using Weblate (Polish) Currently translated at 32.6% (265 of 811 strings) Translation: openScale/Strings Translate-URL: https://hosted.weblate.org/projects/openscale/strings/pl/ * Translated using Weblate (Catalan) Currently translated at 42.4% (355 of 837 strings) Translation: openScale/Strings Translate-URL: https://hosted.weblate.org/projects/openscale/strings/ca/ * Translated using Weblate (Catalan) Currently translated at 100.0% (837 of 837 strings) Translation: openScale/Strings Translate-URL: https://hosted.weblate.org/projects/openscale/strings/ca/ * Translated using Weblate (Chinese (Simplified Han script)) Currently translated at 98.0% (821 of 837 strings) Translation: openScale/Strings Translate-URL: https://hosted.weblate.org/projects/openscale/strings/zh_Hans/ * Translated using Weblate (Catalan) Currently translated at 100.0% (837 of 837 strings) Translation: openScale/Strings Translate-URL: https://hosted.weblate.org/projects/openscale/strings/ca/ * Translated using Weblate (Hebrew) Currently translated at 19.1% (160 of 837 strings) Translation: openScale/Strings Translate-URL: https://hosted.weblate.org/projects/openscale/strings/he/ * Translated using Weblate (Hebrew) Currently translated at 21.1% (177 of 838 strings) Translation: openScale/Strings Translate-URL: https://hosted.weblate.org/projects/openscale/strings/he/ * Translated using Weblate (Catalan) Currently translated at 100.0% (838 of 838 strings) Translation: openScale/Strings Translate-URL: https://hosted.weblate.org/projects/openscale/strings/ca/ * Translated using Weblate (Catalan) Currently translated at 100.0% (861 of 861 strings) Translation: openScale/Strings Translate-URL: https://hosted.weblate.org/projects/openscale/strings/ca/ --------- Co-authored-by: Ddelaqua <d.delaqua@gmail.com> Co-authored-by: Michał Gątkowski <gatkowski.michal@gmail.com> Co-authored-by: Eduard Ereza Martínez <eduard@ereza.cat> Co-authored-by: Rin <rin@users.noreply.hosted.weblate.org> Co-authored-by: Yaron Shahrabani <sh.yaron@gmail.com>
Co-authored-by: Florent Daem <florent.daem@cyu.fr>
Co-authored-by: alma <alberto.marturano@trasna.io>
Share the BLE test fixtures instead of duplicating them: ScaleCatalog now also owns hex() and manufacturerData(), and 23 test files import uuid16 / device / hex from it rather than keeping a private copy. Four different hex-decoding idioms collapse into one. HealthKeep280HandlerTest re-implemented the decode inside the test and stayed green with the handler deleted. HealthKeep280Handler now exposes its frame accessors like HumeDara2Handler does, and the test drives those. Drop 10 BMI monotonicity tests that assert arithmetic rather than the libraries, and the duplicated Fit Plus claim in QNHandlerProtocolTest, which QNHandlerMatchTest already owns. Reuse RoomTestSupport in MeasurementDaoTest and BackupRestoreUseCasesTest instead of hand-building the database and five repositories. Move HuaweiScale3BodyComposition next to the other body-composition formulas in libs/ and switch its test to Truth. CI now also runs on pull_request, so contributor PRs are tested before merge. The release upload and the wiki publish stay guarded by github.event_name == 'push'.
…lag) (#1521) The FT-26R-W is a weight-only (no BIA electrodes), non-connectable QN/AABB broadcast scale. QNHandlerBroadcast already claims it, but never recorded a measurement: the handler treats bit 5 (0x20) of the status byte as "stable", and this model never sets it. Its status runs 0x14 while the weight is moving and latches to 0x15 once the display settles, so every reading was discarded. Move the wire format into QnBroadcastAdv, an internal object in the handler file, and teach it the bit-0 encoding. Because a false "stable" writes a wrong weight into the user's history silently, bit 0 is only honoured for payloads that also carry the FT-26R's fixed signature at bytes [19-21]; every other AABB device keeps the original bit-5-only rule, so an unrecognised model cannot be made worse by this. The signature is derived from a single unit, so it is used only to widen acceptance, never to narrow it. Verified on a physical FT-26R-W. Adds 11 tests driving the real parser and a real ScanRecord; removing the bit-0 support fails two of them.
* 1byone (classic): stop silently discarding valid weigh-ins
The 1byone "Health Scale" dropped roughly half its readings, with no log line
and no user-visible error, so a working connection was indistinguishable from a
broken one. Two independent causes, both found by diffing openScale session logs
against the decompiled vendor app ("New iWellness 4.0", com.lefu.es.*).
Coalesced duplicate frames were parsed as history. The scale sends its final
measurement twice, and the two copies can arrive in a single notification: the
ATT payload caps at 20 bytes, so the buffer is one complete 11-byte frame
followed by the first 9 bytes of its duplicate. parseMeasurementFrame decided
history-vs-live on `size >= 18` alone, so it read bytes 11..17 of that buffer as
a timestamp, produced year 53138 / day 156 / hour 39, and the non-lenient
Calendar threw into a `catch { return }` that discarded the reading. Decide on
the XOR checksum at byte 10 plus a second 0xCF marker at byte 11 instead. A
genuine history frame carries the year high byte (0x07) at byte 11 and
measurement data at byte 10, so the two cases separate cleanly and history reads
on the Eufy C1/P1/A1 models sharing this handler are unaffected.
Zero-impedance frames lost the weight as well. The scale reports impedance 0
when it cannot run the bioimpedance measurement -- socks, shoes, poor foot
contact -- and the handler returned early, losing an otherwise valid weigh-in.
Publish the weight and skip only the derived body composition, matching what
EufyP2Handler and EufyC20Handler already do.
Two smaller fixes in the same paths:
Gate live frames on the lock status (byte 9 in {0x00, 0x36}, the vendor app's
"locked" values) so in-progress readings are not recorded. This replaces the
older `b9 != 1` guess with the protocol's own rule.
Arm a 3s fallback for the `F1 00` clock ACK. This scale never sends it -- the
vendor app never even sends F1 on this model -- yet both the history request and
the "step on the scale" prompt hung off it, so the user got no feedback at all.
waitAckClock is deliberately left set so a late ACK still starts the history
read; only the prompt is forced, once per connection.
Confirmed on hardware with a 1byone Health Scale: the same 103.00 kg weigh-in
taken a minute apart in socks (impedance 0, weight now saved instead of lost)
and barefoot (coalesced 20-byte frame, impedance 351 ohm, full body composition
derived). The unit-test vectors are the captured frames from those sessions plus
the three earlier coalesced frames that were being dropped.
Known gap: when a reading has no impedance the user is told nothing, because a
snackbar emitted at publish time is dismissed by BleConnector's saved-measurement
snackbar ~700 ms later. Fixing that properly belongs in the save path rather
than in a handler, so it is left out of this change and raised separately.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Drop weight-only publishing: it feeds openScale's value carry-over
Removes the weight-without-impedance path added in the previous commit, along
with the byte-9 lock gate that existed to make it safe. What remains is the
coalescing fix and the clock-ACK fallback, both confirmed on hardware.
Publishing a weight with no body composition looked like a strict improvement
over discarding the weigh-in. It is not. BleConnector drops any value that is
not > 0f, so the six body-composition fields reach the database as *missing*
rather than zero, and MeasurementTransformationUseCase.applyValueInheritance
then fills them in from the preceding measurement:
Carry-over: inherited 6 value(s) from measurement 8.
Carry-over: inherited 6 value(s) from measurement 10.
The second line is the tell: measurement 10 is itself a weight-only record, so
the stale values propagate forward from weigh-in to weigh-in. The user is shown
a complete-looking reading whose body fat, water and bone mass are days old. A
discarded reading is visibly absent; this one is quietly wrong.
The same capture also produced two records for one weigh-in. With socks on the
scale retries the bioimpedance measurement, and it re-sent the settled frame
nine seconds later, well outside any plausible de-duplication window.
Both review points are answered by this removal rather than by more machinery.
The lock-status gate is gone, so nothing guesses at Eufy's status byte values.
The rate limiter is untouched from master, so weight-only frames can no longer
consume its window -- there are none.
Weight-only measurements are still worth supporting, but not from a handler:
the carry-over interaction affects every handler that already publishes them
(EufyP2Handler, EufyC20Handler, VitafitVT701Handler's fallback). Raised
separately.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Don't prompt to step on a scale the user is already standing on
The clock-ACK fallback fires 3s after connect, but a reading can arrive sooner
than that -- 2.4s in one capture. The prompt then lands after the weigh-in has
already completed and, because a new snackbar dismisses the current one, it
replaces the "measurement saved" notice with an instruction the user has just
carried out.
Any CF measurement frame proves the user is on the scale, so cancel the pending
prompt when one arrives. Done before parsing rather than at publish, so it also
covers frames that are subsequently discarded: a frame without impedance still
means someone is standing there.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Improve handling of settled and live scale measurement frames
- Update weight frame parsing to ignore history records missing timestamps
- Implement logic to only record the first settled reading in a session, preventing duplicates
- Adjust body composition calculation to gracefully handle zero impedance readings while preserving weight data
- Add new helper functions to correctly check for final/settled reading states on scale data frames
- Update tests to verify correct handling of zero impedance and settled status frames
* Scope the 1byone acceptance rules to the 1byone and fix the build
The byte-9 lock whitelist and weight-only publishing come from the 1byone
vendor app and hardware. Applied family-wide they would silently drop every
live weigh-in on an Eufy C1/P1/A1 using a third status value, so key them off
the model; Eufy keeps the pre-existing b9 != 1 plus impedance required.
Also fix two compile errors from the master merge (ScaleMeasurement lost its
impedance property; the stray write in the no-impedance branch stored 0 ohm),
reported by jremy42, and reset the per-connection state in onConnected().
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: oliexdev <olie.xdev@googlemail.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.