Skip to content

[WiP] Huawei API integration - #204

Draft
yatharthranjan wants to merge 22 commits into
masterfrom
claude/huawei-api-integration-hxdqtl
Draft

[WiP] Huawei API integration#204
yatharthranjan wants to merge 22 commits into
masterfrom
claude/huawei-api-integration-hxdqtl

Conversation

@yatharthranjan

Copy link
Copy Markdown
Member

Initial implementation of Huawei HealthKit, powered by claude

claude added 9 commits July 14, 2026 10:25
…erns

Captures the two connector generations (generic kafka-connect-rest-source
framework used by Fitbit vs. the newer oura-library + thin Connect glue
split used by Oura), the runtime polling/auth/conversion flow, config and
Docker/CI setup, and a checklist for adding a new vendor integration such
as Huawei following the Oura pattern.
Registers the two new Gradle modules, pins radar-schemas-commons
0.9.0-SNAPSHOT (huawei_schemas branch not yet released) as a separate
version-catalog entry so existing modules keep the stable release, and
adds the user/UserRepository domain types mirroring oura-library's
pattern.
Adds the domain logic for the Huawei Health Kit connector, mirroring
oura-library's split of pure-Kotlin logic from Kafka Connect glue:

- request/: RestRequest, RequestGenerator, HuaweiRequestGenerator (offset
  tracking, per-route backoff on 401/403/429/etc), HuaweiResult/HuaweiError.
- route/: HuaweiRoute base (OAuth2-authorized GET/POST + time-range
  chunking), three concrete route kinds covering the Health Kit Data API's
  endpoints - HuaweiSampleSetRoute (POST sampleSet:polymerize, raw or
  groupByTime-aggregated), HuaweiHealthRecordRoute (GET healthRecords),
  HuaweiActivityRecordRoute (GET activityRecords) - and HuaweiRouteFactory,
  a single registry mapping all ~54 Huawei data types from the
  radar-huawei-connector schema spec to their endpoint, dataTypeName, and
  Avro record builder.
- converter/: FieldValues (typed accessor for Huawei's field-value sample
  point format) and generic converters that turn API responses into
  TopicData for the registered Avro records.

Field-value key names are best-effort (Huawei HiHealth Field naming
convention); this was verified to compile against a locally-published
radar-schemas-commons 0.9.0-SNAPSHOT (huawei_schemas branch) since none of
the real snapshot hosts are reachable from this sandbox - flagged in
comments for verification against a live API response.
Mirrors kafka-connect-oura-source: HuaweiSourceConnector (periodic user
refresh + task reconfiguration), HuaweiSourceTask (round-robin polling
across routes, JSON->Avro->SourceRecord conversion via AvroData),
KafkaOffsetManager, and HuaweiServiceUserRepository (Ktor-based
rest-source-authorizer client with OAuth2 client-credentials auth and
cached user/token lookups).

HuaweiRestSourceConnectorConfig is written in Kotlin and loop-generates
its ~110 per-data-type `huawei.<key>.enabled`/`huawei.<key>.topic`
ConfigDef entries from huawei-library's HuaweiRouteFactory.definitions
registry, rather than hand-duplicating a static field per topic as
Fitbit/Oura do - the same registry also drives which routes
HuaweiSourceTask actually builds, so the connector config and the set of
polled routes can never drift out of sync.

