Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
4c911ff
Weblate update (#1425)
oliexdev Jul 18, 2026
01e4f9b
Added complete Hindi translation (#1413)
anvikarande11 Jul 18, 2026
c89905e
Add Bulgarian, Catalan, Hindi, Portuguese, and Persian to language li…
oliexdev Jul 18, 2026
bc23d5a
Add YUNMAI-Scale-3 to YunmaiHandler device whitelist (#1416)
3125589 Jul 18, 2026
09f2df0
Add support for Yunmai X (YMBS-M268) broadcast-only scale (#1418)
pagarsky Jul 18, 2026
9d165a1
feat: add Scale Up broadcast scale protocol (#1419)
brainrom Jul 18, 2026
c7d1134
Added scale support for EEBBL scale. (#1420)
tannermeade Jul 18, 2026
7dbcaff
Add support for Vitafit VT701 body-fat scale (#1423)
gustav-dahl Jul 18, 2026
cf1f27e
Normalize comma decimal separators in NumberInputDialog and UserDetai…
oliexdev Jul 18, 2026
61d3636
Fix crash when importing pre-v2.5 database (issue #1410) (#1421)
DanyPM Jul 18, 2026
c01f7ea
Add minimal Huawei Hagrid WSP support (#1408)
shisoratsu Jul 18, 2026
0320339
Add support for `values_json` in `DatabaseProvider` update path
oliexdev Jul 19, 2026
fe77bcb
Revert MiScaleHandler time sync to device-local timezone (#1427)
jvandenbroek Jul 22, 2026
c261d87
Add support for the FitTrack Dara 2.0 scale (#1430)
richardlt Jul 24, 2026
bf0b91e
Add selectable Scientific body-composition algorithm to Mi Scale (#1…
dkuku Jul 24, 2026
c4ebad3
Move SettingRadioGroup from ScaleDeviceHandler to MiScaleHandler
oliexdev Jul 24, 2026
1ee3510
Add Runstar R6 scale support (#1432)
ldoench Jul 29, 2026
cfec324
Add Keep S3 scale support (#1438)
lekoOwO Aug 7, 2026
3a154c2
Add Relaxmedic scale support (icomon protocol 107) (#1441)
Caiofiga Aug 7, 2026
b6edeb4
Add support for Dr. Trust Smart 505 Scale (India) (#1445)
joshferns Aug 7, 2026
c968404
Complete Traditional Chinese translation (#1439)
lekoOwO Aug 7, 2026
10d3a2b
Improved auto-backup reliability and status reporting
oliexdev Aug 7, 2026
f49ebe3
Fix measurement type filter selection on fresh installs
oliexdev Aug 7, 2026
e4af22f
Name the sending openScale flavor in the sync intents.
oliexdev Aug 9, 2026
1f42b45
Remove orphaned translations for deleted strings
oliexdev Aug 9, 2026
44e1a56
Inherit numeric values from the preceding measurement
oliexdev Aug 9, 2026
7a170e4
update openScale to 3.1.2
oliexdev Aug 9, 2026
8ea4bbd
Fix crash when the overview list keys aggregated rows by id
oliexdev Aug 10, 2026
804a95c
Complete the Turkish translation (154 -> 782 of 811 strings) (#1451)
ahmetabdullahgultekin Aug 11, 2026
1acbe48
Add the missing test for Wla25BodyComposition (#1456)
ahmetabdullahgultekin Aug 11, 2026
2bae837
Keep intValue populated when aggregating INT-typed measurements (#1457)
ahmetabdullahgultekin Aug 11, 2026
7161ca0
Cult Smart Scale Pro: publish muscle as a percentage, not kilograms (…
ahmetabdullahgultekin Aug 11, 2026
3841f90
Store synced values in the column their input type requires
oliexdev Aug 11, 2026
e2069f7
Add support for the Weight Gurus 0376 scale (#1467)
psuedo-tty Aug 14, 2026
6751743
Support for BMI calculations for Active Era BS-06 scales (#1464)
NecroKote Aug 14, 2026
692bc0c
Support Terraillon X-LINE scale (#1461)
pjestin Aug 14, 2026
3889c46
Add support for the Omron HBF-702T and its WLC-protocol siblings (#1463)
ToastyTheBot Aug 14, 2026
5bed482
Add ScaleFactoryTest to cover the registry as a whole — the per-hand…
oliexdev Aug 14, 2026
fa4d792
Added support for Etekcity Fit 8S Scale. (#1472)
basjam Aug 16, 2026
910a436
Log unparseable CSV date/time values and keep the detail screen awake
oliexdev Aug 16, 2026
d3247a4
Update build workflow to publish scale catalog wiki page
oliexdev Aug 16, 2026
61cb196
Update Bluetooth scale handler registry and tests
oliexdev Aug 16, 2026
ccb1cc2
feat: Add support for the ProfiCare PC-PW 3008 BT (#1476)
tiagoagueda Aug 16, 2026
d53f165
Add huawei scale 3 support (#1474)
Nidala96 Aug 16, 2026
8a839ed
Decouple developer mode from saved device identity
oliexdev Aug 17, 2026
286cd11
Unify developer mode indicator with tint and device swap warning
oliexdev Aug 17, 2026
f7a733b
Improvements to the X-LINE (#1480)
pjestin Aug 21, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions .github/workflows/ci_master.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,46 @@ jobs:
files: |
android_app/app/build/outputs/apk/debug/openScale-debug.apk

# Publish the "Supported scales in openScale" wiki page, generated from the handler registry by
# ScaleCatalogTest. The page is a build artifact: hand edits in the wiki are overwritten, while
# the remarks column and the photo gallery are edited in
# android_app/app/src/test/resources/scale_catalog_{remarks.txt,gallery.md}.
publish-scale-catalog:
runs-on: ubuntu-latest
needs: build
permissions:
contents: write
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup JDK
uses: actions/setup-java@v3
with:
distribution: "zulu"
java-version: "21"
- name: Checkout wiki
run: git clone "https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.wiki.git" wiki
- name: Generate the supported scales page
working-directory: android_app
env:
SCALE_CATALOG_OUT: ${{ github.workspace }}/wiki/Supported-scales-in-openScale.md
run: |
./gradlew testDebugUnitTest --tests '*ScaleCatalogTest'
# A skipped (up-to-date) test task would leave the page untouched and the job green.
test -s "$SCALE_CATALOG_OUT"
- name: Push the page if it changed
working-directory: wiki
run: |
if [ -z "$(git status --porcelain)" ]; then
echo "Wiki already up to date."
exit 0
fi
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add Supported-scales-in-openScale.md
git commit -m "Update supported scales from the handler registry (${GITHUB_SHA::7})"
git push

# Keep the `weblate` branch (Weblate's push target) in sync with master so
# Weblate's rebased translation pushes stay fast-forward and don't fail.
sync-weblate:
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ openScale has built-in support for a number of Bluetooth (BLE or "smart") scales
- Silvercrest SBF75, SBF77
- Vigorun
- Xiaomi Mi scale v1 and v2
- Yunmai Mini and SE
- Yunmai Mini, SE and X
- iHealth HS3
- Easy Home 64050
- and many more
Expand Down
4 changes: 2 additions & 2 deletions android_app/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ android {
applicationId = "com.health.openscale"
minSdk = 31
targetSdk = 37
versionCode = 75
versionName = "3.1.1"
versionCode = 76
versionName = "3.1.2"

testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
manifestPlaceholders["appName"] = "openScale"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,12 @@
package com.health.openscale.core.bluetooth

import android.content.Context
import androidx.annotation.VisibleForTesting
import com.health.openscale.core.bluetooth.scales.BeurerBF450Handler
import com.health.openscale.core.bluetooth.scales.ScaleDeviceHandler
import com.health.openscale.core.bluetooth.scales.AAAxHandler
import com.health.openscale.core.bluetooth.scales.FitTrackDaraHandler
import com.health.openscale.core.bluetooth.scales.ScaleupHandler
import com.health.openscale.core.bluetooth.scales.ActiveEraBF06Handler
import com.health.openscale.core.bluetooth.scales.CultSmartScaleProHandler
import com.health.openscale.core.bluetooth.scales.BeurerSanitasHandler
Expand All @@ -36,13 +39,16 @@ import com.health.openscale.core.bluetooth.scales.EufyC20Handler
import com.health.openscale.core.bluetooth.scales.EufyP2Handler
import com.health.openscale.core.bluetooth.scales.EbelterBodyFatB2Handler
import com.health.openscale.core.bluetooth.scales.EtekcityESF551Handler
import com.health.openscale.core.bluetooth.scales.EtekcityFit8SHandler
import com.health.openscale.core.bluetooth.scales.GattScaleAdapter
import com.health.openscale.core.bluetooth.scales.HesleyHandler
import com.health.openscale.core.bluetooth.scales.HoffenBbs8107Handler
import com.health.openscale.core.bluetooth.scales.HuaweiAhCh100Handler
import com.health.openscale.core.bluetooth.scales.HuaweiCH100SHandler
import com.health.openscale.core.bluetooth.scales.HuaweiHagridWspHandler
import com.health.openscale.core.bluetooth.scales.IHealthHS3Handler
import com.health.openscale.core.bluetooth.scales.InlifeHandler
import com.health.openscale.core.bluetooth.scales.KeepS3Handler
import com.health.openscale.core.bluetooth.scales.LinkMode
import com.health.openscale.core.bluetooth.scales.MGBHandler
import com.health.openscale.core.bluetooth.scales.MedisanaBs44xHandler
Expand All @@ -51,15 +57,18 @@ import com.health.openscale.core.bluetooth.scales.MiScaleS400Handler
import com.health.openscale.core.bluetooth.scales.XiaomiS800Handler
import com.health.openscale.core.bluetooth.scales.BodyConnectHandler
import com.health.openscale.core.bluetooth.scales.OkOkHandler
import com.health.openscale.core.bluetooth.scales.OmronWlcHandler
import com.health.openscale.core.bluetooth.scales.OneByoneHandler
import com.health.openscale.core.bluetooth.scales.OneByoneNewHandler
import com.health.openscale.core.bluetooth.scales.QNHandler
import com.health.openscale.core.bluetooth.scales.QNHandlerBroadcast
import com.health.openscale.core.bluetooth.scales.RealmeSmartScaleHandler
import com.health.openscale.core.bluetooth.scales.RenphoES26BBHandler
import com.health.openscale.core.bluetooth.scales.RenphoHandler
import com.health.openscale.core.bluetooth.scales.RelaxmedicHandler
import com.health.openscale.core.bluetooth.scales.RobiS9Handler
import com.health.openscale.core.bluetooth.scales.RunstarR5Handler
import com.health.openscale.core.bluetooth.scales.RunstarR6Handler
import com.health.openscale.core.bluetooth.scales.RyFitHandler
import com.health.openscale.core.bluetooth.scales.SanitasSbf72Handler
import com.health.openscale.core.bluetooth.scales.SenssunHandler
Expand All @@ -68,17 +77,22 @@ import com.health.openscale.core.bluetooth.scales.SoehnleHandler
import com.health.openscale.core.bluetooth.scales.SppScaleAdapter
import com.health.openscale.core.bluetooth.scales.TaylorBIAHandler
import com.health.openscale.core.bluetooth.scales.DrTrustSSW532Handler
import com.health.openscale.core.bluetooth.scales.EEBBLHandler
import com.health.openscale.core.bluetooth.scales.StandardBeurerSanitasHandler
import com.health.openscale.core.bluetooth.scales.TrisaBodyAnalyzeHandler
import com.health.openscale.core.bluetooth.scales.TuningProfile
import com.health.openscale.core.bluetooth.scales.VitafitVT701Handler
import com.health.openscale.core.bluetooth.scales.WeightGurusA3Handler
import com.health.openscale.core.bluetooth.scales.YunmaiHandler
import com.health.openscale.core.bluetooth.scales.YunmaiXHandler
import com.health.openscale.core.facade.MeasurementFacade
import com.health.openscale.core.facade.SettingsFacade
import com.health.openscale.core.facade.UserFacade
import com.health.openscale.core.utils.LogManager
import com.health.openscale.core.service.ScannedDeviceInfo
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withTimeout
Expand All @@ -99,60 +113,97 @@ class ScaleFactory @Inject constructor(
) {
private val TAG = "ScaleHandlerFactory"

// List of modern Kotlin-based device handlers.
// Order matters: createCommunicator() returns the FIRST handler whose supportFor() is non-null.
// TaylorBIAHandler must stay ahead of MGBHandler — both live on service 0xFFB0, and MGBHandler
// also matches that service, so a later position would let MGB wrongly claim the Taylor scale.
private val modernKotlinHandlers: List<ScaleDeviceHandler> = listOf(
BeurerBF450Handler(),
TaylorBIAHandler(),
RyFitHandler(),
CultSmartScaleProHandler(),
RealmeSmartScaleHandler(),
YunmaiHandler(isMini = false),
YunmaiHandler(isMini = true),
TrisaBodyAnalyzeHandler(),
SanitasSbf72Handler(),
StandardBeurerSanitasHandler(),
SoehnleHandler(),
SinocareHandler(),
SenssunHandler(),
RenphoHandler(),
QNHandlerBroadcast(),
QNHandler(),
OneByoneHandler(),
OneByoneNewHandler(),
OkOkHandler(),
MiScaleS400Handler(),
XiaomiS800Handler(),
MiScaleHandler(),
RunstarR5Handler(),
RobiS9Handler(),
MGBHandler(),
MedisanaBs44xHandler(),
InlifeHandler(),
IHealthHS3Handler(),
HuaweiAhCh100Handler(),
HuaweiCH100SHandler(),
HoffenBbs8107Handler(),
HesleyHandler(),
ExingtechY1Handler(),
EbelterBodyFatB2Handler(),
ExcelvanCF36xHandler(),
EtekcityESF551Handler(),
EufyC20Handler(),
EufyP2Handler(),
ESCS20MHandler(),
RenphoES26BBHandler(),
DigooDGSO38HHandler(),
DebugGattHandler(),
CustomOpenScaleHandler(),
BeurerSanitasHandler(),
AAAxHandler(),
ActiveEraBF06Handler(),
DrTrustSSW532Handler(),
BodyConnectHandler(),
)
private val modernKotlinHandlers: List<ScaleDeviceHandler> = createHandlers()

companion object {
/**
* Builds the list of modern Kotlin-based device handlers.
*
* Order matters: [createCommunicator] returns the FIRST handler whose [ScaleDeviceHandler.supportFor]
* is non-null. TaylorBIAHandler, FitTrackDaraHandler, RelaxmedicHandler, RobiS9Handler and
* DrTrustSSW532Handler must stay ahead of MGBHandler — all live on service 0xFFB0, which
* MGBHandler matches on its own, so a later position would let MGB wrongly claim them.
*
* Exposed so the registry (order, device claims, duplicates) can be asserted in unit tests
* without building the Hilt graph — see `ScaleFactoryTest`.
*/
@VisibleForTesting
internal fun createHandlers(): List<ScaleDeviceHandler> = listOf(
// Exact-name match must precede generic LeFu/0xFFF0 handlers (first match wins).
ActiveEraBF06Handler(),
KeepS3Handler(),
OmronWlcHandler(),
BeurerBF450Handler(),
TaylorBIAHandler(),
RyFitHandler(),
CultSmartScaleProHandler(),
RealmeSmartScaleHandler(),
YunmaiHandler(isMini = false),
YunmaiHandler(isMini = true),
YunmaiXHandler(),
TrisaBodyAnalyzeHandler(),
SanitasSbf72Handler(),
StandardBeurerSanitasHandler(),
SoehnleHandler(),
SinocareHandler(),
SenssunHandler(),
RenphoHandler(),
QNHandlerBroadcast(),
QNHandler(),
OneByoneHandler(),
OneByoneNewHandler(),
OkOkHandler(),
MiScaleS400Handler(),
XiaomiS800Handler(),
MiScaleHandler(),
RunstarR6Handler(),
RunstarR5Handler(),
RelaxmedicHandler(),
RobiS9Handler(),
VitafitVT701Handler(),
EEBBLHandler(),
FitTrackDaraHandler(),
DrTrustSSW532Handler(),
MGBHandler(),
MedisanaBs44xHandler(),
InlifeHandler(),
IHealthHS3Handler(),
HuaweiAhCh100Handler(),
HuaweiCH100SHandler(),
HuaweiHagridWspHandler(),
HoffenBbs8107Handler(),
HesleyHandler(),
ExingtechY1Handler(),
EbelterBodyFatB2Handler(),
ExcelvanCF36xHandler(),
EtekcityESF551Handler(),
EtekcityFit8SHandler(),
EufyC20Handler(),
EufyP2Handler(),
ESCS20MHandler(),
RenphoES26BBHandler(),
DigooDGSO38HHandler(),
CustomOpenScaleHandler(),
BeurerSanitasHandler(),
AAAxHandler(),
ScaleupHandler(),
BodyConnectHandler(),
WeightGurusA3Handler(),
)
}

/**
* Reads the current value of a settings [Flow] from a non-suspending context.
*
* Communicator creation happens on the caller's thread, so the few settings needed here are
* read with a short timeout rather than restructuring every call site; `null` means the value
* was unavailable in time and the caller falls back to its default.
*/
private fun <T> readSettingBlocking(flow: Flow<T>): T? = runCatching {
runBlocking(Dispatchers.IO) {
withTimeout(250.milliseconds) { flow.firstOrNull() }
}
}.getOrNull()

/**
* Creates a [ScaleCommunicator] based on a modern [ScaleDeviceHandler].
Expand All @@ -169,13 +220,7 @@ class ScaleFactory @Inject constructor(
): ScaleCommunicator? {
// Resolve effective tuning: prefer user-saved value, fall back to handler default
val effectiveTuning: TuningProfile = run {
val saved: String? = runCatching {
runBlocking(Dispatchers.IO) {
withTimeout(250.milliseconds) {
settingsFacade.savedBluetoothTuneProfile.firstOrNull()
}
}
}.getOrNull()
val saved: String? = readSettingBlocking(settingsFacade.savedBluetoothTuneProfile)

saved?.let { runCatching { TuningProfile.valueOf(it) }.getOrNull() }
?: support.tuningProfile
Expand Down Expand Up @@ -224,6 +269,13 @@ class ScaleFactory @Inject constructor(
val primaryIdentifier = deviceInfo.name
LogManager.d(TAG, "createCommunicator: Searching for communicator for '${primaryIdentifier}' (${deviceInfo.address}). Handler hint: '${deviceInfo.determinedHandlerDisplayName}'")

// 0. Developer mode wins over every registered handler: route to the diagnostic handler,
// which dumps the GATT tree and logs notifications but stores nothing.
if (readSettingBlocking(settingsFacade.developerModeEnabled) == true) {
LogManager.i(TAG, "Developer mode active → routing '$primaryIdentifier' to DebugGattHandler. No measurement will be stored.")
return createModernCommunicator(DebugGattHandler(), DebugGattHandler.SUPPORT)
}

// 1. Check if a modern Kotlin handler explicitly supports the device.
for (handler in modernKotlinHandlers) {
val support = handler.supportFor(deviceInfo)
Expand Down
Loading