Conversation
- add source header/query to item command/state operations in openapi.json - regenerate OpenAPI client types - pass source when sending item commands and state updates
db408fb to
66fb4ad
Compare
| let path = Operations.sendItemCommand.Input.Path(itemname: itemname) | ||
| let body = Operations.sendItemCommand.Input.Body.plainText(.init(command)) | ||
| let response = try await client.sendItemCommand(path: path, body: body) | ||
| let query = Operations.sendItemCommand.Input.Query(source: commandSource()) |
There was a problem hiding this comment.
Is there any way to capture the original information on which "page" this is in the sitemap, and include it as a delegated source? Like the org.openhab.ui.basic$default:03 example from https://www.openhab.org/docs/developer/utils/events.html#the-core-events? That would be ideal. If you can, the UI part would go first, then iOS part: org.openhab.ui.basic$default:03=>org.openhab.ios. For things coming from MainUI, the bundle would just be org.openhab.ui. But perhaps that is already just happening because MainUI is a web view? If it is, all that would be needed is adding the iOS Bundle on the end of the source before the request goes through.
There was a problem hiding this comment.
Addressed: I now get the following sources with local and remote server
source: org.openhab.ui.basic$uicomponents_page_myHome=>org.openhab.ios$XXXXXX=>org.openhab.core.io.rest source: org.openhab.ui.basic$uicomponents_page_myHome=>org.openhab.ios$XXXXXX=>org.openhab.io.openhabcloud=>org.openhab.core.io.rest
@ccutrer would this signature be fine ?
There was a problem hiding this comment.
@ccutrer What should be the source signature for the watchOS app?
source: org.openhab.ui.basic$uicomponents_page_myHome=>org.openhab.watchos$XXXXXX=>org.openhab.io.openhabcloud=>org.openhab.core.io.rest
There was a problem hiding this comment.
would this signature be fine ?
For overall structure, yes that looks fine. I'm still a little confused by the uicomponents_page_myHome portion. Is that an actual example, or just showing the form you expect? I'm looking at the code in sitemapSourcePrefix, and I'm not fully following it, but I'm no Swift expert. I see that you're separating defaultSitemap and pageId with a :, which is good. But is defaultSitemap the id of the currently loaded sitemap, or is it the id of the preference for which sitemap to load by default? And pageId seems reasonable, and if it doesn't match exactly what BasicUI in the browser returns, there's not much we can do (though I'd much rather it be names like it looks like you're getting, rather than numbers that the browser generates, because that seems far less fragile).
What should be the source signature for the watchOS app?
Yeah, org.openhab.watchos seems reasonable to me.
There was a problem hiding this comment.
I'm still a little confused by the uicomponents_page_myHome portion. Is that an actual example, or just showing the form you expect?
uicomponents_page_myHome is a real-world example for the pageId that comes directly from the openHAB REST API's PageDTO.id field, so whatever the server returns is what we use verbatim.
Regarding defaultSitemap — it's the user's preference for which sitemap to load, but in practice it's also the currently loaded sitemap, since the app opens whichever sitemap the preference points to. Each view controller (including sub-pages pushed via navigation) loads this same value. So it accurately reflects which sitemap the command originated from.
On names vs numbers — we use exactly what the server returns as PageDTO.id. We don't generate any numeric IDs on the iOS side, so the values should be as stable/readable as the server provides them.
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
There was a problem hiding this comment.
Pull request overview
This PR implements source tracking for commands sent to openHAB from the iOS app, addressing issue #990. The implementation adds the ability to identify the source of commands by appending source information (app identifier, device ID, and UI component) to command and state update requests sent to the openHAB server.
Changes:
- Added source query parameter and X-OpenHAB-Source header to OpenAPI specification for sendItemCommand and updateItemState endpoints
- Implemented source building logic in OpenAPIService that combines sourcePrefix and deviceId using the format "sourcePrefix=>org.openhab.ios$deviceId"
- Updated OpenHABSitemapViewController to provide source information when sending commands from sitemap widgets
Reviewed changes
Copilot reviewed 10 out of 11 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| openapi-source.patch | Patch file showing OpenAPI specification changes to add source parameter support |
| OpenHABCore/Sources/OpenHABCore/openapi/openapi.json | Updated OpenAPI specification with source query parameter and header for command/state endpoints |
| OpenHABCore/Sources/OpenHABCore/Util/OpenAPIService.swift | Implemented sourceComponent and buildSource methods, updated sendItemCommand and updateItemState to include source parameter |
| openHAB/OpenHABSitemapViewController.swift | Added sitemapSourcePrefix method and updated sendCommand to pass source information |
| OpenHABCore/Sources/OpenHABCore/Util/NetworkTracker.swift | Updated calls to pass nil for sourcePrefix and deviceId parameters |
| OpenHABCore/Tests/OpenHABCoreTests/NetworkTrackerTests.swift | Updated mock implementation to accept new parameters |
| OpenHABCore/Sources/OpenHABCore/GeneratedSources/openapi/Types.swift | Generated code changes for new query and header parameters |
| OpenHABCore/Sources/OpenHABCore/GeneratedSources/openapi/Client.swift | Generated code changes to handle source parameter in requests |
| OpenHABCore/Tests/OpenHABCoreTests/ComparableTests.swift | New test file for Comparable.clamped extension (appears unrelated to PR purpose) |
| BuildTools/.swiftlint.yml | Added CommonUI/.build to exclusion list |
| BuildTools/.swiftformat | Added CommonUI/.build to exclusion list |
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
|
I’ll put this on the (very long now!) list of things todo when I’m back home. Initial thought is to provide callback function that IOS and Android can provide that returns the source signature. I’ll also open an issue up with the Android team for the proposal. |
|
I think we should merge this PR as a first step. Implementation for watchOS will be done in openapigen-swiftui branch. Injection on MainUI can also be done at a later time. |
Extend PR #1064 command source support to the Watch app and deep-link command path. NetworkTracker now forwards sourcePrefix and deviceId to OpenAPIService, and OpenAPIService uses org.openhab.watchos when compiled for watchOS. Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* Improve SegmentedRowView previews and add fallback icon support
- Add fallbackSymbol parameter to IconView for SF Symbol fallback
when network icons are unavailable (useful for previews)
- Add fallbackSymbol parameter to SegmentedRowView, forwarded to IconView
- Create PreviewList helper to reduce boilerplate in previews
- Update previews to use List with proper insets matching SitemapPageView
- Scale fallback symbol to 75% for better visual match with network icons
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* Fix linter warnings
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* Fix iOS slider external updates and merge watchOS slider rows
- Fix iOS SliderRowView not reacting to external updates by using
debounce pattern (matching watchOS approach)
- Merge SliderWithSwitchSupportRow into SliderRow on watchOS
- Add fallbackSymbol support to watchOS IconView for previews
- Improve slider previews with minValue/maxValue/step support
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* Improve slider behavior and show current value while dragging
- Use onEditingChanged for true release-only behavior on iOS
- Show current slider value while dragging on iOS and watchOS
- Fix external update handling with proper isEditing tracking
- Keep pendingValue until server responds to avoid visual jump
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* Wrap PreviewConstants in DEBUG and fix preview labels
- Wrap PreviewConstants in #if DEBUG to exclude from release builds
- Fix SegmentedRowView preview detail labels to use numeric values
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* Style pressReleaseButtons to match segmentedButtons
- Apply consistent styling with rounded background and border
- Add pressed indicator on each button
- Use equal spacing and minimum width for buttons
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* Unify singleMappingButton and pressReleaseButton styling
- Style singleMappingButton as momentary press button
- Match height, width, and indicator styling with segmentedButtons
- Set common minWidth on both single and pressRelease containers
- Add all widget types to All Scenarios preview
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* Align single mapping and pressRelease buttons to trailing edge
- Use trailing-aligned frame for single mapping button
- Use fixedSize with leading padding for pressRelease buttons
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* Add isNilOrEmpty extension and use in SegmentedRowView
- Add isNilOrEmpty computed property on Optional<String>
- Replace verbose nil/empty checks in SegmentedRowView
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* Simplify isNilOrEmpty implementation
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* Redo preferences storage with JSON
* Style pressRelease buttons individually with spacing
- Move background/border to each pressRelease button
- Add spacing between buttons
- Match row height with segmentedButtons
- Add two-button pressRelease preview
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* Revert "Redo preferences storage with JSON"
This reverts commit b92fcb7a50a5ad0b7d029a2b465e1f5e97d9c600.
* Update SegmentedRowView layout and add shutter preview
- Add Office Shutter two-button pressRelease preview
- Restore consistent Spacer logic for button alignment
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* Use isNilOrEmpty for singleMappingButton spacer check
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* Increase spacing between pressRelease buttons
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* Show selected state on single mapping button
Align iOS behavior with Android by highlighting the single mapping
button when the item's state matches the mapping command.
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* Fix slider jumping by always sending final value and adding step
- Always send slider value on release, not just in releaseOnly mode
- Add step parameter to Slider to match watchOS and prevent continuous
vs step-adjusted value mismatch
- Only clear pendingValue when server confirms the value we sent,
preventing jumps from intermediate throttled responses
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* Send single mapping button command on touch down
Simplify the drag gesture to send the command immediately on touch
instead of waiting for release, removing the pressed state tracking.
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* Fix single mapping button sending duplicate commands
Guard against repeated sends in onChanged by tracking singlePressed
state. Add press/release visual feedback and selection animation.
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* Fix crash, bugs, and duplication from PR review
- Replace force unwrap with guard let in loadCurrentPage() to prevent
crash when pollDataForPage returns nil
- Remove duplicate property assignments in OpenHABWidget convenience
init that overwrote nil-coalesced defaults with raw optionals
- Fix ButtonGridButton sending commands twice for press-release buttons
by separating Button action (regular taps) from gesture (press-release)
- Fix onPressGesture firing onPress on every drag movement by using a
stateful ViewModifier that guards against re-entry
- Extract duplicate PreviewList struct into PreviewConstants.swift
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* Move PreviewList to SegmentedRowView as shared internal type
PreviewConstants.swift has no Xcode target membership so types
defined there are invisible to the compiler. Place PreviewList in
SegmentedRowView.swift as a non-private type inside #if DEBUG so
SliderRowView.swift (same module) can reference it too.
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* Reorder ButtonGridRowView declarations and remove lint suppression
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* Remove empty setupActiveConnectionObserver stub
The method body was empty — connection handling moved to the async
for-await loop in init() and the view's onChange modifier.
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* Defer non-essential AppDelegate setup and add placeholder title
Move Firebase, push notifications, audio session, watch connectivity,
and screensaver setup into a deferred Task so the UI renders faster.
Add a redacted placeholder title in SitemapPageView while loading, and
use EmbeddingRowView for placeholder rows.
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* Fix Task leak, hidden TextField, ignored search pref, and linked-page title
- Remove untracked `for await` Task from SitemapPageViewModel.init() that
leaked and duplicated connection handling already done by .onChange
- Always show TextField in TextInputRowView instead of gating on labelValue
- Conditionally apply .searchable in SitemapNavigationView based on the
showSearchField preference
- Store linked-page title as defaultSitemapLabel so pageTitle returns it
immediately
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* Fix connection observer lifecycle and redacted title during loading
Restore for-await connection observer in SitemapPageViewModel with proper
Task tracking (cancelled in deinit) to fix startup hang. Root page
observes all emissions for initial load; linked pages use dropFirst() to
defer loading until the view appears via .task. Remove unreliable
.onChange observer from SitemapPageView. Return empty pageTitle when no
label is available so the navigation bar shows a redacted placeholder
instead of the raw sitemap name.
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* Simplify connection observer and prevent same-URL restart loop
Use dropFirst() so for-await only handles connection changes, not
initial load. Remove connectionObserverTask from linked-page init to
avoid eager loading. Restore unconditional .task for all pages. Add URL
guard in handleActiveConnectionChange to skip when NetworkTracker
re-evaluates to the same connection, preventing long-polling restarts.
Set openHABRootUrl in startPageHandling so the guard works for .task
initiated loads.
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* Default isLoading to true to show skeleton on first render
Eliminates the empty black screen between SwiftUI view creation and
.task firing by starting in the loading state. The skeleton condition
(isLoading && relevantWidgets.isEmpty) still correctly shows content
for the preview init which populates widgets directly.
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* Remove duplicate PreviewConstants and decouple skeleton from preview data
Delete openHAB/PreviewConstants.swift which duplicated CommonUI's
#if DEBUG-guarded version and shipped preview JSON in Release builds.
Replace placeholderWidgets with lightweight static widgets constructed
inline, removing the PreviewConstants dependency from SitemapPageView.
Remove the fake inline placeholder title that was offset from the
navigation bar title.
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* Use semantic gray colors for better segmented control track contrast
Replace tertiarySystemBackground/secondarySystemBackground with
systemGray4 (dark) and systemGray5 (light) for slightly improved
track visibility in both color schemes.
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* Add inline buttons for single and press-release mappings on watchOS
Render inline buttons directly in SegmentRow for single-mapping and
press-release scenarios instead of always navigating to the full-screen
SegmentSelectionView. Multi-segment (2+ regular mappings) retains the
existing NavigationLink flow. Layout adapts based on label presence:
VStack with title row when labeled, HStack when not.
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* Constrain inline button hit area with contentShape
Add contentShape(RoundedRectangle) to inlineButton so DragGesture and
Button tap targets are clipped to the visible button bounds, preventing
touches on the title row above from triggering button actions.
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* Fix inline button hit area extending beyond visible bounds
Move gesture recognizers into an overlay on the button view so the
hit-test area is physically bounded to the button frame. This prevents
touches on the title row above from triggering button actions on watchOS.
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* Validate touch coordinates to constrain button hit area on watchOS
Use GeometryReader inside gesture overlays to check that the touch
startLocation falls within the button bounds, rejecting touches routed
from outside (e.g. the title row above) by watchOS list row hit testing.
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* Skip widgets array reassignment when list structure is unchanged
Only reassign the @Published widgets array when widgets are added,
removed, or reordered. Property-only updates are already applied
in place on existing widget instances, triggering per-row re-renders
via @ObservedObject without rebuilding the ScrollView and losing
scroll position.
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* Copy sendCommand closure when updating existing widget instances
The sendCommand closure on each widget captures a weak reference to
its OpenHABPage. When a long poll replaces the page, the old page is
deallocated and the existing widgets' closures silently fail. Copy
sendCommand from the new widget to keep the command chain alive.
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* Fire single-mapping button on release within bounds, not on press
Move command send from onChanged to onEnded with a bounds check so
the user can cancel by dragging away before lifting. Visual pressed
state tracks the finger position, matching native watchOS Switch
behavior for scroll-safe interaction.
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* Put up to 2 inline buttons on same row as title on watchOS
Single-mapping and press-release buttons with up to 2 mappings now
share a single HStack with icon and title. Buttons get higher
layoutPriority so the title truncates first. Press-release with 3+
buttons keeps the two-row VStack layout.
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* Removing unused code
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* Sync WidgetRowViewModel for all SegmentRow branches
Lift onAppear/onReceive from multiSegmentContent to body so
press-release and single-mapping branches also update the view
model when the widget changes via long polling.
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* Migrate WidgetRowViewModel to Observation framework
Replace ObservableObject/@Published with @Observable macro and switch
from @StateObject to @State in SegmentRow. Replace onReceive with
onChange(of: widget.item?.state) to avoid the DispatchQueue.main.async
deferral hack needed by objectWillChange's pre-change timing.
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* Update project
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* Adopt WidgetRowViewModel and clean up watchOS row views
- Migrate all rows from onReceive(objectWillChange) to
onChange(of: widget.item?.state) for post-change timing
- Eliminate circular selectedIndex sync in SelectionRow and
SegmentSelectionView by using computed bindings
- Consolidate ImageRawRow duplicated modifiers into a Group
- Replace separate numberStateValue/unit with NumberState in
viewModel to fix mixed widget/viewModel access in SetpointRow
- Remove dead code (isIntStep, stateFormat) from SetpointRow
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* Extract WidgetRowFactory and decouple previews from UserData
Extract widget-to-row mapping into WidgetRowFactory and introduce
PreviewWidgetFactory for standalone preview data across all watchOS
row views, removing the dependency on UserData(preview: true).
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* Add PreviewNavigationContainer and use it across all row previews
Introduce a reusable PreviewNavigationContainer that wraps content
in NavigationStack with AppSettings, replacing repetitive boilerplate
in all watchOS row preview blocks.
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* Centralize watchOS typography with WatchTypography and WatchLabelText
Replace hardcoded font sizes, line limits, and scale factors across
all watchOS row views with shared WatchTypography constants and the
WatchLabelText convenience view for consistent styling.
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* Extract WidgetCommandSender for centralized command dispatch
Introduce WidgetCommandSender and WidgetCommandPolicy to replace
inline Task.sleep debounce patterns and direct widget.sendCommand
calls across all watchOS row views with a unified, policy-driven
command sending abstraction.
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* Fix WidgetCommandSender cleanup and setpoint unit preservation
Add deinit to cancel pending tasks in WidgetCommandSender, simplify
segment selection to immediate policy, and preserve unit when creating
fallback NumberState in SetpointRow.
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* Simplify stateless rows by removing unnecessary WidgetRowViewModel
FrameRow, GenericRow, RollershutterRow, and TextRow don't need to
observe item state changes — remove WidgetRowViewModel and simplify
their interfaces (FrameRow now takes a plain title string).
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* Replace IconView with model-driven WatchIconView
Rename IconView to WatchIconView backed by IconRenderModel, moving
icon URL construction logic into OpenHABWidgetExtension. Reorder
init blocks before private methods for consistency across row views.
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* Simplify SwitchRow and SelectionRow with direct data passing
Remove WidgetRowViewModel from SwitchRow and SelectionRow by passing
state and mappings directly through init parameters, reducing
indirection and unnecessary observation overhead.
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* Simplify SliderRow, SetpointRow, and SegmentSelectionView with direct data passing
Remove WidgetRowViewModel from SliderRow, SetpointRow, and
SegmentSelectionView by passing configuration and state directly
through init parameters, completing the migration away from
unnecessary view model indirection.
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* Restore WidgetRowViewModel for complex rows and unify stateToken pattern
Bring back WidgetRowViewModel in SliderRow, SetpointRow, and
SegmentSelectionView where derived state is needed, using a slim
(widget, stateToken) init. Rename SwitchRow's effectiveState to
stateToken for consistency across all row views.
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* Migrate ColorSelection to WidgetCommandSender with debounce policy
Replace manual time-based throttle in ColorSelection with
WidgetCommandSender using a 200ms debounce policy for color wheel
drag and immediate send on force updates.
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* Decouple WatchLabelText and DetailTextLabelView from OpenHABWidget
Make WatchLabelText and DetailTextLabelView pure presentation views
that accept plain String values instead of observing OpenHABWidget
directly, removing their dependency on OpenHABCore.
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* Add watchTextStyle modifier and accessibility labels across watchOS views
Introduce WatchTextModifier with .watchTextStyle() for consistent
typography application, replacing repetitive 4-line font/lineLimit/
minimumScaleFactor/truncationMode blocks. Add accessibility labels
and traits to IconWithAction, setpoint buttons, segment buttons,
slider, and switch views.
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* Adopt stateToken pattern in SegmentRow and ColorPickerRow
Migrate SegmentRow and ColorPickerRow to use stateToken for change
detection, replacing @ObservedObject widget observation and onAppear
with onChange(of: stateToken) for consistency with other row views.
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* Add dedicated watchOS test target and interactive state token previews
Create openHABWatchTests target replacing the old SwiftUI watch app
tests, move OpenHABWatchTests from iOS to watchOS target, and add
InteractiveStateTokenPreview helper with interactive previews for
ColorPickerRow and SegmentRow.
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* Add unit tests for WidgetCommandSender
Test immediate dispatch, debounce coalescing, cancel pending,
press/release ordering, and item update behavior using a
CommandRecorder mock.
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* Fix scroll-to-top on long-poll updates and image flickering
On watchOS, conditionalize openHABSitemapPage assignment to only fire
objectWillChange when the page title actually changes, preventing
ScrollView scroll position resets every long-poll cycle. Also add
missing linkedPage/visibility updates to updateWidgets(with:).
On iOS, replace the updateUI method with in-place widget property
updates that preserve @ObservedObject identity, avoiding full body
re-evaluation that causes chart/image flickering from regenerated URLs.
Remove unnecessary isUpdating toggles during long-poll that fired
objectWillChange twice per cycle for no reason.
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* Fix ButtonGrid commands to target parent widget item
Route button grid commands through the parent widget's item instead of
the synthesized button widget, matching the openHAB server expectation.
Also propagate releaseCommand from mappings to support press-release
button behavior.
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* Move test helpers above test suites for consistency
Reorder MockURLProtocol and helper functions to appear before the test
suites that use them, matching Swift convention of declaring types
before usage.
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* Fix webview authentication in SwiftUI sitemap renderer
Add respondTo challenge delegate to WebRowView's Coordinator so that
webviews requiring HTTP Basic Auth receive credentials, matching the
existing behavior in WebUITableViewCell. Fixes #1034.
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* Minor cleanup
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* Provide command source for SwiftUI and watchOS
Extend PR #1064 command source support to the Watch app and
deep-link command path. NetworkTracker now forwards sourcePrefix
and deviceId to OpenAPIService, and OpenAPIService uses
org.openhab.watchos when compiled for watchOS.
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* Move WidgetCommandDispatcher into OpenHABCore for shared use
Consolidate command dispatching logic from the watchOS-only
WidgetCommandSender into a shared WidgetCommandDispatcher in
OpenHABCore, making debounce and immediate command policies
available to both iOS SwiftUI rows and watchOS rows. Update
SitemapPageViewModel and all row views to use the shared dispatcher.
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* Extract WidgetDisplayState for consistent widget rendering
Introduce an immutable WidgetDisplayState snapshot in OpenHABCore
that centralizes label text, effective state, value ranges, and
mapping resolution. All iOS SwiftUI row views and the watchOS
WidgetRowViewModel now derive display properties from this single
source instead of querying widget/item properties directly.
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* Fix watchOS commands silently failing after long poll
The sendCommand closures on widgets captured [weak self] where self
was the OpenHABPage. When the page title didn't change during long
polling, the new page was never retained and got deallocated, killing
all command closures. Wire closures directly to UserData instead,
matching what iOS SitemapPageViewModel.injectSendCommand already does.
Also extract WidgetRenderingKind for shared row-type resolution and
simplify OpenAPIService server URL handling.
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* Align setpoint value formatting across iOS and watchOS
Both platforms now use valueText(step:) + widget.unit consistently.
iOS no longer prefers the server labelValue (which could differ in
format), and watchOS no longer switches between server and local
formatting on button press.
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* Fix thread safety, command source, and display state consistency
Address code review findings: add @MainActor to WidgetCommandDispatcher,
include sourcePrefix/deviceId in SitemapPageViewModel commands, snapshot
displayState once per render in all row views, restore ColorPicker
throttle+debounce for real-time feedback, and align SegmentedRowView
to use displayState.mappings consistently.
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* Missing return
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* Refactor SwiftUI views for improved readability
Extract inline view code into computed properties in settings views
and reorder helper functions after body in row views for consistent
structure.
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* Add OHTextToken design system and improve value formatting
Introduce OHTextToken style tokens to standardize typography across all
row views, replacing ad-hoc font/lineLimit/truncation settings. Add
number pattern formatting for setpoint and slider values on both iOS
and watchOS. Improve page title fallback handling and video layout.
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* Cancel long-polling on view disappear to prevent duplicate tasks
Stop page handling when SitemapPageView disappears so old polling
tasks don't continue running alongside new ones when navigating
between sitemaps. This also eliminates redundant getSitemaps calls
caused by multiple view model instances polling simultaneously.
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* Add deduplication and staleness guards to page handling
Introduce a key-based deduplication check (sitemap+pageId) to skip
redundant startPageHandling() calls, and a UUID-based run ID to
discard results from superseded polling tasks. Adds structured
logging with reason/runID/key for easier debugging. Refines
connection change handling to avoid unnecessary polling restarts
when the URL hasn't changed.
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* Add command lifecycle tracking and refactor page handling
Introduce WidgetCommandLifecycleState to track sending/failed states
for commands, with versioned state management and auto-reset. Add
command lifecycle indicator in the navigation toolbar. Extend
WidgetCommandPolicy with finalOnly and pressRelease modes, and add
WidgetCommandPhase to support press/change/release semantics in
ButtonGrid, Segmented, and Slider rows. Refactor startPageHandling()
into smaller focused methods for readability.
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* Move command lifecycle indicator to leading toolbar and hide when idle
Switch title display mode to automatic and conditionally show the
command lifecycle indicator on the leading side only when not idle.
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* Add toolbar search button with on-demand searchable field
Replace always-visible search bar with a magnifying glass toolbar
button that toggles search presentation. On iOS 17+ uses the
isPresented parameter of searchable; on older versions falls back
to a custom bottom search bar with focus management.
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* Fix slider yo-yo and stale state by reconciling widget identity
Rewrite SliderRowView to switch binding source based on editing state:
the getter returns the server value when idle and the local @State
value while dragging, eliminating the pendingValue mechanism and its
associated feedback loops.
Introduce reconcileWidgets in SitemapPageViewModel to preserve existing
widget object references across structure changes, preventing
@ObservedObject subscriptions from going stale. Use \.self identity in
the List so SwiftUI can correctly track reconciled objects.
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* Remove legacy UIKit sitemap view controller and cell providers
Delete OpenHABSitemapViewController and all UITableViewCell subclasses
(Slider, Switch, Frame, Generic, Selection, Segmented, Setpoint,
Rollershutter, ColorPicker, DatePicker, TextInput, Image, MapView,
Video, Web) along with their cell provider wrappers, replaced by
SwiftUI row views. Add VideoEncoding enum to VideoRowView.
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* Extract SliderRowState and prefer adjustedValue for item-backed sliders
Refactor slider server-value logic into a dedicated SliderRowState struct
with a static factory method. For item-backed sliders, use adjustedValue
directly as it is derived from item state and avoids stale text/state
snapshots. Add onChange(of: state.serverValue) to sync the local slider
position when not actively editing.
Signed-off-by: Tim Muehlhoff <tim.muehlhoff@gmail.com>
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* Add defensive resets for slider widget identity changes and stuck editing
Reset slider state when widgetId changes to handle SwiftUI reusing
view-local @State across identity changes. Also force isEditing back
to false when the server value diverges significantly from the local
slider value, preventing stuck editing state from an interrupted gesture.
Signed-off-by: Tim Muehlhoff <tim.muehlhoff@gmail.com>
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* Prefer widget pattern over item stateDescription for slider formatting
Use widget.pattern when available as it reflects the sitemap-defined
format, falling back to item stateDescription numberPattern. Also add
monospacedDigit to the value label to prevent layout shifts during
slider dragging.
Signed-off-by: Tim Muehlhoff <tim.muehlhoff@gmail.com>
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* Replace persistent slider state with ephemeral drag value and version tracking
Use an optional dragValue that only exists during active drags instead of a
persistent sliderValue that required threshold-based reconciliation. Track
per-widget update versions in the view model so mid-drag server refreshes
deterministically cancel local state rather than relying on value heuristics.
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* Fix command truncation for setpoint/slider with integer display format
Separate display formatting from command values by adding commandString
to NumberState. The display format (e.g. %d) could truncate fractional
step values (30.3 → 30) when sent as commands, matching Android issue
openhab/openhab-android#3882. All three command-sending paths now use
commandString which preserves full precision. Also migrate
NumberStateTests to Swift Testing.
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* Update project: remove Cells group, Swift 6 for watch tests, random test order
Remove the Cells filesystem-synchronized group from the openHAB target,
bump openHABWatchTests Swift version to 6.0, and enable random test
execution ordering in the openHABWatch scheme.
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* Add openHABWatch test plan and wire into scheme
Add TestPlans/openHABWatch.xctestplan with parallel test execution and
performance antipattern checking enabled, reference it from the
openHABWatch scheme, and register it in the project file.
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* Add optimistic selection to SegmentedRowView and SelectionRowView
Immediately reflect the user's tap in the UI without waiting for server
confirmation, eliminating the snap-back flicker. The optimistic state is
held until the server state actually changes from the pre-command value,
and cleared immediately on widget identity changes to avoid bleed-across.
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* Refactor: introduce input structs and align optimistic naming
Extract SegmentedRowInput and SelectionRowInput value types to snapshot
all widget-derived properties once at the top of body, avoiding repeated
ObservedObject property lookups throughout the view tree. Align
SelectionRowView optimistic state variable names with SegmentedRowView
(pending* → optimistic*) for consistency. No behaviour changes.
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* Move PreviewWidgetFactory to CommonUI, replace per-file preview helpers
Promote PreviewWidgetFactory from the watchOS target to the shared
CommonUI module. The watch target becomes a thin typealias. Replace the
hand-rolled makeWidget helpers in SegmentedRowView and SliderRowView
previews with calls to the shared factory, eliminating duplicated widget
construction boilerplate across three files.
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* Fold WatchTypography into OHTextToken, remove parallel watch type system
Add a watchOS branch to OHTextTokenStyle.from(_:) with watch-appropriate
font sizes, then replace WatchTextModifier's verbose per-case font
application with ohTextToken calls. Delete the WatchTypography enum
entirely. Both platforms now resolve typography through CommonUI's
OHTextToken infrastructure.
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* Add PreviewWidgetFactory implementation and tests to CommonUI
Add the shared PreviewWidgetFactory source that the watchOS typealias
and iOS row view previews now reference, along with unit tests covering
the factory methods.
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* Add minimum hit targets to iOS buttons; replace ad-hoc fonts on watch
Apply ohMinimumHitTarget() to the rollershutter and setpoint icon
buttons on iOS for better tap accuracy. Replace remaining raw .font(...)
calls in watchOS views with watchTextStyle tokens so all watch typography
flows through the shared token system.
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* Skip copyWidgetProperties when widget is unchanged; reconcile frame children recursively
Introduce typed Equatable key structs (WidgetRenderKey and friends) to
compare incoming vs existing widgets by value. copyWidgetProperties is now
only called when a meaningful diff is detected, avoiding unnecessary
@ObservedObject mutations and SwiftUI re-renders on polling echoes.
When the parent key is unchanged but the widget has nested children (e.g.
frames), recursively reconcile the children so their item-state updates
are not silently dropped.
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* Extract *RowInput structs to WidgetRowInputs.swift; split Slider/Selection into content views
Consolidate per-row data-preparation structs (SliderRowInput, SelectionRowInput,
SegmentedRowInput) into a shared WidgetRowInputs.swift, replacing the private
per-file copies and renaming SliderRowState to SliderRowInput for consistency.
Introduce SliderRowContent and SelectionRowContent as private inner views that
depend only on value types and explicit callbacks, keeping @ObservedObject and
@EnvironmentObject confined to the thin outer wrapper views.
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* Introduce immutable row-input pipeline; drive SitemapPageView list from rowInputs
Add SitemapRowInput (typed enum), SitemapRowInputMapper (pure widget→input
function), and EmbeddingRowInputView (transitional adapter that resolves a
RowID back to its OpenHABWidget for existing row rendering).
SitemapPageViewModel now maintains a @Published rowInputs array and a
rowWidgetIndex ([RowID: OpenHABWidget]) rebuilt together in rebuildRowInputs().
The list in SitemapPageView is driven by rowInputs; widget lookup from
EmbeddingRowInputView is O(1) via the index.
This is deliberately transitional: the immutable list pipeline is active while
row rendering still goes through widget-backed views. The renderSignatures for
all typed cases (slider, selection, segmented) include labelColor and valueColor.
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* Migrate slider, selection, segmented rows to input-driven views
Add SliderRowInputView, SelectionRowInputView, SegmentedRowInputView — each
resolves its widget via the O(1) rowWidgetIndex and delegates to the existing
*RowContent view with explicit closures, removing the @ObservedObject widget
dependency from the list layer.
EmbeddingRowInputView now dispatches these three cases directly; all other
row kinds remain on the legacy EmbeddingRowView path via the default branch.
Row insets and background for migrated cases match EmbeddingRowView's
regular-row values exactly.
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* Migrate setpoint and toggle rows to typed input pipeline; refactor slider config
- Add SetpointRowInput to WidgetRowInputs with robust serverValue parsing
- Split SetpointRowView into SetpointRowContent + SetpointRowInputView + thin
SetpointRowView; share sendSetpointValue free function between both paths
- Upgrade SitemapRowInput.setpoint from BasicWidgetRowInput to SetpointRowInput
with full renderSignature (minValue/maxValue/step/unit/numberPattern/readOnly)
- Migrate SwitchRowView to SwitchRowContent + SwitchRowInputView + thin wrapper
via makeSwitchRowContent free function
- Route .setpoint and .toggle in EmbeddingRowInputView to typed *RowInputView
- Introduce SliderRowConfig struct and makeSliderRowContent free function to
eliminate six-parameter call site duplication between SliderRowView/InputView
- Switch SitemapRowInputMapper to explicit return statements
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* Migrate text, rollershutter, colorPicker, buttonGrid, and input rows to typed pipeline
- Add ColorPickerRowInput to WidgetRowInputs with colorCommandKey; upgrade
SitemapRowInput.colorPicker from BasicWidgetRowInput to ColorPickerRowInput
with full renderSignature
- Split TextRowView, RollershutterRowView, ColorPickerRowView, ButtonGridRowView,
DatePickerInputRowView, TextInputRowView each into *Content + *InputView + thin
*RowView wrapper via Config struct + factory free function
- Add InputRowInputView dispatcher: routes .input rowID to DatePickerInputRowInputView
or TextInputRowInputView based on renderingKind
- Route .text, .rollershutter, .input, .colorPicker, .buttonGrid in
EmbeddingRowInputView to their respective typed *RowInputView
- ButtonGridRowContent retains @ObservedObject widget for child button reactivity;
input: BasicWidgetRowInput used only for the header label/icon section
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* Migrate remaining row types to typed inputs; complete BasicWidgetRowInput retirement
- Add ToggleRowInput, RollershutterRowInput, InputRowInput, ButtonGridRowInput,
GenericRowInput, ColorTemperatureRowInput, MediaRowInput to WidgetRowInputs
- Upgrade all SitemapRowInput cases from BasicWidgetRowInput to typed inputs;
add full renderSignatures for toggle, rollershutter, input, buttonGrid,
colorTemperature, generic, and media cases
- Split FrameRowView, GenericRowView, ColorTemperaturePickerRowView to
*Content + *InputView + thin wrapper via Config + factory free function
- Add MediaRowInputView dispatcher routing to existing media row views by kind
- Route .frame, .media, .colorTemperature in EmbeddingRowInputView to typed views
- Replace default: with explicit .generic: + @unknown default: so the compiler
warns when new SitemapRowInput cases are added without a handler
- Fix InputRowInputView default branch to use TextInputRowInputView instead of
legacy TextInputRowView
- ColorTemperatureRowInput captures serverValue for a better onAppear fallback
- Add SitemapRowInputMapperTests covering linkedPage→generic and typed mapping
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* Retire BasicWidgetRowInput; add FrameRowInput, TextRowInput; wire GenericRowInputView
- Add FrameRowInput (widgetId + displayState only; no color/readOnly/refresh
since frame headers use a fixed section style) and TextRowInput (adds
labelColor/valueColor) to WidgetRowInputs
- Delete BasicWidgetRowInput entirely; all SitemapRowInput cases now carry
dedicated typed inputs
- Upgrade SitemapRowInput.frame and .text to FrameRowInput/TextRowInput;
tighten their renderSignatures to only the fields each row actually uses
- Add hasLinkedPage: Bool to GenericRowInput; include it in the .generic
renderSignature so the row re-evaluates if a linked page appears/disappears
- Wire GenericRowInputView in EmbeddingRowInputView: linked-page .generic rows
keep the legacy EmbeddingRowView path for NavigationLink; non-linked rows
route to GenericRowInputView
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* Introduce LinkedPageRowInput and .linked case; migrate media rows to typed inputs
- Add LinkedPageRowInput (linkedPageLink, linkedPageTitle, isFrame) with failable
from(widget:); map any widget with a linkedPage to .linked before renderingKind
dispatch, replacing the previous .generic fallback
- Add SitemapRowInput.linked case with full renderSignature; remove hasLinkedPage
from GenericRowInput now that linked rows carry their own case
- Add LinkedPageRowInputView in EmbeddingRowInputView: resolves widget via
rowWidgetIndex, builds NavigationLink to a new SitemapPageViewModel, uses
RowViewFactory for the label; frame-type linked rows get frame insets/background
- .generic now always routes to GenericRowInputView; remove @unknown default
(same-module enum gives compile errors on unhandled cases, which is stricter)
- Remove @EnvironmentObject var viewModel from EmbeddingRowInputView; all child
*InputView types manage their own environment dependencies
- Split ImageRowView, VideoRowView, WidgetWebViewContainer, MapRowView into
*Content + *InputView + thin wrapper via Config + factory free function
- Update MediaRowInputView to dispatch to *InputView variants
- Update SitemapRowInputMapperTests: rename test, assert linkedPageLink/Title
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* Decouple icon and command dispatch from OpenHABWidget in content views
- Add RowIconInput (icon, iconColor, staticIcon, iconState, showIcon) and
IconInputView; content views for text, generic, toggle, buttonGrid, and
linked-page rows now render icons from the typed snapshot
- Add ToggleRowInput.icon and .itemName; SwitchRowContent no longer needs an
iconWidget or commandWidget — sends via viewModel.sendCommand(itemname:)
- Add ButtonGridRowInput.ButtonInput typed snapshot (0-based row/column,
stateless Bool, effectiveState, itemName, icon); compute gridRows/gridColumns
once at snapshot time
- ButtonGridRowContent and ButtonGridButton fully decoupled from
@ObservedObject widget — last widget reference in content views removed;
commands routed through onSendCommand closure with itemName fallback chain
- ButtonGridRowInputView no longer requires widget lookup
- Add itemname-based send/cancelPending/sendDebounced/commandKey overloads to
WidgetCommandDispatcher; add matching sendCommand(for:itemname:policy:phase:key:)
to SitemapPageViewModel
- Add LinkedPageRowContent rendering icon + label + value for navigation rows
- Extend renderSignatures for .toggle, .buttonGrid, .generic, .linked, .text
to include icon fields; .buttonGrid includes full per-button signatures
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* Retire legacy widget paths; replace renderSignature with Equatable synthesis
Delete EmbeddingRowView and RowViewFactory — all row types now rendered
through typed *RowInputView paths in EmbeddingRowInputView. Delete
widget-based GenericRowView, ImageRowView, VideoRowView, WidgetWebViewContainer,
and the rowID-lookup MapRowInputView.
Replace SitemapRowInput's manual renderSignature/kindKey equality with
auto-synthesised Equatable: all *RowInput structs, WidgetDisplayState, and
OpenHABWidgetMapping gain Equatable conformance, letting the compiler own
the diffing logic and catch missing cases at compile time.
Add WidgetMediaImageDescriptor to snapshot chart/image URL generation as a
value type, fully decoupling ImageRowContent, VideoRowContent, WebContainerContent,
and MapRowContent from @ObservedObject widget. Inline RowLayoutPolicy and
RowBackgroundKind as internal types in EmbeddingRowInputView, with exhaustive
case coverage so new cases fail to compile rather than silently falling through.
Replace synthetic placeholder OpenHABWidget array with PlaceholderRowView.
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* Retire widget-based row views; add icon/itemName to typed inputs
All remaining widget-based *RowView wrappers (Segmented, Selection,
Setpoint, ButtonGrid, ColorPicker, ColorTemperaturePicker,
DatePickerInput, Rollershutter, Switch, Text, TextInput, Frame) are
deleted. Their *InputView counterparts are now the sole public types
and drive dispatch entirely through typed inputs.
SliderRowInputView drops rowID and both iconWidget/commandWidget
references; SliderRowContent switches from IconView(widget:) to
IconInputView(input:) and resolves the item name directly from
SliderRowInput.itemName.
WidgetRowInputs: SelectionRowInput, SegmentedRowInput, SetpointRowInput,
ColorPickerRowInput, RollershutterRowInput, InputRowInput, and
ColorTemperatureRowInput each gain icon: RowIconInput and
itemName: String?. SetpointRowInput adds a private resolvedSetpointUnit
helper that reads widget.unit or parses the unit suffix from state text.
IconInputView gains fallbackSymbol: SFSymbol? = nil (with an explicit
memberwise init) so SegmentedRowContent can pass the fallback symbol
through to the icon view, matching the capability of IconView(widget:).
SitemapPageViewModel adds two item-name-based dispatch overloads:
cancelPendingCommand(for: String, key:) and
sendToUpdate(itemname: String, state: NumberState?, ...) which
SetpointRowInputView now uses in place of a direct sendCommand call.
EmbeddingRowInputView drops rowID from all dispatch cases except
.slider (now also dropped here via SliderRowInputView losing rowID).
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* Add SitemapInteractionSummary; surface network status in toolbar
Introduces SitemapInteractionSummary, a unified status enum that
combines network tracker state (connected/connecting/offline) with
command lifecycle state (sending/failed). SitemapPageViewModel exposes
this as sitemapInteractionSummary, a computed property derived from the
new @Published trackerStatus and the existing commandLifecycleSummary.
A new networkStatusObserverTask observes networkTracker.$status and
keeps trackerStatus in sync. Observer setup is extracted into
startObservers(), which both init overloads now call — fixing a bug
where init(pageUrl:title:pageId:) skipped connectionObserverTask setup
entirely, leaving linked pages unresponsive to connection changes.
SitemapNavigationView's toolbar indicator is expanded from
commandLifecycleIndicator (sending/failed only) to interactionIndicator,
which now also shows a "Connecting" spinner and an "Offline" badge when
the network tracker is not in the connected state. Search and hamburger
toolbar buttons gain .ohMinimumHitTarget(). Typography in the search
bar and indicators switches from .font(.footnote/.caption) to
.ohTextToken(.secondary).
SliderRowView (widget-based struct) is deleted; previews migrate to
SliderRowInputView.createPreviewInput returning SliderRowInput directly.
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* Rename *InputView to *RowView; add offline command queuing
All remaining *InputView public type names are simplified to *RowView,
removing the now-redundant Input qualifier since the input-based path
is the only one remaining:
FrameRowInputView → FrameRowView
SliderRowInputView → SliderRowView
SelectionRowInputView → SelectionRowView
SegmentedRowInputView → SegmentedRowView
SetpointRowInputView → SetpointRowView
TextRowInputView → TextRowView
SwitchRowInputView → SwitchRowView
RollershutterRowInputView → RollershutterRowView
InputRowInputView → InputRowView (file renamed)
ColorPickerRowInputView → ColorPickerRowView
MediaRowInputView → MediaRowView (file renamed)
ColorTemperaturePickerRowInputView → ColorTemperaturePickerRowView
ButtonGridRowInputView → ButtonGridRowView
GenericRowInputView → GenericRowView
ImageRowInputView → ImageRowView
MapRowInputView → MapRowView
VideoRowInputView → VideoRowView
WidgetWebViewContainerInputView → WidgetWebViewContainerView
EmbeddingRowInputView updated to use all new names. Xcode project
updated for the two file renames.
WidgetCommandLifecycleState gains a .queued case for commands held
while the network tracker is not connected.
SitemapPageViewModel adds offline command queuing: sendCommand(itemname:
command:) stores a QueuedCommand (command + version) and sets state to
.queued when trackerStatus != .connected; flushQueuedCommands() is
called on reconnection, discarding stale versions and dispatching the
rest via sendCommandNow. cancelPendingCommand clears the queued entry
when key is nil. rowInteractionState(for:) exposes per-item interaction
state (idle/offline/queued/sending/failed) for row-level UI feedback.
WidgetRenderKey gains an explicit == to avoid a Swift type-checker
timeout on the synthesised implementation (216ms vs 200ms limit).
SitemapInteractionSummary gains .queued(count:) between .offline and
.sending; sitemapInteractionSummary reflects it. SitemapNavigationView
shows a clock icon for the queued state.
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* Add slider override tracking; extract SitemapPageViewModel support types
- Extract private render-key types (WidgetRenderKey, WidgetItemKey, etc.)
from SitemapPageViewModel into SitemapPageViewModel+SupportTypes.swift
- Track slider value overrides so the thumb holds position until the server
confirms the new state; fall back after 5 s or on command failure/cancel
- Clear overrides automatically when server state converges within step tolerance
- Add SliderOverrideSyncTests covering cleared/kept override scenarios
- Replace XCTestCaseExtension.swift (unused) with Swift Testing equivalents
- Improve SelectionRowView mapping label resolution to handle numeric and
whitespace-trimmed command comparisons
- Thread PageUpdateOrigin and CommandSendOrigin through update/send paths
for structured debug logging
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* Fix ColorPickerRowView: suppress echo sends; clamp/round HSB values
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* Update AGENTS.md: iPhone 17 Pro simulator, SwiftUI-first, Swift Testing notes
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* CI: trigger TestFlight build on push to openapigen-swiftui
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* Add Version.xcconfig; fix extension CFBundleVersion; update Fastfile for feature branch versioning
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* Fix slider override change notification; reorder SupportTypes; add swiftlint annotation
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* Fix ColorTemperaturePickerRowView: suppress echo sends; guard drag state; clamp/round temperature
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* Throttle onDragStateChanged to fire once per drag in CustomSliderView
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* CI: remove push trigger from TestFlight workflow; keep workflow_dispatch only
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* Add merge driver for Version.xcconfig to always keep ours
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* ColorTemperaturePickerRowView layout adjustments (#1073)
Signed-off-by: DigiH <17110652+DigiH@users.noreply.github.com>
* fastlane: replace missing get/update_xcconfig_value plugin with plain Ruby
get_xcconfig_value and update_xcconfig_value are not available as
built-in Fastlane actions and the xcconfig_utils plugin does not exist
on RubyGems. Read and write Version.xcconfig directly using File.read/
write with a regex instead.
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* fastlane: use absolute path for Version.xcconfig in increment_build
Fastlane's working directory is not guaranteed to be the repo root, so
resolve the xcconfig path relative to __dir__ (the fastlane/ folder).
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* fastlane: replace remaining get/update_xcconfig_value calls with plain Ruby
Five more usages in the version-bumping lane were still using the
non-existent xcconfig_utils plugin actions. Replace them with
File.read/write + regex, consistent with the increment_build lane.
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* fastlane: replace get_build_number with xcconfig read
agvtool (used by get_build_number) cannot find CURRENT_PROJECT_VERSION
now that it lives in Version.xcconfig. Read it directly from the file.
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* openHABWatch: fix Release build failures due to #Preview type-checking
Xcode 26.2 type-checks #Preview macro content in Release builds, so
any #if DEBUG-guarded types referenced inside #Preview cause compile
errors in Archive builds.
- Remove #if DEBUG from PreviewWidgetFactory.swift and
PreviewNavigationContainer.swift so the types are always available.
CommonUI.PreviewWidgetFactory itself has no DEBUG guard, so the
typealias is safe to expose unconditionally.
- Register openHABWatch/Views/Utils/PreviewConstants.swift in the
Xcode project (it existed on disk but was never added), providing a
non-DEBUG PreviewConstants that satisfies all #Preview references.
- Fix UserData.init(preview:) to only load preview sitemap data when
preview == true; previously it loaded preview JSON unconditionally,
meaning the production shared singleton also loaded it.
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* openHAB iOS: fix Release build failures due to #Preview type-checking
Xcode 26.2 type-checks #Preview macro content in Release builds, so
DEBUG-only types (PreviewConstants, PreviewList, createPreviewInput)
referenced inside #Preview blocks cause Archive failures.
- Remove #if DEBUG from PreviewList and createPreviewInput in
SegmentedRowView/SliderRowView — these are small helpers with no
runtime impact and are needed by #Preview blocks in both files.
- Wrap every #Preview block that references CommonUI.PreviewConstants
(which is #if DEBUG) in #if DEBUG / #endif across all 14 affected
iOS SwiftUI files.
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* project: pre-apply Xcode 26.2 project.pbxproj normalisations
Xcode 26.2 modifies project.pbxproj during an archive build, causing
ensure_git_status_clean to fail. Apply the three deterministic changes
it makes so the file is already in the normalised form before CI runs:
- Fix (null) comment on a PBXBuildFile entry
- Remove spurious blank line after Begin PBXFileReference section
- Expand PBXFileSystemSynchronizedRootGroup to multi-line format
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* fastlane: clear FL_CHANGELOG after use to reduce summary noise
changelog_from_git_commits sets the FL_CHANGELOG shared value which
Fastlane prints in full in its lane summary table. Clear it after
capturing the value in `comments` since TestFlight uses read_changelog
output instead.
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* Pre-apply Xcode 26.2 project.pbxproj normalizations and fix SetpointRowView formatting
- Fix (null) comment in PBXFrameworksBuildPhase for DADC420A2E7AB899004E866F
- Fix indentation of PreviewConstants.swift in Utils group
- Remove empty packageProductDependencies from openHABWatchTests target
- Add CODE_SIGN_IDENTITY and set CODE_SIGN_STYLE=Manual for openHABWatchTests Release
- SetpointRowView: prefer displayState.labelValue when available
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* committed version bump: 3.2.1 (203)
Signed-off-by: openhab-bot <info@openhabfoundation.org>
* fastlane: restore project.pbxproj after build to keep git clean
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* committed version bump: 3.2.2 (204)
Signed-off-by: openhab-bot <info@openhabfoundation.org>
* project: restore DADC420A PBXBuildFile to correct SDWebImage entry
The earlier (null)->BuildFile normalisation incorrectly stripped the
productRef from this entry. Since git restore now handles post-build
project.pbxproj cleanup, revert it to the proper SDWebImage form to
match develop and eliminate the merge conflict.
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* watchOS: remove duplicate decorateWidgetsWithSendCommand introduced by merge
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* iOS: fix chart widget identity and image flicker
- SitemapRowInputMapper: add rowIdentityWidgetID() that uses
'chart-<itemName>' as the stable key for chart widgets so that
long-poll updates don't create new RowIDs and tear down the view
- SitemapPageViewModel: use rowIdentityWidgetID for RowID building
- ImageRowView: separate chart rendering path — hold last rendered
image as placeholder so the chart never flashes to empty while a
new URL loads; skip the local refresh timer for chart widgets since
they are updated via sitemap long-poll
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* iOS: match UIKit alert style for text/number input rows
- Title: 'Enter new value' (was 'Enter value')
- Message: 'Current value for <label> is <value>'
- Confirm button: 'Set value' with .destructive role (red font)
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* committed version bump: 3.2.3 (205)
Signed-off-by: openhab-bot <info@openhabfoundation.org>
* iOS: add input filtering and validation for text/number input widget
Extracts InputCommandFormatter with filteredDraftInput (sanitises
keystrokes for number hint) and command (validates/normalises final
value). Wires TextField to a filtered binding so invalid characters
are rejected while typing. Adds InputCommandFormatterTests covering
text passthrough, dot/comma decimal, leading decimal, invalid value
rejection, and character filtering.
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* project: restore WatchTypography.swift to openHABWatch build on openapigen-swiftui
The develop→openapigen-swiftui merge (d85d65a5) propagated the removal of
WatchTypography.swift pbxproj entries that was correct for develop (where the
file doesn't exist) but wrong for openapigen-swiftui (where the file is real
and defines the watchTextStyle modifier used across all watch views).
Restore PBXFileReference, PBXBuildFile, Utils group child, and Sources build
phase entry.
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* iOS: sync chart display URL from viewModel on appear and relevant changes
Introduces chartDisplayURL state in ImageRowContent and a syncChartDisplayURL()
helper. The URL is (re-)derived from viewModel.openHABRootUrl and the current
chart style on appear and whenever widgetId, chart period, or color scheme
changes, preventing stale URLs when the view is reused.
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* iOS: guard chart URL reuse with a display key to prevent stale placeholder
Introduces chartDisplayKey (widgetId|period|theme) so that the cached
chartDisplayURL is only used when all three parameters match the current
render. When the key changes (different widget, period, or color scheme)
the view falls back to the live URL, avoiding a mismatched placeholder
from a previous chart being shown briefly.
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* iOS: converge chart rendering to stable, flicker-free architecture
Separate chart and regular-image rendering into dedicated @ViewBuilder
properties so resolveImagePayload (which generates a random cache-busting
token on every call) is never invoked during a render pass for charts —
only inside syncChartDisplayURL().
Key changes:
- chartImageView uses chartDisplayURL exclusively; falls back to nil
(sized gray placeholder) instead of an inline resolveImagePayload call,
eliminating the double-load caused by differing random tokens between
the render pass and onAppear
- Sized placeholder (200 pt gray) prevents the height-jump from 0→chart
height that caused DigiH's "jumpy" appearance on first chart load
- .id(currentChartKey) keeps stable KFImage identity across URL updates
within the same chart; lastChartImage persists as placeholder between
server refreshes
- Add .onChange(of: viewModel.openHABRootUrl) so syncChartDisplayURL()
fires when the active server connection changes
- chartDisplayKey guards against stale chartDisplayURL being used if
SwiftUI reuses a view instance for a different widget
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* iOS: refine chart state management with ChartDisplayState and task-based sync
Replace separate chartDisplayURL/chartDisplayKey state with an atomic
ChartDisplayState struct, eliminating any inconsistency window between
the two fields.
Replace onAppear + four onChange sync triggers with a single
.task(id: chartSyncToken) that covers all relevant dimensions in one
token: isChartByMediaKind, widgetId, period, url, openHABRootUrl, and
color scheme. This also closes the gap where two same-period chart
widgets for different items shared an identical token.
makeChartDisplayKey() now includes openHABRootUrl so KFImage identity
changes when the active server changes, and retains widgetId to keep
same-period charts from different widgets distinct.
Guard lastChartImage updates in onSuccess against stale in-flight loads
so a slow network response for a previous chart cannot overwrite the
placeholder for the current one.
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* iOS: drive chart refresh from widget version; fix cache buster entropy; skip classic number icon
Endpoint.chart: replace Int.random(0..<1000) with UUID().uuidString as the
cache-busting token, eliminating the 1-in-1000 collision probability that
could return a stale Kingfisher cache entry for a new chart request.
ImageRowView: add chartWidgetVersion (widgetUpdateVersions counter) to
chartSyncToken so .task(id:) fires whenever the server pushes a sitemap
update for the chart widget, triggering syncChartDisplayURL() and a fresh
UUID URL. This is the correct refresh driver for charts; a separate local
timer races with server updates and can show intermittent lag.
Use effectiveChartURL?.absoluteString as the KFImage .id (falling back to
currentChartKey while URL is nil) so each server-pushed URL change produces
a clean view transition.
Endpoint.icon: return nil for oh:classic "number" icon to avoid a
guaranteed failed request — the classic icon set has no number icon.
Add EndpointTests coverage for this case.
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* iOS: decode SVG images at full size for media/image widgets
OpenHABImageProcessor gains an optional svgMaxSize parameter (default
64x64 for icon contexts). Passing nil disables the size cap so SVGs
embedded in Image widgets are decoded at their natural resolution instead
of being constrained to the 64x64 icon limit.
ImageView and ImageRowView (regularImageView) now set the processor with
svgMaxSize: nil. The cache identifier includes a size suffix
(.fullsize / .WxH) so full-size and icon-size renders never share a
Kingfisher cache entry.
String.dataImageBase64Payload replaces the hard-coded
deletingPrefix("data:image/png;base64,") in ImageView, correctly
extracting the base64 payload for any MIME type (svg+xml, png, jpeg …).
Add tests for the new cache identifier variants and for
dataImageBase64Payload including nil edge cases.
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* iOS/watchOS: force page refresh when app returns from background
Track scene/app lifecycle in SitemapNavigationView (iOS) and OpenHABWatch
App (watchOS); use a wasInBackground guard so only genuine background→active
transitions trigger a forced startPageHandling/refreshUrl restart.
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* iOS: keep sitemap long-polling alive across transient failures
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* iOS: refresh embedded sitemap images when payload changes
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* Segmented Controls layout adjustments for iPads (#1078)
Signed-off-by: DigiH <17110652+DigiH@users.noreply.github.com>
* watchOS: remove PreviewConstants dependency from row previews
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* committed version bump: 3.2.4 (206)
Signed-off-by: openhab-bot <info@openhabfoundation.org>
* Proposal to restrict the hit traget to the area after the Selection row icon and label (#1079)
Signed-off-by: DigiH <17110652+DigiH@users.noreply.github.com>
* Handle empty SVG icons without warning fallback
* iOS/watchOS: fix stale widget state on background→foreground (#1075)
- Remove hasRelevantWidgetDiff gate in iOS reconcileWidgets: always copy
server properties so group summary/label changes are never silently
dropped when the WidgetRenderKey is unchanged
- Add missing fields to iOS copyWidgetProperties (isLeaf, text, inputHint,
encoding, labelSource, releaseOnly, row/column, releaseCommand, command,
stateless, yAxisDecimalPattern)
- Fix watchOS refreshUrl guard: was checking openHABRootUrl (often empty)
instead of sitemapForWatch, causing refresh to bail out silently
- Mirror same missing-field additions to watchOS updateWidgets
- iOS pull-to-refresh now also force-restarts the long-polling task after
the snapshot reload
- Add pull-to-refresh to watchOS SitemapPageView
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* committed version bump: 3.2.5 (208)
Signed-off-by: openhab-bot <info@openhabfoundation.org>
* watch: disable sitemap request caching for long-poll service
* Migrate remaining regex usage to Swift 6 regex syntax
* watchOS: force row recreation on widget state change; update AGENTS.md
Add refreshToken computed property to WidgetRowView and apply .id() to
the WidgetRowFactory-produced content. When item.state, effectiveState or
labelValue change, SwiftUI destroys and recreates the row view, ensuring
group and other widget states render correctly even when @ObservedObject
propagation through the factory does not trigger a re-render.
Fix typo in AGENTS.md ("abd" → "and"); add Swift Regex / Swift Testing
guidelines and git signing requirement.
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
* chore: igno…
Addresses #990
@digitaldan, @ccutrer : what should be the signature for the iOS app?
@digitaldan: I didn't implement the injection for Main UI. Could you have a look at this part?