Could not compile this module in this sandbox: packages.confluent.io
(needed for kafka-connect-api/kafka-connect-avro-converter) is blocked by
the sandbox's egress policy - confirmed this is pre-existing and applies
equally to kafka-connect-oura-source, not something introduced here.
huawei-library (the part of this change with real logic to verify) does
compile cleanly against a locally-published radar-schemas-commons
0.9.0-SNAPSHOT.
Adds docker/source-huawei.properties.template, a radar-huawei-connector
docker-compose service, a README section, and registers the
kafka-connect-huawei-source image in both CI workflow matrices. Updates
ARCHITECTURE.md to document the Huawei module and the route-registry
config-generation pattern it introduces for connectors with a large
number of data types.
huawei-library tests (verified passing in this sandbox against a locally
published radar-schemas-commons 0.9.0-SNAPSHOT):
- FieldValuesTest: typed-value array and flattened-object parsing.
- HuaweiRouteFactoryTest: builds every one of the ~54 registered route
  definitions, feeds each a fixture payload shaped for its endpoint kind,
  and asserts the converter produces exactly one record on the expected
  topic without throwing - this caught two real bugs during development
  (a bad endTime field on HuaweiCgmBloodGlucose, and a private extension
  function shadowing HuaweiDataConverter's).

kafka-connect-huawei-source gets a config test mirroring the existing
*RestSourceConnectorConfigTest convention, plus checks that
enabledTopics() reflects the shared HuaweiRouteFactory.definitions
registry and honors per-type huawei.<key>.enabled overrides. Could not
run this one in-sandbox (see prior commit: packages.confluent.io is
blocked here for every kafka-connect-*-source module, pre-existing and
unrelated to this change).

Also switches huawei-library's JUnit integration from kotlin-test-junit
(JUnit4) to kotlin-test-junit5, since oura-library's copy-pasted
dependency block conflicts with the JUnit Platform the radar-kotlin
Gradle plugin configures the test task with - previously unnoticed only
because no *-library module had tests yet.
`./gradlew check` runs ktlint over every module; auto-formatted the
mechanical issues (semicolons, wrapping) and manually split a handful of
lines ktlint couldn't auto-correct. huawei-library:check is now fully
green (ktlint + all 5 tests) in this sandbox.
ktlint is static analysis and doesn't need dependency resolution to run,
so this module's Kotlin sources could be linted even though they can't
be compiled in this sandbox. Wraps long ConfigDef.define() calls and
doc-string constants, and simplifies the getUserRepository()/
initialize() flow slightly in the process.
Adds yatharthranjan@onsentia.com alongside the existing KCL address in
the Huawei Docker image label and CI workflow author fields.
@yatharthranjan
yatharthranjan marked this pull request as draft July 14, 2026 13:16
@yatharthranjan yatharthranjan changed the title [WiP] Claude/huawei api integration hxdqtl [WiP] Huawei API integration Jul 14, 2026
claude added 5 commits July 14, 2026 13:45
- Wrap the okhttp3.HttpUrl from getHuaweiUserRepositoryUrl() in
  URLBuilder(...).build() before passing it as createClient's Ktor
  Url parameter, matching how the token URL was already handled
  (and how OuraServiceUserRepository does it).
- Throwable.message is nullable; fall back to a default string before
  passing it to UserNotAuthorizedException's non-null constructor.

These only surfaced in real CI since packages.confluent.io is blocked in
this sandbox, so kafka-connect-huawei-source couldn't be compiled here -
verified via ktlint (which needs no dependency resolution) that the fix
introduces no new style issues.
ktlintKotlinScriptCheck also lints .gradle.kts files; wraps the two
GitHub Packages credential lines that exceeded 100 chars.
Applies a standard Apache-2.0 copyright header (Copyright 2026 Onsentia)
to every .kt/.java file in huawei-library and kafka-connect-huawei-source
- the two modules added for the Huawei Health Kit integration. Files
that had copied The Hyve's 2018 header from the Oura pattern get it
replaced; files with no header get one prepended. Pre-existing
Fitbit/Oura files are left untouched since their copyright belongs to
their original authors.
Was still copied from Oura's Dockerfile (Copyright 2018 The Hyve);
updates to match the Onsentia header applied to the rest of this
module's files.
Adds an @author yatharthranjan KDoc/Javadoc tag to the primary
class/interface/object of every .kt/.java file in huawei-library and
kafka-connect-huawei-source: appended to existing class-level doc
comments where present, added as a new minimal doc comment otherwise.
claude added 8 commits July 28, 2026 13:25
Lets the Huawei connector run against per-user YAML credential files
under huawei.user.dir, mirroring Fitbit's YamlUserRepository, so it can
be tested locally without a rest-source-authorizer webservice. Adds a
docker/huawei-user.yml.template and README instructions for the flow.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w6G3vGzKQ4ogcLgtWwzun
isAuthorizedOverride was mapped to the same JSON key ("isAuthorized")
as the isAuthorized computed property, causing Jackson to fail with
"Conflicting getter definitions" when reading user YAML files. Rename
the manual-override field's JSON key to "authorized".

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w6G3vGzKQ4ogcLgtWwzun
The 400/401/403 branches previously logged only a hardcoded generic
message, discarding the real error body Huawei's API returned. This
made it impossible to diagnose the actual cause of a failed request
(e.g. wrong client/app, invalid scope) from the logs.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w6G3vGzKQ4ogcLgtWwzun
HuaweiHealthRecordRoute sent the health record type identifier under
a "subDataTypeName" query parameter, but Huawei's healthRecords API
expects it under "dataTypeName" - the API was silently rejecting every
health_record_* request with "DataTypeName is null" since it never
received the parameter it actually looks for. Renamed the parameter
(and the route/factory field) to dataTypeName throughout.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w6G3vGzKQ4ogcLgtWwzun
Per Huawei's official "Querying Health Records of a Data Type" spec:
- The endpoint is on API version v2, not v1.
- The data type query parameter is named "dataType", not "dataTypeName"
  (and not "subDataTypeName" as it was before that).
- startTime/endTime, both in the request and in each returned record,
  are in nanoseconds since the epoch, not milliseconds.

This was causing every health_record_* request to fail with
"DataTypeName is null", and would have produced wildly wrong
timestamps for any record that did come back.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w6G3vGzKQ4ogcLgtWwzun
The generic error branch (405/409/500/502/503/590, and anything else
not explicitly handled) hardcoded its HuaweiGenericError's code to
"500" regardless of the actual response status, which was misleading
for diagnostics.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w6G3vGzKQ4ogcLgtWwzun
Per the official Health Kit REST API reference (Postman "HMS Core"
collection, "Querying Sampling Data Statistics of Multiple Days"), the
groupByTime-aggregated variant of a data type is obtained by polymerizing
the underlying *raw* dataTypeName with groupByTime, not by sending a
literal "*.statistics"-suffixed dataTypeName - that suffix is only
RADAR-Schemas'/this connector's own label for "the daily-aggregated
route", not a real Huawei data type identifier. Sending it verbatim is
exactly why Huawei's API had no dataCollector for e.g.
"com.huawei.continuous.heart_rate.statistics", "com.huawei.vo2max.statistics",
"com.huawei.resting_calories.statistics", etc. - across every single
statistics route built via sampleSetDefinition().

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w6G3vGzKQ4ogcLgtWwzun
…erize

Huawei's live API confirmed the root cause behind most "no default
dataCollector found"/"Invalid dataTypeName" errors on statistics routes:
sampleSet:polymerize does not support a groupByTime-aggregated query for
every data type (confirmed live: "com.huawei.resting_calories does not
support the query mode, please use dailyPolymerize API"). Per the
official REST API reference for "Querying Sampling Data Statistics of
Multiple Days", the day-aggregated variant of a data type must instead
go through the dedicated POST /healthkit/v2/sampleSet:dailyPolymerize
endpoint, which takes a startDay/endDay (yyyyMMdd) + timeZone request
body and returns a differently-shaped, doubly-nested
group[].sampleSet[].samplePoints[] response (with group-level times in
milliseconds but sample-point times in nanoseconds).

Adds HuaweiDailyPolymerizeRoute/HuaweiDailyPolymerizeConverter and
routes every "*.statistics" definition through it instead of
HuaweiSampleSetRoute, which now only handles raw (non-statistics) data
types and has had its now-dead groupByTime support removed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w6G3vGzKQ4ogcLgtWwzun
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: In Progress

Development

Successfully merging this pull request may close these issues.

2 participants