Skip to content

feat: Phase 4 domains (cover/scene/sensor) + deep-review hardening - #3

Open
gentslava wants to merge 11 commits into
feat/real-ha-integrationfrom
feat/phase4-domains
Open

feat: Phase 4 domains (cover/scene/sensor) + deep-review hardening#3
gentslava wants to merge 11 commits into
feat/real-ha-integrationfrom
feat/phase4-domains

Conversation

@gentslava

@gentslava gentslava commented Jun 15, 2026

Copy link
Copy Markdown
Owner

Stacked on top of #2 (real-HA MVP). Two things: adds three HA domains, then a full pass of fixes from a deep code review.

Phase 4 — new domains

cover / scene / sensor across all three apps, following the light/switch/lock pattern:

  • cover — state-dependent open/close/stop
  • scene — single Activate (state masked to a stable token)
  • sensor — read-only; EntityCard.primary made optional (additive, no v bump), unit folded into state

Also fixes a latent arkts bug: EntityDetails rendered the primary label via $r() (resource id), which never resolved the companion's plain-text labels — now plain Text.

Deep-review hardening

A 5-agent review (companion / watch-lite / watch-arkts / security / cross-platform) drove these:

Repository hygiene

  • Git history was rewritten and force-pushed — re-clone any local copies (a regular pull will fail).

Lifecycle (companion)

  • Wear Engine bridge moved into a foreground service (HaBridgeService), survives Activity recreation / background; watch reconnect with exponential backoff (5s→60s); bridge starts after first Connect. Verified on device.
  • Crash fix: FGS type connectedDevice crashed on Android 14 (needs a BT/network runtime perm) → switched to dataSync.
  • @Volatile on the shared device (send-path race).

Error handling

  • element-by-element getStates (one bad entity no longer fails the whole sync) · no actions for unavailable/unknown · OkHttp callTimeout within the 8s budget · protocol-version guard · onIncoming try/catch (arkts) · refreshEntity error capture.

Contract

  • watch-lite renders the companion's primary/secondary actions instead of re-deriving HA semantics locally.
  • arkts allow-list peer fixed to the real companion bundle.
  • runtime upgrade Mock → real P2P once the phone is reachable.

Quality

  • CI (GitHub Actions: companion test + assembleDebug, path-filtered).
  • HaService interface + HaBridge unit tests (the central orchestration node was untested).
  • docs synced with reality (companion exists, peer configured, 6 domains, AGP 8.13.2).
  • watch-lite: missing Settings icons added, dead Promise file removed, debugMode off by default, offline auto-retry.

Verification

  • companion: ./gradlew test green (incl. new HaBridge/EntityMapper/version cases).
  • watch-lite + watch-arkts: assembleHap successful.
  • On-device: foreground service starts the bridge, no crash; full P2P still gated on Wear Engine AGC approval (unchanged).

Deliberately out of scope

  • inbound allow-list / path encoding — channel is pairing-gated; owner's call.
  • i18n localization of watch strings — large, non-functional; deferred.
  • networkSecurityConfig — Android config can't express a CIDR for arbitrary local HA IPs.

🤖 Generated with Claude Code

gentslava and others added 11 commits June 15, 2026 11:48
Add three HA domains to the companion mapper, following the light/switch/lock
pattern:
- cover:  state-dependent open/close/stop (+ all three as secondary)
- scene:  single turn_on ("Activate"); state masked to a stable "scene" token
          (HA's raw state is a last-activated timestamp)
- sensor: read-only — no primary action, unit_of_measurement folded into
          state ("21.5 °C")

EntityCard.primary becomes optional (additive, NO protocol v bump): senders
that always include it stay valid, readers must tolerate its absence so a
read-only sensor renders no primary tile.

Update docs (p2p-protocol, ha-integration-notes) and EntityMapper tests
(cover/scene/sensor cases; sensor is now mapped, not filtered out).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…plain text

watch-lite + watch-arkts mirror the companion's new domains:
- cover:  state-dependent open/close/stop
- scene:  single Activate
- sensor: read-only — no primary (EntityCard.primary now optional)

Also fixes a latent arkts bug: EntityDetails rendered the primary label via
$r() (a resource id), which never resolved the companion's plain-text labels
("Open", "Activate", …). Now rendered as plain Text, which also fixes the
existing light/switch/lock primary tile on real devices. Mock labels switched
to plain text to match.

Adds cover/scene watch-lite icons (128px, monochrome) and mock entries on
both watches. All three apps build: companion tests green, watch-lite and
watch-arkts assembleHap successful.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- K2: @volatile on WearEngineP2pService.device — it's written from HMS
  callbacks and read from send coroutines; without a barrier sendRaw could
  read a stale null and silently drop a reply.
- B3: getStates parses element-by-element — one malformed HA state object no
  longer fails the entire sync (partial data > all-or-nothing).
- B4: unavailable/unknown entities get no primary/secondary action, per
  ha-integration-notes (uses the previously-unused HaState.isAvailable).
- B5: OkHttp callTimeout(7s) so a large /api/states stays within the watch's
  8s P2P budget.
- B6: parseIncoming validates protocol v — a mismatched version returns an
  explicit error ACK (UnsupportedVersion) instead of being mis-parsed as v1.

Adds tests for unavailable-no-actions and version rejection. All green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ew K3-K5)

