Skip to content

RyFitHandler: age is off by one across leap years; EEBBLHandler: BODY_COMPOSITION listed as implemented but never written #1460

Description

@ahmetabdullahgultekin

Two small, independent defects found while reading the handlers. Both are self-contained and need no hardware to judge; happy to send a PR for either or both.

1. RyFitHandler computes age with DAY_OF_YEAR, which is off by one across a leap day

core/bluetooth/scales/RyFitHandler.kt:288-294 defines a private extension that shadows the property ScaleUser already exposes:

private fun ScaleUser.getAge(): Int {
    val calNow = Calendar.getInstance()
    val calBirth = Calendar.getInstance().apply { time = this@getAge.birthday }
    var age = calNow.get(Calendar.YEAR) - calBirth.get(Calendar.YEAR)
    if (calNow.get(Calendar.DAY_OF_YEAR) < calBirth.get(Calendar.DAY_OF_YEAR)) age--
    return age.coerceIn(10, 100)
}

Comparing DAY_OF_YEAR across different years is unsound, because 29 February shifts every subsequent day by one:

born today this code correct
2000-03-01 2023-03-01 22 23
2000-12-31 2023-12-31 22 23
2000-03-01 2023-06-15 23 23
1995-03-01 2024-03-01 29 29

Born in a leap year, on or after 1 March: DAY_OF_YEAR is 61 for 2000-03-01 but 60 for 2023-03-01, so 60 < 61 triggers and a year is subtracted. The user is a year younger than they are, on their birthday and for the rest of that year.

That age is fed into the body-composition request (sendC0 uses age), so it changes the numbers the scale computes.

ScaleUser already has this solved: core/bluetooth/data/ScaleUser.kt:37 provides getAge(todayDate) and :49 an age property, both comparing month and day rather than day-of-year, and core/utils/CalculationUtils.kt:25-30 has ageOn() built on java.time.Period — which is what the rest of the codebase uses. The fix is to delete the private extension and use the existing property; the coerceIn(10, 100) clamp can stay at the call site if it's wanted.

2. EEBBLHandler lists BODY_COMPOSITION as implemented, but never writes any

core/bluetooth/scales/EEBBLHandler.kt:54:

implemented = setOf(
    DeviceCapability.LIVE_WEIGHT_STREAM,
    DeviceCapability.TIME_SYNC,
    DeviceCapability.USER_SYNC,
    DeviceCapability.UNIT_CONFIG,
    DeviceCapability.BODY_COMPOSITION   // partial: weight reliable; full metrics need parse impl + good measurement log
),

The comment says the parsing isn't implemented, and the file confirms it: grepping the whole handler for fat =, water =, muscle = or bone = returns zero assignments. Every one of its five publish(...) calls sends weight only.

Since implemented is what BluetoothScreen.kt:513,532 uses to decide whether a capability chip is shown as active or dimmed, this tells users the device delivers body composition when it doesn't.

Removing that one line is enough — capabilities at :44 still declares it, so the dimmed "supported but not implemented" chip stays, which looks like exactly the case this distinction was designed for.


I haven't touched either, since both are in handlers for scales I don't own — though neither claim depends on hardware behaviour: the first is arithmetic, the second is the file contradicting its own comment. Let me know if you'd like PRs.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementIndicates new feature requests

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions