Skip to content

Commit f43a61b

Browse files
committed
Migrate to App Intents
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
1 parent 013980f commit f43a61b

10 files changed

Lines changed: 1186 additions & 12 deletions

File tree

AppIntents/GetItemState.swift

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
// Copyright (c) 2010-2025 Contributors to the openHAB project
2+
//
3+
// See the NOTICE file(s) distributed with this work for additional
4+
// information.
5+
//
6+
// This program and the accompanying materials are made available under the
7+
// terms of the Eclipse Public License 2.0 which is available at
8+
// http://www.eclipse.org/legal/epl-2.0
9+
//
10+
// SPDX-License-Identifier: EPL-2.0
11+
12+
import AppIntents
13+
import Foundation
14+
import OpenHABCore
15+
16+
enum GetItemStateError: Error, CustomLocalizedStringResourceConvertible {
17+
case invalidHomeIdentifier
18+
case unknownHome
19+
case itemNotFound(String)
20+
21+
var localizedStringResource: LocalizedStringResource {
22+
switch self {
23+
case .invalidHomeIdentifier:
24+
"Invalid home identifier"
25+
case .unknownHome:
26+
"Unknown home"
27+
case let .itemNotFound(itemName):
28+
"Item '\(itemName)' not found"
29+
}
30+
}
31+
}
32+
33+
@available(iOS 16.0, macOS 13.0, watchOS 9.0, tvOS 16.0, *)
34+
struct GetItemState: AppIntent, CustomIntentMigratedAppIntent, PredictableIntent {
35+
struct StringOptionsProvider: DynamicOptionsProvider {
36+
func results() async throws -> [String] {
37+
let allItems = await OpenHABItemCache.instance.getAllCachedItems()
38+
return allItems.flatMap { $0.value.map(\.name) }
39+
}
40+
}
41+
42+
static let intentClassName = "OpenHABGetItemStateIntent"
43+
44+
static let title: LocalizedStringResource = "Get Item State"
45+
static let description = IntentDescription("Retrieve the current state of an item")
46+
47+
// swiftlint:disable type_contents_order
48+
@Parameter(title: "Item", optionsProvider: StringOptionsProvider())
49+
var item: String?
50+
51+
@Parameter(title: "home")
52+
var home: Home?
53+
54+
static var parameterSummary: some ParameterSummary {
55+
Summary("Get \(\.$item) State") {
56+
\.$home
57+
}
58+
}
59+
60+
// swiftlint:enable type_contents_order
61+
62+
static var predictionConfiguration: some IntentPredictionConfiguration {
63+
IntentPrediction(parameters: (\.$item, \.$home)) { item, _ in
64+
DisplayRepresentation(
65+
title: "Get \(item!) State",
66+
subtitle: ""
67+
)
68+
}
69+
}
70+
71+
func perform() async throws -> some IntentResult & ReturnsValue<String> {
72+
guard let itemName = item, !itemName.isEmpty else {
73+
throw $item.needsValueError()
74+
}
75+
76+
guard let home else {
77+
throw $home.needsValueError()
78+
}
79+
80+
guard let homeId = UUID(uuidString: home.id) else {
81+
throw GetItemStateError.invalidHomeIdentifier
82+
}
83+
84+
let homeExists = await MainActor.run {
85+
Preferences.shared.storedHomes[homeId] != nil
86+
}
87+
88+
guard homeExists else {
89+
throw GetItemStateError.unknownHome
90+
}
91+
92+
guard let item = await OpenHABItemCache.instance.getItemUncached(name: itemName, home: homeId) else {
93+
throw GetItemStateError.itemNotFound(itemName)
94+
}
95+
96+
let state = item.state ?? "Unknown state"
97+
98+
return .result(
99+
value: state,
100+
dialog: .responseSuccess(item: itemName, state: state)
101+
)
102+
}
103+
}
104+
105+
@available(iOS 16.0, macOS 13.0, watchOS 9.0, tvOS 16.0, *)
106+
private extension IntentDialog {
107+
static var itemParameterConfiguration: Self {
108+
"Item Name"
109+
}
110+
111+
static var homeParameterConfiguration: Self {
112+
"Home name"
113+
}
114+
115+
static var homeParameterPrompt: Self {
116+
"blabla"
117+
}
118+
119+
static var homeParameterDisambiguationSelection: Self {
120+
"For which home do you want to get the value?"
121+
}
122+
123+
static func homeParameterDisambiguationIntro(count: Int, item: String) -> Self {
124+
"There are \(count) configured homes with an item named '\(item)'’."
125+
}
126+
127+
static func homeParameterConfirmation(home: Home) -> Self {
128+
"Just to confirm, you wanted ‘\(home)’?"
129+
}
130+
131+
static func responseSuccess(item: String, state: String) -> Self {
132+
"The state of \(item) is \(state)"
133+
}
134+
135+
static func responseFailureInvalidItem(item: String) -> Self {
136+
"Sorry can't find \(item)"
137+
}
138+
}

