Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
774 commits
Select commit Hold shift + click to select a range
0771e67
Send single mapping button command on touch down
timbms Feb 5, 2026
1f28532
Fix single mapping button sending duplicate commands
timbms Feb 6, 2026
a511e8a
Merge branch 'develop' into migrateToAppIntents2
timbms Feb 6, 2026
b6de847
Fix search returning no items for GenericItemEntity
timbms Feb 6, 2026
fc0a76e
Silence linters for CommonUI directory
timbms Feb 6, 2026
78a8a47
Fix crash, bugs, and duplication from PR review
timbms Feb 6, 2026
9fd4930
Move PreviewList to SegmentedRowView as shared internal type
timbms Feb 6, 2026
7f9e776
Reorder ButtonGridRowView declarations and remove lint suppression
timbms Feb 6, 2026
9d69736
Remove empty setupActiveConnectionObserver stub
timbms Feb 6, 2026
7afbc8f
Defer non-essential AppDelegate setup and add placeholder title
timbms Feb 6, 2026
2167c86
Fix Task leak, hidden TextField, ignored search pref, and linked-page…
timbms Feb 6, 2026
8b10413
Fix connection observer lifecycle and redacted title during loading
timbms Feb 7, 2026
4ec8a52
Simplify connection observer and prevent same-URL restart loop
timbms Feb 7, 2026
5154a61
Default isLoading to true to show skeleton on first render
timbms Feb 7, 2026
798b5ff
Remove duplicate PreviewConstants and decouple skeleton from preview …
timbms Feb 7, 2026
6c31741
Use semantic gray colors for better segmented control track contrast
timbms Feb 8, 2026
f909def
Add inline buttons for single and press-release mappings on watchOS
timbms Feb 8, 2026
0800e10
Constrain inline button hit area with contentShape
timbms Feb 8, 2026
1987513
Fix inline button hit area extending beyond visible bounds
timbms Feb 8, 2026
b3bad83
Validate touch coordinates to constrain button hit area on watchOS
timbms Feb 8, 2026
05f15fc
Skip widgets array reassignment when list structure is unchanged
timbms Feb 8, 2026
b89b6fa
Copy sendCommand closure when updating existing widget instances
timbms Feb 8, 2026
36fd106
Fire single-mapping button on release within bounds, not on press
timbms Feb 8, 2026
3dee5cc
Put up to 2 inline buttons on same row as title on watchOS
timbms Feb 8, 2026
8bc10e0
Removing unused code
timbms Feb 8, 2026
07fe9eb
Sync WidgetRowViewModel for all SegmentRow branches
timbms Feb 8, 2026
314bca4
Migrate WidgetRowViewModel to Observation framework
timbms Feb 9, 2026
8e02461
Update project
timbms Feb 9, 2026
b5be93a
Adopt WidgetRowViewModel and clean up watchOS row views
timbms Feb 9, 2026
8a5f408
Extract WidgetRowFactory and decouple previews from UserData
timbms Feb 9, 2026
ce492e4
Add PreviewNavigationContainer and use it across all row previews
timbms Feb 9, 2026
50d3d76
Centralize watchOS typography with WatchTypography and WatchLabelText
timbms Feb 9, 2026
30b610d
Extract WidgetCommandSender for centralized command dispatch
timbms Feb 9, 2026
b67e80e
Fix WidgetCommandSender cleanup and setpoint unit preservation
timbms Feb 10, 2026
ab7b3fe
Simplify stateless rows by removing unnecessary WidgetRowViewModel
timbms Feb 10, 2026
c634c23
Replace IconView with model-driven WatchIconView
timbms Feb 10, 2026
cf4d654
Simplify SwitchRow and SelectionRow with direct data passing
timbms Feb 10, 2026
456c620
Simplify SliderRow, SetpointRow, and SegmentSelectionView with direct…
timbms Feb 10, 2026
b1d06bb
Restore WidgetRowViewModel for complex rows and unify stateToken pattern
timbms Feb 10, 2026
c37ae53
Migrate ColorSelection to WidgetCommandSender with debounce policy
timbms Feb 10, 2026
0fd73d1
Decouple WatchLabelText and DetailTextLabelView from OpenHABWidget
timbms Feb 10, 2026
45fb0df
Add watchTextStyle modifier and accessibility labels across watchOS v…
timbms Feb 10, 2026
0bc1bcc
Adopt stateToken pattern in SegmentRow and ColorPickerRow
timbms Feb 10, 2026
8b4b75d
Add dedicated watchOS test target and interactive state token previews
timbms Feb 10, 2026
29fb7d6
Add unit tests for WidgetCommandSender
timbms Feb 10, 2026
9d32fb7
Merge branch 'develop' into openapigen-swiftui
timbms Feb 10, 2026
3fd1504
Merge branch 'develop' into migrateToAppIntents2
timbms Feb 10, 2026
4b5a649
Fix scroll-to-top on long-poll updates and image flickering
timbms Feb 10, 2026
c7c0c95
Fix ButtonGrid commands to target parent widget item
timbms Feb 10, 2026
d7353da
Move test helpers above test suites for consistency
timbms Feb 10, 2026
e938fd2
Merge branch 'develop' into openapigen-swiftui
timbms Feb 11, 2026
b88f390
Fix webview authentication in SwiftUI sitemap renderer
timbms Feb 11, 2026
5a558a3
Minor cleanup
timbms Feb 11, 2026
b9436c8
Add Player, DateTime, and Location item types to iOS Shortcuts
timbms Feb 11, 2026
0e184b1
Merge branch 'develop' into migrateToAppIntents2
timbms Feb 11, 2026
126de64
Lint check
timbms Feb 11, 2026
af10036
Merge branch 'develop' into openapigen-swiftui
timbms Feb 12, 2026
b5b4bfe
Merge branch 'develop' into migrateToAppIntents2
timbms Feb 12, 2026
6d9e7df
Merge branch 'develop' into openapigen-swiftui
timbms Feb 12, 2026
719fe96
Provide command source for SwiftUI and watchOS
timbms Feb 12, 2026
44aee42
Move WidgetCommandDispatcher into OpenHABCore for shared use
timbms Feb 13, 2026
74154ff
Extract WidgetDisplayState for consistent widget rendering
timbms Feb 13, 2026
7e1685e
Fix watchOS commands silently failing after long poll
timbms Feb 14, 2026
37a833c
Align setpoint value formatting across iOS and watchOS
timbms Feb 14, 2026
9521bcc
Fix thread safety, command source, and display state consistency
timbms Feb 14, 2026
f53e773
Missing return
timbms Feb 14, 2026
fa0ce12
Refactor SwiftUI views for improved readability
timbms Feb 14, 2026
694d185
Merge branch 'develop' into openapigen-swiftui
timbms Feb 14, 2026
5bfbcc0
Add OHTextToken design system and improve value formatting
timbms Feb 14, 2026
5bfb239
Cancel long-polling on view disappear to prevent duplicate tasks
timbms Feb 14, 2026
dd1760f
Add deduplication and staleness guards to page handling
timbms Feb 14, 2026
e72fb01
Add command lifecycle tracking and refactor page handling
timbms Feb 14, 2026
afacbfc
Move command lifecycle indicator to leading toolbar and hide when idle
timbms Feb 14, 2026
9f352a5
Add toolbar search button with on-demand searchable field
timbms Feb 15, 2026
c60ecaf
Fix slider yo-yo and stale state by reconciling widget identity
timbms Feb 17, 2026
20f6769
Remove legacy UIKit sitemap view controller and cell providers
timbms Feb 17, 2026
96e78be
Extract SliderRowState and prefer adjustedValue for item-backed sliders
timbms Feb 17, 2026
e6d51d9
Add defensive resets for slider widget identity changes and stuck edi…
timbms Feb 17, 2026
0d45208
Prefer widget pattern over item stateDescription for slider formatting
timbms Feb 17, 2026
981fe7d
Replace persistent slider state with ephemeral drag value and version…
timbms Feb 18, 2026
3f7d778
Fix command truncation for setpoint/slider with integer display format
timbms Feb 18, 2026
2b32010
Update project: remove Cells group, Swift 6 for watch tests, random t…
timbms Feb 18, 2026
9e7a52f
Add openHABWatch test plan and wire into scheme
timbms Feb 18, 2026
5c32e51
Add optimistic selection to SegmentedRowView and SelectionRowView
timbms Feb 18, 2026
f5f05bc
Refactor: introduce input structs and align optimistic naming
timbms Feb 18, 2026
a29fb82
Move PreviewWidgetFactory to CommonUI, replace per-file preview helpers
timbms Feb 18, 2026
0f6463f
Fold WatchTypography into OHTextToken, remove parallel watch type system
timbms Feb 18, 2026
eebbc9c
Add PreviewWidgetFactory implementation and tests to CommonUI
timbms Feb 18, 2026
664d598
Add minimum hit targets to iOS buttons; replace ad-hoc fonts on watch
timbms Feb 18, 2026
98ac24f
Skip copyWidgetProperties when widget is unchanged; reconcile frame c…
timbms Feb 19, 2026
8d69b29
Extract *RowInput structs to WidgetRowInputs.swift; split Slider/Sele…
timbms Feb 19, 2026
496ee3a
Introduce immutable row-input pipeline; drive SitemapPageView list fr…
timbms Feb 19, 2026
2d21331
Migrate slider, selection, segmented rows to input-driven views
timbms Feb 19, 2026
3769acd
Migrate setpoint and toggle rows to typed input pipeline; refactor sl…
timbms Feb 19, 2026
8be0f39
Migrate text, rollershutter, colorPicker, buttonGrid, and input rows …
timbms Feb 19, 2026
cb3b415
Migrate remaining row types to typed inputs; complete BasicWidgetRowI…
timbms Feb 19, 2026
cacda40
Retire BasicWidgetRowInput; add FrameRowInput, TextRowInput; wire Gen…
timbms Feb 19, 2026
d8b9e7b
Introduce LinkedPageRowInput and .linked case; migrate media rows to …
timbms Feb 19, 2026
f2e7aad
Decouple icon and command dispatch from OpenHABWidget in content views
timbms Feb 19, 2026
5f4c1a1
Retire legacy widget paths; replace renderSignature with Equatable sy…
timbms Feb 19, 2026
773ee43
Retire widget-based row views; add icon/itemName to typed inputs
timbms Feb 19, 2026
d9c98b5
Add SitemapInteractionSummary; surface network status in toolbar
timbms Feb 19, 2026
147382a
Rename *InputView to *RowView; add offline command queuing
timbms Feb 19, 2026
039e719
Add slider override tracking; extract SitemapPageViewModel support types
timbms Feb 19, 2026
9b800d0
Fix ColorPickerRowView: suppress echo sends; clamp/round HSB values
timbms Feb 20, 2026
a540ec5
Update AGENTS.md: iPhone 17 Pro simulator, SwiftUI-first, Swift Testi…
timbms Feb 20, 2026
316d840
CI: trigger TestFlight build on push to openapigen-swiftui
timbms Feb 20, 2026
923febd
Add Version.xcconfig; fix extension CFBundleVersion; update Fastfile …
timbms Feb 20, 2026
089bb5c
Fix slider override change notification; reorder SupportTypes; add sw…
timbms Feb 20, 2026
cdf6f21
Fix ColorTemperaturePickerRowView: suppress echo sends; guard drag st…
timbms Feb 20, 2026
8ca0882
Throttle onDragStateChanged to fire once per drag in CustomSliderView
timbms Feb 20, 2026
9067f09
CI: remove push trigger from TestFlight workflow; keep workflow_dispa…
timbms Feb 20, 2026
8135bf6
Add merge driver for Version.xcconfig to always keep ours
timbms Feb 21, 2026
219466b
Merge develop into openapigen-swiftui: unify xcconfig versioning
timbms Feb 21, 2026
cc782b2
Merge branch 'develop' into migrateToAppIntents2
timbms Feb 21, 2026
8777e88
ColorTemperaturePickerRowView layout adjustments (#1073)
DigiH Feb 21, 2026
882f054
fastlane: replace missing get/update_xcconfig_value plugin with plain…
timbms Feb 21, 2026
8dba571
fastlane: use absolute path for Version.xcconfig in increment_build
timbms Feb 21, 2026
b7f12f6
fastlane: replace remaining get/update_xcconfig_value calls with plai…
timbms Feb 21, 2026
2044d56
fastlane: replace get_build_number with xcconfig read
timbms Feb 21, 2026
6cf8647
openHABWatch: fix Release build failures due to #Preview type-checking
timbms Feb 21, 2026
f4cff9d
openHAB iOS: fix Release build failures due to #Preview type-checking
timbms Feb 22, 2026
b022d4b
project: pre-apply Xcode 26.2 project.pbxproj normalisations
timbms Feb 22, 2026
b3ecc89
fastlane: clear FL_CHANGELOG after use to reduce summary noise
timbms Feb 22, 2026
514cd41
Pre-apply Xcode 26.2 project.pbxproj normalizations and fix SetpointR…
timbms Feb 22, 2026
54ae0f5
committed version bump: 3.2.1 (203)
Feb 22, 2026
98794e0
fastlane: restore project.pbxproj after build to keep git clean
timbms Feb 22, 2026
6789603
committed version bump: 3.2.2 (204)
Feb 22, 2026
5a10664
project: restore DADC420A PBXBuildFile to correct SDWebImage entry
timbms Feb 22, 2026
fcb9e84
Merge branch 'develop' into openapigen-swiftui
timbms Feb 22, 2026
c7a53a7
watchOS: remove duplicate decorateWidgetsWithSendCommand introduced b…
timbms Feb 22, 2026
dfd5a1d
iOS: fix chart widget identity and image flicker
timbms Feb 22, 2026
b386d6a
iOS: match UIKit alert style for text/number input rows
timbms Feb 22, 2026
93c1521
committed version bump: 3.2.3 (205)
Feb 22, 2026
2f3e975
iOS: add input filtering and validation for text/number input widget
timbms Feb 23, 2026
d85d65a
Merge branch 'develop' into openapigen-swiftui
timbms Feb 23, 2026
c396e33
project: restore WatchTypography.swift to openHABWatch build on opena…
timbms Feb 23, 2026
361600d
iOS: sync chart display URL from viewModel on appear and relevant cha…
timbms Feb 23, 2026
a17bc88
iOS: guard chart URL reuse with a display key to prevent stale placeh…
timbms Feb 23, 2026
dbae12c
iOS: converge chart rendering to stable, flicker-free architecture
timbms Feb 23, 2026
b45be08
iOS: refine chart state management with ChartDisplayState and task-ba…
timbms Feb 23, 2026
2896b97
iOS: drive chart refresh from widget version; fix cache buster entrop…
timbms Feb 24, 2026
f2a9d9a
iOS: decode SVG images at full size for media/image widgets
timbms Feb 24, 2026
4b8f7e2
iOS/watchOS: force page refresh when app returns from background
timbms Feb 24, 2026
00d1501
iOS: keep sitemap long-polling alive across transient failures
timbms Feb 24, 2026
1ac0ea1
iOS: refresh embedded sitemap images when payload changes
timbms Feb 24, 2026
91f5aa3
Segmented Controls layout adjustments for iPads (#1078)
DigiH Feb 24, 2026
3a4ad16
watchOS: remove PreviewConstants dependency from row previews
timbms Feb 25, 2026
249f363
committed version bump: 3.2.4 (206)
Feb 25, 2026
f6f1fe7
Proposal to restrict the hit traget to the area after the Selection r…
DigiH Feb 25, 2026
7ed5543
Merge branch 'develop' into openapigen-swiftui
timbms Feb 26, 2026
1973b0e
Merge remote-tracking branch 'origin/openapigen-swiftui'
timbms Feb 26, 2026
6184bae
Handle empty SVG icons without warning fallback
timbms Feb 26, 2026
355d876
iOS/watchOS: fix stale widget state on background→foreground (#1075)
timbms Feb 26, 2026
0cedf5c
committed version bump: 3.2.5 (208)
Feb 27, 2026
75ffdb6
watch: disable sitemap request caching for long-poll service
timbms Feb 27, 2026
199c5df
Migrate remaining regex usage to Swift 6 regex syntax
timbms Feb 27, 2026
c0b12a5
watchOS: force row recreation on widget state change; update AGENTS.md
timbms Feb 27, 2026
c62e53d
chore: ignore Claude Code skills-lock.json and .agents/
timbms Feb 27, 2026
a69379b
watchOS: remove redundant item.state from WidgetRowView refreshToken
timbms Feb 27, 2026
e2a3bd9
committed version bump: 3.2.6 (209)
Feb 27, 2026
d188dbf
iOS: refresh on every scene-active transition; fix watchOS backoff ov…
timbms Feb 28, 2026
40d9cac
committed version bump: 3.2.7 (210)
Feb 28, 2026
0cef450
Recreate number validation function from UIKit sitemap
TAKeanice Feb 28, 2026
eb2f202
Validate number input during typing, improve normalization
TAKeanice Feb 28, 2026
c38f6bf
iOS: disable URL cache for sitemap service; fix scene-active launch race
timbms Mar 1, 2026
4956eaa
allow 0 as number input
TAKeanice Mar 1, 2026
95a9fb3
allow multiline labels and improve layout of text or number input rows
TAKeanice Mar 1, 2026
9c5232d
added flow to some more rows, hide icon if showIcon is false
TAKeanice Mar 1, 2026
a59b805
Improve sitemap foreground refresh and drawer resume handling
timbms Mar 2, 2026
bb38ac4
Remove unnecessary async wrapper and stale connection fallback
timbms Mar 2, 2026
6465a0b
Handle foreground resume from App Switcher without open drawer
timbms Mar 2, 2026
c93af23
Fix auth challenge host matching and watch icon transient dropout
timbms Mar 2, 2026
10994ce
Revmove flow in segmented control row, improved spacing for text views
TAKeanice Mar 2, 2026
67d8bab
Display rollershutter value state in iOS and watchOS rows
timbms Mar 5, 2026
060e603
iOS & watchOSRollershutter & Setpoint Chevrons convergence (#1094)
DigiH Mar 6, 2026
0cf255f
Extract SetpointDisplayFormatter for iOS and watchOS setpoint display
timbms Mar 7, 2026
b27038b
Separate serverValue from currentValue in watch SetpointRow
timbms Mar 8, 2026
8fc30a3
Merge branch 'develop' into openapigen-swiftui
timbms Mar 8, 2026
363bfce
Update CHANGELOG with openapigen-swiftui unreleased changes
timbms Mar 8, 2026
6762380
disable linting and format for swiftui auto build
TAKeanice Dec 19, 2025
137f567
make DatePickerRowView not send programmatic updates of the date
TAKeanice Mar 8, 2026
a4c15c5
iOS & watchOS Rollershutter & Setpoint circled controls (#1096)
DigiH Mar 9, 2026
f28db68
committed version bump: 3.2.8 (212)
Mar 10, 2026
1eaaab2
Fix corrupt project.pbxproj entries
timbms Mar 10, 2026
a696fb8
Fix notifications back button and nav bar
timbms Mar 10, 2026
e291a6f
committed version bump: 3.2.9 (213)
Mar 10, 2026
5368628
watchOS Rollershutter Stop glyph change (#1097)
DigiH Mar 10, 2026
05f9d71
Fix manage homes back button and nav bar
timbms Mar 10, 2026
3111f98
Replace stale notification categories
timbms Mar 11, 2026
9d48865
iOS Homes dialogs adjusments (#1099)
DigiH Mar 11, 2026
b5054dd
Fix screensaver layout sizing for large text (#1098)
timbms Mar 12, 2026
df68360
Use FormatStyle for screen saver date and 12-hour time
timbms Mar 12, 2026
ebfbd25
Use FormatStyle for Double.valueText and add tests
timbms Mar 12, 2026
a17d51b
Use FormatStyle in log and notification views
timbms Mar 12, 2026
f01c7b7
Migrate CGFloat(state:divisor:) to FormatStyle API; add tests
timbms Mar 12, 2026
2a7f326
Migrate JSONDecoder date strategy to FormatStyle API; add DateFormatt…
timbms Mar 12, 2026
f8543f8
Migrate StringExtension to FormatStyle API; add StringExtensionTests
timbms Mar 12, 2026
0f934d8
Fix StringExtensionTests: add @testable import OpenHABCore
timbms Mar 12, 2026
ce4b21f
Add plain 'Test' configuration to openHABTests test plan
timbms Mar 12, 2026
045e2e5
Fix openHABTestsSwift linker errors for host-app dynamic frameworks
timbms Mar 12, 2026
91793cc
Remove SDWebImageSVGCoder direct import from OpenHABSVGTests
timbms Mar 12, 2026
a71b306
Decouple OpenAPIRuntime from call sites via OpenAPIErrorInspector
timbms Mar 12, 2026
4245189
Fix optional binding error on non-optional ClientError.underlyingError
timbms Mar 12, 2026
29c718b
Fix script editor scroll truncated on iPhone (issue #1092)
timbms Mar 13, 2026
a7ca3cf
Associate openHABTests test plan with openHABTestsSwift scheme
timbms Mar 13, 2026
683456d
Remove ASAN configuration from openHABTests test plan
timbms Mar 13, 2026
b1e0bc6
committed version bump: 3.2.10 (214)
Mar 13, 2026
0819b67
Fix empty string commands and switch sendItemCommand to JSON payload …
timbms Mar 13, 2026
e204de9
committed version bump: 3.2.11 (215)
Mar 13, 2026
3e05c4d
Merge openapigen-swiftui into migrateToAppIntents2
timbms Mar 15, 2026
aa37a05
improve TextInputRowView legibility and corresponding tests
TAKeanice Mar 15, 2026
54e2555
Fix unbalanced brace in project.pbxproj from auto-merge
timbms Mar 15, 2026
32525a8
Localisation transition to String Catalog (#1101)
DigiH Mar 15, 2026
1c3938c
Remove stale references to ControlItemIntent.swift and openHABIntents…
timbms Mar 15, 2026
b533929
Update Package.resolved after merging openapigen-swiftui
timbms Mar 15, 2026
6493c4d
Merge openapigen-swiftui into migrateToAppIntents2
timbms Mar 15, 2026
7882bc2
Merge origin/develop into migrateToAppIntents2
timbms Apr 5, 2026
e5faf7e
Fix duplicate Localizable.xcstrings in same target
timbms Apr 5, 2026
4d44453
Fix duplicate MainLaunchScreen.nib in build
timbms Apr 5, 2026
91aeac6
Remove duplicate Sources/Resources entries shadowed by file system sync
timbms Apr 5, 2026
5c04a72
Fix build failures caused by duplicate files and stale project refere…
timbms Apr 6, 2026
4c92da6
Fix intentsLocalizations/intentsPlaceholders tests after App Intents …
timbms Apr 7, 2026
5f80d53
Remove duplicate Sources/Resources entries for openHABWatch and openH…
timbms Apr 7, 2026
9c704eb
Merge branch 'develop' into migrateToAppIntents2
timbms Apr 9, 2026
23c8e86
Migrate AppIntents strings to Localizable.xcstrings
timbms Apr 9, 2026
b428138
Merge branch 'develop' into migrateToAppIntents2
timbms Apr 10, 2026
96da456
Remove stale build file references from project
timbms Apr 10, 2026
3428b2c
Reformat Localizable.xcstrings to Xcode style
timbms Apr 10, 2026
ad2fec2
Remove obsolete openHABIntents SiriKit extension artifacts
timbms Apr 10, 2026
434b800
Add AppIntents validation tests
timbms Apr 10, 2026
e4290db
Fix App Intents localization tests after string catalog migration
timbms Apr 10, 2026
877907f
Remove stale legacy PBXGroup hierarchy for openHAB target
timbms Apr 12, 2026
14b24ac
Fetch live item state in GetItemStateIntent instead of using cached v…
timbms Apr 12, 2026
7b767ad
Persist item stubs to UserDefaults to prevent Shortcuts losing entity…
timbms Apr 12, 2026
ed5fafe
Merge branch 'develop' into migrateToAppIntents2
timbms Apr 12, 2026
56b7735
Adjust German App Intents state terminology
timbms Apr 12, 2026
e554883
Avoid localizing stored App Intents entity names
timbms Apr 12, 2026
1a8d17b
Mark App Intents home name as non-translatable
timbms Apr 12, 2026
89b7a14
Merge branch 'develop' into migrateToAppIntents2
timbms Apr 13, 2026
461ac52
Merge branch 'develop' into migrateToAppIntents2
timbms Apr 13, 2026
8abb112
Additional German translations
DigiH Apr 13, 2026
267b903
Small German translation correctons and adjustments
DigiH Apr 13, 2026
ebcfcca
Merge branch 'develop' into migrateToAppIntents2
timbms Apr 13, 2026
8b3497e
Run App Intents without opening the app
timbms Apr 13, 2026
299795b
Reduce App Intent background loading
timbms Apr 13, 2026
f99c980
Improve shortcuts item search and unique home resolution
timbms Apr 13, 2026
58791bb
Infer homes for App Intents and stop localizing home names
timbms Apr 14, 2026
c5a9d94
Merge branch 'develop' into migrateToAppIntents2
timbms Apr 27, 2026
b5ff5d5
Use stable cross-device identifier for Home AppEntity (#1163)
timbms Apr 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions AppIntents/ContactState.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// Copyright (c) 2010-2026 Contributors to the openHAB project
//
// See the NOTICE file(s) distributed with this work for additional
// information.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0
//
// SPDX-License-Identifier: EPL-2.0

import AppIntents
import Foundation

@available(iOS 17.0, macOS 14.0, watchOS 10.0, *)
enum ContactState: String, AppEnum {
case on = "ON"
case off = "OFF"

static let typeDisplayRepresentation = TypeDisplayRepresentation(name: "Contact State")

static let caseDisplayRepresentations: [Self: DisplayRepresentation] = [
.on: "On",
.off: "Off"
]
}
240 changes: 240 additions & 0 deletions AppIntents/Home.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
// Copyright (c) 2010-2026 Contributors to the openHAB project
//
// See the NOTICE file(s) distributed with this work for additional
// information.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0
//
// SPDX-License-Identifier: EPL-2.0

import AppIntents
import Foundation
import OpenHABCore

// MARK: - Stable Cross-Device Identifier

extension HomePreferences {
/// A stable identifier that is the same on every device configured for the same openHAB server.
/// Used as Home.id so that shortcuts synced via iCloud resolve correctly on a second device.
///
/// Priority:
/// 1. cloudUserId — unique per myopenhab.org account
/// 2. remote URL — unique for self-hosted cloud servers
/// 3. local URL — fallback for local-only setups
/// 4. UUID string — last resort (no cross-device benefit, but avoids an empty identifier)
var stableIdentifier: String {
if let cloudId = remoteConnectionConfig.cloudUserId, !cloudId.isEmpty {
return cloudId
}
if !remoteConnectionConfig.url.isEmpty {
return remoteConnectionConfig.url
}
if !localConnectionConfig.url.isEmpty {
return localConnectionConfig.url
}
return id.uuidString
}
}

// MARK: - Home AppEntity

struct Home: AppEntity {
struct HomeQuery: EntityQuery {
@MainActor
func entities(for identifiers: [Home.ID]) async throws -> [Home] {
let storedHomes = Preferences.shared.storedHomes
return identifiers.compactMap { identifier in
// Current format: match by stable cross-device identifier
if let match = storedHomes.values.first(where: { $0.stableIdentifier == identifier }) {
return Home(homePrefs: match)
}
// Legacy format: identifier is a device-local UUID string (shortcuts before this fix)
if let uuid = UUID(uuidString: identifier), let match = storedHomes[uuid] {
return Home(homePrefs: match)
}
return nil
}
}

@MainActor
func suggestedEntities() async throws -> [Home] {
Preferences.shared.storedHomes.values
.sorted {
let nameOrder = $0.homeName.localizedCaseInsensitiveCompare($1.homeName)
if nameOrder != .orderedSame {
return nameOrder == .orderedAscending
}
return $0.id.uuidString < $1.id.uuidString
}
.map { Home(homePrefs: $0) }
}
}

static let typeDisplayRepresentation = TypeDisplayRepresentation(name: "Home")

static let defaultQuery = HomeQuery()

var id: String
var displayString: String
var displayRepresentation: DisplayRepresentation {
DisplayRepresentation(title: "\(displayString)")
}

init(id: String, displayString: String) {
self.id = id
self.displayString = displayString
}

@MainActor
init(homePrefs: HomePreferences) {
self.init(id: homePrefs.stableIdentifier, displayString: homePrefs.homeName)
}
}

// MARK: - Errors

enum HomeResolutionError: Error, CustomLocalizedStringResourceConvertible {
case unknownHome
case ambiguousHomeSelection(String)

var localizedStringResource: LocalizedStringResource {
switch self {
case .unknownHome:
"Unknown home"
case let .ambiguousHomeSelection(itemName):
"Select a home for '\(itemName)'"
}
}
}

// MARK: - HomeResolver

enum HomeResolver {
/// Validates that `selectedHome` matches `itemHomeId` and returns the resolved local UUID.
///
/// Resolution order:
/// 1. Match `selectedHome.id` against entries in `stableIdentifierToLocalUUID`.
/// 2. Fall back to treating `selectedHome.id` as a legacy device-local UUID string.
///
/// The default empty array skips stable-identifier lookup and falls straight through to the
/// UUID fallback — this keeps unit tests working without any Preferences setup.
static func resolvedHomeId<E: Error>(
selectedHome: Home?,
itemHomeId: UUID,
itemLabel: String,
stableIdentifierToLocalUUID: [(String, UUID)] = [],
mismatchError: (String, String) -> E
) throws -> UUID {
guard let selectedHome else {
return itemHomeId
}

let homeId: UUID
if let match = stableIdentifierToLocalUUID.first(where: { $0.0 == selectedHome.id }) {
homeId = match.1
} else if let uuid = UUID(uuidString: selectedHome.id) {
homeId = uuid
} else {
throw HomeResolutionError.unknownHome
}

guard homeId == itemHomeId else {
throw mismatchError(itemLabel, selectedHome.displayString)
}

return homeId
}

/// Production overload — builds the stable-identifier map on the main actor before delegating
/// to the testable sync overload. Intent `perform()` methods call this with `try await`.
@MainActor
static func resolvedHomeId<E: Error>(
selectedHome: Home?,
itemHomeId: UUID,
itemLabel: String,
mismatchError: (String, String) -> E
) async throws -> UUID {
let map = Preferences.shared.storedHomes.values.map { ($0.stableIdentifier, $0.id) }
return try resolvedHomeId(
selectedHome: selectedHome,
itemHomeId: itemHomeId,
itemLabel: itemLabel,
stableIdentifierToLocalUUID: map,
mismatchError: mismatchError
)
}

/// Production overload for item-by-name resolution (iOS 16 compat intents).
/// Builds a stable-identifier-aware `findHomeId` closure and delegates to the testable overload.
static func resolveHomeId(
selectedHome: Home?,
itemName: String,
allowedTypes: [OpenHABItem.ItemType]? = nil
) async throws -> UUID {
try await resolveHomeId(
selectedHome: selectedHome,
itemName: itemName,
findHomeId: { identifier in
// All HomePreferences property access must happen on the main actor.
await MainActor.run {
let storedHomes = Preferences.shared.storedHomes
if let match = storedHomes.values.first(where: { $0.stableIdentifier == identifier }) {
return match.id
}
if let uuid = UUID(uuidString: identifier), storedHomes[uuid] != nil {
return uuid
}
return nil
}
},
listStoredHomes: { await Preferences.shared.listStoredHomes() },
exactMatchedHomes: {
let searchResults = await OpenHABItemCache.instance.searchCachedOrPersistedItems(
searchTerm: itemName,
types: allowedTypes
)
let normalizedItemName = itemName.trimmingCharacters(in: .whitespacesAndNewlines)

return Set(searchResults.flatMap { homeId, items in
items.compactMap { item in
item.name.localizedCaseInsensitiveCompare(normalizedItemName) == .orderedSame ? homeId : nil
}
})
}
)
}

/// Testable overload. All parameters are injected via closures so unit tests can provide
/// mock implementations.
///
/// `findHomeId` defaults to UUID-string parsing so existing tests that only exercise the
/// `selectedHome: nil` path compile and run without modification.
static func resolveHomeId(
selectedHome: Home?,
itemName: String,
findHomeId: @escaping (String) async -> UUID? = { UUID(uuidString: $0) },
listStoredHomes: @escaping () async -> [UUID],
exactMatchedHomes: @escaping () async -> Set<UUID>
) async throws -> UUID {
if let selectedHome {
guard let homeId = await findHomeId(selectedHome.id) else {
throw HomeResolutionError.unknownHome
}
return homeId
}

let storedHomes = await listStoredHomes()
if storedHomes.count == 1, let onlyHomeId = storedHomes.first {
return onlyHomeId
}

let matchedHomes = await exactMatchedHomes()
if matchedHomes.count == 1, let matchedHomeId = matchedHomes.first {
return matchedHomeId
}

throw HomeResolutionError.ambiguousHomeSelection(itemName)
}
}
77 changes: 77 additions & 0 deletions AppIntents/Intents/ContactStateIntent.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// Copyright (c) 2010-2026 Contributors to the openHAB project
//
// See the NOTICE file(s) distributed with this work for additional
// information.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0
//
// SPDX-License-Identifier: EPL-2.0

import AppIntents
import OpenHABCore

enum ContactStateError: Error, CustomLocalizedStringResourceConvertible {
case itemNotInHome(String, String)
case commandFailed(String)

var localizedStringResource: LocalizedStringResource {
switch self {
case let .itemNotInHome(itemName, homeName):
"Item '\(itemName)' is not in home '\(homeName)'"
case let .commandFailed(message):
"Command failed: \(message)"
}
}
}

@available(iOS 17.0, macOS 14.0, watchOS 10.0, *)
struct ContactStateIntent: AppIntent {
static var openAppWhenRun: Bool { false }

static var allowedItemTypes: [OpenHABItem.ItemType] { [.contact] }

static var parameterSummary: some ParameterSummary {
Summary("Set the state of \(\.$itemEntity) to \(\.$state)") {
\.$home
}
}

static let title: LocalizedStringResource = "Set Contact State Value"
static let description = IntentDescription("Set the state of a contact open or closed")

@Parameter(title: "Home")
var home: Home?

@Parameter(
title: "Item",
requestValueDialog: IntentDialog("Search for an item")
)
var itemEntity: ContactItemEntity

@Parameter(title: "State")
var state: ContactState

func perform() async throws -> some IntentResult & ProvidesDialog {
// Validate that the item belongs to the selected home
let homeId = try await HomeResolver.resolvedHomeId(
selectedHome: home,
itemHomeId: itemEntity.homeId,
itemLabel: itemEntity.label,
mismatchError: ContactStateError.itemNotInHome
)

do {
try await OpenHABItemCache.instance.sendCommand(
to: itemEntity.item,
home: homeId,
command: state.rawValue
)
} catch {
throw ContactStateError.commandFailed(error.localizedDescription)
}

return .result(dialog: "The state of \(itemEntity.label) was set to \(state.rawValue)")
}
}
Loading
Loading