Fix device-specific settings parity with Z2M frontend#91
Merged
Conversation
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
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
bridge/info.config.devices[ieee](real Z2M source of truth); fall back to per-deviceoptionsfor the docker seeder.Toggle/Picker. Each option lives in its ownSectionso the description renders as a proper iOS-style footer beneath a standard-height row.Double-aware viavalue_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; inferredsfortransition/throttle/debounce/retentionsince upstream doesn't ship a unit for those.bridge/request/device/options { id: <ieee>, options: { hue_native_control: true } }. Z2M echoes viabridge/info.config.devices, andphilipsTz.philipsLightTzreadsmeta.options.hue_native_controlon subsequent/setcalls — no other app-side wiring needed.bridge/info.config.devicesto match real Z2M behavior. Addshue_native_control(Binary) andeffect_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
sunit, State Action / Color Sync toggles.bridge/info).0.5), confirm decimal keypad and value persists.bridge/request/device/optionswrites (previously triggered on appear).🤖 Generated with Claude Code