Skip to content

feat(bluetooth): support the iHealth HS2S family and the Wyze Scale (WHSCL1) - #1569

Open
HerbFargus wants to merge 1 commit into
oliexdev:masterfrom
HerbFargus:feat/ihealth-hs2s
Open

HerbFargus wants to merge 1 commit into
oliexdev:masterfrom
HerbFargus:feat/ihealth-hs2s

Conversation

@HerbFargus

Copy link
Copy Markdown

Scale

Wyze Scale, model WHSCL1 (the original Wyze Scale, not the S/X/Ultra). Requested in #853.

It turned out to be an iHealth/Jiuan OEM build. Its GATT service is the ASCII string
com.jiuan.BFSV21 (636f6d2e-6a69-7561-6e2e-424653563231), the model prefix is JA.SC, and
it speaks iHealth's HS2S command set almost byte for byte. So the handler is written for the
family: iHealth HS2S (com.jiuan.BFSV22), iHealth HS2S Pro (com.jiuan.HS2S02) and the
Wyze Scale (com.jiuan.BFSV21). Notify and write characteristics are the same names with
sed. / rec. in place of com..

Protocol

Frames are B0|A0 len seqHi seqLo A9 cmd payload… sum (B0 host→scale, A0 scale→host,
sum = sum of the len bytes & 0xFF), split into 20-byte writes/notifications.

Authentication is iHealth's IdentifyIns2 challenge-response:

  1. FA: host sends 16 random ASCII bytes R1.
  2. FB: scale answers R2 (16) · XXTEA(R1, K) (20) · seed (16), where seed = MAC + 10 zero bytes.
  3. FC: host checks R1 and replies XXTEA(R2, K). The scale acks FD or rejects FE.

XXTEA here is iHealth's variant: little-endian words, encrypt appends the plaintext length as
an extra word. K = XXTEA(seed, Ka) with a per-model key Ka:

  • HS2S / HS2S Pro: Ka ships in iHealth's MyVitals app, so K comes from the MAC alone.
    The handler reproduces a logged HS2S Pro handshake byte for byte (test below).
  • Wyze Scale: Wyze doesn't ship Ka. The Wyze app gets each scale's K ("device_key") from
    wyze-scale-service.wyzecam.com and caches it. I checked every 16-byte constant in two Wyze app
    versions, iHealth's app and both native libraries; none is the master key. So the user fetches
    their scale's key once with wyze-scale-key
    (a small Python script using wyze-sdk and bleak) and pastes it into the device settings, as
    with the S400 bind key. The key is fixed per scale: the one I fetched in 2026 authenticates my
    2022 captures.

Session, in the order the vendor apps use: time sync (10, reply carries the battery
level) → create/update user (21) → select user (23; 01 = busy, retry after the scale sends
24) → offline records (31, paged with a read offset, then deleted with 32) → live weight
(40), stable weight (41, acked) and result (42, acked, then disconnect).

Records carry weight, impedances (4 on the WHSCL1, 6 on the HS2S), user number, gender, age,
height, a timestamp, and the scale's own body fat, muscle mass, bone mass, water and protein.
The WHSCL1 adds a body-type byte and ends with a 4-byte encrypted impedance that only feeds
metabolic age in a closed Holtek library, so it's ignored. LBM, BMR (370 + 21.6 × LBM) and
visceral fat (LBM × 0.142 − 11.218 + BMI × 0.433, clamped 1–59) follow the Wyze app's own
formulas, which reproduce its values on all four weighings I compared. The HS2S reports its own
visceral fat, which is used instead.

What works

Tested on a Wyze Scale WHSCL1 with a Pixel 10 Pro (GrapheneOS), debug build of this branch:

  • Handshake with the fetched key: FD, every time.
  • Full session: time sync, user create/select, offline query, live weight, result.
  • Measurement saved with the scale's timestamp: 93.73 kg, fat 28.7 %, water 52.2 %, muscle
    62.7 kg, bone 4.1 kg, protein 14.7 %, LBM 66.8 kg, BMR 1814, visceral fat 9. The Wyze cloud
    record of an earlier weighing matched the decoded fields exactly.
  • The 42 ack is delivered before disconnecting. (The first build disconnected straight away,
    the queued ack was cancelled, and the scale kept the result. Fixed by waiting 1 s, as
    TaylorBIAHandler does.)

Not tested on hardware: iHealth HS2S and HS2S Pro (I don't have one). Both follow iHealth's
own Hs2sInsSet parser and user-record builders; the HS2S Pro key derivation is verified
against a real iHealth handshake. Their catalog remarks say "untested".

Changes

  • IHealthHs2sLib: framing, reassembly, XXTEA, key derivation and challenge answer, user record
    and offline request builders, record parsers, the Wyze BMR/visceral-fat formulas. No Android
    types, so it's unit tested directly.
  • IHealthHs2sHandler: detection by service UUID (name fallback WHSCL1 / HS2S / HS2S PRO),
    the session above, and a device-key field in DeviceConfigurationUi (same pattern as the S400).
  • ScaleFactory: registers the handler after IHealthHS3Handler.
  • strings.xml: device-key label, description, error and three warnings.
  • ScaleCatalog fixtures and scale_catalog_remarks.txt lines for the three scales.
  • IHealthHs2sLibTest (14 tests), using public data only:
    • the HS2S Pro handshake from iHealth's app log (session key and FC byte for byte);
    • WHSCL1 frames from the snoop logs attached to Wyze Scale (WHSCL1) Support #853: FA rebuilt, FB reassembled, the
      result decoded to the values the Wyze app showed, the user record matching what the Wyze app
      sent, and the visceral-fat/BMR formulas against the app.

./gradlew testDebugUnitTest compileDebugAndroidTestKotlin assembleDebug: everything passes
except 4 tests in BackupRestoreUseCasesTest ("Main DB file 'openScale.db' is not a valid
openScale database"), which fail the same way on unmodified master on my Windows machine.

Closes #853

🤖 Generated with Claude Code

…WHSCL1)

The original Wyze Scale (WHSCL1) is an iHealth/Jiuan OEM build that speaks
the iHealth HS2S protocol, so one handler covers the iHealth HS2S, the
HS2S Pro and the Wyze Scale.

- XXTEA challenge-response (iHealth IdentifyIns2). The HS2S and HS2S Pro
  derive their session key from the scale MAC and a model key shipped in
  iHealth's app. The Wyze Scale uses a per-scale key that Wyze serves from
  its cloud; the user fetches it once with wyze-scale-key and pastes it
  into the device settings.
- Session as in the vendor apps: time sync, user update/select (retried
  after 0x24 while the scale is busy), paged offline records then delete,
  live result. The result is acked before disconnecting.
- Body fat, water, muscle, bone and protein come from the scale; LBM, BMR
  and visceral fat follow the Wyze app's formulas (the HS2S reports its own
  visceral fat).

Tested on a Wyze Scale (WHSCL1) with a Pixel 10 Pro. The HS2S and HS2S Pro
are implemented from iHealth's SDK and are untested on hardware.

Refs oliexdev#853

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@HerbFargus HerbFargus mentioned this pull request Oct 20, 2026
1 task done
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Wyze Scale (WHSCL1) Support

1 participant