AppIntents/Home.swift

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
// Copyright (c) 2010-2025 Contributors to the openHAB project
2+
//
3+
// See the NOTICE file(s) distributed with this work for additional
4+
// information.
5+
//
6+
// This program and the accompanying materials are made available under the
7+
// terms of the Eclipse Public License 2.0 which is available at
8+
// http://www.eclipse.org/legal/epl-2.0
9+
//
10+
// SPDX-License-Identifier: EPL-2.0
11+
12+
import AppIntents
13+
import Foundation
14+
import OpenHABCore
15+
16+
@available(iOS 16.0, macOS 13.0, watchOS 9.0, tvOS 16.0, *)
17+
struct Home: AppEntity {
18+
struct HomeQuery: EntityQuery {
19+
@MainActor
20+
func entities(for identifiers: [Home.ID]) async throws -> [Home] {
21+
identifiers.compactMap { identifier in
22+
guard let uuid = UUID(uuidString: identifier),
23+
let homePrefs = Preferences.shared.storedHomes[uuid] else {
24+
return nil
25+
}
26+
return Home(id: homePrefs.id.uuidString, displayString: homePrefs.homeName)
27+
}
28+
}
29+
30+
@MainActor
31+
func suggestedEntities() async throws -> [Home] {
32+
Preferences.shared.storedHomes.map { homePrefs in
33+
Home(id: homePrefs.value.id.uuidString, displayString: homePrefs.value.homeName)
34+
}
35+
}
36+
}
37+
38+
static let typeDisplayRepresentation = TypeDisplayRepresentation(name: "Home")
39+
40+
static let defaultQuery = HomeQuery()
41+
42+
var id: String // if your identifier is not a String, conform the entity to EntityIdentifierConvertible.
43+
var displayString: String
44+
var displayRepresentation: DisplayRepresentation {
45+
DisplayRepresentation(title: "\(displayString)")
46+
}
47+
48+
init(id: String, displayString: String) {
49+
self.id = id
50+
self.displayString = displayString
51+
}
52+
}

