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.
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.
RyFitHandlercomputes age withDAY_OF_YEAR, which is off by one across a leap daycore/bluetooth/scales/RyFitHandler.kt:288-294defines a private extension that shadows the propertyScaleUseralready exposes:Comparing
DAY_OF_YEARacross different years is unsound, because 29 February shifts every subsequent day by one:Born in a leap year, on or after 1 March:
DAY_OF_YEARis 61 for 2000-03-01 but 60 for 2023-03-01, so60 < 61triggers 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 (
sendC0usesage), so it changes the numbers the scale computes.ScaleUseralready has this solved:core/bluetooth/data/ScaleUser.kt:37providesgetAge(todayDate)and:49anageproperty, both comparing month and day rather than day-of-year, andcore/utils/CalculationUtils.kt:25-30hasageOn()built onjava.time.Period— which is what the rest of the codebase uses. The fix is to delete the private extension and use the existing property; thecoerceIn(10, 100)clamp can stay at the call site if it's wanted.2.
EEBBLHandlerlistsBODY_COMPOSITIONas implemented, but never writes anycore/bluetooth/scales/EEBBLHandler.kt:54:The comment says the parsing isn't implemented, and the file confirms it: grepping the whole handler for
fat =,water =,muscle =orbone =returns zero assignments. Every one of its fivepublish(...)calls sends weight only.Since
implementedis whatBluetoothScreen.kt:513,532uses 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 —
capabilitiesat:44still 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.