|
| 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