Skip to content

Fix device-specific settings parity with Z2M frontend#91

Merged
tashda merged 1 commit into
devfrom
fix/device-options
May 4, 2026
Merged

Fix device-specific settings parity with Z2M frontend#91
tashda merged 1 commit into
devfrom
fix/device-options

Conversation

@tashda
Copy link
Copy Markdown
Owner

@tashda tashda commented May 4, 2026

Summary

Per-device options (e.g. Hue Native Control, Transition, Color Sync) were missing, broken, or rendered with custom non-native controls. This PR brings them to parity with the Z2M windfront frontend using native iOS controls.

What changed

  • Data source: Read option values from bridge/info.config.devices[ieee] (real Z2M source of truth); fall back to per-device options for the docker seeder.
  • Native controls: Pill-style tristate replaced with native Toggle / Picker. Each option lives in its own Section so the description renders as a proper iOS-style footer beneath a standard-height row.
  • Labels: Title Case ("Hue Native Control", "State Action", "Color Sync") while preserving multi-uppercase acronyms like "QoS" and "RGB".
  • Numeric input: Double-aware via value_step, decimal pad when fractional. Now only writes on user commit — previously re-published every numeric option to MQTT whenever the screen appeared. Always shows unit; inferred s for transition / throttle / debounce / retention since upstream doesn't ship a unit for those.
  • Hue Native Control end-to-end: Toggling sends bridge/request/device/options { id: <ieee>, options: { hue_native_control: true } }. Z2M echoes via bridge/info.config.devices, and philipsTz.philipsLightTz reads meta.options.hue_native_control on subsequent /set calls — no other app-side wiring needed.
  • Seeder: Now mirrors per-device options into bridge/info.config.devices to match real Z2M behavior. Adds hue_native_control (Binary) and effect_color_mode (Enum) to the Philips color-lamp fixtures.

Why this fixes every device, not just Philips

The renderer is fully driven by definition.options. Description text, native toggles, label casing, numeric units, write-on-commit semantics — every model whose options ship in the bridge payload now renders and round-trips correctly without per-model code.

Test plan

  • Open Device Settings on a Philips color lamp (e.g. Xamento, 9290024896) — verify Hue Native Control toggle, Effect Color Mode picker, Transition with s unit, State Action / Color Sync toggles.
  • Toggle Hue Native Control on; confirm bridge echoes the new value (round-trip via bridge/info).
  • Open Device Settings on a non-Philips light — confirm only generic options appear, no regressions.
  • Edit Transition, type a value with decimal step (e.g. 0.5), confirm decimal keypad and value persists.
  • Open then immediately close Device Settings — confirm no spurious bridge/request/device/options writes (previously triggered on appear).
  • Existing AppStore unit tests pass.

🤖 Generated with Claude Code

Per-device options (e.g. Hue Native Control, transition, color sync) were
either missing, broken, or rendered with custom non-native controls.

- Source option values from bridge/info.config.devices[ieee] (real Z2M
  source of truth); fall back to device.options for the docker seeder.
- Render expose.description as a Section footer per option, native row
  height for each control.
- Replace pill-style tristate with native Toggle / Picker.
- Title-case labels ("Hue Native Control", "State Action") while
  preserving multi-uppercase acronyms ("QoS", "RGB").
- Numeric input is Double-aware via value_step, only writes on commit
  (was auto-publishing every option on screen appear), and shows the unit
  always — including inferred "s" for transition/throttle/debounce/retention.
- Send options keyed by ieee_address (survives mid-flight rename).
- Seeder now mirrors options into bridge/info.config.devices to match
  real Z2M, and seeds hue_native_control + effect_color_mode on Philips
  color lamp fixtures.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@tashda tashda merged commit d36ae3f into dev May 4, 2026
@tashda tashda mentioned this pull request May 4, 2026
20 tasks
tashda added a commit that referenced this pull request May 4, 2026
* v1.6.1: Hide Groups by default, trim empty device stats, replace Settings + with More menu

- HomeLayout: one-time migration force-hides the Groups card for users
  upgrading from 1.6.0, matching new-install behavior
- HomeDevicesCard: only render Online/Offline/Untracked stats when count > 0
- SettingsView: toolbar + becomes a More (ellipsis) menu — Add Bridge
  always, Disconnect only in single-bridge mode; removes duplicate
  Disconnect section at bottom of single-bridge layout

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Restart Required notice: long-press → Go to Log