AppIntents/SetColorValue.swift

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
// Copyright (c) 2010-2025 Contributors to the openHAB project
2+
//
3+
// See the NOTICE file(s) distributed with this work for additional
4+
// information.
5+
//
6+
// This program and the accompanying materials are made available under the
7+
// terms of the Eclipse Public License 2.0 which is available at
8+
// http://www.eclipse.org/legal/epl-2.0
9+
//
10+
// SPDX-License-Identifier: EPL-2.0
11+
12+
import AppIntents
13+
import Foundation
14+
import OpenHABCore
15+
16+
enum SetColorValueError: Error, CustomLocalizedStringResourceConvertible {
17+
case invalidHomeIdentifier
18+
case unknownHome
19+
case itemNotFound(String)
20+
case invalidValue(String, String)
21+
22+
var localizedStringResource: LocalizedStringResource {
23+
switch self {
24+
case .invalidHomeIdentifier:
25+
"Invalid home identifier"
26+
case .unknownHome:
27+
"Unknown home"
28+
case let .itemNotFound(itemName):
29+
"Item '\(itemName)' not found"
30+
case let .invalidValue(value, itemName):
31+
"Invalid value: \(value) for \(itemName) must be HSB (0-360,0-100,0-100)"
32+
}
33+
}
34+
}
35+
36+
@available(iOS 16.0, macOS 13.0, watchOS 9.0, tvOS 16.0, *)
37+
struct SetColorValue: AppIntent, CustomIntentMigratedAppIntent, PredictableIntent {
38+
struct StringOptionsProvider: DynamicOptionsProvider {
39+
func results() async throws -> [String] {
40+
let allItems = await OpenHABItemCache.instance.getAllCachedItems()
41+
let items = allItems.flatMap(\.value).filter { $0.type == .color }
42+
return items.map(\.name)
43+
}
44+
}
45+
46+
static let intentClassName = "OpenHABSetColorValueIntent"
47+
48+
static let title: LocalizedStringResource = "Set Color Control Value"
49+
static let description = IntentDescription("Set the color of a color control item")
50+
51+
// swiftlint:disable type_contents_order
52+
@Parameter(title: "Item", optionsProvider: StringOptionsProvider())
53+
var item: String?
54+
55+
@Parameter(title: "Value", default: "240,100,100")
56+
var value: String?
57+
58+
@Parameter(title: "home")
59+
var home: Home?
60+
61+
static var parameterSummary: some ParameterSummary {
62+
Summary("Set \(\.$item) to \(\.$value) (HSB)") {
63+
\.$home
64+
}
65+
}
66+
67+
// swiftlint:enable type_contents_order
68+
69+
static var predictionConfiguration: some IntentPredictionConfiguration {
70+
IntentPrediction(parameters: (\.$item, \.$value, \.$home)) { item, value, _ in
71+
DisplayRepresentation(
72+
title: "Set \(item!) to \(value!) (HSB)",
73+
subtitle: ""
74+
)
75+
}
76+
}
77+
78+
func perform() async throws -> some IntentResult & ProvidesDialog {
79+
guard let itemName = item, !itemName.isEmpty else {
80+
throw $item.needsValueError()
81+
}
82+
83+
guard var value else {
84+
throw $value.needsValueError()
85+
}
86+
87+
guard let home else {
88+
throw $home.needsValueError()
89+
}
90+
91+
guard let homeId = UUID(uuidString: home.id) else {
92+
throw SetColorValueError.invalidHomeIdentifier
93+
}
94+
95+
let homeExists = await MainActor.run {
96+
Preferences.shared.storedHomes[homeId] != nil
97+
}
98+
99+
guard homeExists else {
100+
throw SetColorValueError.unknownHome
101+
}
102+
103+
let hsb = value.split(separator: ",")
104+
guard hsb.count == 3,
105+
let hue = Int(hsb[0]), (0 ... 360).contains(hue),
106+
let sat = Int(hsb[1]), (0 ... 100).contains(sat),
107+
let val = Int(hsb[2]), (0 ... 100).contains(val) else {
108+
throw SetColorValueError.invalidValue(value, itemName)
109+
}
110+
111+
value = "\(hue),\(sat),\(val)"
112+
113+
guard let items = await OpenHABItemCache.instance.getCachedItem(name: itemName, home: homeId),
114+
!items.isEmpty else {
115+
throw SetColorValueError.itemNotFound(itemName)
116+
}
117+
118+
let item = items[0]
119+
120+
await OpenHABItemCache.instance.sendCommand(to: item, home: homeId, command: value)
121+
122+
return .result(dialog: .responseSuccess(value: value, item: itemName))
123+
}
124+
}
125+
126+
@available(iOS 16.0, macOS 13.0, watchOS 9.0, tvOS 16.0, *)
127+
private extension IntentDialog {
128+
static var itemParameterConfiguration: Self {
129+
"Dimmer/Roller Name"
130+
}
131+
132+
static var homeParameterConfiguration: Self {
133+
"Home name"
134+
}
135+
136+
static var homeParameterDisambiguationSelection: Self {
137+
"For which home do you want to get the value?"
138+
}
139+
140+
static func homeParameterDisambiguationIntro(count: Int, item: String) -> Self {
141+
"There are \(count) configured homes with an item named '\(item)''."
142+
}
143+
144+
static func homeParameterConfirmation(home: Home) -> Self {
145+
"Just to confirm, you wanted '\(home)'?"
146+
}
147+
148+
static func responseSuccess(value: String, item: String) -> Self {
149+
"Sent the color value of \(value) to \(item)"
150+
}
151+
152+
static func responseFailureInvalidItem(item: String) -> Self {
153+
"Sorry can't find \(item)"
154+
}
155+
156+
static func responseFailureInvalidValue(value: String, item: String) -> Self {
157+
"Invalid value: \(value) for \(item) must be HSB (0-360,0-100,0-100)"
158+
}
159+
}

0 commit comments

Comments
 (0)