From 58e98cfa4402f81dd38d6a943dc435f833a1364b Mon Sep 17 00:00:00 2001 From: "D.Voi" <129060703+D-Voi@users.noreply.github.com> Date: Fri, 28 Aug 2026 23:11:33 +0300 Subject: [PATCH 1/4] Added impedance support for Huawei AH/CH 100 Enabled the impedance reading in the handler and added all the fields that derive from the impedance. Tested the changes in my phone with HUAWEI AH100 and they work. Please let me know if these changes make it into the play store app by emailing me in: voikiaetsi@gmail.com --- .../bluetooth/scales/HuaweiAhCh100Handler.kt | 31 +++++++++++++------ 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/android_app/app/src/main/java/com/health/openscale/core/bluetooth/scales/HuaweiAhCh100Handler.kt b/android_app/app/src/main/java/com/health/openscale/core/bluetooth/scales/HuaweiAhCh100Handler.kt index 127d129bc..0473e276f 100644 --- a/android_app/app/src/main/java/com/health/openscale/core/bluetooth/scales/HuaweiAhCh100Handler.kt +++ b/android_app/app/src/main/java/com/health/openscale/core/bluetooth/scales/HuaweiAhCh100Handler.kt @@ -20,6 +20,7 @@ package com.health.openscale.core.bluetooth.scales import com.health.openscale.R import com.health.openscale.core.bluetooth.data.ScaleMeasurement import com.health.openscale.core.bluetooth.data.ScaleUser +import com.health.openscale.core.bluetooth.libs.StandardImpedanceLib import com.health.openscale.core.service.ScannedDeviceInfo import java.io.ByteArrayOutputStream import java.security.GeneralSecurityException @@ -247,7 +248,7 @@ class HuaweiAhCh100Handler : ScaleDeviceHandler() { // tuning sometimes packs the whole composition into one fat // notification because the negotiated MTU is large enough. if (deobfTail.size >= 15) { - tryDecodeFirstHalfImmediately(op) + tryDecodeFirstHalfImmediately(op, user) } } } @@ -259,7 +260,7 @@ class HuaweiAhCh100Handler : ScaleDeviceHandler() { pendingFirst = null pendingType = 0x00 if (first != null) { - decodeAndPublish(first, type) + decodeAndPublish(first, type, user) } } } @@ -284,12 +285,12 @@ class HuaweiAhCh100Handler : ScaleDeviceHandler() { } } - private fun tryDecodeFirstHalfImmediately(type: Byte) { + private fun tryDecodeFirstHalfImmediately(type: Byte, user: ScaleUser) { val first = pendingFirst ?: return val mk = magicKey ?: return try { val m = decodeFirstHalf(first, mk, macBytes()) - publishMeasurement(m, viaSingleFrame = true) + publishMeasurement(m, user, viaSingleFrame = true) // We've consumed the data; if the second half ever arrives the // 0x8E branch will see pendingFirst = null and silently drop it. pendingFirst = null @@ -302,14 +303,14 @@ class HuaweiAhCh100Handler : ScaleDeviceHandler() { } } - private fun decodeAndPublish(first: ByteArray, type: Byte) { + private fun decodeAndPublish(first: ByteArray, type: Byte, user: ScaleUser) { val mk = magicKey ?: run { logW("magicKey missing; dropping measurement") return } try { val m = decodeFirstHalf(first, mk, macBytes()) - publishMeasurement(m, viaSingleFrame = false) + publishMeasurement(m, user, viaSingleFrame = false) } catch (e: GeneralSecurityException) { logW("AES-CTR failed on measurement: ${e.message}") return @@ -320,7 +321,7 @@ class HuaweiAhCh100Handler : ScaleDeviceHandler() { if (type == NTFY_HISTORY_RECORD) sendGetHistoryNext() } - private fun publishMeasurement(m: Measurement, viaSingleFrame: Boolean) { + private fun publishMeasurement(m: Measurement, user: ScaleUser, viaSingleFrame: Boolean) { lastMeasuredWeightTenthKg = (m.weightKg * 10f).toInt() val sm = ScaleMeasurement().apply { @@ -328,11 +329,21 @@ class HuaweiAhCh100Handler : ScaleDeviceHandler() { this.dateTime = m.dateTime ?: Date() this.weight = m.weightKg this.fat = m.fatPct - // The scale reports impedance but the v2.5.4 reference doesn't - // derive water/muscle/bone from it; openScale's existing - // StandardImpedanceLib can be wired in later for that. + if (m.impedanceOhm in 1..3999) { this.impedance = m.impedanceOhm.toDouble() + val lib = StandardImpedanceLib( + gender = user.gender, + age = user.age, + weightKg = m.weightKg.toDouble(), + heightM = user.bodyHeight / 100.0, + impedance = m.impedanceOhm.toDouble(), + ) + this.water = lib.totalBodyWaterPercentage.toFloat() + this.muscle = lib.skeletalMusclePercentage.toFloat() + this.bone = lib.boneMassKg.toFloat() + this.bmr = lib.basalMetabolicRate.toFloat() + this.lbm = lib.fatFreeMassKg.toFloat() } } publish(sm) From 69477712f377183bb64c152d33c3ca0d5b6bd165 Mon Sep 17 00:00:00 2001 From: "D.Voi" <129060703+D-Voi@users.noreply.github.com> Date: Fri, 28 Aug 2026 23:30:01 +0300 Subject: [PATCH 2/4] Edge Case fix Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../bluetooth/scales/HuaweiAhCh100Handler.kt | 28 +++++++++++-------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/android_app/app/src/main/java/com/health/openscale/core/bluetooth/scales/HuaweiAhCh100Handler.kt b/android_app/app/src/main/java/com/health/openscale/core/bluetooth/scales/HuaweiAhCh100Handler.kt index 0473e276f..678bf636a 100644 --- a/android_app/app/src/main/java/com/health/openscale/core/bluetooth/scales/HuaweiAhCh100Handler.kt +++ b/android_app/app/src/main/java/com/health/openscale/core/bluetooth/scales/HuaweiAhCh100Handler.kt @@ -332,18 +332,22 @@ class HuaweiAhCh100Handler : ScaleDeviceHandler() { if (m.impedanceOhm in 1..3999) { this.impedance = m.impedanceOhm.toDouble() - val lib = StandardImpedanceLib( - gender = user.gender, - age = user.age, - weightKg = m.weightKg.toDouble(), - heightM = user.bodyHeight / 100.0, - impedance = m.impedanceOhm.toDouble(), - ) - this.water = lib.totalBodyWaterPercentage.toFloat() - this.muscle = lib.skeletalMusclePercentage.toFloat() - this.bone = lib.boneMassKg.toFloat() - this.bmr = lib.basalMetabolicRate.toFloat() - this.lbm = lib.fatFreeMassKg.toFloat() + + val heightM = user.bodyHeight / 100.0 + if (heightM > 0.0 && user.age > 0 && m.impedanceOhm in 1 until 1500) { + val lib = StandardImpedanceLib( + gender = user.gender, + age = user.age, + weightKg = m.weightKg.toDouble(), + heightM = heightM, + impedance = m.impedanceOhm.toDouble(), + ) + this.water = lib.totalBodyWaterPercentage.toFloat() + this.muscle = lib.skeletalMusclePercentage.toFloat() + this.bone = lib.boneMassKg.toFloat() + this.bmr = lib.basalMetabolicRate.toFloat() + this.lbm = lib.fatFreeMassKg.toFloat() + } } } publish(sm) From 78dd6c95d1f34afbb358e0c590e753d683b5a695 Mon Sep 17 00:00:00 2001 From: "D.Voi" <129060703+D-Voi@users.noreply.github.com> Date: Fri, 28 Aug 2026 23:35:50 +0300 Subject: [PATCH 3/4] More edge case fixes Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../openscale/core/bluetooth/scales/HuaweiAhCh100Handler.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android_app/app/src/main/java/com/health/openscale/core/bluetooth/scales/HuaweiAhCh100Handler.kt b/android_app/app/src/main/java/com/health/openscale/core/bluetooth/scales/HuaweiAhCh100Handler.kt index 678bf636a..84fee023c 100644 --- a/android_app/app/src/main/java/com/health/openscale/core/bluetooth/scales/HuaweiAhCh100Handler.kt +++ b/android_app/app/src/main/java/com/health/openscale/core/bluetooth/scales/HuaweiAhCh100Handler.kt @@ -334,7 +334,7 @@ class HuaweiAhCh100Handler : ScaleDeviceHandler() { this.impedance = m.impedanceOhm.toDouble() val heightM = user.bodyHeight / 100.0 - if (heightM > 0.0 && user.age > 0 && m.impedanceOhm in 1 until 1500) { + if (heightM > 0.0 && user.age > 0 && m.weightKg > 0f && m.impedanceOhm in 1 until 1500) { val lib = StandardImpedanceLib( gender = user.gender, age = user.age, From c3f73761d6e66decae5f86c0aa0f6534dd182e7c Mon Sep 17 00:00:00 2001 From: "D.Voi" <129060703+D-Voi@users.noreply.github.com> Date: Sat, 29 Aug 2026 00:12:47 +0300 Subject: [PATCH 4/4] Some AI edge case fixes --- .../bluetooth/scales/HuaweiAhCh100Handler.kt | 49 ++++++++++--------- 1 file changed, 25 insertions(+), 24 deletions(-) diff --git a/android_app/app/src/main/java/com/health/openscale/core/bluetooth/scales/HuaweiAhCh100Handler.kt b/android_app/app/src/main/java/com/health/openscale/core/bluetooth/scales/HuaweiAhCh100Handler.kt index 84fee023c..4b99c599e 100644 --- a/android_app/app/src/main/java/com/health/openscale/core/bluetooth/scales/HuaweiAhCh100Handler.kt +++ b/android_app/app/src/main/java/com/health/openscale/core/bluetooth/scales/HuaweiAhCh100Handler.kt @@ -325,35 +325,32 @@ class HuaweiAhCh100Handler : ScaleDeviceHandler() { lastMeasuredWeightTenthKg = (m.weightKg * 10f).toInt() val sm = ScaleMeasurement().apply { - this.userId = m.userId + // FIX: Use openScale's internal app user ID, not the scale's hardware slot + this.userId = user.id this.dateTime = m.dateTime ?: Date() this.weight = m.weightKg this.fat = m.fatPct if (m.impedanceOhm in 1..3999) { this.impedance = m.impedanceOhm.toDouble() - - val heightM = user.bodyHeight / 100.0 - if (heightM > 0.0 && user.age > 0 && m.weightKg > 0f && m.impedanceOhm in 1 until 1500) { - val lib = StandardImpedanceLib( - gender = user.gender, - age = user.age, - weightKg = m.weightKg.toDouble(), - heightM = heightM, - impedance = m.impedanceOhm.toDouble(), - ) - this.water = lib.totalBodyWaterPercentage.toFloat() - this.muscle = lib.skeletalMusclePercentage.toFloat() - this.bone = lib.boneMassKg.toFloat() - this.bmr = lib.basalMetabolicRate.toFloat() - this.lbm = lib.fatFreeMassKg.toFloat() - } + val lib = StandardImpedanceLib( + gender = user.gender, + age = user.age, + weightKg = m.weightKg.toDouble(), + heightM = user.bodyHeight / 100.0, + impedance = m.impedanceOhm.toDouble(), + ) + this.water = lib.totalBodyWaterPercentage.toFloat() + this.muscle = lib.skeletalMusclePercentage.toFloat() + this.bone = lib.boneMassKg.toFloat() + this.bmr = lib.basalMetabolicRate.toFloat() + this.lbm = lib.fatFreeMassKg.toFloat() } } publish(sm) logI( "Measurement: ${m.weightKg} kg, fat=${m.fatPct}%, impedance=${m.impedanceOhm} Ω, " + - "userId=${m.userId} @ ${m.dateTime?.let(::ts) ?: "now"} " + + "scaleSlot=${m.userId} appUser=${user.id} @ ${m.dateTime?.let(::ts) ?: "now"} " + "(${if (viaSingleFrame) "single-frame" else "paired"})" ) @@ -731,7 +728,7 @@ class HuaweiAhCh100Handler : ScaleDeviceHandler() { * * Layout (little-endian unless noted): * ``` - * [0] userId (1..10) + * [0] userId (1..10) * [1..2] weight in tenth-kg (uint16 LE) * [3..4] fat in tenth-percent (uint16 LE) * [5..6] year (uint16 LE) @@ -760,11 +757,15 @@ class HuaweiAhCh100Handler : ScaleDeviceHandler() { val impedance = u16le(decrypted, 13) val date: Date? = try { - val cal = Calendar.getInstance().apply { - clear() - set(year, month - 1, day, hour, minute, second) + if (year < 2015) { + null // Fix: Prevent 2 BC time travel from dead batteries + } else { + val cal = Calendar.getInstance().apply { + clear() + set(year, month - 1, day, hour, minute, second) + } + cal.time } - cal.time } catch (_: Throwable) { null } @@ -818,4 +819,4 @@ class HuaweiAhCh100Handler : ScaleDeviceHandler() { } } } -} +} \ No newline at end of file