- K3: new HaBridgeService (foreground, START_STICKY) owns the P2P transport
  instead of MainActivity, so it survives Activity recreation (rotation /
  theme) and keeps the watch reachable while backgrounded. stop() now runs on
  service destroy — no leaked scope / duplicate receivers.
- K4: the bridge starts after first-time "Connect & test" succeeds, not only
  if config was already present at onCreate — fixes the first-run case where
  the watch couldn't reach HA until an app restart.
- K5: WearEngineP2pService re-resolves the watch and re-registers the receiver
  with a 5s retry — handles the watch connecting late, dropping, or
  reconnecting on a new Device (was a one-shot registration before).

Manifest: foreground-service + connected-device + notifications permissions,
service declaration (foregroundServiceType=connectedDevice). Builds clean;
on-device verification pending (adb device dropped).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- T2: extract HaService interface; HaClient implements it and HaBridge depends
  on it — the bridge is now unit-testable without HTTP, and a future WebSocket
  client can replace HaClient without touching the bridge.
- T1: add HaBridgeTest (fake HaService) covering the orchestration node:
  sync success/failure, call-service pass-through + error, unsupported version,
  unrecognized message — all previously untested.
- T4: companion UI entityGlyph/entityAccent now cover cover/scene/sensor
  (were "?" / inactive after the domains were added).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…w K6,B8-B10,V3)

- K6: wearEngineRemoteAppNameList -> ru.gentslava.homeassistant.companion
  (was the com.yourcompany.ha.bridge placeholder). It must match the bundle in
  setRemoteApp or Wear Engine rejects P2P and the app silently drops to Mock.
- B9: onIncoming wraps JSON.parse in try/catch — a malformed payload from the
  companion no longer throws out of the Wear Engine receiver callback.
- B10: refreshEntity now catches errors into lastError (was try/finally only,
  so a P2P timeout produced an unhandled rejection and no offline banner).
- B8: Services.tryUpgradeToReal() + store.sync calls it — if the app fell back
  to Mock at startup, it reconnects the real P2P repo once the phone is
  reachable instead of being stuck on fake data forever.
- V3: runAction refreshes the entity after callAction so the details screen
  reflects the new state (Mock updated in place but the UI never refreshed).

All compile: assembleHap successful.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… B7,K7,C1)

- B7: the entity screen now renders the companion-provided primary+secondary
  actions (per the P2P contract — the watch shouldn't re-derive HA semantics)
  and sends each action's own domain/service/data on tap. The local per-domain
  table is kept only as a mock/offline fallback. navEntity carries the actions
  so they survive a back-and-return.
- K7: add the 4 missing Settings icons (connection/server/debug/info, 128px
  monochrome) — the Settings screen referenced non-existent files.
- C1: dead MockHomeAssistantRepository.js (used Promise, banned on lite) was
  removed in the previous commit — it was superseded by mockItems() + haRepository.

watch-lite assembleHap successful.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ct backoff

On-device the foreground service crashed at startForeground: FGS type
connectedDevice on Android 14+ (targetSDK 35) additionally requires a
BT/network runtime permission. Switched to dataSync (semantically: syncing HA
state <-> watch) which needs only the normal FOREGROUND_SERVICE_DATA_SYNC.

Also: the reconnect loop polled getBondedDevices every fixed 5s forever while
the watch was away (CPU wakeups / battery). Now exponential backoff 5s..60s,
reset on connect. HA itself is never polled — the companion calls HA only on a
watch request (SYNC/CALL_SERVICE), so there are no background HA requests.

Verified on device: no crash, service starts, bridge reaches requestPermission
(code=8 Scope unauthorized — pending Wear Engine approval, as before).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The monorepo exists to protect the shared P2P contract, but nothing ran on
push. The companion is the only app with a headless suite, so guard it:
gradle test + assembleDebug on every push/PR touching apps/phone-android or
the protocol doc. Path-filtered per ADR-0006. The watch apps have no
CI-runnable build (DevEco/JerryScript, device-only).

Builds without agconnect-services.json (AGC plugin stays commented out).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…review B11)

The docs described an earlier state and actively misled agents:
- companion was "not written / separate repo" — it's implemented in
  apps/phone-android/ (README, architecture.md).
- setRemoteApp / PEER_FINGERPRINT described as "commented out / placeholder" —
  they're configured with the real companion bundle + debug-cert fingerprint
  (p2p-protocol.md, ADR-0003, watch-arkts AGENTS.md, CLAUDE.md, DEVELOPMENT.md).
- supported domains listed as 3 — now 6 (light/switch/lock/cover/scene/sensor),
  and the "ask first before adding domains" gate updated (root AGENTS.md,
  architecture.md, spec).
- AGP 8.7 -> 8.13.2 (phone AGENTS.md).

Remaining real placeholder noted accurately: module.json5 client_id.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…nused imports

- watch-lite TD7: debugMode defaults to false — a fresh install with no
  companion now shows the honest "Offline — open the phone app" instead of
  fake mock data.
- watch-lite M2: index onShow retries loadEntities when the list is empty
  (offline first load), so returning to the screen auto-recovers instead of
  requiring a manual pull-down with no on-screen affordance.
- watch-arkts: drop unused ArcListAttribute/ArcListItemAttribute imports
  (Index/EntityDetails/Settings) — Code Linter warnings.

Both watch apps assembleHap successfully.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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.

1 participant