Capture the id of the Z2M log entry whose message contains "restart
required" when bridgeInfo.restartRequired flips false → true (and clear
on the reverse). The Settings and per-bridge "Restart Required" notice
gains a context menu with "Go to Log" that deep-links into LogsView
filtered to that entry, with a live-scan fallback when no id is captured.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Fix device-specific settings parity with Z2M frontend (#91)

Per-device options (e.g. Hue Native Control, transition, color sync) were
either missing, broken, or rendered with custom non-native controls.

- Source option values from bridge/info.config.devices[ieee] (real Z2M
  source of truth); fall back to device.options for the docker seeder.
- Render expose.description as a Section footer per option, native row
  height for each control.
- Replace pill-style tristate with native Toggle / Picker.
- Title-case labels ("Hue Native Control", "State Action") while
  preserving multi-uppercase acronyms ("QoS", "RGB").
- Numeric input is Double-aware via value_step, only writes on commit
  (was auto-publishing every option on screen appear), and shows the unit
  always — including inferred "s" for transition/throttle/debounce/retention.
- Send options keyed by ieee_address (survives mid-flight rename).
- Seeder now mirrors options into bridge/info.config.devices to match
  real Z2M, and seeds hue_native_control + effect_color_mode on Philips
  color lamp fixtures.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Remove "Go to Log" from Restart Required notice

Z2M never logs the words "restart required" — the flag is set silently
inside settings.apply() and settings.changeEntityOptions(). Our keyword
search could never match, so "Go to Log" was just opening the Logs page
unfiltered. Match windfront's honest minimalism: tap to restart, stop.

Reverts the AppStore tracking, route type, navigation state, and context
menu added in #85.

Fixes #88

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Don't link Device Card back to itself in log detail

When LogDetailView is opened from a device's own log feed, the device
hero card it renders should not navigate back to that same device — a
dead-end interaction. Thread an originDeviceIEEE through both call sites
and suppress the NavigationLink overlay when it matches.

Fixes #89

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Move Connect button to toolbar on connect editor

The Save flow already lived in the navigation toolbar; the Connect flow
floated over the form. Use the same confirmationAction toolbar slot for
both modes — same placement, same affordance — and gate the enabled
state per mode (connect: canConnect; save: also requires a real edit).

Fixes #86

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Don't fabricate brightness in log state snapshot

State-change diffs (and ON/OFF-only publishes) omit brightness when it
didn't change between events. The snapshot card was reading
context.brightnessPercent — which falls back to range.upperBound (100%)
when brightnessValue is nil — and rendering an invented "100%" instead
of the actual prior value. Require a real brightnessValue before
showing the percent; otherwise show "On"/"Off".

Fixes #90

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Add Logs section to Group Detail

Mirror the Device Detail pattern: a Logs section at the bottom showing
the most recent entries scoped to the group's friendly name, plus a
"See All Logs" navigation link to a dedicated GroupLogsView with
search.

Fixes #92

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Restart: pop to Settings root and clear stale runtime stats

Two paper-cuts after tapping Restart from a deep settings page:
- The user stayed on the page they came from, with no signal that the
  action took. Pop back to Settings root via @Environment(\.dismiss) on
  ServerDetailView and BridgeSettingsView so the restart has a clear
  endpoint.
- Home tile kept rendering pre-restart uptime / published / received
  counts because Z2M won't republish bridge/health until reconnect.
  Optimistically clear bridgeHealth and bridgeOnline in
  AppEnvironment.restartBridge so those stats vanish immediately;
  they'll repopulate on the next bridge/health publish. Routed
  scope.restart() through environment.restartBridge so every restart
  surface gets the same clear.

Fixes #87

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Logs tab: register Device/Group navigation destinations

LogDetailView's hero card pushes a DeviceRoute or GroupRoute when tapped.
The Devices and Groups tabs each register handlers on their own stacks,
but LogsView's NavigationStack didn't — so opening a log via Settings →
Logs and tapping the device/group card emitted a SwiftUI runtime warning
and silently failed. Register both destinations on the LogsView stack so
the link works regardless of entry surface.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Group state changes: log the first arrival, not just subsequent ones

The state-change diff suppressed any "empty → value" transition to avoid
flooding logs with retained-state arrivals after MQTT connect. That's
correct for devices, but Z2M only publishes group state on an actual
change — so the very first arrival for a group IS the user's toggle and
was being silently swallowed. Group Detail's Logs section then showed
"No logs" until the second toggle made the first one visible.

Skip the empty-previous suppression when the entity is a group so the
first interaction shows up immediately. Devices still rely on the
suppression to keep startup quiet.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Format 'Failed to ping' warnings into structured fields

Z2M's "Failed to ping" warnings encode device, attempt counter, ZCL
command, an options JSON blob, and a nested failure reason — all jammed
into one text line. Render them as a labelled summary plus structured
CopyableRow fields, with the options blob as a separate grouped section.
The raw text remains accessible via the existing { } toolbar toggle.

Adds a small parser registry (LogMessageParser) so other recurring z2m
warning shapes can be added without touching the view; unrecognized
messages fall back to the original raw rendering.

Fixes #93

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Log Detail: render full Light Card from state-change payload

Synthesized state-change entries built from `bridge/state` events used
to carry only the diff — so the snapshot card collapsed to the single
changed property even when the payload had brightness, color_temp, and
color all present. Capture the full state on `LogContext.payload` at
mapping time and prefer it in `logTimeState` so the device hero card
renders the complete light state at log time. Diff-only fallback
preserved for older entries.

For groups with light-shaped payloads, also render the same Light Card
(snapshot mode) keyed off a light member device. Non-light payloads
still fall through to the generic field breakdown.

Fixes #94 #95

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Log sheet: register Device/Group destinations so card taps navigate

Tapping the device or group hero card from a log opened via Home →
Recent Events (or a notification) silently failed. The LogSheetHost
sheet wraps LogDetailView in its own NavigationStack but never
registered handlers for DeviceRoute / GroupRoute, so the push emitted a
runtime warning and the user saw the sheet sit unresponsive on top of
Home — visually as if the tap "navigated to homepage". Wire the same
destinations the in-tab Logs stack already registers; covers both the
single-entry and notificationSheetStyle (multi-entry) branches.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Multi-bridge Bridges row matches single-bridge Connection card

Replace the status-dot + URL row in the multi-bridge Bridges section
with the same BridgeConnectionCardLabel the single-bridge Connection
card uses. Same tinted bridge-color icon, same display name, status
text in place of the URL. Restart-required badge and connect toggle
preserved; status dot dropped (the colored icon + status text already
communicate connection state).

Fixes #96

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Remove redundant color swatch from snapshot info row

The small filled circle next to the snapshot value (e.g. next to "2202 K"
on the color-temperature row) duplicated information already conveyed by
the card's eyebrow tint and the ON badge. Drop it; the gradient and
state pill carry the cue.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Light Card: render real color even when color_mode lies

Two intertwined bugs landed the snapshot card on the color-temperature
surface during a green color change:

- The card compared color_mode against "color_xy"/"color_hs", but Z2M
  publishes the field as "xy"/"hs"/"color_temp" (no "color_" prefix).
  Verified against Z2M's own publish.test.ts. The check never matched
  so any real color change fell through to the temperature branch.
- Hue lights with hue_native_control enabled publish color_mode set to
  "color_temp" alongside a fresh color object during color changes —
  the bulb reports the equivalent CT and the renderable color in the
  same payload. Trusting color_mode there shows "Color Temperature
  6535 K" with a peach tint while the bulb is rendering green.

Replace literal string comparisons with a new
LightControlContext.isColorMode that prefers a recognizable color
object (x/y, hue/saturation, h/s, or r/g/b) over the color_mode
signal. LightDisplayColor.resolve gets the same priority swap so the
eyebrow tint follows the actual color. hasColorTemperatureReading is
now suppressed when isColorMode wins so the snapshot row matches the
card's surface.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Light Card snapshot: include nested color, trust color_mode, drop swatches

Three intertwined fixes for the Log Detail Light Card surface:

- exposesScopedState filtered the payload by `flattenedLeaves` of the
  device's exposes. For the color_xy / color_hs feature whose `property`
  resolves to "color", that strips the entire nested color object from
  the state passed to the snapshot card — the leaves are `x` / `y`,
  but z2m publishes the value at the parent key. Use `flattened` so
  the parent property survives the filter and the color object reaches
  LightControlContext intact. This is why every color change still
  rendered as Color Temperature regardless of any earlier mode
  detection work.
- isColorMode now trusts `color_mode` authoritatively (`"xy"` or
  `"hs"`). The earlier override that preferred a recognizable color
  object misread the OFF group case where Hue publishes a stale color
  object alongside `color_mode: "color_temp"` — the snapshot then
  wrongly painted a Color row. LightDisplayColor.resolve restored to
  matching: color_mode == "color_temp" wins for tint, color object
  next, temperature fallback last.
- Drop the redundant color swatch from the COLOR snapshot row to match
  the earlier removal on the COLOR TEMPERATURE row. The eyebrow tint
  and ON pill already convey state.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Color temperature: use Tanner Helland CCT-to-RGB so warm vs cool actually differ

The previous formula pinned red at 1.0 and ramped green/blue linearly
from 1000–6500K. The result: every white above ~5000K landed on a peach
tint that was nearly indistinguishable from neighbouring values, and
genuinely cool whites (6500K+) came out pinkish instead of the
characteristic blue-white. Replace with the standard Tanner Helland
approximation: 2000K reads amber, 2700K tungsten, 4000K neutral, 5500K
daylight, 6500K+ cool blue-white. Clamp to 1000–10000K so unusual bulb
reports still render usefully.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <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