diff --git a/AGENTS.md b/AGENTS.md index 7c356f16f..a6944ee22 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,9 +2,8 @@ ## Build/Test Commands - Build: `xcodebuild -workspace openHAB.xcworkspace -scheme openHAB` -- Test all: `fastlane unittests` or `xcodebuild test -workspace openHAB.xcworkspace -scheme openHABTestsSwift -destination 'platform=iOS Simulator,name=iPhone 17 Pro'` -- Single test: `xcodebuild test -workspace openHAB.xcworkspace -scheme openHABTestsSwift -destination 'platform=iOS Simulator,name=iPhone 17 Pro' -only-testing:openHABTestsSwift/TestClassName/testMethodName` -- If the exact simulator is unavailable, switch to an available iPhone simulator +- Test all: `fastlane unittests` or `xcodebuild test -workspace openHAB.xcworkspace -scheme openHABTestsSwift -destination 'platform=iOS Simulator,name=iPhone 16 Pro'` +- Single test: `xcodebuild test -workspace openHAB.xcworkspace -scheme openHABTestsSwift -destination 'platform=iOS Simulator,name=iPhone 16 Pro' -only-testing:openHABTestsSwift/TestClassName/testMethodName` - Beta build: `fastlane beta` - UI tests: `xcodebuild test -workspace openHAB.xcworkspace -scheme openHABUITests` @@ -13,16 +12,17 @@ - **Core library**: OpenHABCore/ - Swift Package with shared business logic, models, API clients - **Watch app**: openHABWatch/ - watchOS companion app (watchOS 10+) - **Extensions**: openHABIntents/ (Siri shortcuts), NotificationService/ (rich notifications) -- **Tests**: openHABTestsSwift/ (Swift Testing), openHABUITests/ (UI automation). For targeted bug fixes, run only focused tests by default. +- **Tests**: openHABTestsSwift/ (XCTest), openHABUITests/ (UI automation) - **Dependencies**: Kingfisher (image loading), SwiftUI, Firebase, OpenAPI runtime, SFSafeSymbols ## Code Style - Swift 6 -- SwiftUI for new views +- SwiftUI for new views, UIKit legacy TableViewCells for sitemap rendering - Naming: PascalCase classes, camelCase properties/methods, OpenHAB prefix for core types - Use SFSafeSymbols for SF Symbols - Avoid force unwrapping, prefer optionals -- Error handling: Result types in OpenHABCore, UIKit error alerts in main app but transition to SwiftUI wherever possible +- TableViewCell pattern: GenericUITableViewCell subclasses for sitemap widgets +- Error handling: Result types in OpenHABCore, UIKit error alerts in main app - Avoid trailing closure syntax when passing multiple closures (use parentheses for all closures to prevent multiple_closures_with_trailing_closure warnings) - Respect "BuildTools/.swiftformat" and "BuildTools/.swiftlint.yml" - Always use Swift Regex with Swift 6 syntax @@ -37,4 +37,4 @@ - Add a parameter with a default value (e.g. `networkTracker: NetworkTracker = .shared`) to make functions testable without coupling them to singletons ## git -- Always use git commit with -s -S +- Always use git commit with -s -S diff --git a/AppIntents/ContactState.swift b/AppIntents/ContactState.swift new file mode 100644 index 000000000..139241c74 --- /dev/null +++ b/AppIntents/ContactState.swift @@ -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" + ] +} diff --git a/AppIntents/Home.swift b/AppIntents/Home.swift new file mode 100644 index 000000000..d02958711 --- /dev/null +++ b/AppIntents/Home.swift @@ -0,0 +1,51 @@ +// 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 + +struct Home: AppEntity { + struct HomeQuery: EntityQuery { + @MainActor + func entities(for identifiers: [Home.ID]) async throws -> [Home] { + identifiers.compactMap { identifier in + guard let uuid = UUID(uuidString: identifier), + let homePrefs = Preferences.shared.storedHomes[uuid] else { + return nil + } + return Home(id: homePrefs.id.uuidString, displayString: homePrefs.homeName) + } + } + + @MainActor + func suggestedEntities() async throws -> [Home] { + Preferences.shared.storedHomes.map { homePrefs in + Home(id: homePrefs.value.id.uuidString, displayString: homePrefs.value.homeName) + } + } + } + + static let typeDisplayRepresentation = TypeDisplayRepresentation(name: "Home") + + static let defaultQuery = HomeQuery() + + var id: String // if your identifier is not a String, conform the entity to EntityIdentifierConvertible. + var displayString: String + var displayRepresentation: DisplayRepresentation { + DisplayRepresentation(title: "\(displayString)") + } + + init(id: String, displayString: String) { + self.id = id + self.displayString = displayString + } +} diff --git a/AppIntents/Intents/ContactStateIntent.swift b/AppIntents/Intents/ContactStateIntent.swift new file mode 100644 index 000000000..9524f05e9 --- /dev/null +++ b/AppIntents/Intents/ContactStateIntent.swift @@ -0,0 +1,72 @@ +// 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 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 + guard let homeId = UUID(uuidString: home.id), homeId == itemEntity.homeId else { + throw ContactStateError.itemNotInHome(itemEntity.label, home.displayString) + } + + do { + try await OpenHABItemCache.instance.sendCommand( + to: itemEntity.item, + home: itemEntity.homeId, + command: state.rawValue + ) + } catch { + throw ContactStateError.commandFailed(error.localizedDescription) + } + + return .result(dialog: "The state of \(itemEntity.label) was set to \(state.rawValue)") + } +} diff --git a/AppIntents/Intents/GetItemStateIntent.swift b/AppIntents/Intents/GetItemStateIntent.swift new file mode 100644 index 000000000..5659bb237 --- /dev/null +++ b/AppIntents/Intents/GetItemStateIntent.swift @@ -0,0 +1,59 @@ +// 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 ItemStateError: Error, CustomLocalizedStringResourceConvertible { + case itemNotInHome(String, String) + + var localizedStringResource: LocalizedStringResource { + switch self { + case let .itemNotInHome(itemName, homeName): + "Item '\(itemName)' is not in home '\(homeName)'" + } + } +} + +@available(iOS 17.0, macOS 14.0, watchOS 10.0, *) +struct GetItemStateIntent: AppIntent { + static var parameterSummary: some ParameterSummary { + Summary("Get \(\.$itemEntity) State") { + \.$home + } + } + + static let title: LocalizedStringResource = "Get Item State" + static let description = IntentDescription("Retrieve the current state of an item") + + @Parameter(title: "Home") + var home: Home + + @Parameter( + title: "Item", + requestValueDialog: IntentDialog("Search for an item") + ) + var itemEntity: GenericItemEntity + + func perform() async throws -> some IntentResult & ReturnsValue & ProvidesDialog { + // Validate that the item belongs to the selected home + guard let homeId = UUID(uuidString: home.id), homeId == itemEntity.homeId else { + throw ItemStateError.itemNotInHome(itemEntity.label, home.displayString) + } + + let state = itemEntity.item.state ?? "Unknown state" + + return .result( + value: state, + dialog: "The state of \(itemEntity.label) is \(state)" + ) + } +} diff --git a/AppIntents/Intents/SetColorValueIntent.swift b/AppIntents/Intents/SetColorValueIntent.swift new file mode 100644 index 000000000..ba1cbecd0 --- /dev/null +++ b/AppIntents/Intents/SetColorValueIntent.swift @@ -0,0 +1,85 @@ +// 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 ColorValueError: Error, CustomLocalizedStringResourceConvertible { + case itemNotInHome(String, String) + case invalidValue(String, String) + case commandFailed(String) + + var localizedStringResource: LocalizedStringResource { + switch self { + case let .itemNotInHome(itemName, homeName): + "Item '\(itemName)' is not in home '\(homeName)'" + case let .invalidValue(value, itemName): + "Invalid value: \(value) for \(itemName) must be HSB (0-360,0-100,0-100)" + case let .commandFailed(message): + "Command failed: \(message)" + } + } +} + +@available(iOS 17.0, macOS 14.0, watchOS 10.0, *) +struct SetColorValueIntent: AppIntent { + static var allowedItemTypes: [OpenHABItem.ItemType] { [.color] } + static var parameterSummary: some ParameterSummary { + Summary("Set \(\.$itemEntity) to \(\.$value) (HSB)") { + \.$home + } + } + + static let title: LocalizedStringResource = "Set Color Control Value" + static let description = IntentDescription("Set the color of a color control item") + + @Parameter(title: "Home") + var home: Home + + @Parameter( + title: "Item", + requestValueDialog: IntentDialog("Search for an item") + ) + var itemEntity: ColorItemEntity + + @Parameter(title: "Value", default: "240,100,100") + var value: String + + func perform() async throws -> some IntentResult & ProvidesDialog { + // Validate that the item belongs to the selected home + guard let homeId = UUID(uuidString: home.id), homeId == itemEntity.homeId else { + throw ColorValueError.itemNotInHome(itemEntity.label, home.displayString) + } + + var colorValue = value + let hsb = colorValue.split(separator: ",") + guard hsb.count == 3, + let hue = Int(hsb[0]), (0 ... 360).contains(hue), + let sat = Int(hsb[1]), (0 ... 100).contains(sat), + let val = Int(hsb[2]), (0 ... 100).contains(val) else { + throw ColorValueError.invalidValue(colorValue, itemEntity.label) + } + + colorValue = "\(hue),\(sat),\(val)" + + do { + try await OpenHABItemCache.instance.sendCommand( + to: itemEntity.item, + home: itemEntity.homeId, + command: colorValue + ) + } catch { + throw ColorValueError.commandFailed(error.localizedDescription) + } + + return .result(dialog: "Sent the color value of \(colorValue) to \(itemEntity.label)") + } +} diff --git a/AppIntents/Intents/SetDimmerRollerValueIntent.swift b/AppIntents/Intents/SetDimmerRollerValueIntent.swift new file mode 100644 index 000000000..af0df991f --- /dev/null +++ b/AppIntents/Intents/SetDimmerRollerValueIntent.swift @@ -0,0 +1,78 @@ +// 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 DimmerRollerValueError: Error, CustomLocalizedStringResourceConvertible { + case itemNotInHome(String, String) + case invalidValue(Int, String) + case commandFailed(String) + + var localizedStringResource: LocalizedStringResource { + switch self { + case let .itemNotInHome(itemName, homeName): + "Item '\(itemName)' is not in home '\(homeName)'" + case let .invalidValue(value, itemName): + "Invalid value \(value) for \(itemName) (0-100)" + case let .commandFailed(message): + "Command failed: \(message)" + } + } +} + +@available(iOS 17.0, macOS 14.0, watchOS 10.0, *) +struct SetDimmerRollerValueIntent: AppIntent { + static var allowedItemTypes: [OpenHABItem.ItemType] { [.dimmer, .rollershutter] } + static var parameterSummary: some ParameterSummary { + Summary("Set \(\.$itemEntity) to \(\.$value)") { + \.$home + } + } + + static let title: LocalizedStringResource = "Set Dimmer or Roller Shutter Value" + static let description = IntentDescription("Set the integer value of a dimmer or roller shutter") + + @Parameter(title: "Home") + var home: Home + + @Parameter( + title: "Item", + requestValueDialog: IntentDialog("Search for an item") + ) + var itemEntity: DimmerItemEntity + + @Parameter(title: "Value") + var value: Int + + func perform() async throws -> some IntentResult & ProvidesDialog { + // Validate that the item belongs to the selected home + guard let homeId = UUID(uuidString: home.id), homeId == itemEntity.homeId else { + throw DimmerRollerValueError.itemNotInHome(itemEntity.label, home.displayString) + } + + guard (0 ... 100).contains(value) else { + throw DimmerRollerValueError.invalidValue(value, itemEntity.label) + } + + do { + try await OpenHABItemCache.instance.sendCommand( + to: itemEntity.item, + home: itemEntity.homeId, + command: "\(value)" + ) + } catch { + throw DimmerRollerValueError.commandFailed(error.localizedDescription) + } + + return .result(dialog: "Sent the value of \(value) to \(itemEntity.label)") + } +} diff --git a/AppIntents/Intents/SetNumberValueIntent.swift b/AppIntents/Intents/SetNumberValueIntent.swift new file mode 100644 index 000000000..d4e576c5b --- /dev/null +++ b/AppIntents/Intents/SetNumberValueIntent.swift @@ -0,0 +1,71 @@ +// 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 NumberValueError: 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 SetNumberValueIntent: AppIntent { + static var allowedItemTypes: [OpenHABItem.ItemType] { [.number, .numberWithDimension] } + static var parameterSummary: some ParameterSummary { + Summary("Set \(\.$itemEntity) to \(\.$value)") { + \.$home + } + } + + static let title: LocalizedStringResource = "Set Number Control Value" + static let description = IntentDescription("Set the decimal value of a number control item") + + @Parameter(title: "Home") + var home: Home + + @Parameter( + title: "Item", + requestValueDialog: IntentDialog("Search for an item") + ) + var itemEntity: NumberItemEntity + + @Parameter(title: "Value") + var value: Double + + func perform() async throws -> some IntentResult & ProvidesDialog { + // Validate that the item belongs to the selected home + guard let homeId = UUID(uuidString: home.id), homeId == itemEntity.homeId else { + throw NumberValueError.itemNotInHome(itemEntity.label, home.displayString) + } + + do { + try await OpenHABItemCache.instance.sendCommand( + to: itemEntity.item, + home: itemEntity.homeId, + command: String(value) + ) + } catch { + throw NumberValueError.commandFailed(error.localizedDescription) + } + + return .result(dialog: "Sent the number \(value) to \(itemEntity.label)") + } +} diff --git a/AppIntents/Intents/SetStringValueIntent.swift b/AppIntents/Intents/SetStringValueIntent.swift new file mode 100644 index 000000000..e65416693 --- /dev/null +++ b/AppIntents/Intents/SetStringValueIntent.swift @@ -0,0 +1,71 @@ +// 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 StringValueError: 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 SetStringValueIntent: AppIntent { + static var allowedItemTypes: [OpenHABItem.ItemType] { [.stringItem] } + static var parameterSummary: some ParameterSummary { + Summary("Set \(\.$itemEntity) to \(\.$value)") { + \.$home + } + } + + static let title: LocalizedStringResource = "Set String Control Value" + static let description = IntentDescription("Set the string of a string control item") + + @Parameter(title: "Home") + var home: Home + + @Parameter( + title: "Item", + requestValueDialog: IntentDialog("Search for an item") + ) + var itemEntity: StringItemEntity + + @Parameter(title: "Value") + var value: String + + func perform() async throws -> some IntentResult & ProvidesDialog { + // Validate that the item belongs to the selected home + guard let homeId = UUID(uuidString: home.id), homeId == itemEntity.homeId else { + throw StringValueError.itemNotInHome(itemEntity.label, home.displayString) + } + + do { + try await OpenHABItemCache.instance.sendCommand( + to: itemEntity.item, + home: itemEntity.homeId, + command: value + ) + } catch { + throw StringValueError.commandFailed(error.localizedDescription) + } + + return .result(dialog: "Sent the string \(value) to \(itemEntity.label)") + } +} diff --git a/AppIntents/Intents/SetSwitchItemIntent.swift b/AppIntents/Intents/SetSwitchItemIntent.swift new file mode 100644 index 000000000..f3e699a41 --- /dev/null +++ b/AppIntents/Intents/SetSwitchItemIntent.swift @@ -0,0 +1,76 @@ +// 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 +import WidgetKit + +enum ControlItemError: 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 SetSwitchItemIntent: AppIntent { + static var allowedItemTypes: [OpenHABItem.ItemType] { [.switchItem] } + + static var parameterSummary: some ParameterSummary { + Summary("Send \(\.$action) to \(\.$itemEntity)") { + \.$home + } + } + + static let title: LocalizedStringResource = "Set Switch State" + static let description = IntentDescription("Set the state of a switch on or off, or toggle its state") + + @Parameter(title: "Home") + var home: Home + + @Parameter( + title: "Item", + requestValueDialog: IntentDialog("Search for an item") + ) + var itemEntity: SwitchItemEntity + + @Parameter(title: "Action") + var action: SwitchAction + + func perform() async throws -> some IntentResult { + // Validate that the item belongs to the selected home + guard let homeId = UUID(uuidString: home.id), homeId == itemEntity.homeId else { + throw ControlItemError.itemNotInHome(itemEntity.label, home.displayString) + } + + do { + try await OpenHABItemCache.instance.sendCommand( + to: itemEntity.item, + home: itemEntity.homeId, + command: action.rawValue + ) + + // Reload widgets immediately after toggle to reflect the change + WidgetCenter.shared.reloadTimelines(ofKind: "OpenHABSwitchWidget") + } catch { + throw ControlItemError.commandFailed(error.localizedDescription) + } + + return .result() + } +} diff --git a/AppIntents/ItemEntity.swift b/AppIntents/ItemEntity.swift new file mode 100644 index 000000000..4ce349957 --- /dev/null +++ b/AppIntents/ItemEntity.swift @@ -0,0 +1,58 @@ +// 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 + +// MARK: - Shared Protocol for All Item Entities + +@available(iOS 17.0, macOS 14.0, watchOS 10.0, *) +protocol ItemEntity: AppEntity where ID == ItemIdentifier { + var id: ItemIdentifier { get set } + var item: OpenHABItem { get set } + var homeName: String? { get set } + + init(id: ItemIdentifier, item: OpenHABItem, homeName: String?) + init(_ openHABItem: OpenHABItem, homeId: UUID, homeName: String?) +} + +@available(iOS 17.0, macOS 14.0, watchOS 10.0, *) +extension ItemEntity { + var homeId: UUID { id.homeId } + var itemName: String { id.itemName } + + // Convenient access to common item properties + var label: String { item.label } + var category: String { item.category } + var type: OpenHABItem.ItemType? { item.type } + var state: String? { item.state } + var link: String { item.link } + var name: String { item.name } + + var displayRepresentation: DisplayRepresentation { + if let homeName { + DisplayRepresentation( + title: "\(label)", + subtitle: "\(name) • \(homeName)" + ) + } else { + DisplayRepresentation(title: "\(label)", subtitle: "\(name)") + } + } + + init(_ openHABItem: OpenHABItem, homeId: UUID, homeName: String? = nil) { + self.init( + id: ItemIdentifier(homeId: homeId, itemName: openHABItem.name), + item: openHABItem, + homeName: homeName + ) + } +} diff --git a/AppIntents/ItemEntityQuery.swift b/AppIntents/ItemEntityQuery.swift new file mode 100644 index 000000000..3898eec27 --- /dev/null +++ b/AppIntents/ItemEntityQuery.swift @@ -0,0 +1,101 @@ +// 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 + +// MARK: - Shared Query Protocol + +@available(iOS 17.0, macOS 14.0, watchOS 10.0, *) +protocol ItemEntityQuery: EntityStringQuery { + associatedtype EntityType: ItemEntity + var allowedTypes: [OpenHABItem.ItemType] { get set } + var selectedHomeId: UUID? { get } +} + +@available(iOS 17.0, macOS 14.0, watchOS 10.0, *) +extension ItemEntityQuery { + @MainActor + func getHomeName(for homeId: UUID) -> String? { + Preferences.shared.storedHomes[homeId]?.homeName + } + + func entities(for identifiers: [ItemIdentifier]) async throws -> [EntityType] { + var result: [EntityType] = [] + + for identifier in identifiers { + if let items = await OpenHABItemCache.instance.getCachedItem( + name: identifier.itemName, + home: identifier.homeId + ), let item = items.first { + let homeName = await getHomeName(for: identifier.homeId) + result.append(EntityType(item, homeId: identifier.homeId, homeName: homeName)) + } + } + + return result + } + + func suggestedEntities() async throws -> [EntityType] { + let allItems = await OpenHABItemCache.instance.getAllCachedItems() + var result: [EntityType] = [] + + // If the user selected a Home in the intent UI, scope results to that home. + if let selectedHomeId { + if let items = allItems[selectedHomeId] { + let homeName = await getHomeName(for: selectedHomeId) + let filteredItems = items.filter { item in + guard let type = item.type else { return false } + return allowedTypes.isEmpty || allowedTypes.contains(type) + } + result.append(contentsOf: filteredItems.map { EntityType($0, homeId: selectedHomeId, homeName: homeName) }) + } + return result + } + + // Fallback (e.g. Siri request without an explicit Home selection): return items across all homes. + for (homeId, items) in allItems { + let homeName = await getHomeName(for: homeId) + let filteredItems = items.filter { item in + guard let type = item.type else { return false } + return allowedTypes.isEmpty || allowedTypes.contains(type) + } + result.append(contentsOf: filteredItems.map { EntityType($0, homeId: homeId, homeName: homeName) }) + } + + return result + } + + func entities(matching string: String) async throws -> [EntityType] { + let searchResults = await OpenHABItemCache.instance.searchItems( + searchTerm: string, + types: allowedTypes.isEmpty ? nil : allowedTypes + ) + var result: [EntityType] = [] + + // If the user selected a Home in the intent UI, scope results to that home. + if let selectedHomeId { + if let items = searchResults[selectedHomeId] { + let homeName = await getHomeName(for: selectedHomeId) + result.append(contentsOf: items.map { EntityType($0, homeId: selectedHomeId, homeName: homeName) }) + } + return result + } + + // Fallback (e.g. Siri request without an explicit Home selection): return matches across all homes. + for (homeId, items) in searchResults { + let homeName = await getHomeName(for: homeId) + result.append(contentsOf: items.map { EntityType($0, homeId: homeId, homeName: homeName) }) + } + + return result + } +} diff --git a/AppIntents/ItemIdentifier.swift b/AppIntents/ItemIdentifier.swift new file mode 100644 index 000000000..dc3d48105 --- /dev/null +++ b/AppIntents/ItemIdentifier.swift @@ -0,0 +1,33 @@ +// 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 + +struct ItemIdentifier: Hashable, Codable { + let homeId: UUID + let itemName: String +} + +extension ItemIdentifier: EntityIdentifierConvertible { + var entityIdentifierString: String { + "\(homeId.uuidString):\(itemName)" + } + + static func entityIdentifier(for entityIdentifierString: String) -> ItemIdentifier? { + let components = entityIdentifierString.split(separator: ":", maxSplits: 1) + guard components.count == 2, + let homeId = UUID(uuidString: String(components[0])) else { + return nil + } + return ItemIdentifier(homeId: homeId, itemName: String(components[1])) + } +} diff --git a/AppIntents/OpenHABAppShortcutsProvider.swift b/AppIntents/OpenHABAppShortcutsProvider.swift new file mode 100644 index 000000000..3310a7c4a --- /dev/null +++ b/AppIntents/OpenHABAppShortcutsProvider.swift @@ -0,0 +1,142 @@ +// 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 +internal import SFSafeSymbols + +/// App Shortcuts Provider for openHAB app +/// +/// This provider registers app shortcuts with iOS, enabling: +/// - Siri phrase suggestions (e.g., "Toggle Kitchen Light in openHAB") +/// - Shortcuts in Spotlight search +/// - Add to Home Screen functionality +/// - Siri Suggestions on lock screen and search +/// +/// ## Design Note +/// Due to iOS AppShortcuts limitation of one parameter per phrase, +/// shortcuts are designed for specific, common actions: +/// - Toggle switch items (most common switch operation) +/// - Get item state (universal query) +/// - Open/Close roller shutters (specific directional commands) +/// +/// The action is embedded in the phrase itself since we can only +/// pass the item name as a parameter. +/// +/// ## SF Symbol Names +/// Apple's AppShortcut requires literal strings for `systemImageName` - +/// constants and SFSafeSymbols cannot be used due to compile-time metadata extraction. +/// Symbol names use SF Symbols naming convention (e.g., "light.beacon.max"). +/// Reference: https://developer.apple.com/sf-symbols/ +/// +/// ## Note +/// This file was present in PR #742 but was missing from PR #1028. +/// Adding it restores iOS app shortcuts functionality. +/// +/// ## Usage +/// This struct is automatically discovered by iOS at build time. +/// No explicit registration is required - just having this file +/// in the app target is sufficient. +/// +/// ## References +/// - Apple Documentation: https://developer.apple.com/documentation/appintents/app-shortcuts +/// - PR #742: Original implementation +/// - PR #1028: Enhanced intent implementations +@available(iOS 17.0, macOS 13.0, watchOS 9.0, tvOS 16.0, *) +struct OpenHABAppShortcutsProvider: AppShortcutsProvider { + static var appShortcuts: [AppShortcut] { + // MARK: - Switch Control - Toggle Action + + // Note: Since AppShortcuts only allow one parameter per phrase, + // the action (toggle) is embedded in the phrase itself + + AppShortcut( + intent: SetSwitchItemIntent(), + phrases: [ + "Toggle \(\.$itemEntity) in \(.applicationName)", + "Toggle switch \(\.$itemEntity) in \(.applicationName)" + ], + shortTitle: "Toggle Switch", + systemImageName: "light.beacon.max" // SFSymbol: .lightBeaconMax + ) + + // MARK: - Get Item State + + // Query the current state of any item + + AppShortcut( + intent: GetItemStateIntent(), + phrases: [ + "Get \(\.$itemEntity) in \(.applicationName)", + "Check \(\.$itemEntity) in \(.applicationName)", + "What is \(\.$itemEntity) in \(.applicationName)" + ], + shortTitle: "Get Item State", + systemImageName: "info.circle" // SFSymbol: .infoCircle + ) + + // MARK: - Roller Shutter Control - UP Command + + // Separate shortcut for raising/opening roller shutters + + AppShortcut( + intent: SetDimmerRollerValueIntent(), + phrases: [ + "Open \(\.$itemEntity) in \(.applicationName)", + "Raise \(\.$itemEntity) in \(.applicationName)", + "Roll up \(\.$itemEntity) in \(.applicationName)" + ], + shortTitle: "Open Shutter", + systemImageName: "arrow.up.square" // SFSymbol: .arrowUpSquare + ) + + // MARK: - Roller Shutter Control - DOWN Command + + // Separate shortcut for lowering/closing roller shutters + + AppShortcut( + intent: SetDimmerRollerValueIntent(), + phrases: [ + "Close \(\.$itemEntity) in \(.applicationName)", + "Lower \(\.$itemEntity) in \(.applicationName)", + "Roll down \(\.$itemEntity) in \(.applicationName)" + ], + shortTitle: "Close Shutter", + systemImageName: "arrow.down.square" // SFSymbol: .arrowDownSquare + ) + } + + /// The color used for shortcut tiles in the Shortcuts app and Siri interface + /// Orange matches the openHAB brand color + static let shortcutTileColor: ShortcutTileColor = .orange +} + +//// MARK: - AppShortcut + SFSymbol Extension +// +// @available(iOS 16.0, macOS 13.0, watchOS 9.0, tvOS 16.0, *) +// extension AppShortcut { +// /// Creates an app shortcut with a type-safe SF Symbol. +// /// +// /// - Parameters: +// /// - intent: The intent to run when the shortcut is invoked. +// /// - phrases: The phrases that trigger this shortcut. +// /// - shortTitle: A short title displayed in the Shortcuts app. +// /// - systemImage: The SF Symbol to display for this shortcut. +// /// +// init(intent: Intent, phrases: [AppShortcutPhrase], shortTitle: LocalizedStringResource, systemImage: SFSymbol) where Intent : AppIntent { +// +// self.init( +// intent: intent, +// phrases: phrases, +// shortTitle: shortTitle, +// systemImageName: systemImage.rawValue +// ) +// } +// } diff --git a/AppIntents/SwitchAction.swift b/AppIntents/SwitchAction.swift new file mode 100644 index 000000000..92e41de85 --- /dev/null +++ b/AppIntents/SwitchAction.swift @@ -0,0 +1,28 @@ +// 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 SwitchAction: String, AppEnum { + case on = "ON" + case off = "OFF" + case toggle = "TOGGLE" + + static let typeDisplayRepresentation = TypeDisplayRepresentation(name: "Switch Action") + + static let caseDisplayRepresentations: [Self: DisplayRepresentation] = [ + .on: "On", + .off: "Off", + .toggle: "Toggle" + ] +} diff --git a/AppIntents/SwitchItemEntity.swift b/AppIntents/SwitchItemEntity.swift new file mode 100644 index 000000000..0374f40a5 --- /dev/null +++ b/AppIntents/SwitchItemEntity.swift @@ -0,0 +1,209 @@ +// 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 + +// MARK: - DimmerItemEntity + +@available(iOS 17.0, macOS 14.0, watchOS 10.0, *) +struct DimmerItemEntity: ItemEntity { + struct DimmerItemQuery: ItemEntityQuery { + typealias EntityType = DimmerItemEntity + + @IntentParameterDependency(\.$home) + var intent + + var allowedTypes: [OpenHABItem.ItemType] = [.dimmer, .rollershutter] + var selectedHomeId: UUID? { UUID(uuidString: intent?.home.id ?? "") } + } + + static let typeDisplayRepresentation = TypeDisplayRepresentation(name: "Dimmer/Roller Item") + static let defaultQuery = DimmerItemQuery() + + var id: ItemIdentifier + var item: OpenHABItem + var homeName: String? + + init(id: ItemIdentifier, item: OpenHABItem, homeName: String? = nil) { + self.id = id + self.item = item + self.homeName = homeName + } +} + +// MARK: - ColorItemEntity + +@available(iOS 17.0, macOS 14.0, watchOS 10.0, *) +struct ColorItemEntity: ItemEntity { + struct ColorItemQuery: ItemEntityQuery { + typealias EntityType = ColorItemEntity + + @IntentParameterDependency(\.$home) + var intent + + var allowedTypes: [OpenHABItem.ItemType] = [.color] + var selectedHomeId: UUID? { UUID(uuidString: intent?.home.id ?? "") } + } + + static let typeDisplayRepresentation = TypeDisplayRepresentation(name: "Color Item") + static let defaultQuery = ColorItemQuery() + + var id: ItemIdentifier + var item: OpenHABItem + var homeName: String? + + init(id: ItemIdentifier, item: OpenHABItem, homeName: String? = nil) { + self.id = id + self.item = item + self.homeName = homeName + } +} + +// MARK: - NumberItemEntity + +@available(iOS 17.0, macOS 14.0, watchOS 10.0, *) +struct NumberItemEntity: ItemEntity { + struct NumberItemQuery: ItemEntityQuery { + typealias EntityType = NumberItemEntity + + @IntentParameterDependency(\.$home) + var intent + + var allowedTypes: [OpenHABItem.ItemType] = [.number, .numberWithDimension] + var selectedHomeId: UUID? { UUID(uuidString: intent?.home.id ?? "") } + } + + static let typeDisplayRepresentation = TypeDisplayRepresentation(name: "Number Item") + static let defaultQuery = NumberItemQuery() + + var id: ItemIdentifier + var item: OpenHABItem + var homeName: String? + + init(id: ItemIdentifier, item: OpenHABItem, homeName: String? = nil) { + self.id = id + self.item = item + self.homeName = homeName + } +} + +// MARK: - StringItemEntity + +@available(iOS 17.0, macOS 14.0, watchOS 10.0, *) +struct StringItemEntity: ItemEntity { + struct StringItemQuery: ItemEntityQuery { + typealias EntityType = StringItemEntity + + @IntentParameterDependency(\.$home) + var intent + + var allowedTypes: [OpenHABItem.ItemType] = [.stringItem] + var selectedHomeId: UUID? { UUID(uuidString: intent?.home.id ?? "") } + } + + static let typeDisplayRepresentation = TypeDisplayRepresentation(name: "String Item") + static let defaultQuery = StringItemQuery() + + var id: ItemIdentifier + var item: OpenHABItem + var homeName: String? + + init(id: ItemIdentifier, item: OpenHABItem, homeName: String? = nil) { + self.id = id + self.item = item + self.homeName = homeName + } +} + +// MARK: - ContactItemEntity + +@available(iOS 17.0, macOS 14.0, watchOS 10.0, *) +struct ContactItemEntity: ItemEntity { + struct ContactItemQuery: ItemEntityQuery { + typealias EntityType = ContactItemEntity + + @IntentParameterDependency(\.$home) + var intent + + var allowedTypes: [OpenHABItem.ItemType] = [.contact] + var selectedHomeId: UUID? { UUID(uuidString: intent?.home.id ?? "") } + } + + static let typeDisplayRepresentation = TypeDisplayRepresentation(name: "Contact Item") + static let defaultQuery = ContactItemQuery() + + var id: ItemIdentifier + var item: OpenHABItem + var homeName: String? + + init(id: ItemIdentifier, item: OpenHABItem, homeName: String? = nil) { + self.id = id + self.item = item + self.homeName = homeName + } +} + +// MARK: - GenericItemEntity (for ItemStateIntent - all types) + +@available(iOS 17.0, macOS 14.0, watchOS 10.0, *) +struct GenericItemEntity: ItemEntity { + struct GenericItemQuery: ItemEntityQuery { + typealias EntityType = GenericItemEntity + + @IntentParameterDependency(\.$home) + var intent + + var allowedTypes: [OpenHABItem.ItemType] = [] // Empty means all types + var selectedHomeId: UUID? { UUID(uuidString: intent?.home.id ?? "") } + } + + static let typeDisplayRepresentation = TypeDisplayRepresentation(name: "Item") + static let defaultQuery = GenericItemQuery() + + var id: ItemIdentifier + var item: OpenHABItem + var homeName: String? + + init(id: ItemIdentifier, item: OpenHABItem, homeName: String? = nil) { + self.id = id + self.item = item + self.homeName = homeName + } +} + +// MARK: - SwitchItemEntity + +@available(iOS 17.0, macOS 14.0, watchOS 10.0, *) +struct SwitchItemEntity: ItemEntity { + struct SwitchItemQuery: ItemEntityQuery { + typealias EntityType = SwitchItemEntity + + @IntentParameterDependency(\.$home) + var intent + + var allowedTypes: [OpenHABItem.ItemType] = [.switchItem] + var selectedHomeId: UUID? { UUID(uuidString: intent?.home.id ?? "") } + } + + static let typeDisplayRepresentation = TypeDisplayRepresentation(name: "Switch Item") + static let defaultQuery = SwitchItemQuery() + + var id: ItemIdentifier + var item: OpenHABItem + var homeName: String? + + init(id: ItemIdentifier, item: OpenHABItem, homeName: String? = nil) { + self.id = id + self.item = item + self.homeName = homeName + } +} diff --git a/AppIntents/iOS16/ActionMapper.swift b/AppIntents/iOS16/ActionMapper.swift new file mode 100644 index 000000000..719c423cc --- /dev/null +++ b/AppIntents/iOS16/ActionMapper.swift @@ -0,0 +1,48 @@ +// 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 Foundation + +enum ActionMapper { + /// Provides the standard on/off action options for dynamic option providers + static var onOffOptions: [String] { + [ + String(localized: "on").capitalized, + String(localized: "off").capitalized + ] + } + + /// Provides on/off/toggle action options for dynamic option providers + static var onOffToggleOptions: [String] { + [ + String(localized: "on").capitalized, + String(localized: "off").capitalized, + String(localized: "toggle").capitalized + ] + } + + /// Maps a localized action label (e.g., "On", "Off", "Toggle") to an openHAB command (e.g., "ON", "OFF", "TOGGLE") + /// - Parameter localizedAction: The localized action string from user input + /// - Returns: The corresponding openHAB command, or nil if the action is not recognized + static func command(from localizedAction: String) -> String? { + let onLabel = String(localized: "on").capitalized + let offLabel = String(localized: "off").capitalized + let toggleLabel = String(localized: "toggle").capitalized + + let actionMap: [String: String] = [ + onLabel: "ON", + offLabel: "OFF", + toggleLabel: "TOGGLE" + ] + + return actionMap[localizedAction] + } +} diff --git a/AppIntents/iOS16/GetItemState.swift b/AppIntents/iOS16/GetItemState.swift new file mode 100644 index 000000000..6af74c111 --- /dev/null +++ b/AppIntents/iOS16/GetItemState.swift @@ -0,0 +1,101 @@ +// 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 + +enum GetItemStateError: Error, CustomLocalizedStringResourceConvertible { + case invalidHomeIdentifier + case unknownHome + case itemNotFound(String) + + var localizedStringResource: LocalizedStringResource { + switch self { + case .invalidHomeIdentifier: + "Invalid home identifier" + case .unknownHome: + "Unknown home" + case let .itemNotFound(itemName): + "Item '\(itemName)' not found" + } + } +} + +@available(iOS, introduced: 16.0, obsoleted: 17.0, message: "Use GetItemStateIntent for iOS 17+") +struct GetItemState: AppIntent, CustomIntentMigratedAppIntent, PredictableIntent { + struct StringOptionsProvider: DynamicOptionsProvider { + func results() async throws -> [String] { + let allItems = await OpenHABItemCache.instance.getAllCachedItems() + return allItems.flatMap { $0.value.map(\.name) } + } + } + + static let intentClassName = "OpenHABGetItemStateIntent" + + static let title: LocalizedStringResource = "Get Item State" + static let description = IntentDescription("Retrieve the current state of an item") + + // swiftlint:disable type_contents_order + @Parameter(title: "Item", optionsProvider: StringOptionsProvider()) + var item: String + + @Parameter(title: "Home") + var home: Home + + static var parameterSummary: some ParameterSummary { + Summary("Get \(\.$item) State") { + \.$home + } + } + + // swiftlint:enable type_contents_order + + static var predictionConfiguration: some IntentPredictionConfiguration { + IntentPrediction(parameters: (\.$item, \.$home)) { item, _ in + DisplayRepresentation( + title: "Get \(item) State", + subtitle: "" + ) + } + } + + func perform() async throws -> some IntentResult & ReturnsValue { + guard let homeId = UUID(uuidString: home.id) else { + throw GetItemStateError.invalidHomeIdentifier + } + + let homeExists = await MainActor.run { + Preferences.shared.storedHomes[homeId] != nil + } + + guard homeExists else { + throw GetItemStateError.unknownHome + } + + guard let openHABItem = await OpenHABItemCache.instance.getItemUncached(name: item, home: homeId) else { + throw GetItemStateError.itemNotFound(item) + } + + let state = openHABItem.state ?? "Unknown state" + + return .result( + value: state, + dialog: .responseSuccess(item: item, state: state) + ) + } +} + +private extension IntentDialog { + static func responseSuccess(item: String, state: String) -> Self { + "The state of \(item) is \(state)" + } +} diff --git a/AppIntents/iOS16/SetColorValue.swift b/AppIntents/iOS16/SetColorValue.swift new file mode 100644 index 000000000..8339a62fb --- /dev/null +++ b/AppIntents/iOS16/SetColorValue.swift @@ -0,0 +1,155 @@ +// 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 + +enum SetColorValueError: Error, CustomLocalizedStringResourceConvertible { + case invalidHomeIdentifier + case unknownHome + case itemNotFound(String) + case invalidValue(String, String) + case commandFailed(String) + + var localizedStringResource: LocalizedStringResource { + switch self { + case .invalidHomeIdentifier: + "Invalid home identifier" + case .unknownHome: + "Unknown home" + case let .itemNotFound(itemName): + "Item '\(itemName)' not found" + case let .invalidValue(value, itemName): + "Invalid value: \(value) for \(itemName) must be HSB (0-360,0-100,0-100)" + case let .commandFailed(message): + "Command failed: \(message)" + } + } +} + +@available(iOS, introduced: 16.0, obsoleted: 17.0, message: "Use SetColorValueIntent for iOS 17+") +struct SetColorValue: AppIntent, CustomIntentMigratedAppIntent, PredictableIntent { + struct StringOptionsProvider: DynamicOptionsProvider { + func results() async throws -> [String] { + let allItems = await OpenHABItemCache.instance.getAllCachedItems() + let items = allItems.flatMap(\.value).filter { $0.type == .color } + return items.map(\.name) + } + } + + static let intentClassName = "OpenHABSetColorValueIntent" + + static let title: LocalizedStringResource = "Set Color Control Value" + static let description = IntentDescription("Set the color of a color control item") + + // swiftlint:disable type_contents_order + @Parameter(title: "Item", optionsProvider: StringOptionsProvider()) + var item: String + + @Parameter(title: "Value", default: "240,100,100") + var value: String + + @Parameter(title: "Home") + var home: Home + + static var parameterSummary: some ParameterSummary { + Summary("Set \(\.$item) to \(\.$value) (HSB)") { + \.$home + } + } + + // swiftlint:enable type_contents_order + + static var predictionConfiguration: some IntentPredictionConfiguration { + IntentPrediction(parameters: (\.$item, \.$value, \.$home)) { item, value, _ in + DisplayRepresentation( + title: "Set \(item) to \(value) (HSB)", + subtitle: "" + ) + } + } + + func perform() async throws -> some IntentResult & ProvidesDialog { + var colorValue = value + + guard let homeId = UUID(uuidString: home.id) else { + throw SetColorValueError.invalidHomeIdentifier + } + + let homeExists = await MainActor.run { + Preferences.shared.storedHomes[homeId] != nil + } + + guard homeExists else { + throw SetColorValueError.unknownHome + } + + let hsb = colorValue.split(separator: ",") + guard hsb.count == 3, + let hue = Int(hsb[0]), (0 ... 360).contains(hue), + let sat = Int(hsb[1]), (0 ... 100).contains(sat), + let val = Int(hsb[2]), (0 ... 100).contains(val) else { + throw SetColorValueError.invalidValue(colorValue, item) + } + + colorValue = "\(hue),\(sat),\(val)" + + guard let items = await OpenHABItemCache.instance.getCachedItem(name: item, home: homeId), + !items.isEmpty else { + throw SetColorValueError.itemNotFound(item) + } + + let openHABItem = items[0] + + do { + try await OpenHABItemCache.instance.sendCommand(to: openHABItem, home: homeId, command: colorValue) + } catch { + throw SetColorValueError.commandFailed(error.localizedDescription) + } + + return .result(dialog: .responseSuccess(value: colorValue, item: item)) + } +} + +private extension IntentDialog { + static var itemParameterConfiguration: Self { + "Color Item Name" + } + + static var homeParameterConfiguration: Self { + "Home name" + } + + static var homeParameterDisambiguationSelection: Self { + "For which home do you want to get the value?" + } + + static func homeParameterDisambiguationIntro(count: Int, item: String) -> Self { + "There are \(count) configured homes with an item named '\(item)'." + } + + static func homeParameterConfirmation(home: Home) -> Self { + "Just to confirm, you wanted '\(home)'?" + } + + static func responseSuccess(value: String, item: String) -> Self { + "Sent the color value of \(value) to \(item)" + } + + static func responseFailureInvalidItem(item: String) -> Self { + "Sorry can't find \(item)" + } + + static func responseFailureInvalidValue(value: String, item: String) -> Self { + "Invalid value: \(value) for \(item) must be HSB (0-360,0-100,0-100)" + } +} diff --git a/AppIntents/iOS16/SetContactStateValue.swift b/AppIntents/iOS16/SetContactStateValue.swift new file mode 100644 index 000000000..00d92a868 --- /dev/null +++ b/AppIntents/iOS16/SetContactStateValue.swift @@ -0,0 +1,157 @@ +// 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 + +enum SetContactStateValueError: Error, CustomLocalizedStringResourceConvertible { + case invalidHomeIdentifier + case unknownHome + case itemNotFound(String) + case invalidState(String, String) + case commandFailed(String) + + var localizedStringResource: LocalizedStringResource { + switch self { + case .invalidHomeIdentifier: + "Invalid home identifier" + case .unknownHome: + "Unknown home" + case let .itemNotFound(itemName): + "Item '\(itemName)' not found" + case let .invalidState(state, itemName): + "State invalid: \(state) for \(itemName)" + case let .commandFailed(message): + "Command failed: \(message)" + } + } +} + +@available(iOS, introduced: 16.0, obsoleted: 17.0, message: "Use ContactStateIntent for iOS 17+") +struct SetContactStateValue: AppIntent, CustomIntentMigratedAppIntent, PredictableIntent { + struct ItemOptionsProvider: DynamicOptionsProvider { + func results() async throws -> [String] { + let allItems = await OpenHABItemCache.instance.getAllCachedItems() + let items = allItems.flatMap(\.value).filter { $0.type == .contact } + return items.map(\.name) + } + } + + struct StateOptionsProvider: DynamicOptionsProvider { + func results() async throws -> [String] { + ActionMapper.onOffOptions + } + } + + static let intentClassName = "OpenHABSetContactStateValueIntent" + + static let title: LocalizedStringResource = "Set Contact State Value" + static let description = IntentDescription("Set the state of a contact open or closed") + + // swiftlint:disable type_contents_order + @Parameter(title: "Item", optionsProvider: ItemOptionsProvider()) + var item: String + + @Parameter(title: "State", optionsProvider: StateOptionsProvider()) + var state: String + + @Parameter(title: "Home") + var home: Home + + static var parameterSummary: some ParameterSummary { + Summary("Set the state of \(\.$item) to \(\.$state)") { + \.$home + } + } + + // swiftlint:enable type_contents_order + + static var predictionConfiguration: some IntentPredictionConfiguration { + IntentPrediction(parameters: (\.$item, \.$state, \.$home)) { item, state, _ in + DisplayRepresentation( + title: "Set the state of \(item) to \(state)", + subtitle: "" + ) + } + } + + func perform() async throws -> some IntentResult & ProvidesDialog { + guard let homeId = UUID(uuidString: home.id) else { + throw SetContactStateValueError.invalidHomeIdentifier + } + + let homeExists = await MainActor.run { + Preferences.shared.storedHomes[homeId] != nil + } + + guard homeExists else { + throw SetContactStateValueError.unknownHome + } + + guard let realState = ActionMapper.command(from: state) else { + throw SetContactStateValueError.invalidState(state, item) + } + + guard let items = await OpenHABItemCache.instance.getCachedItem(name: item, home: homeId), + !items.isEmpty else { + throw SetContactStateValueError.itemNotFound(item) + } + + let openHABItem = items[0] + + do { + try await OpenHABItemCache.instance.sendCommand(to: openHABItem, home: homeId, command: realState) + } catch { + throw SetContactStateValueError.commandFailed(error.localizedDescription) + } + + return .result(dialog: .responseSuccess(item: item, state: state)) + } +} + +private extension IntentDialog { + static var itemParameterConfiguration: Self { + "Switch name" + } + + static var stateParameterConfiguration: Self { + "Action" + } + + static var homeParameterConfiguration: Self { + "Home name" + } + + static var homeParameterDisambiguationSelection: Self { + "For which home do you want to get the value?" + } + + static func homeParameterDisambiguationIntro(count: Int, item: String) -> Self { + "There are \(count) configured homes with an item named '\(item)'." + } + + static func homeParameterConfirmation(home: Home) -> Self { + "Just to confirm, you wanted '\(home)'?" + } + + static func responseSuccess(item: String, state: String) -> Self { + "The state of \(item) was set to \(state)" + } + + static func responseFailureInvalidItem(item: String) -> Self { + "Sorry can't find \(item)" + } + + static func responseFailureInvalidAction(state: String, item: String) -> Self { + "State invalid: \(state) for \(item)" + } +} diff --git a/AppIntents/iOS16/SetDimmerRollerValue.swift b/AppIntents/iOS16/SetDimmerRollerValue.swift new file mode 100644 index 000000000..c0ac4e5ce --- /dev/null +++ b/AppIntents/iOS16/SetDimmerRollerValue.swift @@ -0,0 +1,151 @@ +// 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 + +enum SetDimmerRollerValueError: Error, CustomLocalizedStringResourceConvertible { + case invalidHomeIdentifier + case unknownHome + case itemNotFound(String) + case invalidValue(Int, String) + case commandFailed(String) + + var localizedStringResource: LocalizedStringResource { + switch self { + case .invalidHomeIdentifier: + "Invalid Home identifier" + case .unknownHome: + "Unknown Home" + case let .itemNotFound(itemName): + "Item '\(itemName)' not found" + case let .invalidValue(value, itemName): + "Invalid value \(value) for \(itemName) (0-100)" + case let .commandFailed(message): + "Command failed: \(message)" + } + } +} + +@available(iOS, introduced: 16.0, obsoleted: 17.0, message: "Use SetDimmerRollerValueIntent for iOS 17+") +struct SetDimmerRollerValue: AppIntent, CustomIntentMigratedAppIntent, PredictableIntent { + struct StringOptionsProvider: DynamicOptionsProvider { + func results() async throws -> [String] { + let allItems = await OpenHABItemCache.instance.getAllCachedItems() + let items = allItems.flatMap(\.value).filter { $0.type == .dimmer || $0.type == .rollershutter } + return items.map(\.name) + } + } + + static let intentClassName = "OpenHABSetDimmerRollerValueIntent" + + static let title: LocalizedStringResource = "Set Dimmer or Roller Shutter Value" + static let description = IntentDescription("Set the integer value of a dimmer or roller shutter") + + // swiftlint:disable type_contents_order + @Parameter(title: "Item", optionsProvider: StringOptionsProvider()) + var item: String + + @Parameter(title: "Value") + var value: Int + + @Parameter(title: "Home") + var home: Home + + static var parameterSummary: some ParameterSummary { + Summary("Set \(\.$item) to \(\.$value)") { + \.$home + } + } + + // swiftlint:enable type_contents_order + + static var predictionConfiguration: some IntentPredictionConfiguration { + IntentPrediction(parameters: (\.$item, \.$value, \.$home)) { item, value, _ in + DisplayRepresentation( + title: "Set \(item) to \(value)", + subtitle: "" + ) + } + } + + func perform() async throws -> some IntentResult & ProvidesDialog { + guard let homeId = UUID(uuidString: home.id) else { + throw SetDimmerRollerValueError.invalidHomeIdentifier + } + + let homeExists = await MainActor.run { + Preferences.shared.storedHomes[homeId] != nil + } + + guard homeExists else { + throw SetDimmerRollerValueError.unknownHome + } + + guard (0 ... 100).contains(value) else { + throw SetDimmerRollerValueError.invalidValue(value, item) + } + + guard let items = await OpenHABItemCache.instance.getCachedItem(name: item, home: homeId), + !items.isEmpty else { + throw SetDimmerRollerValueError.itemNotFound(item) + } + + let openHABItem = items[0] + + do { + try await OpenHABItemCache.instance.sendCommand(to: openHABItem, home: homeId, command: "\(value)") + } catch { + throw SetDimmerRollerValueError.commandFailed(error.localizedDescription) + } + + return .result(dialog: .responseSuccess(value: value, item: item)) + } +} + +private extension IntentDialog { + static var itemParameterConfiguration: Self { + "Dimmer/Roller Name" + } + + static var homeParameterConfiguration: Self { + "Home name" + } + + static var homeParameterDisambiguationSelection: Self { + "For which home do you want to get the value?" + } + + static func homeParameterDisambiguationIntro(count: Int, item: String) -> Self { + "There are \(count) configured homes with an item named '\(item)'." + } + + static func homeParameterConfirmation(home: Home) -> Self { + "Just to confirm, you wanted '\(home)'?" + } + + static func responseSuccess(value: Int, item: String) -> Self { + "Sent the value of \(value) to \(item)" + } + + static func responseFailureInvalidItem(item: String) -> Self { + "Sorry can't find \(item)" + } + + static func responseFailureEmptyValue(item: String) -> Self { + "Invalid empty value for \(item)" + } + + static func responseFailureInvalidValue(value: Int, item: String) -> Self { + "Invalid value \(value) for \(item) (0-100)" + } +} diff --git a/AppIntents/iOS16/SetNumberValue.swift b/AppIntents/iOS16/SetNumberValue.swift new file mode 100644 index 000000000..7c299dc83 --- /dev/null +++ b/AppIntents/iOS16/SetNumberValue.swift @@ -0,0 +1,140 @@ +// 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 + +enum SetNumberValueError: Error, CustomLocalizedStringResourceConvertible { + case invalidHomeIdentifier + case unknownHome + case itemNotFound(String) + case commandFailed(String) + + var localizedStringResource: LocalizedStringResource { + switch self { + case .invalidHomeIdentifier: + "Invalid home identifier" + case .unknownHome: + "Unknown home" + case let .itemNotFound(itemName): + "Item '\(itemName)' not found" + case let .commandFailed(message): + "Command failed: \(message)" + } + } +} + +@available(iOS, introduced: 16.0, obsoleted: 17.0, message: "Use SetNumberValueIntent for iOS 17+") +struct SetNumberValue: AppIntent, CustomIntentMigratedAppIntent, PredictableIntent { + struct StringOptionsProvider: DynamicOptionsProvider { + func results() async throws -> [String] { + let allItems = await OpenHABItemCache.instance.getAllCachedItems() + let items = allItems.flatMap(\.value).filter { $0.type == .number } + return items.map(\.name) + } + } + + static let intentClassName = "OpenHABSetNumberValueIntent" + + static let title: LocalizedStringResource = "Set Number Control Value" + static let description = IntentDescription("Set the decimal value of a number control item") + + // swiftlint:disable type_contents_order + @Parameter(title: "Item", optionsProvider: StringOptionsProvider()) + var item: String + + @Parameter(title: "Value") + var value: Double + + @Parameter(title: "Home") + var home: Home + + static var parameterSummary: some ParameterSummary { + Summary("Set \(\.$item) to \(\.$value)") { + \.$home + } + } + + // swiftlint:enable type_contents_order + + static var predictionConfiguration: some IntentPredictionConfiguration { + IntentPrediction(parameters: (\.$item, \.$value, \.$home)) { item, value, _ in + DisplayRepresentation( + title: "Set \(item) to \(value)", + subtitle: "" + ) + } + } + + func perform() async throws -> some IntentResult & ProvidesDialog { + guard let homeId = UUID(uuidString: home.id) else { + throw SetNumberValueError.invalidHomeIdentifier + } + + let homeExists = await MainActor.run { + Preferences.shared.storedHomes[homeId] != nil + } + + guard homeExists else { + throw SetNumberValueError.unknownHome + } + + guard let items = await OpenHABItemCache.instance.getCachedItem(name: item, home: homeId), + !items.isEmpty else { + throw SetNumberValueError.itemNotFound(item) + } + + let openHABItem = items[0] + + do { + try await OpenHABItemCache.instance.sendCommand(to: openHABItem, home: homeId, command: String(value)) + } catch { + throw SetNumberValueError.commandFailed(error.localizedDescription) + } + + return .result(dialog: .responseSuccess(value: value, item: item)) + } +} + +private extension IntentDialog { + static var itemParameterConfiguration: Self { + "Number Item Name" + } + + static var homeParameterConfiguration: Self { + "Home name" + } + + static var homeParameterDisambiguationSelection: Self { + "For which home do you want to get the value?" + } + + static func homeParameterDisambiguationIntro(count: Int, item: String) -> Self { + "There are \(count) configured homes with an item named '\(item)'." + } + + static func homeParameterConfirmation(home: Home) -> Self { + "Just to confirm, you wanted '\(home)'?" + } + + static func responseSuccess(value: Double, item: String) -> Self { + "Sent the number \(value) to \(item)" + } + + static func responseFailureInvalidItem(item: String) -> Self { + "Sorry can't find \(item)" + } + + static func responseFailureEmptyValue(item: String) -> Self { + "Invalid empty value for \(item)" + } +} diff --git a/AppIntents/iOS16/SetStringValue.swift b/AppIntents/iOS16/SetStringValue.swift new file mode 100644 index 000000000..dd7c83d41 --- /dev/null +++ b/AppIntents/iOS16/SetStringValue.swift @@ -0,0 +1,140 @@ +// 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 + +enum SetStringValueError: Error, CustomLocalizedStringResourceConvertible { + case invalidHomeIdentifier + case unknownHome + case itemNotFound(String) + case commandFailed(String) + + var localizedStringResource: LocalizedStringResource { + switch self { + case .invalidHomeIdentifier: + "Invalid home identifier" + case .unknownHome: + "Unknown home" + case let .itemNotFound(itemName): + "Item '\(itemName)' not found" + case let .commandFailed(message): + "Command failed: \(message)" + } + } +} + +@available(iOS, introduced: 16.0, obsoleted: 17.0, message: "Use SetStringValueIntent for iOS 17+") +struct SetStringValue: AppIntent, CustomIntentMigratedAppIntent, PredictableIntent { + struct StringOptionsProvider: DynamicOptionsProvider { + func results() async throws -> [String] { + let allItems = await OpenHABItemCache.instance.getAllCachedItems() + let items = allItems.flatMap(\.value).filter { $0.type == .stringItem } + return items.map(\.name) + } + } + + static let intentClassName = "OpenHABSetStringValueIntent" + + static let title: LocalizedStringResource = "Set String Control Value" + static let description = IntentDescription("Set the string of a string control item") + + // swiftlint:disable type_contents_order + @Parameter(title: "Item", optionsProvider: StringOptionsProvider()) + var item: String + + @Parameter(title: "Value") + var value: String + + @Parameter(title: "Home") + var home: Home + + static var parameterSummary: some ParameterSummary { + Summary("Set \(\.$item) to \(\.$value)") { + \.$home + } + } + + // swiftlint:enable type_contents_order + + static var predictionConfiguration: some IntentPredictionConfiguration { + IntentPrediction(parameters: (\.$item, \.$value, \.$home)) { item, value, _ in + DisplayRepresentation( + title: "Set \(item) to \(value)", + subtitle: "" + ) + } + } + + func perform() async throws -> some IntentResult & ProvidesDialog { + guard let homeId = UUID(uuidString: home.id) else { + throw SetStringValueError.invalidHomeIdentifier + } + + let homeExists = await MainActor.run { + Preferences.shared.storedHomes[homeId] != nil + } + + guard homeExists else { + throw SetStringValueError.unknownHome + } + + guard let items = await OpenHABItemCache.instance.getCachedItem(name: item, home: homeId), + !items.isEmpty else { + throw SetStringValueError.itemNotFound(item) + } + + let openHABItem = items[0] + + do { + try await OpenHABItemCache.instance.sendCommand(to: openHABItem, home: homeId, command: value) + } catch { + throw SetStringValueError.commandFailed(error.localizedDescription) + } + + return .result(dialog: .responseSuccess(value: value, item: item)) + } +} + +private extension IntentDialog { + static var itemParameterConfiguration: Self { + "String Item Name" + } + + static var homeParameterConfiguration: Self { + "Home name" + } + + static var homeParameterDisambiguationSelection: Self { + "For which home do you want to get the value?" + } + + static func homeParameterDisambiguationIntro(count: Int, item: String) -> Self { + "There are \(count) configured homes with an item named '\(item)'." + } + + static func homeParameterConfirmation(home: Home) -> Self { + "Just to confirm, you wanted '\(home)'?" + } + + static func responseSuccess(value: String, item: String) -> Self { + "Sent the string \(value) to \(item)" + } + + static func responseFailureInvalidItem(item: String) -> Self { + "Sorry can't find \(item)" + } + + static func responseFailureEmptyValue(item: String) -> Self { + "Invalid empty value for \(item)" + } +} diff --git a/AppIntents/iOS16/SetSwitchState.swift b/AppIntents/iOS16/SetSwitchState.swift new file mode 100644 index 000000000..12c873846 --- /dev/null +++ b/AppIntents/iOS16/SetSwitchState.swift @@ -0,0 +1,121 @@ +// 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 + +@available(iOS, introduced: 16.0, obsoleted: 17.0) +enum SetSwitchStateError: Error, CustomLocalizedStringResourceConvertible { + case invalidHomeIdentifier + case itemNotFound(String) + case invalidAction(String, String) + case itemNotInHome(String, String) + case commandFailed(String) + + var localizedStringResource: LocalizedStringResource { + switch self { + case .invalidHomeIdentifier: + "Invalid home identifier" + case let .itemNotFound(itemName): + "Item '\(itemName)' not found" + case let .invalidAction(action, itemName): + "Action invalid: \(action) for \(itemName)" + case let .itemNotInHome(itemName, homeName): + "Item '\(itemName)' is not in home '\(homeName)'" + case let .commandFailed(message): + "Command failed: \(message)" + } + } +} + +// @available(iOS, introduced: 16.0, obsoleted: 17.0, message: "Use SwitchStateIntent for iOS 17+") +struct SetSwitchState: AppIntent, CustomIntentMigratedAppIntent, PredictableIntent { + // swiftlint:disable type_contents_order + + static let intentClassName = "OpenHABSetSwitchStateIntent" + static let title: LocalizedStringResource = "Set Switch State" + static let description = IntentDescription("Set the state of a switch on or off") + + @Parameter(title: "Home") + var home: Home + + struct ItemOptionsProvider: DynamicOptionsProvider { + func results() async throws -> [String] { + let allItems = await OpenHABItemCache.instance.getAllCachedItems() + let items = allItems.flatMap(\.value).filter { $0.type == .switchItem } + return items.map(\.name) + } + } + + @Parameter(title: "Item", optionsProvider: ItemOptionsProvider()) + var item: String + + struct ActionOptionsProvider: DynamicOptionsProvider { + func results() async throws -> [String] { + ActionMapper.onOffOptions + } + } + + @Parameter(title: "Action", optionsProvider: ActionOptionsProvider()) + var action: String + + static var parameterSummary: some ParameterSummary { + Summary("Send \(\.$action) to \(\.$item)") { + \.$home + } + } + + // swiftlint:enable type_contents_order + + static var predictionConfiguration: some IntentPredictionConfiguration { + IntentPrediction(parameters: (\.$item, \.$action, \.$home)) { item, action, _ in + DisplayRepresentation( + title: "Send \(action) to \(item)", + subtitle: "" + ) + } + } + + func perform() async throws -> some IntentResult & ProvidesDialog { + guard let homeId = UUID(uuidString: home.id) else { + throw SetSwitchStateError.invalidHomeIdentifier + } + + guard let command = ActionMapper.command(from: action) else { + throw SetSwitchStateError.invalidAction(action, item) + } + guard let items = await OpenHABItemCache.instance.getCachedItem(name: item, home: homeId), + !items.isEmpty else { + throw SetSwitchStateError.itemNotFound(item) + } + + let openHABItem = items[0] + + do { + try await OpenHABItemCache.instance.sendCommand(to: openHABItem, home: homeId, command: command) + } catch { + throw SetSwitchStateError.commandFailed(error.localizedDescription) + } + + return .result(dialog: .responseSuccess(action: action, item: item)) + } +} + +private extension IntentDialog { + static func responseSuccess(action: String, item: String) -> Self { + "Sent the action of \(action) to switch \(item)" + } + + static func responseFailureInvalidAction(action: String, item: String) -> Self { + "Action invalid: \(action) for \(item)" + } +} diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 000000000..009cc8ca1 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,91 @@ +# openHAB iOS Development + +This document provides guidance for working with the openHAB iOS project. + +## Build Command + +The standard build command for this project: + +```bash +xcodebuild -workspace openHAB.xcworkspace -scheme openHAB -configuration Debug clean build -destination 'generic/platform=iOS' +``` + +Use this command when building the app for testing or distribution. + +### Build Variants + +- **Debug build** (standard): + ```bash + xcodebuild -workspace openHAB.xcworkspace -scheme openHAB -configuration Debug clean build -destination 'generic/platform=iOS' + ``` + +- **Release build**: + ```bash + xcodebuild -workspace openHAB.xcworkspace -scheme openHAB -configuration Release clean build -destination 'generic/platform=iOS' + ``` + +- **For simulator**: + ```bash + xcodebuild -workspace openHAB.xcworkspace -scheme openHAB -configuration Debug clean build -destination 'generic/platform=iOS Simulator' + ``` + +## Project Structure + +This is an iOS application with the following components: + +### Main Targets + +- **openHAB**: Main iOS application +- **openHABWatch**: Apple Watch companion app +- **openHABWidgetExtension**: iOS widgets for home screen +- **NotificationService**: Notification service extension + +### Test Targets + +- **openHABTestsSwift**: Swift unit tests +- **openHABUITests**: UI tests +- **openHABWatchSwiftUI Watch AppTests**: Watch app tests +- **openHABWatchSwiftUI Watch AppUITests**: Watch app UI tests + +## Development Notes + +### Workspace vs Project + +- Always use `openHAB.xcworkspace` (not `openHAB.xcodeproj`) +- The workspace includes Swift Package Manager dependencies + +### Current Branch + +- Main development branch: `develop` +- Feature branches follow the pattern: `feature/` + +### Code Quality + +The project uses: +- SwiftLint for code linting +- SwiftFormat for code formatting + +These run automatically during build via build phases. + +## Common Tasks + +### Building the project +```bash +xcodebuild -workspace openHAB.xcworkspace -scheme openHAB -configuration Debug clean build -destination 'generic/platform=iOS' +``` + +### Running tests +```bash +xcodebuild -workspace openHAB.xcworkspace -scheme openHAB -destination 'platform=iOS Simulator,name=iPhone 16' test +``` + +### Listing available schemes +```bash +xcodebuild -list -workspace openHAB.xcworkspace +``` + +## Additional Information + +For more details about the openHAB project, visit: +- Project repository: https://github.com/openhab/openhab-ios +- openHAB documentation: https://www.openhab.org/docs/ diff --git a/CommonUI/Sources/CommonUI/OHTextTokenStyle.swift b/CommonUI/Sources/CommonUI/OHTextTokenStyle.swift index 97551d17f..be9c5a26a 100644 --- a/CommonUI/Sources/CommonUI/OHTextTokenStyle.swift +++ b/CommonUI/Sources/CommonUI/OHTextTokenStyle.swift @@ -23,7 +23,7 @@ public enum OHTextToken { } public enum OHAccessibilityToken { - public static let minimumHitTarget: CGFloat = 32 + public static let minimumHitTarget: CGFloat = 44 } private struct OHTextTokenModifier: ViewModifier { diff --git a/CommonUI/Sources/CommonUI/PreviewWidgetFactory.swift b/CommonUI/Sources/CommonUI/PreviewWidgetFactory.swift index 8b901e8db..69db997f3 100644 --- a/CommonUI/Sources/CommonUI/PreviewWidgetFactory.swift +++ b/CommonUI/Sources/CommonUI/PreviewWidgetFactory.swift @@ -34,7 +34,7 @@ public enum PreviewWidgetFactory { step: step, switchSupport: switchSupport ) - widget.pattern = pattern + widget.pattern = pattern ?? "" return widget } @@ -69,7 +69,7 @@ public enum PreviewWidgetFactory { maxValue: maxValue, step: step ) - widget.unit = unit + widget.unit = unit ?? "" return widget } diff --git a/OpenHABCore/Sources/OpenHABCore/Util/ItemEventStream.swift b/OpenHABCore/Sources/OpenHABCore/Util/ItemEventStream.swift index 6c0f6849a..a62f1a0de 100644 --- a/OpenHABCore/Sources/OpenHABCore/Util/ItemEventStream.swift +++ b/OpenHABCore/Sources/OpenHABCore/Util/ItemEventStream.swift @@ -47,7 +47,7 @@ public enum StateStreamMessage: Sendable, Equatable { } public actor EventStream { - // Alive and Item State Chnage message structures + // Alive and Item State Change message structures private struct Alive: Decodable { let type: String; let interval: Int } // Multiple items can come in a single message which makes this a little more complicated private struct ItemStateChanges: Decodable { @@ -67,7 +67,6 @@ public actor EventStream { private var currentConfig: ConnectionConfiguration? private var sessionUUID: String? private var service: OpenAPIService? - private let jsonDecoder = JSONDecoder() public func stream() -> AsyncStream> { AsyncStream { continuation in @@ -114,18 +113,15 @@ public actor EventStream { /// Try and send the item right away, if the connection is not up, they will be sent when its ready private func sendTrackedItemsIfPossible() async { - guard - !trackedItems.isEmpty, - let uuid = sessionUUID, - let service - else { return } + guard !trackedItems.isEmpty, let uuid = sessionUUID, let service else { return } + do { try await service.updateItemListForStateUpdates( connectionId: uuid, items: Array(trackedItems) ) } catch { - Logger.restAPI.error("Failed to update item list: \(error.localizedDescription)") + Logger.restAPI.error("Failed to update SSE item list: \(error.localizedDescription)") } } @@ -181,19 +177,19 @@ public actor EventStream { return [.ready(uuid: uuid, lastEventID: sse.id)] } case "alive": - if let data = sse.data!.data(using: String.Encoding.utf8), - let obj = try? jsonDecoder.decode( - Alive.self, from: data - ) { + if let dataString = sse.data, + let data = dataString.data(using: String.Encoding.utf8), + let obj = try? JSONDecoder().decode(Alive.self, from: data) { return [.alive(interval: obj.interval)] } default: // sometime message omit the `event:` field and send only `data:` with the JSON. if let data = sse.data?.data(using: String.Encoding.utf8), - let changes = try? jsonDecoder.decode(ItemStateChanges.self, from: data) { - return changes.wrapped.map { key, value in - .state(item: key, state: value.state) + let changes = try? JSONDecoder().decode(ItemStateChanges.self, from: data) { + let messages = changes.wrapped.map { key, value in + StateStreamMessage.state(item: key, state: value.state) } + return messages } } return [.unknown(raw: sse.data ?? "nil")] diff --git a/OpenHABCore/Sources/OpenHABCore/Util/OpenHABItemCache.swift b/OpenHABCore/Sources/OpenHABCore/Util/OpenHABItemCache.swift index ced139a2e..d5a7a5437 100644 --- a/OpenHABCore/Sources/OpenHABCore/Util/OpenHABItemCache.swift +++ b/OpenHABCore/Sources/OpenHABCore/Util/OpenHABItemCache.swift @@ -13,6 +13,23 @@ import Combine import Foundation import os.log +public enum OpenHABItemCacheError: Error, LocalizedError { + case homeNotReachable(UUID) + case commandFailed(any Error) + case stateFailed(any Error) + + public var errorDescription: String? { + switch self { + case let .homeNotReachable(homeId): + "Home \(homeId) is not reachable" + case let .commandFailed(error): + "Could not send command: \(error.localizedDescription)" + case let .stateFailed(error): + "Could not send state: \(error.localizedDescription)" + } + } +} + public actor OpenHABItemCache { public static let instance = OpenHABItemCache() @@ -49,29 +66,31 @@ public actor OpenHABItemCache { return try? await networkTracker.getItemByName(id: name) } - public func sendCommand(to item: OpenHABItem, home: UUID, command: String) async { + public func sendCommand(to item: OpenHABItem, home: UUID, command: String) async throws { guard let networkTracker = await assureNetworkTracker(homeId: home) else { Logger.itemCache.error("Home \(home) not reachable") - return + throw OpenHABItemCacheError.homeNotReachable(home) } do { try await networkTracker.send(to: item, command: command) } catch { Logger.itemCache.info("Could not send command: \(error.localizedDescription)") + throw OpenHABItemCacheError.commandFailed(error) } } - public func sendState(_ item: OpenHABItem, home: UUID, state: String) async { + public func sendState(_ item: OpenHABItem, home: UUID, state: String) async throws { guard let networkTracker = await assureNetworkTracker(homeId: home) else { Logger.itemCache.error("Home \(home) not reachable") - return + throw OpenHABItemCacheError.homeNotReachable(home) } do { try await networkTracker.updateState(item: item, state: state) } catch { Logger.itemCache.info("Could not send state: \(error.localizedDescription)") + throw OpenHABItemCacheError.stateFailed(error) } } @@ -170,12 +189,58 @@ public actor OpenHABItemCache { } } +public extension OpenHABItem { + /// Checks if the item matches the specified types filter + /// - Parameter types: Optional array of item types to match. If nil, all items match. + /// - Returns: True if the item matches the filter, false otherwise + func matches(types: [OpenHABItem.ItemType]?) -> Bool { + types == nil || (type.flatMap { types?.contains($0) } == true) + } +} + public extension [OpenHABItem] { func filtered(by searchTerm: String? = nil, for types: [OpenHABItem.ItemType]? = nil) -> [OpenHABItem] { // TODO: maybe allow home name for filtering and fuzzier search filter { - (searchTerm == nil || $0.name.contains(searchTerm.orEmpty)) && - (types == nil || ($0.type != nil && types!.contains($0.type!))) + let matchesSearchTerm = searchTerm == nil || $0.name.contains(searchTerm.orEmpty) + let matchesType = $0.matches(types: types) + return matchesSearchTerm && matchesType + } + } +} + +public extension OpenHABItemCache { + func getItemNames(searchTerm: String?, types: [OpenHABItem.ItemType]?, home: UUID) async -> [String] { + await reloadCacheIfNeeded(homes: [home]) + return items[home]? + .filtered(by: searchTerm, for: types) + .sorted(by: \.name) + .map(\.name) ?? [] + } + + func getCachedItems(types: [OpenHABItem.ItemType]?, home: UUID) async -> [OpenHABItem] { + await reloadCacheIfNeeded(homes: [home]) + return items[home]? + .filtered(for: types) + .sorted(by: \.name) ?? [] + } + + func searchItems(searchTerm: String, types: [OpenHABItem.ItemType]? = nil) async -> [UUID: [OpenHABItem]] { + let allItems = await getAllCachedItems() + var result: [UUID: [OpenHABItem]] = [:] + + for (homeId, homeItems) in allItems { + let filtered = homeItems.filter { item in + let matchesSearch = item.name.localizedCaseInsensitiveContains(searchTerm) || + item.label.localizedCaseInsensitiveContains(searchTerm) + let matchesType = item.matches(types: types) + return matchesSearch && matchesType + } + if !filtered.isEmpty { + result[homeId] = filtered + } } + + return result } } diff --git a/OpenHABCore/Sources/OpenHABCore/Util/WidgetItemRegistry.swift b/OpenHABCore/Sources/OpenHABCore/Util/WidgetItemRegistry.swift new file mode 100644 index 000000000..8ee987af5 --- /dev/null +++ b/OpenHABCore/Sources/OpenHABCore/Util/WidgetItemRegistry.swift @@ -0,0 +1,163 @@ +// 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 Foundation +import os.log + +/// Manages tracking of which openHAB items have active widgets. +/// Uses shared UserDefaults to communicate between the app and widget extension. +public actor WidgetItemRegistry { + public static let shared = WidgetItemRegistry() + + private let defaults: UserDefaults? + private let itemsKey = "widgetTrackedItems" + private let timestampsKey = "widgetTrackedItemTimestamps" + + private init() { + defaults = UserDefaults(suiteName: "group.org.openhab.app") + } + + /// Register an item as being displayed in a widget + /// - Parameters: + /// - itemName: The name of the openHAB item + /// - homeId: The UUID of the home containing this item + public func registerItem(name itemName: String, homeId: UUID) { + guard let defaults else { + Logger.widgets.error("Failed to access shared UserDefaults") + return + } + + var items = getRegisteredItems() + let key = "\(homeId.uuidString):\(itemName)" + items.insert(key) + + // Update timestamp for this item + var timestamps = getTimestamps() + timestamps[key] = Date() + + defaults.set(Array(items), forKey: itemsKey) + setTimestamps(timestamps) + Logger.widgets.info("Registered widget item: \(itemName) for home \(homeId)") + } + + /// Unregister an item (when widget is removed) + /// - Parameters: + /// - itemName: The name of the openHAB item + /// - homeId: The UUID of the home containing this item + public func unregisterItem(name itemName: String, homeId: UUID) { + guard let defaults else { + Logger.widgets.error("Failed to access shared UserDefaults") + return + } + + var items = getRegisteredItems() + let key = "\(homeId.uuidString):\(itemName)" + items.remove(key) + + // Remove timestamp for this item + var timestamps = getTimestamps() + timestamps.removeValue(forKey: key) + + defaults.set(Array(items), forKey: itemsKey) + setTimestamps(timestamps) + Logger.widgets.info("Unregistered widget item: \(itemName) for home \(homeId)") + } + + /// Get all registered items grouped by home + /// - Returns: Dictionary mapping home UUIDs to arrays of item names + public func getItemsByHome() -> [UUID: [String]] { + let items = getRegisteredItems() + var result: [UUID: [String]] = [:] + + for item in items { + let components = item.split(separator: ":", maxSplits: 1) + guard components.count == 2, + let homeId = UUID(uuidString: String(components[0])) else { + continue + } + + let itemName = String(components[1]) + result[homeId, default: []].append(itemName) + } + + return result + } + + /// Get all registered item names (without home grouping) + /// - Returns: Array of all item names that have widgets + public func getAllItemNames() -> [String] { + let itemsByHome = getItemsByHome() + return Array(Set(itemsByHome.values.flatMap(\.self))) + } + + /// Clear all registered items (useful for testing or cleanup) + public func clearAll() { + defaults?.removeObject(forKey: itemsKey) + defaults?.removeObject(forKey: timestampsKey) + Logger.widgets.info("Cleared all registered widget items") + } + + /// Remove items that haven't been updated recently (stale widgets) + /// - Parameter olderThan: Time interval in seconds (default: 1 hour) + /// - Returns: Number of items removed + @discardableResult + public func removeStaleItems(olderThan interval: TimeInterval = 3600) -> Int { + guard let defaults else { return 0 } + + let cutoffDate = Date().addingTimeInterval(-interval) + var items = getRegisteredItems() + var timestamps = getTimestamps() + + var removedCount = 0 + var timestampsUpdated = false + + for itemKey in Array(items) { + // If no timestamp exists or timestamp is too old, remove it + if let timestamp = timestamps[itemKey], timestamp < cutoffDate { + items.remove(itemKey) + timestamps.removeValue(forKey: itemKey) + removedCount += 1 + Logger.widgets.debug("Removed stale widget item: \(itemKey)") + } else if timestamps[itemKey] == nil { + // Item has no timestamp, probably from old version - keep it but add timestamp + timestamps[itemKey] = Date() + timestampsUpdated = true + } + } + + if removedCount > 0 || timestampsUpdated { + defaults.set(Array(items), forKey: itemsKey) + setTimestamps(timestamps) + Logger.widgets.info("Removed \(removedCount) stale widget items") + } + + return removedCount + } + + private func getRegisteredItems() -> Set { + guard let defaults else { return [] } + let items = defaults.stringArray(forKey: itemsKey) ?? [] + return Set(items) + } + + private func getTimestamps() -> [String: Date] { + guard let defaults else { return [:] } + guard let data = defaults.data(forKey: timestampsKey) else { return [:] } + return (try? JSONDecoder().decode([String: Date].self, from: data)) ?? [:] + } + + private func setTimestamps(_ timestamps: [String: Date]) { + guard let defaults else { return } + if let data = try? JSONEncoder().encode(timestamps) { + defaults.set(data, forKey: timestampsKey) + } + } +} diff --git a/OpenHABCore/Tests/OpenHABCoreTests/ETagCheckerTests.swift b/OpenHABCore/Tests/OpenHABCoreTests/ETagCheckerTests.swift index 4c0059f4e..797e537d5 100644 --- a/OpenHABCore/Tests/OpenHABCoreTests/ETagCheckerTests.swift +++ b/OpenHABCore/Tests/OpenHABCoreTests/ETagCheckerTests.swift @@ -13,70 +13,6 @@ import Foundation @testable import OpenHABCore import Testing -// MARK: - Mock URLProtocol - -class MockURLProtocol: URLProtocol { - // Use nonisolated(unsafe) to suppress concurrency warnings - // Safe because tests run with .serialized, ensuring sequential execution - nonisolated(unsafe) static var mockResponses: [URL: (statusCode: Int, headers: [String: String])] = [:] - nonisolated(unsafe) static var shouldFail = false - nonisolated(unsafe) static var error: Error? - - static func reset() { - mockResponses.removeAll() - shouldFail = false - error = nil - } - - override class func canInit(with request: URLRequest) -> Bool { - true - } - - override class func canonicalRequest(for request: URLRequest) -> URLRequest { - request - } - - override func startLoading() { - if MockURLProtocol.shouldFail { - let error = MockURLProtocol.error ?? URLError(.badServerResponse) - client?.urlProtocol(self, didFailWithError: error) - return - } - - guard let url = request.url else { - client?.urlProtocol(self, didFailWithError: URLError(.badURL)) - return - } - - guard let mock = MockURLProtocol.mockResponses[url] else { - // Return 404 if no mock configured - let response = HTTPURLResponse( - url: url, - statusCode: 404, - httpVersion: "HTTP/1.1", - headerFields: nil - )! - client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed) - client?.urlProtocol(self, didLoad: Data()) - client?.urlProtocolDidFinishLoading(self) - return - } - - let response = HTTPURLResponse( - url: url, - statusCode: mock.statusCode, - httpVersion: "HTTP/1.1", - headerFields: mock.headers - )! - - client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed) - client?.urlProtocol(self, didLoad: Data()) - client?.urlProtocolDidFinishLoading(self) - } - - override func stopLoading() {} -} - @Suite("ETagChecker Tests", .serialized) struct ETagCheckerTests { @Test("First run stores ETag and returns changed") @@ -310,3 +246,67 @@ struct ETagCheckerTests { ) } } + +// MARK: - Mock URLProtocol + +class MockURLProtocol: URLProtocol { + // Use nonisolated(unsafe) to suppress concurrency warnings + // Safe because tests run with .serialized, ensuring sequential execution + nonisolated(unsafe) static var mockResponses: [URL: (statusCode: Int, headers: [String: String])] = [:] + nonisolated(unsafe) static var shouldFail: Bool = false + nonisolated(unsafe) static var error: Error? + + override class func canInit(with request: URLRequest) -> Bool { + true + } + + override class func canonicalRequest(for request: URLRequest) -> URLRequest { + request + } + + override func startLoading() { + if MockURLProtocol.shouldFail { + let error = MockURLProtocol.error ?? URLError(.badServerResponse) + client?.urlProtocol(self, didFailWithError: error) + return + } + + guard let url = request.url else { + client?.urlProtocol(self, didFailWithError: URLError(.badURL)) + return + } + + guard let mock = MockURLProtocol.mockResponses[url] else { + // Return 404 if no mock configured + let response = HTTPURLResponse( + url: url, + statusCode: 404, + httpVersion: "HTTP/1.1", + headerFields: nil + )! + client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed) + client?.urlProtocol(self, didLoad: Data()) + client?.urlProtocolDidFinishLoading(self) + return + } + + let response = HTTPURLResponse( + url: url, + statusCode: mock.statusCode, + httpVersion: "HTTP/1.1", + headerFields: mock.headers + )! + + client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed) + client?.urlProtocol(self, didLoad: Data()) + client?.urlProtocolDidFinishLoading(self) + } + + override func stopLoading() {} + + static func reset() { + mockResponses.removeAll() + shouldFail = false + error = nil + } +} diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 6fc54409b..ea8a561bb 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -160,36 +160,23 @@ platform :ios do type = options[:bump] - xcconfig_path = File.expand_path('../Version.xcconfig', __dir__) - if options[:isfeaturebuild] - # Feature branch: always patch-bump MARKETING_VERSION in xcconfig - xcconfig = File.read(xcconfig_path) - parts = xcconfig.match(/MARKETING_VERSION\s*=\s*([\d.]+)/)[1].split('.').map(&:to_i) - parts[2] += 1 - File.write(xcconfig_path, xcconfig.gsub( - /MARKETING_VERSION\s*=\s*[\d.]+/, - "MARKETING_VERSION = #{parts.join('.')}" - )) - elsif type != 'none' - # develop: bump MARKETING_VERSION in xcconfig - xcconfig = File.read(xcconfig_path) - parts = xcconfig.match(/MARKETING_VERSION\s*=\s*([\d.]+)/)[1].split('.').map(&:to_i) - case type - when 'patch' then parts[2] += 1 - when 'minor' then parts[1] += 1; parts[2] = 0 - when 'major' then parts[0] += 1; parts[1] = 0; parts[2] = 0 + if !options[:isfeaturebuild] && type != 'none' + # Increment version for all targets + ['openHAB', 'openHABWatch', 'openHABIntents', 'NotificationService', 'openHABTestsSwift', 'openHABUITests', 'openHABWatchSwiftUI Watch AppTests', 'openHABWatchSwiftUI Watch AppUITests'].each do |target| + increment_version_number_in_xcodeproj( + bump_type: type, + xcodeproj: 'openHAB.xcodeproj', + target: target + ) end - File.write(xcconfig_path, xcconfig.gsub( - /MARKETING_VERSION\s*=\s*[\d.]+/, - "MARKETING_VERSION = #{parts.join('.')}" - )) end # increment_version_if_required # NOTE: this resolves packages of the project so need to clean sh("cd .. && git clean -fd") - build_number = File.read(xcconfig_path).match(/CURRENT_PROJECT_VERSION\s*=\s*(\d+)/)[1].to_i - version = File.read(xcconfig_path).match(/MARKETING_VERSION\s*=\s*([\d.]+)/)[1] + build_number = get_build_number + version = get_version_number(xcodeproj: 'openHAB.xcodeproj', + target: 'openHAB') # get the last commit comments from Git history # and creates our changelog @@ -200,9 +187,8 @@ platform :ios do match_lightweight_tag: false, merge_commit_filtering: 'exclude_merges' ) - # Clear FL_CHANGELOG so it doesn't bloat the Fastlane summary table - Actions.lane_context[Fastlane::Actions::SharedValues::FL_CHANGELOG] = nil + # Write git commits to CHANGELOG.md File.write('../CHANGELOG.md', "# Change Log\n\n## [Unreleased]\n\n#{comments}\n\n#{File.read('../CHANGELOG.md').gsub(/^# Change Log\s*\n\n## \[Unreleased\].*?\n\n/m, '') rescue ''}") @@ -238,14 +224,12 @@ platform :ios do clean_build_artifacts - # Restore project.pbxproj modified by update_code_signing_settings and Xcode normalisation - sh("git restore ../openHAB.xcodeproj/project.pbxproj") - # commit to git the changes from bumping version number - git_add(path: 'Version.xcconfig') - git_commit( - path: ['Version.xcconfig', 'CHANGELOG.md'], - message: "committed version bump: #{version} (#{build_number})" + commit_version_bump( + message: "committed version bump: #{version} (#{build_number})", + xcodeproj: 'openHAB.xcodeproj', + include: %w[CHANGELOG.md], + force: true ) if is_ci? sh 'git commit --amend --no-edit --signoff --author="openhab-bot "' @@ -324,16 +308,13 @@ platform :ios do desc 'Increment the build number.' private_lane :increment_build do - xcconfig_path = File.expand_path('../Version.xcconfig', __dir__) - xcconfig = File.read(xcconfig_path) - xcode_build = xcconfig.match(/CURRENT_PROJECT_VERSION\s*=\s*(\d+)/)[1].to_i + xcode_build = get_build_number.to_i testflight_build = latest_testflight_build_number build_no = (xcode_build > testflight_build ? xcode_build : testflight_build) + 1 - File.write(xcconfig_path, xcconfig.gsub( - /CURRENT_PROJECT_VERSION\s*=\s*\d+/, - "CURRENT_PROJECT_VERSION = #{build_no}" - )) + increment_build_number_in_xcodeproj( + build_number: "#{build_no}" + ) end desc 'Increment the version number if required.' diff --git a/openHAB.xcodeproj/project.pbxproj b/openHAB.xcodeproj/project.pbxproj index f2d7b3284..42a1729f0 100644 --- a/openHAB.xcodeproj/project.pbxproj +++ b/openHAB.xcodeproj/project.pbxproj @@ -7,24 +7,35 @@ objects = { /* Begin PBXBuildFile section */ - 2F21508A2F75C2FC001BA057 /* GetItemStateIntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F2150732F75C2FC001BA057 /* GetItemStateIntentHandler.swift */; }; - 2F21508B2F75C2FC001BA057 /* IntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F2150752F75C2FC001BA057 /* IntentHandler.swift */; }; - 2F21508D2F75C2FC001BA057 /* OpenHABIntentHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F2150812F75C2FC001BA057 /* OpenHABIntentHelper.swift */; }; - 2F21508E2F75C2FC001BA057 /* SetColorValueIntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F2150832F75C2FC001BA057 /* SetColorValueIntentHandler.swift */; }; - 2F21508F2F75C2FC001BA057 /* SetContactStateValueIntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F2150842F75C2FC001BA057 /* SetContactStateValueIntentHandler.swift */; }; - 2F2150902F75C2FC001BA057 /* SetDimmerRollerValueIntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F2150852F75C2FC001BA057 /* SetDimmerRollerValueIntentHandler.swift */; }; - 2F2150912F75C2FC001BA057 /* SetNumberValueIntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F2150862F75C2FC001BA057 /* SetNumberValueIntentHandler.swift */; }; - 2F2150922F75C2FC001BA057 /* SetStringValueIntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F2150872F75C2FC001BA057 /* SetStringValueIntentHandler.swift */; }; - 2F2150932F75C2FC001BA057 /* SetSwitchStateIntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F2150882F75C2FC001BA057 /* SetSwitchStateIntentHandler.swift */; }; - 2F2150982F75C306001BA057 /* Intents.intentdefinition in Sources */ = {isa = PBXBuildFile; fileRef = 2F2150972F75C306001BA057 /* Intents.intentdefinition */; }; - 2F2150A22F75C434001BA057 /* Intents.intentdefinition in Resources */ = {isa = PBXBuildFile; fileRef = 2F2150972F75C306001BA057 /* Intents.intentdefinition */; }; + 06C2B76B8F58F04FA6B785AC /* OpenHABAppShortcutsProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34F338E29713BCD67CEE7AB2 /* OpenHABAppShortcutsProvider.swift */; }; + 1C94955D24D0D22516697E9C /* SetDimmerRollerValueIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = E64C2A634B624A5C65CA38F8 /* SetDimmerRollerValueIntent.swift */; }; + 1CA59BC8C2ECF37BF2A70780 /* ContactState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7680E329B5D2C3F519D55082 /* ContactState.swift */; }; + 1CC8CADFF909E48BBD6EBC7B /* Home.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A34B13C1EA57B4C182ED0AC /* Home.swift */; }; + 2010EBB1D7959601DE8690B2 /* SetSwitchItemIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA92D8864812F1C6F4F6A274 /* SetSwitchItemIntent.swift */; }; + 2571672ED4A60F95506B4D92 /* ItemEntity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94534997A178B9B14184C15D /* ItemEntity.swift */; }; + 2AE557B26AF10D16E65EB541 /* openHABWidgetExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 396B3EC10279D6D55CD888E8 /* openHABWidgetExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 2F399AC92F54599400F72A30 /* Flow in Frameworks */ = {isa = PBXBuildFile; productRef = 2F399AC82F54599400F72A30 /* Flow */; }; - 4D6470DA2561F935007B03FC /* openHABIntents.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 4D6470D32561F935007B03FC /* openHABIntents.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + 30C55CD81731B44CC2EBA0D6 /* OpenHABAppShortcutsProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34F338E29713BCD67CEE7AB2 /* OpenHABAppShortcutsProvider.swift */; }; + 43A66B27B252B9239BA2FB30 /* SwitchAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BA9AAD8C9E144F3F9419488 /* SwitchAction.swift */; }; + 4A7298F5D24042099F10A59C /* SetDimmerRollerValueIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = E64C2A634B624A5C65CA38F8 /* SetDimmerRollerValueIntent.swift */; }; + 4DD8D7C33C57B1A0B673B221 /* SwitchItemEntity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BE936331F9B6136215C0198 /* SwitchItemEntity.swift */; }; + 5111C4CC62E66D2FFA5C92EB /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 06A8490213945021806A13D1 /* SwiftUI.framework */; }; + 573BCD1FE26AA5788187EF63 /* Home.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A34B13C1EA57B4C182ED0AC /* Home.swift */; }; + 57BADF3F73A8E5F06721234D /* SetStringValueIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = F853E3F26D2A76CEE4F09441 /* SetStringValueIntent.swift */; }; + 5B539FBC26BAFDD1B4DEC425 /* ContactState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7680E329B5D2C3F519D55082 /* ContactState.swift */; }; + 5F4A972F3F21542F218677EF /* GetItemStateIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02B6D2BF8C847E9053C172D3 /* GetItemStateIntent.swift */; }; 6557AF8F2C0241C10094D0C8 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 6557AF8E2C0241C10094D0C8 /* PrivacyInfo.xcprivacy */; }; 6557AF902C0241C10094D0C8 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 6557AF8E2C0241C10094D0C8 /* PrivacyInfo.xcprivacy */; }; 6557AF922C039D140094D0C8 /* FirebaseMessaging in Frameworks */ = {isa = PBXBuildFile; productRef = 6557AF912C039D140094D0C8 /* FirebaseMessaging */; }; 657144552C1E438700C8A1F3 /* NotificationService.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 6571444E2C1E438700C8A1F3 /* NotificationService.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 657144962C30A16700C8A1F3 /* OpenHABCore in Frameworks */ = {isa = PBXBuildFile; productRef = 657144952C30A16700C8A1F3 /* OpenHABCore */; }; + 66AC88669C02CEC625555579 /* SwitchItemEntity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BE936331F9B6136215C0198 /* SwitchItemEntity.swift */; }; + 7624B6BA0EEF12ACAA0611E4 /* ItemIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD5BA21AA4348E466BBF954C /* ItemIdentifier.swift */; }; + 79168B9B7C119E095AE5C0F8 /* GetItemStateIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02B6D2BF8C847E9053C172D3 /* GetItemStateIntent.swift */; }; + 7B41E21D01029585A03F711F /* ItemEntityQuery.swift in Sources */ = {isa = PBXBuildFile; fileRef = 921C09F41840233BD245E772 /* ItemEntityQuery.swift */; }; + 7DB6396F170A27B47038CFF9 /* ContactStateIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82D28A7090A68C2872943168 /* ContactStateIntent.swift */; }; + 7E7B3B323F5B25A467E53507 /* ItemEntity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94534997A178B9B14184C15D /* ItemEntity.swift */; }; + 8903433ABC95021BB53F0446 /* ContactStateIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82D28A7090A68C2872943168 /* ContactStateIntent.swift */; }; 934E592528F16EBA00162004 /* OpenHABCore in Frameworks */ = {isa = PBXBuildFile; productRef = 934E592428F16EBA00162004 /* OpenHABCore */; }; 934E592728F16EBA00162004 /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 934E592628F16EBA00162004 /* Kingfisher */; }; 934E592928F16EBA00162004 /* DeviceKit in Frameworks */ = {isa = PBXBuildFile; productRef = 934E592828F16EBA00162004 /* DeviceKit */; }; @@ -34,40 +45,51 @@ 937E4488270B37A600A98C26 /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 937E4487270B37A600A98C26 /* Kingfisher */; }; 937E448C270B37CA00A98C26 /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 937E448B270B37CA00A98C26 /* Kingfisher */; }; 937E448E270B37D200A98C26 /* DeviceKit in Frameworks */ = {isa = PBXBuildFile; productRef = 937E448D270B37D200A98C26 /* DeviceKit */; }; - 937E4492270B37FE00A98C26 /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 937E4491270B37FE00A98C26 /* Kingfisher */; }; - 937E44E2270B393C00A98C26 /* OpenHABCore in Frameworks */ = {isa = PBXBuildFile; productRef = 937E44E1270B393C00A98C26 /* OpenHABCore */; }; 9397EDEC2587837000F266E1 /* openHABWatch.app in Embed Watch Content */ = {isa = PBXBuildFile; fileRef = DA0775152346705D0086C685 /* openHABWatch.app */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 93F8063527AE6C620035A6B0 /* FirebaseCrashlytics in Frameworks */ = {isa = PBXBuildFile; productRef = 93F8063427AE6C620035A6B0 /* FirebaseCrashlytics */; }; 93F8064727AE7A050035A6B0 /* SwiftMessages in Frameworks */ = {isa = PBXBuildFile; productRef = 93F8064627AE7A050035A6B0 /* SwiftMessages */; }; 93F8064A27AE7A2E0035A6B0 /* FlexColorPicker in Frameworks */ = {isa = PBXBuildFile; productRef = 93F8064927AE7A2E0035A6B0 /* FlexColorPicker */; }; 93F8065027AE7A830035A6B0 /* SideMenu in Frameworks */ = {isa = PBXBuildFile; productRef = 93F8064F27AE7A830035A6B0 /* SideMenu */; }; + 9D35698E8462B3AA05093643 /* SwitchAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BA9AAD8C9E144F3F9419488 /* SwitchAction.swift */; }; + AC614D177A98492FAC5396BC /* SetColorValueIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97B581426A726467A6B65FCB /* SetColorValueIntent.swift */; }; + AFCA75B7032CED93471FA489 /* SetSwitchItemIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA92D8864812F1C6F4F6A274 /* SetSwitchItemIntent.swift */; }; + AFE7BDC9A9901B49AF9D6635 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 16CFF4D57F6DF603FF1C1B9D /* Foundation.framework */; }; + B76C3BEEBF1F4730710AAB6E /* SDWebImageSVGCoder in Frameworks */ = {isa = PBXBuildFile; productRef = E92D1BEE08AD7DCB6F993340 /* SDWebImageSVGCoder */; }; + C314F9C9CB55D0AA25E4BB0A /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0F298FB19C2A149258FE7CC6 /* WidgetKit.framework */; }; + D2806DCDBD35AC9A75BBD5CA /* SetColorValueIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97B581426A726467A6B65FCB /* SetColorValueIntent.swift */; }; + D67C24016873FD61BF88CEB6 /* ItemIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD5BA21AA4348E466BBF954C /* ItemIdentifier.swift */; }; DA10161B2DC7BAE500552D14 /* SFSafeSymbols in Frameworks */ = {isa = PBXBuildFile; productRef = DA10161A2DC7BAE500552D14 /* SFSafeSymbols */; }; DA28C362225241DE00AB409C /* WebKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DA28C361225241DE00AB409C /* WebKit.framework */; settings = {ATTRIBUTES = (Required, ); }; }; DA2C4FD52B4F573300D1C533 /* SDWebImageSVGCoder in Frameworks */ = {isa = PBXBuildFile; productRef = DA2C4FD42B4F573300D1C533 /* SDWebImageSVGCoder */; }; DA4D4DB5233F9ACB00B37E37 /* README.md in Resources */ = {isa = PBXBuildFile; fileRef = DA4D4DB4233F9ACB00B37E37 /* README.md */; }; - DA7ACD5F2DC3DB130055CFC7 /* SFSafeSymbols in Frameworks */ = {isa = PBXBuildFile; productRef = DA7ACD5E2DC3DB130055CFC7 /* SFSafeSymbols */; settings = {ATTRIBUTES = (Required, ); }; }; DA817E7A234BF39B00C91824 /* CHANGELOG.md in Resources */ = {isa = PBXBuildFile; fileRef = DA817E79234BF39B00C91824 /* CHANGELOG.md */; }; DA9A7EFD2D668D5900824156 /* SFSafeSymbols in Frameworks */ = {isa = PBXBuildFile; productRef = DA9A7EFC2D668D5900824156 /* SFSafeSymbols */; }; DA9A7EFF2D66915900824156 /* SFSafeSymbols in Frameworks */ = {isa = PBXBuildFile; productRef = DA9A7EFE2D66915900824156 /* SFSafeSymbols */; }; + C380A8DFA36E43D49400A87E /* SFSafeSymbols in Frameworks */ = {isa = PBXBuildFile; productRef = 1F9D3602163D4BEBB384DDEE /* SFSafeSymbols */; }; DABB5E332D98972F009A4B8A /* SDWebImageSVGCoder in Frameworks */ = {isa = PBXBuildFile; productRef = DABB5E322D98972F009A4B8A /* SDWebImageSVGCoder */; }; DAC949FA2E219F0D007E67B7 /* CommonUI in Frameworks */ = {isa = PBXBuildFile; productRef = DAC949F92E219F0D007E67B7 /* CommonUI */; }; DAC949FC2E219F30007E67B7 /* CommonUI in Frameworks */ = {isa = PBXBuildFile; productRef = DAC949FB2E219F30007E67B7 /* CommonUI */; }; DAEFAA7F2F63536A00AC300B /* OpenHABCore in Frameworks */ = {isa = PBXBuildFile; productRef = DAEFAA7E2F63536A00AC300B /* OpenHABCore */; }; DAEFAA812F63537600AC300B /* SDWebImageSVGCoder in Frameworks */ = {isa = PBXBuildFile; productRef = DAEFAA802F63537600AC300B /* SDWebImageSVGCoder */; }; DAFF80982E4F47830084513E /* SDWebImage in Frameworks */ = {isa = PBXBuildFile; productRef = DAFF80972E4F47830084513E /* SDWebImage */; }; + DB4AC5BC3C45D4A9AFDC4FD2 /* SetStringValueIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = F853E3F26D2A76CEE4F09441 /* SetStringValueIntent.swift */; }; DFB2622B18830A3600D3244D /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DFB2622A18830A3600D3244D /* Foundation.framework */; }; DFB2622D18830A3600D3244D /* CoreGraphics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DFB2622C18830A3600D3244D /* CoreGraphics.framework */; }; DFB2622F18830A3600D3244D /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DFB2622E18830A3600D3244D /* UIKit.framework */; }; DFE10414197415F900D94943 /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DFE10413197415F900D94943 /* Security.framework */; }; + EE6B566C271031A1EB5780D2 /* OpenHABCore in Frameworks */ = {isa = PBXBuildFile; productRef = 2247FFEA7206BFFC71AB02C4 /* OpenHABCore */; }; + F6828FBB52E6C50B8CE2EF9F /* SetNumberValueIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CCC302957C5D59E31121A8D /* SetNumberValueIntent.swift */; }; + F811606815878D7012ACB0F5 /* ItemEntityQuery.swift in Sources */ = {isa = PBXBuildFile; fileRef = 921C09F41840233BD245E772 /* ItemEntityQuery.swift */; }; + FE1E8BC7D7F471860C3DFF52 /* SetNumberValueIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CCC302957C5D59E31121A8D /* SetNumberValueIntent.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ - 4D6470D82561F935007B03FC /* PBXContainerItemProxy */ = { + 1124E731EFDB779420004716 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = DFB2621F18830A3600D3244D /* Project object */; proxyType = 1; - remoteGlobalIDString = 4D6470D22561F935007B03FC; - remoteInfo = openHABIntents; + remoteGlobalIDString = 6116C7DDD25EE245FE191A47; + remoteInfo = openHABWidgetExtension; }; 657144532C1E438700C8A1F3 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; @@ -138,8 +160,8 @@ dstPath = ""; dstSubfolderSpec = 13; files = ( - 4D6470DA2561F935007B03FC /* openHABIntents.appex in Embed Foundation Extensions */, 657144552C1E438700C8A1F3 /* NotificationService.appex in Embed Foundation Extensions */, + 2AE557B26AF10D16E65EB541 /* openHABWidgetExtension.appex in Embed Foundation Extensions */, ); name = "Embed Foundation Extensions"; runOnlyForDeploymentPostprocessing = 0; @@ -167,32 +189,34 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - 2F2150732F75C2FC001BA057 /* GetItemStateIntentHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetItemStateIntentHandler.swift; sourceTree = ""; }; - 2F2150742F75C2FC001BA057 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 2F2150752F75C2FC001BA057 /* IntentHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntentHandler.swift; sourceTree = ""; }; - 2F2150812F75C2FC001BA057 /* OpenHABIntentHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenHABIntentHelper.swift; sourceTree = ""; }; - 2F2150822F75C2FC001BA057 /* openHABIntents.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = openHABIntents.entitlements; sourceTree = ""; }; - 2F2150832F75C2FC001BA057 /* SetColorValueIntentHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetColorValueIntentHandler.swift; sourceTree = ""; }; - 2F2150842F75C2FC001BA057 /* SetContactStateValueIntentHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetContactStateValueIntentHandler.swift; sourceTree = ""; }; - 2F2150852F75C2FC001BA057 /* SetDimmerRollerValueIntentHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetDimmerRollerValueIntentHandler.swift; sourceTree = ""; }; - 2F2150862F75C2FC001BA057 /* SetNumberValueIntentHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetNumberValueIntentHandler.swift; sourceTree = ""; }; - 2F2150872F75C2FC001BA057 /* SetStringValueIntentHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetStringValueIntentHandler.swift; sourceTree = ""; }; - 2F2150882F75C2FC001BA057 /* SetSwitchStateIntentHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetSwitchStateIntentHandler.swift; sourceTree = ""; }; - 2F2150962F75C306001BA057 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.intentdefinition; name = Base; path = Base.lproj/Intents.intentdefinition; sourceTree = ""; }; - 2F2150992F75C325001BA057 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Intents.strings; sourceTree = ""; }; - 2F21509A2F75C327001BA057 /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/Intents.strings; sourceTree = ""; }; - 2F21509B2F75C329001BA057 /* fi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fi; path = fi.lproj/Intents.strings; sourceTree = ""; }; - 2F21509C2F75C32B001BA057 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Intents.strings; sourceTree = ""; }; - 2F21509D2F75C32D001BA057 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Intents.strings; sourceTree = ""; }; - 2F21509E2F75C32F001BA057 /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/Intents.strings; sourceTree = ""; }; - 2F21509F2F75C331001BA057 /* nb */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nb; path = nb.lproj/Intents.strings; sourceTree = ""; }; - 2F2150A02F75C333001BA057 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/Intents.strings; sourceTree = ""; }; - 2F2150A12F75C335001BA057 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/Intents.strings; sourceTree = ""; }; - 4D6470D32561F935007B03FC /* openHABIntents.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = openHABIntents.appex; sourceTree = BUILT_PRODUCTS_DIR; }; + 02B6D2BF8C847E9053C172D3 /* GetItemStateIntent.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = GetItemStateIntent.swift; sourceTree = ""; }; + 06A8490213945021806A13D1 /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS18.0.sdk/System/Library/Frameworks/SwiftUI.framework; sourceTree = DEVELOPER_DIR; }; + 0F298FB19C2A149258FE7CC6 /* WidgetKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS18.0.sdk/System/Library/Frameworks/WidgetKit.framework; sourceTree = DEVELOPER_DIR; }; + 15C8963F2390FB56E94A65CB /* SetContactStateValue.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SetContactStateValue.swift; sourceTree = ""; }; + 16CFF4D57F6DF603FF1C1B9D /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS18.0.sdk/System/Library/Frameworks/Foundation.framework; sourceTree = DEVELOPER_DIR; }; + 2A34B13C1EA57B4C182ED0AC /* Home.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = Home.swift; sourceTree = ""; }; + + 34F338E29713BCD67CEE7AB2 /* OpenHABAppShortcutsProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = OpenHABAppShortcutsProvider.swift; sourceTree = ""; }; + 396B3EC10279D6D55CD888E8 /* openHABWidgetExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = openHABWidgetExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; + 3CCC302957C5D59E31121A8D /* SetNumberValueIntent.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SetNumberValueIntent.swift; sourceTree = ""; }; + 4BE936331F9B6136215C0198 /* SwitchItemEntity.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SwitchItemEntity.swift; sourceTree = ""; }; + 6557AF8E2C0241C10094D0C8 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; 6571444E2C1E438700C8A1F3 /* NotificationService.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = NotificationService.appex; sourceTree = BUILT_PRODUCTS_DIR; }; + 7680E329B5D2C3F519D55082 /* ContactState.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ContactState.swift; sourceTree = ""; }; + 7ED04F4B8BE70C368C55EB69 /* SetNumberValue.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SetNumberValue.swift; sourceTree = ""; }; + 82D28A7090A68C2872943168 /* ContactStateIntent.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ContactStateIntent.swift; sourceTree = ""; }; + 8BA9AAD8C9E144F3F9419488 /* SwitchAction.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SwitchAction.swift; sourceTree = ""; }; + 921C09F41840233BD245E772 /* ItemEntityQuery.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ItemEntityQuery.swift; sourceTree = ""; }; 933D7F0422E7015000621A03 /* openHABUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = openHABUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - 935D340A257B7DC00020A404 /* Intents.intentdefinition */ = {isa = PBXFileReference; lastKnownFileType = file.intentdefinition; name = Intents.intentdefinition; path = openHABIntents/Intents.intentdefinition; sourceTree = SOURCE_ROOT; }; + + 94534997A178B9B14184C15D /* ItemEntity.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ItemEntity.swift; sourceTree = ""; }; + 97B581426A726467A6B65FCB /* SetColorValueIntent.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SetColorValueIntent.swift; sourceTree = ""; }; + B28BFFDD5B9D4F838265EA96 /* ActionMapper.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ActionMapper.swift; sourceTree = ""; }; + B3E04F8EAC093F5C3F234420 /* SetDimmerRollerValue.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SetDimmerRollerValue.swift; sourceTree = ""; }; + BD9B600A8915F5A132CF3F4B /* SetSwitchState.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SetSwitchState.swift; sourceTree = ""; }; + C2E317AE4D9229E1A072DFB1 /* GetItemState.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = GetItemState.swift; sourceTree = ""; }; + D4921B57D9C5A83E513EB52C /* SetStringValue.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SetStringValue.swift; sourceTree = ""; }; DA0775152346705D0086C685 /* openHABWatch.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = openHABWatch.app; sourceTree = BUILT_PRODUCTS_DIR; }; DA0DA9E12E0C9B74000C5D0A /* BuildTools */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = BuildTools; sourceTree = ""; }; DA28C361225241DE00AB409C /* WebKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WebKit.framework; path = System/Library/Frameworks/WebKit.framework; sourceTree = SDKROOT; }; @@ -202,6 +226,7 @@ DA817E79234BF39B00C91824 /* CHANGELOG.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = CHANGELOG.md; sourceTree = ""; }; DA83C81D2F48AF7600CDACED /* Version.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Version.xcconfig; sourceTree = ""; }; DA8B15512F3BB74B007753FD /* openHABWatchTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = openHABWatchTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + DA92D8864812F1C6F4F6A274 /* SetSwitchItemIntent.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SetSwitchItemIntent.swift; sourceTree = ""; }; DACC0F7B2E883AC700B62043 /* AGENTS.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = AGENTS.md; sourceTree = ""; }; DAD0856C2AE4782B001D36BE /* openHABWatchSwiftUI Watch AppTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "openHABWatchSwiftUI Watch AppTests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; DAD085762AE4782E001D36BE /* openHABWatchSwiftUI Watch AppUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "openHABWatchSwiftUI Watch AppUITests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -212,17 +237,21 @@ DFB2622E18830A3600D3244D /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = System/Library/Frameworks/UIKit.framework; sourceTree = SDKROOT; }; DFB2624C18830A3600D3244D /* XCTest.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = XCTest.framework; path = Library/Frameworks/XCTest.framework; sourceTree = DEVELOPER_DIR; }; DFE10413197415F900D94943 /* Security.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Security.framework; path = System/Library/Frameworks/Security.framework; sourceTree = SDKROOT; }; + E5275A8912CBDEEDA408BFE8 /* SetColorValue.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SetColorValue.swift; sourceTree = ""; }; + E64C2A634B624A5C65CA38F8 /* SetDimmerRollerValueIntent.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SetDimmerRollerValueIntent.swift; sourceTree = ""; }; + F853E3F26D2A76CEE4F09441 /* SetStringValueIntent.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SetStringValueIntent.swift; sourceTree = ""; }; + FD5BA21AA4348E466BBF954C /* ItemIdentifier.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ItemIdentifier.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ - 2FD4E1E82F66EF4800EBB340 /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = { + 2FD4E1E82F66EF4800EBB340 /* Exceptions for "NotificationService" folder in "NotificationService" target */ = { isa = PBXFileSystemSynchronizedBuildFileExceptionSet; membershipExceptions = ( NotificationService.swift, ); target = 6571444D2C1E438700C8A1F3 /* NotificationService */; }; - 2FD4E2CA2F66F9A500EBB340 /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = { + 2FD4E2CA2F66F9A500EBB340 /* Exceptions for "openHABWatch" folder in "openHABWatch" target */ = { isa = PBXFileSystemSynchronizedBuildFileExceptionSet; membershipExceptions = ( Extension/Info.plist, @@ -232,7 +261,7 @@ ); target = DA0775142346705D0086C685 /* openHABWatch */; }; - 2FD4E2CF2F66F9F700EBB340 /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = { + 2FD4E2CF2F66F9F700EBB340 /* Exceptions for "openHABUITests" folder in "openHABUITests" target */ = { isa = PBXFileSystemSynchronizedBuildFileExceptionSet; membershipExceptions = ( OpenHABUITests.swift, @@ -240,28 +269,28 @@ ); target = 933D7F0322E7015000621A03 /* openHABUITests */; }; - 2FD4E2F02F66F9FE00EBB340 /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = { + 2FD4E2F02F66F9FE00EBB340 /* Exceptions for "openHABTestsSwift" folder in "openHABTestsSwift" target */ = { isa = PBXFileSystemSynchronizedBuildFileExceptionSet; membershipExceptions = ( Info.plist, ); target = DA2DC22E21F2736C00830730 /* openHABTestsSwift */; }; - 2FD4E2F62F66FA2900EBB340 /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = { + 2FD4E2F62F66FA2900EBB340 /* Exceptions for "TestPlans" folder in "openHAB" target */ = { isa = PBXFileSystemSynchronizedBuildFileExceptionSet; membershipExceptions = ( openHABTests.xctestplan, ); target = DFB2622618830A3600D3244D /* openHAB */; }; - 2FD4E58D2F670BA500EBB340 /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = { + 2FD4E58D2F670BA500EBB340 /* Exceptions for "openHAB" folder in "openHAB" target */ = { isa = PBXFileSystemSynchronizedBuildFileExceptionSet; membershipExceptions = ( "Supporting Files/openHAB-Info.plist", ); target = DFB2622618830A3600D3244D /* openHAB */; }; - 2FD4E58E2F670BA600EBB340 /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = { + 2FD4E58E2F670BA600EBB340 /* Exceptions for "openHAB" folder in "openHABWatch" target */ = { isa = PBXFileSystemSynchronizedBuildFileExceptionSet; membershipExceptions = ( Images/AppIcon.icon, @@ -269,29 +298,150 @@ ); target = DA0775142346705D0086C685 /* openHABWatch */; }; - 3AE8ADF52F6849AA00AA4B6A /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = { + 4D140F69148E28C6B524B346 /* Exceptions for "openHABWidget" folder in "openHABWidgetExtension" target */ = { isa = PBXFileSystemSynchronizedBuildFileExceptionSet; membershipExceptions = ( - "Supporting Files/Localizable.xcstrings", + Info.plist, ); - target = 4D6470D22561F935007B03FC /* openHABIntents */; + target = 6116C7DDD25EE245FE191A47 /* openHABWidgetExtension */; }; /* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ /* Begin PBXFileSystemSynchronizedRootGroup section */ - 2FD4E1E22F66EF0200EBB340 /* fastlane */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = fastlane; sourceTree = ""; }; - 2FD4E1E62F66EF4800EBB340 /* NotificationService */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (2FD4E1E82F66EF4800EBB340 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = NotificationService; sourceTree = ""; }; - 2FD4E1EB2F66EF4F00EBB340 /* openHABWatchSwiftUI Watch AppUITests */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = "openHABWatchSwiftUI Watch AppUITests"; sourceTree = ""; }; - 2FD4E1EF2F66EF5500EBB340 /* openHABIntentsTests */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = openHABIntentsTests; sourceTree = ""; }; - 2FD4E29C2F66F9A500EBB340 /* openHABWatch */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (2FD4E2CA2F66F9A500EBB340 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = openHABWatch; sourceTree = ""; }; - 2FD4E2CD2F66F9F600EBB340 /* openHABUITests */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (2FD4E2CF2F66F9F700EBB340 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = openHABUITests; sourceTree = ""; }; - 2FD4E2E02F66F9FE00EBB340 /* openHABTestsSwift */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (2FD4E2F02F66F9FE00EBB340 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = openHABTestsSwift; sourceTree = ""; }; - 2FD4E2F42F66FA2900EBB340 /* TestPlans */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (2FD4E2F62F66FA2900EBB340 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = TestPlans; sourceTree = ""; }; - 2FD4E52B2F670BA500EBB340 /* openHAB */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (2FD4E58D2F670BA500EBB340 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, 2FD4E58E2F670BA600EBB340 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, 3AE8ADF52F6849AA00AA4B6A /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = openHAB; sourceTree = ""; }; - DA8B15522F3BB74B007753FD /* openHABWatchTests */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = openHABWatchTests; sourceTree = ""; }; + 2FD4E1E22F66EF0200EBB340 /* fastlane */ = { + isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + ); + explicitFileTypes = { + }; + explicitFolders = ( + ); + path = fastlane; + sourceTree = ""; + }; + 2FD4E1E62F66EF4800EBB340 /* NotificationService */ = { + isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + 2FD4E1E82F66EF4800EBB340 /* Exceptions for "NotificationService" folder in "NotificationService" target */, + ); + explicitFileTypes = { + }; + explicitFolders = ( + ); + path = NotificationService; + sourceTree = ""; + }; + 2FD4E1EB2F66EF4F00EBB340 /* openHABWatchSwiftUI Watch AppUITests */ = { + isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + ); + explicitFileTypes = { + }; + explicitFolders = ( + ); + path = "openHABWatchSwiftUI Watch AppUITests"; + sourceTree = ""; + }; + 2FD4E29C2F66F9A500EBB340 /* openHABWatch */ = { + isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + 2FD4E2CA2F66F9A500EBB340 /* Exceptions for "openHABWatch" folder in "openHABWatch" target */, + ); + explicitFileTypes = { + }; + explicitFolders = ( + ); + path = openHABWatch; + sourceTree = ""; + }; + 2FD4E2CD2F66F9F600EBB340 /* openHABUITests */ = { + isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + 2FD4E2CF2F66F9F700EBB340 /* Exceptions for "openHABUITests" folder in "openHABUITests" target */, + ); + explicitFileTypes = { + }; + explicitFolders = ( + ); + path = openHABUITests; + sourceTree = ""; + }; + 2FD4E2E02F66F9FE00EBB340 /* openHABTestsSwift */ = { + isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + 2FD4E2F02F66F9FE00EBB340 /* Exceptions for "openHABTestsSwift" folder in "openHABTestsSwift" target */, + ); + explicitFileTypes = { + }; + explicitFolders = ( + ); + path = openHABTestsSwift; + sourceTree = ""; + }; + 2FD4E2F42F66FA2900EBB340 /* TestPlans */ = { + isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + 2FD4E2F62F66FA2900EBB340 /* Exceptions for "TestPlans" folder in "openHAB" target */, + ); + explicitFileTypes = { + }; + explicitFolders = ( + ); + path = TestPlans; + sourceTree = ""; + }; + 2FD4E52B2F670BA500EBB340 /* openHAB */ = { + isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + 2FD4E58D2F670BA500EBB340 /* Exceptions for "openHAB" folder in "openHAB" target */, + 2FD4E58E2F670BA600EBB340 /* Exceptions for "openHAB" folder in "openHABWatch" target */, + ); + explicitFileTypes = { + }; + explicitFolders = ( + ); + path = openHAB; + sourceTree = ""; + }; + D41D0290EB2365F7E247EC0A /* openHABWidget */ = { + isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + 4D140F69148E28C6B524B346 /* Exceptions for "openHABWidget" folder in "openHABWidgetExtension" target */, + ); + explicitFileTypes = { + }; + explicitFolders = ( + ); + path = openHABWidget; + sourceTree = ""; + }; + DA8B15522F3BB74B007753FD /* openHABWatchTests */ = { + isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + ); + explicitFileTypes = { + }; + explicitFolders = ( + ); + path = openHABWatchTests; + sourceTree = ""; + }; /* End PBXFileSystemSynchronizedRootGroup section */ /* Begin PBXFrameworksBuildPhase section */ + 2EBB199B3035138E609434CB /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + AFE7BDC9A9901B49AF9D6635 /* Foundation.framework in Frameworks */, + C314F9C9CB55D0AA25E4BB0A /* WidgetKit.framework in Frameworks */, + 5111C4CC62E66D2FFA5C92EB /* SwiftUI.framework in Frameworks */, + EE6B566C271031A1EB5780D2 /* OpenHABCore in Frameworks */, + B76C3BEEBF1F4730710AAB6E /* SDWebImageSVGCoder in Frameworks */, + C380A8DFA36E43D49400A87E /* SFSafeSymbols in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 39C91164B60A5677322E8DE2 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -308,16 +458,6 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - 4D6470D02561F935007B03FC /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - DA7ACD5F2DC3DB130055CFC7 /* SFSafeSymbols in Frameworks */, - 937E4492270B37FE00A98C26 /* Kingfisher in Frameworks */, - 937E44E2270B393C00A98C26 /* OpenHABCore in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; 6571444B2C1E438700C8A1F3 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -392,31 +532,46 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - 2F2150892F75C2FC001BA057 /* openHABIntents */ = { + + 964901D665A05573A8E2E7B2 /* iOS */ = { + isa = PBXGroup; + children = ( + 16CFF4D57F6DF603FF1C1B9D /* Foundation.framework */, + 0F298FB19C2A149258FE7CC6 /* WidgetKit.framework */, + 06A8490213945021806A13D1 /* SwiftUI.framework */, + ); + name = iOS; + sourceTree = ""; + }; + AECF3235391FE9E33E933799 /* iOS16 */ = { isa = PBXGroup; children = ( - 2F2150732F75C2FC001BA057 /* GetItemStateIntentHandler.swift */, - 2F2150742F75C2FC001BA057 /* Info.plist */, - 2F2150752F75C2FC001BA057 /* IntentHandler.swift */, - 2F2150972F75C306001BA057 /* Intents.intentdefinition */, - 2F2150812F75C2FC001BA057 /* OpenHABIntentHelper.swift */, - 2F2150822F75C2FC001BA057 /* openHABIntents.entitlements */, - 2F2150832F75C2FC001BA057 /* SetColorValueIntentHandler.swift */, - 2F2150842F75C2FC001BA057 /* SetContactStateValueIntentHandler.swift */, - 2F2150852F75C2FC001BA057 /* SetDimmerRollerValueIntentHandler.swift */, - 2F2150862F75C2FC001BA057 /* SetNumberValueIntentHandler.swift */, - 2F2150872F75C2FC001BA057 /* SetStringValueIntentHandler.swift */, - 2F2150882F75C2FC001BA057 /* SetSwitchStateIntentHandler.swift */, - ); - path = openHABIntents; + B28BFFDD5B9D4F838265EA96 /* ActionMapper.swift */, + C2E317AE4D9229E1A072DFB1 /* GetItemState.swift */, + E5275A8912CBDEEDA408BFE8 /* SetColorValue.swift */, + 15C8963F2390FB56E94A65CB /* SetContactStateValue.swift */, + B3E04F8EAC093F5C3F234420 /* SetDimmerRollerValue.swift */, + 7ED04F4B8BE70C368C55EB69 /* SetNumberValue.swift */, + D4921B57D9C5A83E513EB52C /* SetStringValue.swift */, + BD9B600A8915F5A132CF3F4B /* SetSwitchState.swift */, + ); + name = iOS16; + path = iOS16; sourceTree = ""; }; - 2FD4E92D2F67571C00EBB340 /* Recovered References */ = { + D77C0F0DE468E68ACAE72627 /* Intents */ = { isa = PBXGroup; children = ( - 935D340A257B7DC00020A404 /* Intents.intentdefinition */, - ); - name = "Recovered References"; + 82D28A7090A68C2872943168 /* ContactStateIntent.swift */, + 02B6D2BF8C847E9053C172D3 /* GetItemStateIntent.swift */, + 97B581426A726467A6B65FCB /* SetColorValueIntent.swift */, + E64C2A634B624A5C65CA38F8 /* SetDimmerRollerValueIntent.swift */, + 3CCC302957C5D59E31121A8D /* SetNumberValueIntent.swift */, + F853E3F26D2A76CEE4F09441 /* SetStringValueIntent.swift */, + DA92D8864812F1C6F4F6A274 /* SetSwitchItemIntent.swift */, + ); + name = Intents; + path = Intents; sourceTree = ""; }; DFB2621E18830A3600D3244D = { @@ -432,11 +587,9 @@ 2FD4E2E02F66F9FE00EBB340 /* openHABTestsSwift */, 2FD4E2CD2F66F9F600EBB340 /* openHABUITests */, 2FD4E29C2F66F9A500EBB340 /* openHABWatch */, - 2FD4E1EF2F66EF5500EBB340 /* openHABIntentsTests */, 2FD4E1EB2F66EF4F00EBB340 /* openHABWatchSwiftUI Watch AppUITests */, 2FD4E1E62F66EF4800EBB340 /* NotificationService */, DA8B15522F3BB74B007753FD /* openHABWatchTests */, - 2F2150892F75C2FC001BA057 /* openHABIntents */, DFB2622818830A3600D3244D /* Products */, DFB2622918830A3600D3244D /* Frameworks */, 2FD4E1E22F66EF0200EBB340 /* fastlane */, @@ -444,6 +597,8 @@ DA0DA9E12E0C9B74000C5D0A /* BuildTools */, DA83C81D2F48AF7600CDACED /* Version.xcconfig */, 2FD4E92D2F67571C00EBB340 /* Recovered References */, + F7C952206B2CCE1B8A9F6293 /* AppIntents */, + D41D0290EB2365F7E247EC0A /* openHABWidget */, ); sourceTree = ""; }; @@ -454,11 +609,12 @@ DA2DC22F21F2736C00830730 /* openHABTestsSwift.xctest */, 933D7F0422E7015000621A03 /* openHABUITests.xctest */, DA0775152346705D0086C685 /* openHABWatch.app */, - 4D6470D32561F935007B03FC /* openHABIntents.appex */, + DAD0856C2AE4782B001D36BE /* openHABWatchSwiftUI Watch AppTests.xctest */, DAD085762AE4782E001D36BE /* openHABWatchSwiftUI Watch AppUITests.xctest */, 6571444E2C1E438700C8A1F3 /* NotificationService.appex */, DA8B15512F3BB74B007753FD /* openHABWatchTests.xctest */, + 396B3EC10279D6D55CD888E8 /* openHABWidgetExtension.appex */, ); name = Products; sourceTree = ""; @@ -472,33 +628,55 @@ DFE10413197415F900D94943 /* Security.framework */, DFB2622E18830A3600D3244D /* UIKit.framework */, DFB2624C18830A3600D3244D /* XCTest.framework */, + 964901D665A05573A8E2E7B2 /* iOS */, ); name = Frameworks; sourceTree = ""; }; + F7C952206B2CCE1B8A9F6293 /* AppIntents */ = { + isa = PBXGroup; + children = ( + 7680E329B5D2C3F519D55082 /* ContactState.swift */, + 2A34B13C1EA57B4C182ED0AC /* Home.swift */, + 94534997A178B9B14184C15D /* ItemEntity.swift */, + 921C09F41840233BD245E772 /* ItemEntityQuery.swift */, + FD5BA21AA4348E466BBF954C /* ItemIdentifier.swift */, + 34F338E29713BCD67CEE7AB2 /* OpenHABAppShortcutsProvider.swift */, + 8BA9AAD8C9E144F3F9419488 /* SwitchAction.swift */, + 4BE936331F9B6136215C0198 /* SwitchItemEntity.swift */, + D77C0F0DE468E68ACAE72627 /* Intents */, + AECF3235391FE9E33E933799 /* iOS16 */, + ); + name = AppIntents; + path = AppIntents; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ - 4D6470D22561F935007B03FC /* openHABIntents */ = { + 6116C7DDD25EE245FE191A47 /* openHABWidgetExtension */ = { isa = PBXNativeTarget; - buildConfigurationList = 4D6470DB2561F935007B03FC /* Build configuration list for PBXNativeTarget "openHABIntents" */; + buildConfigurationList = 7E431775FC423F4117A62BF2 /* Build configuration list for PBXNativeTarget "openHABWidgetExtension" */; buildPhases = ( - 4D6470CF2561F935007B03FC /* Sources */, - 4D6470D02561F935007B03FC /* Frameworks */, - 4D6470D12561F935007B03FC /* Resources */, + F706ED1570DF7DE91D2D6056 /* Sources */, + 2EBB199B3035138E609434CB /* Frameworks */, + BA216D3704F6BDD838E96877 /* Resources */, ); buildRules = ( ); dependencies = ( ); - name = openHABIntents; + fileSystemSynchronizedGroups = ( + D41D0290EB2365F7E247EC0A /* openHABWidget */, + ); + name = openHABWidgetExtension; packageProductDependencies = ( - 937E4491270B37FE00A98C26 /* Kingfisher */, - 937E44E1270B393C00A98C26 /* OpenHABCore */, - DA7ACD5E2DC3DB130055CFC7 /* SFSafeSymbols */, + 2247FFEA7206BFFC71AB02C4 /* OpenHABCore */, + E92D1BEE08AD7DCB6F993340 /* SDWebImageSVGCoder */, + 1F9D3602163D4BEBB384DDEE /* SFSafeSymbols */, ); - productName = openHABIntents; - productReference = 4D6470D32561F935007B03FC /* openHABIntents.appex */; + productName = "$(TARGET_NAME)"; + productReference = 396B3EC10279D6D55CD888E8 /* openHABWidgetExtension.appex */; productType = "com.apple.product-type.app-extension"; }; 6571444D2C1E438700C8A1F3 /* NotificationService */ = { @@ -666,11 +844,10 @@ ); dependencies = ( DA07753A2346705F0086C685 /* PBXTargetDependency */, - 4D6470D92561F935007B03FC /* PBXTargetDependency */, 657144542C1E438700C8A1F3 /* PBXTargetDependency */, + 50C8570F40A5885164DA68E6 /* PBXTargetDependency */, ); fileSystemSynchronizedGroups = ( - 2FD4E1EF2F66EF5500EBB340 /* openHABIntentsTests */, 2FD4E52B2F670BA500EBB340 /* openHAB */, ); name = openHAB; @@ -686,6 +863,7 @@ DA9A7EFE2D66915900824156 /* SFSafeSymbols */, DABB5E322D98972F009A4B8A /* SDWebImageSVGCoder */, 2F399AC82F54599400F72A30 /* Flow */, + DAC949F92E219F0D007E67B7 /* CommonUI */, ); productName = openHAB; productReference = DFB2622718830A3600D3244D /* openHAB.app */; @@ -793,31 +971,31 @@ DA2DC22E21F2736C00830730 /* openHABTestsSwift */, 933D7F0322E7015000621A03 /* openHABUITests */, DA0775142346705D0086C685 /* openHABWatch */, - 4D6470D22561F935007B03FC /* openHABIntents */, DAD0856B2AE4782A001D36BE /* openHABWatchSwiftUI Watch AppTests */, DAD085752AE4782D001D36BE /* openHABWatchSwiftUI Watch AppUITests */, 6571444D2C1E438700C8A1F3 /* NotificationService */, DA8B15502F3BB74B007753FD /* openHABWatchTests */, + 6116C7DDD25EE245FE191A47 /* openHABWidgetExtension */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ - 4D6470D12561F935007B03FC /* Resources */ = { + 6571444C2C1E438700C8A1F3 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; - 6571444C2C1E438700C8A1F3 /* Resources */ = { + 933D7F0222E7015000621A03 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; - 933D7F0222E7015000621A03 /* Resources */ = { + BA216D3704F6BDD838E96877 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( @@ -864,7 +1042,6 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - 2F2150A22F75C434001BA057 /* Intents.intentdefinition in Resources */, 6557AF8F2C0241C10094D0C8 /* PrivacyInfo.xcprivacy in Resources */, DA817E7A234BF39B00C91824 /* CHANGELOG.md in Resources */, DA4D4DB5233F9ACB00B37E37 /* README.md in Resources */, @@ -896,23 +1073,6 @@ /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ - 4D6470CF2561F935007B03FC /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 2F21508A2F75C2FC001BA057 /* GetItemStateIntentHandler.swift in Sources */, - 2F21508B2F75C2FC001BA057 /* IntentHandler.swift in Sources */, - 2F2150982F75C306001BA057 /* Intents.intentdefinition in Sources */, - 2F21508D2F75C2FC001BA057 /* OpenHABIntentHelper.swift in Sources */, - 2F21508E2F75C2FC001BA057 /* SetColorValueIntentHandler.swift in Sources */, - 2F21508F2F75C2FC001BA057 /* SetContactStateValueIntentHandler.swift in Sources */, - 2F2150902F75C2FC001BA057 /* SetDimmerRollerValueIntentHandler.swift in Sources */, - 2F2150912F75C2FC001BA057 /* SetNumberValueIntentHandler.swift in Sources */, - 2F2150922F75C2FC001BA057 /* SetStringValueIntentHandler.swift in Sources */, - 2F2150932F75C2FC001BA057 /* SetSwitchStateIntentHandler.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; 6571444A2C1E438700C8A1F3 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -966,16 +1126,54 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 5B539FBC26BAFDD1B4DEC425 /* ContactState.swift in Sources */, + 573BCD1FE26AA5788187EF63 /* Home.swift in Sources */, + 7E7B3B323F5B25A467E53507 /* ItemEntity.swift in Sources */, + F811606815878D7012ACB0F5 /* ItemEntityQuery.swift in Sources */, + D67C24016873FD61BF88CEB6 /* ItemIdentifier.swift in Sources */, + 30C55CD81731B44CC2EBA0D6 /* OpenHABAppShortcutsProvider.swift in Sources */, + 9D35698E8462B3AA05093643 /* SwitchAction.swift in Sources */, + 4DD8D7C33C57B1A0B673B221 /* SwitchItemEntity.swift in Sources */, + 7DB6396F170A27B47038CFF9 /* ContactStateIntent.swift in Sources */, + 5F4A972F3F21542F218677EF /* GetItemStateIntent.swift in Sources */, + D2806DCDBD35AC9A75BBD5CA /* SetColorValueIntent.swift in Sources */, + 1C94955D24D0D22516697E9C /* SetDimmerRollerValueIntent.swift in Sources */, + FE1E8BC7D7F471860C3DFF52 /* SetNumberValueIntent.swift in Sources */, + DB4AC5BC3C45D4A9AFDC4FD2 /* SetStringValueIntent.swift in Sources */, + AFCA75B7032CED93471FA489 /* SetSwitchItemIntent.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F706ED1570DF7DE91D2D6056 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 1CA59BC8C2ECF37BF2A70780 /* ContactState.swift in Sources */, + 1CC8CADFF909E48BBD6EBC7B /* Home.swift in Sources */, + 2571672ED4A60F95506B4D92 /* ItemEntity.swift in Sources */, + 7B41E21D01029585A03F711F /* ItemEntityQuery.swift in Sources */, + 7624B6BA0EEF12ACAA0611E4 /* ItemIdentifier.swift in Sources */, + 06C2B76B8F58F04FA6B785AC /* OpenHABAppShortcutsProvider.swift in Sources */, + 43A66B27B252B9239BA2FB30 /* SwitchAction.swift in Sources */, + 66AC88669C02CEC625555579 /* SwitchItemEntity.swift in Sources */, + 8903433ABC95021BB53F0446 /* ContactStateIntent.swift in Sources */, + 79168B9B7C119E095AE5C0F8 /* GetItemStateIntent.swift in Sources */, + AC614D177A98492FAC5396BC /* SetColorValueIntent.swift in Sources */, + 4A7298F5D24042099F10A59C /* SetDimmerRollerValueIntent.swift in Sources */, + F6828FBB52E6C50B8CE2EF9F /* SetNumberValueIntent.swift in Sources */, + 57BADF3F73A8E5F06721234D /* SetStringValueIntent.swift in Sources */, + 2010EBB1D7959601DE8690B2 /* SetSwitchItemIntent.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ - 4D6470D92561F935007B03FC /* PBXTargetDependency */ = { + 50C8570F40A5885164DA68E6 /* PBXTargetDependency */ = { isa = PBXTargetDependency; - target = 4D6470D22561F935007B03FC /* openHABIntents */; - targetProxy = 4D6470D82561F935007B03FC /* PBXContainerItemProxy */; + name = openHABWidgetExtension; + target = 6116C7DDD25EE245FE191A47 /* openHABWidgetExtension */; + targetProxy = 1124E731EFDB779420004716 /* PBXContainerItemProxy */; }; 657144542C1E438700C8A1F3 /* PBXTargetDependency */ = { isa = PBXTargetDependency; @@ -1014,92 +1212,35 @@ }; /* End PBXTargetDependency section */ -/* Begin PBXVariantGroup section */ - 2F2150972F75C306001BA057 /* Intents.intentdefinition */ = { - isa = PBXVariantGroup; - children = ( - 2F2150962F75C306001BA057 /* Base */, - 2F2150992F75C325001BA057 /* en */, - 2F21509A2F75C327001BA057 /* nl */, - 2F21509B2F75C329001BA057 /* fi */, - 2F21509C2F75C32B001BA057 /* fr */, - 2F21509D2F75C32D001BA057 /* de */, - 2F21509E2F75C32F001BA057 /* it */, - 2F21509F2F75C331001BA057 /* nb */, - 2F2150A02F75C333001BA057 /* ru */, - 2F2150A12F75C335001BA057 /* es */, - ); - name = Intents.intentdefinition; - sourceTree = ""; - }; -/* End PBXVariantGroup section */ - /* Begin XCBuildConfiguration section */ - 4D6470DC2561F935007B03FC /* Debug */ = { + 302B3CB364B7849B15A8EF86 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CODE_SIGN_ENTITLEMENTS = openHABIntents/openHABIntents.entitlements; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - INFOPLIST_FILE = openHABIntents/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 16.0; + ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; + CLANG_ENABLE_OBJC_WEAK = NO; + CODE_SIGN_ENTITLEMENTS = openHABWidget/OpenHABWidget.entitlements; + CURRENT_PROJECT_VERSION = 104; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = openHABWidget/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = openHABWidget; + INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2025 openHAB e.V. All rights reserved."; + IPHONEOS_DEPLOYMENT_TARGET = 26.2; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; - MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = org.openhab.app.openHABIntents; + MARKETING_VERSION = 3.1.47; + PRODUCT_BUNDLE_IDENTIFIER = org.openhab.app.openHABWidget; PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; }; - 4D6470DD2561F935007B03FC /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CODE_SIGN_ENTITLEMENTS = openHABIntents/openHABIntents.entitlements; - CODE_SIGN_IDENTITY = "Apple Distribution"; - CODE_SIGN_STYLE = Manual; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - INFOPLIST_FILE = openHABIntents/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 16.0; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@executable_path/../../Frameworks", - ); - MTL_ENABLE_DEBUG_INFO = NO; - MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = org.openhab.app.openHABIntents; - PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE_SPECIFIER = "match AppStore org.openhab.app.openHABIntents"; - SKIP_INSTALL = YES; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Release; - }; 657144562C1E438700C8A1F3 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -1175,6 +1316,35 @@ }; name = Release; }; + 7BB32D6084716CB8C70925BA /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; + CLANG_ENABLE_OBJC_WEAK = NO; + CODE_SIGN_ENTITLEMENTS = openHABWidget/OpenHABWidget.entitlements; + CURRENT_PROJECT_VERSION = 104; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = openHABWidget/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = openHABWidget; + INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2025 openHAB e.V. All rights reserved."; + IPHONEOS_DEPLOYMENT_TARGET = 26.2; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = 3.1.47; + PRODUCT_BUNDLE_IDENTIFIER = org.openhab.app.openHABWidget; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; 933D7F0B22E7015100621A03 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -1846,20 +2016,20 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ - 4D6470DB2561F935007B03FC /* Build configuration list for PBXNativeTarget "openHABIntents" */ = { + 657144582C1E438700C8A1F3 /* Build configuration list for PBXNativeTarget "NotificationService" */ = { isa = XCConfigurationList; buildConfigurations = ( - 4D6470DC2561F935007B03FC /* Debug */, - 4D6470DD2561F935007B03FC /* Release */, + 657144562C1E438700C8A1F3 /* Debug */, + 657144572C1E438700C8A1F3 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - 657144582C1E438700C8A1F3 /* Build configuration list for PBXNativeTarget "NotificationService" */ = { + 7E431775FC423F4117A62BF2 /* Build configuration list for PBXNativeTarget "openHABWidgetExtension" */ = { isa = XCConfigurationList; buildConfigurations = ( - 657144562C1E438700C8A1F3 /* Debug */, - 657144572C1E438700C8A1F3 /* Release */, + 7BB32D6084716CB8C70925BA /* Release */, + 302B3CB364B7849B15A8EF86 /* Debug */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; @@ -2029,6 +2199,11 @@ /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ + 2247FFEA7206BFFC71AB02C4 /* OpenHABCore */ = { + isa = XCSwiftPackageProductDependency; + package = 3A009E9A2F63003B00A0490E /* XCLocalSwiftPackageReference "CommonUI" */; + productName = OpenHABCore; + }; 2F399AC82F54599400F72A30 /* Flow */ = { isa = XCSwiftPackageProductDependency; package = 2F399AC72F54599400F72A30 /* XCRemoteSwiftPackageReference "SwiftUI-Flow" */; @@ -2085,15 +2260,6 @@ package = 937E4483270B379900A98C26 /* XCRemoteSwiftPackageReference "DeviceKit" */; productName = DeviceKit; }; - 937E4491270B37FE00A98C26 /* Kingfisher */ = { - isa = XCSwiftPackageProductDependency; - package = 937E4486270B37A600A98C26 /* XCRemoteSwiftPackageReference "Kingfisher" */; - productName = Kingfisher; - }; - 937E44E1270B393C00A98C26 /* OpenHABCore */ = { - isa = XCSwiftPackageProductDependency; - productName = OpenHABCore; - }; 93F8063427AE6C620035A6B0 /* FirebaseCrashlytics */ = { isa = XCSwiftPackageProductDependency; package = 93F8063327AE6C620035A6B0 /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */; @@ -2124,17 +2290,17 @@ package = DA2C4FD32B4F573300D1C533 /* XCRemoteSwiftPackageReference "SDWebImageSVGCoder" */; productName = SDWebImageSVGCoder; }; - DA7ACD5E2DC3DB130055CFC7 /* SFSafeSymbols */ = { + DA9A7EFC2D668D5900824156 /* SFSafeSymbols */ = { isa = XCSwiftPackageProductDependency; package = DA3B75AC2C59729200E219AB /* XCRemoteSwiftPackageReference "SFSafeSymbols" */; productName = SFSafeSymbols; }; - DA9A7EFC2D668D5900824156 /* SFSafeSymbols */ = { + DA9A7EFE2D66915900824156 /* SFSafeSymbols */ = { isa = XCSwiftPackageProductDependency; package = DA3B75AC2C59729200E219AB /* XCRemoteSwiftPackageReference "SFSafeSymbols" */; productName = SFSafeSymbols; }; - DA9A7EFE2D66915900824156 /* SFSafeSymbols */ = { + 1F9D3602163D4BEBB384DDEE /* SFSafeSymbols */ = { isa = XCSwiftPackageProductDependency; package = DA3B75AC2C59729200E219AB /* XCRemoteSwiftPackageReference "SFSafeSymbols" */; productName = SFSafeSymbols; @@ -2146,6 +2312,7 @@ }; DAC949F92E219F0D007E67B7 /* CommonUI */ = { isa = XCSwiftPackageProductDependency; + package = 3A009E9A2F63003B00A0490E /* XCLocalSwiftPackageReference "CommonUI" */; productName = CommonUI; }; DAC949FB2E219F30007E67B7 /* CommonUI */ = { @@ -2165,6 +2332,11 @@ isa = XCSwiftPackageProductDependency; productName = SDWebImage; }; + E92D1BEE08AD7DCB6F993340 /* SDWebImageSVGCoder */ = { + isa = XCSwiftPackageProductDependency; + package = DA2C4FD32B4F573300D1C533 /* XCRemoteSwiftPackageReference "SDWebImageSVGCoder" */; + productName = SDWebImageSVGCoder; + }; /* End XCSwiftPackageProductDependency section */ }; rootObject = DFB2621F18830A3600D3244D /* Project object */; diff --git a/openHAB.xcworkspace/xcshareddata/swiftpm/Package.resolved b/openHAB.xcworkspace/xcshareddata/swiftpm/Package.resolved index d80759747..6e7a94d43 100644 --- a/openHAB.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/openHAB.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "668dbf2009df850e4ef082e371adec396e99832c3e3a6ccfdf654b717dc3b002", + "originHash" : "a3d6a84b02c1b8834fd269bf6ba9ea8ea281c9fb3127ec89a34283699d19a7b2", "pins" : [ { "identity" : "abseil-cpp-binary", diff --git a/openHAB/AppDelegate.swift b/openHAB/AppDelegate.swift index 98f66806e..67f6b298c 100644 --- a/openHAB/AppDelegate.swift +++ b/openHAB/AppDelegate.swift @@ -119,6 +119,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate { ScreenSaverManager.shared.startMonitoring(window: keyWindow, configuration: config) } + // Start monitoring items for widget updates after the app is configured. + WidgetItemMonitor.shared.startMonitoring() } @MainActor @@ -274,6 +276,11 @@ extension AppDelegate { ScreenSaverManager.shared.startMonitoring(window: keyWindow, configuration: config) } + + // Clean up stale widget items when app becomes active + Task { + await WidgetItemMonitor.shared.cleanupStaleItems() + } } func applicationWillTerminate(_ application: UIApplication) { diff --git a/openHAB/Images/Images.xcassets/openHABIcon.imageset/Contents.json b/openHAB/Images/Images.xcassets/openHABIcon.imageset/Contents.json index 3dd040950..783b06466 100644 --- a/openHAB/Images/Images.xcassets/openHABIcon.imageset/Contents.json +++ b/openHAB/Images/Images.xcassets/openHABIcon.imageset/Contents.json @@ -1,22 +1,15 @@ { "images" : [ { - "idiom" : "universal", - "filename" : "oh_logo_only.pdf" - }, - { - "idiom" : "universal", "filename" : "oh_logo_only_dark.pdf", - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ] + "idiom" : "universal" } ], "info" : { - "version" : 1, - "author" : "xcode" + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true } -} \ No newline at end of file +} diff --git a/openHAB/WidgetItemMonitor.swift b/openHAB/WidgetItemMonitor.swift new file mode 100644 index 000000000..7975f22bf --- /dev/null +++ b/openHAB/WidgetItemMonitor.swift @@ -0,0 +1,132 @@ +// 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 Foundation +import OpenHABCore +import os.log +import WidgetKit + +/// Monitors openHAB items that have active widgets and reloads widgets when items change +@MainActor +class WidgetItemMonitor { + static let shared = WidgetItemMonitor() + + private var monitoringTask: Task? + private var streamTask: Task? + private var isMonitoring = false + + private init() {} + + /// Start monitoring items that have widgets + func startMonitoring() { + guard !isMonitoring else { + return + } + + isMonitoring = true + Logger.widgets.info("Starting widget item monitoring") + + // Start network monitoring for SSE connection + monitoringTask = Task { + await ItemEventStream.startMonitoringNetwork() + } + + // Start listening to item state changes + // Run on background thread to avoid MainActor blocking + streamTask = Task.detached { [weak self] in + guard let self else { return } + + // Update tracked items initially + await updateTrackedItems() + + // Listen for state change events + for await message in await ItemEventStream.shared.stream() { + await handle(message) + } + } + } + + /// Stop monitoring + func stopMonitoring() { + guard isMonitoring else { return } + + Logger.widgets.info("Stopping widget item monitoring") + isMonitoring = false + + monitoringTask?.cancel() + streamTask?.cancel() + monitoringTask = nil + streamTask = nil + } + + /// Manually refresh the list of tracked items (call when widgets are added/removed) + func refreshTrackedItems() async { + await updateTrackedItems() + } + + /// Clean up stale widget items that haven't been refreshed recently + /// Widgets refresh every 5 minutes, so items not updated in 1+ hours are likely deleted + func cleanupStaleItems() async { + let removedCount = await WidgetItemRegistry.shared.removeStaleItems(olderThan: 3600) // 1 hour + + if removedCount > 0 { + Logger.widgets.info("Cleaned up \(removedCount) stale widget items") + await updateTrackedItems() + } + } + + private func updateTrackedItems() async { + let itemNames = await WidgetItemRegistry.shared.getAllItemNames() + + guard !itemNames.isEmpty else { + await ItemEventStream.trackItems([]) + return + } + + Logger.widgets.info("Tracking \(itemNames.count) widget items: \(itemNames.joined(separator: ", "))") + await ItemEventStream.trackItems(itemNames) + } + + private func handle(_ message: StreamOutput) async { + switch message { + case .connected: + await updateTrackedItems() + + case let .event(event): + switch event { + case let .state(item, state): + Logger.widgets.info("Item '\(item)' changed to '\(state)' - reloading widgets") + await reloadWidgets(for: item) + + case .ready: + await updateTrackedItems() + + case .alive: + break + + case let .unknown(raw): + Logger.widgets.warning("Unknown SSE message: \(raw)") + } + + case let .disconnected(error): + if let error { + Logger.widgets.warning("SSE disconnected: \(error.localizedDescription)") + } + } + } + + private func reloadWidgets(for itemName: String) async { + // Reload all switch and sensor widgets + // Note: This reloads all widgets of these types, not just the specific item + WidgetCenter.shared.reloadTimelines(ofKind: "OpenHABSwitchWidget") + WidgetCenter.shared.reloadTimelines(ofKind: "OpenHABSensorWidget") + } +} diff --git a/openHABIntents/Base.lproj/Intents.intentdefinition b/openHABIntents/Base.lproj/Intents.intentdefinition deleted file mode 100644 index 7a7156bc3..000000000 --- a/openHABIntents/Base.lproj/Intents.intentdefinition +++ /dev/null @@ -1,2060 +0,0 @@ - - - - - INEnums - - INIntentDefinitionModelVersion - 1.2 - INIntentDefinitionNamespace - aK4nIm - INIntentDefinitionSystemVersion - 25D2128 - INIntentDefinitionToolsBuildVersion - 17C529 - INIntentDefinitionToolsVersion - 26.3 - INIntents - - - INIntentCategory - request - INIntentClassPrefix - OpenHAB - INIntentConfigurable - - INIntentDescription - Retrieve the current state of an item - INIntentDescriptionID - GD4RTw - INIntentIneligibleForSuggestions - - INIntentKeyParameter - item - INIntentLastParameterTag - 5 - INIntentManagedParameterCombinations - - item,home - - INIntentParameterCombinationSupportsBackgroundExecution - - INIntentParameterCombinationTitle - Get ${item} State - INIntentParameterCombinationTitleID - m9yAKr - INIntentParameterCombinationUpdatesLinked - - - - INIntentName - GetItemState - INIntentParameters - - - INIntentParameterConfigurable - - INIntentParameterDisplayName - Item - INIntentParameterDisplayNameID - cBrxnz - INIntentParameterDisplayPriority - 1 - INIntentParameterMetadata - - INIntentParameterMetadataCapitalization - Sentences - INIntentParameterMetadataDefaultValueID - WSKSgZ - - INIntentParameterName - item - INIntentParameterPromptDialogs - - - INIntentParameterPromptDialogCustom - - INIntentParameterPromptDialogFormatString - Item Name - INIntentParameterPromptDialogFormatStringID - NehcMF - INIntentParameterPromptDialogType - Configuration - - - INIntentParameterPromptDialogCustom - - INIntentParameterPromptDialogType - Primary - - - INIntentParameterSupportsDynamicEnumeration - - INIntentParameterTag - 1 - INIntentParameterType - String - - - INIntentParameterConfigurable - - INIntentParameterCustomDisambiguation - - INIntentParameterDisplayName - home - INIntentParameterDisplayNameID - k6pj4o - INIntentParameterDisplayPriority - 2 - INIntentParameterName - home - INIntentParameterObjectType - Home - INIntentParameterObjectTypeNamespace - aK4nIm - INIntentParameterPromptDialogs - - - INIntentParameterPromptDialogCustom - - INIntentParameterPromptDialogFormatString - Home name - INIntentParameterPromptDialogFormatStringID - P9kblb - INIntentParameterPromptDialogType - Configuration - - - INIntentParameterPromptDialogCustom - - INIntentParameterPromptDialogFormatString - Home name - INIntentParameterPromptDialogFormatStringID - rsrXB9 - INIntentParameterPromptDialogType - Primary - - - INIntentParameterPromptDialogCustom - - INIntentParameterPromptDialogFormatString - There are ${count} configured homes with an item named '${item}'’. - INIntentParameterPromptDialogFormatStringID - 7BE642 - INIntentParameterPromptDialogType - DisambiguationIntroduction - - - INIntentParameterPromptDialogCustom - - INIntentParameterPromptDialogFormatString - For which home do you want to get the value? - INIntentParameterPromptDialogFormatStringID - 4WCzFG - INIntentParameterPromptDialogType - DisambiguationSelection - - - INIntentParameterPromptDialogCustom - - INIntentParameterPromptDialogFormatString - Just to confirm, you wanted ‘${home}’? - INIntentParameterPromptDialogFormatStringID - iKSmqU - INIntentParameterPromptDialogType - Confirmation - - - INIntentParameterSupportsDynamicEnumeration - - INIntentParameterSupportsResolution - - INIntentParameterTag - 5 - INIntentParameterType - Object - - - INIntentResponse - - INIntentResponseCodes - - - INIntentResponseCodeFormatString - The state of ${item} is ${state} - INIntentResponseCodeFormatStringID - 6hUiXZ - INIntentResponseCodeName - success - INIntentResponseCodeSuccess - - - - INIntentResponseCodeName - failure - - - INIntentResponseCodeFormatString - Sorry can't find ${item} - INIntentResponseCodeFormatStringID - hflKDd - INIntentResponseCodeName - failureInvalidItem - - - INIntentResponseLastParameterTag - 2 - INIntentResponseOutput - state - INIntentResponseParameters - - - INIntentResponseParameterDisplayPriority - 1 - INIntentResponseParameterName - item - INIntentResponseParameterTag - 1 - INIntentResponseParameterType - String - - - INIntentResponseParameterDisplayName - State - INIntentResponseParameterDisplayNameID - m5DFXq - INIntentResponseParameterDisplayPriority - 2 - INIntentResponseParameterName - state - INIntentResponseParameterTag - 2 - INIntentResponseParameterType - String - - - - INIntentTitle - Get Item State - INIntentTitleID - 5Uu4bK - INIntentType - Custom - INIntentVerb - Request - - - INIntentCategory - generic - INIntentClassPrefix - OpenHAB - INIntentConfigurable - - INIntentDescription - Set the state of a switch on or off - INIntentDescriptionID - oOjAU0 - INIntentKeyParameter - item - INIntentLastParameterTag - 4 - INIntentManagedParameterCombinations - - item,action,home - - INIntentParameterCombinationSupportsBackgroundExecution - - INIntentParameterCombinationTitle - Send ${action} to ${item} - INIntentParameterCombinationTitleID - oUolm5 - INIntentParameterCombinationUpdatesLinked - - - - INIntentName - SetSwitchState - INIntentParameterCombinations - - item,action,home - - INIntentParameterCombinationIsLinked - - INIntentParameterCombinationIsPrimary - - INIntentParameterCombinationSupportsBackgroundExecution - - INIntentParameterCombinationTitle - Send ${action} to ${item} - INIntentParameterCombinationTitleID - jyXazc - - - INIntentParameters - - - INIntentParameterConfigurable - - INIntentParameterDisplayName - Item - INIntentParameterDisplayNameID - M8mX5O - INIntentParameterDisplayPriority - 1 - INIntentParameterMetadata - - INIntentParameterMetadataCapitalization - Sentences - INIntentParameterMetadataDefaultValueID - h0kR74 - - INIntentParameterName - item - INIntentParameterPromptDialogs - - - INIntentParameterPromptDialogCustom - - INIntentParameterPromptDialogFormatString - Switch name - INIntentParameterPromptDialogFormatStringID - lqlrGx - INIntentParameterPromptDialogType - Configuration - - - INIntentParameterPromptDialogCustom - - INIntentParameterPromptDialogType - Primary - - - INIntentParameterSupportsDynamicEnumeration - - INIntentParameterTag - 1 - INIntentParameterType - String - - - INIntentParameterConfigurable - - INIntentParameterDisplayName - Action - INIntentParameterDisplayNameID - i8fP5e - INIntentParameterDisplayPriority - 2 - INIntentParameterMetadata - - INIntentParameterMetadataCapitalization - Sentences - INIntentParameterMetadataDefaultValueID - L1xpr9 - - INIntentParameterName - action - INIntentParameterPromptDialogs - - - INIntentParameterPromptDialogCustom - - INIntentParameterPromptDialogFormatString - Action - INIntentParameterPromptDialogFormatStringID - 1GaCDV - INIntentParameterPromptDialogType - Configuration - - - INIntentParameterPromptDialogCustom - - INIntentParameterPromptDialogType - Primary - - - INIntentParameterSupportsDynamicEnumeration - - INIntentParameterTag - 2 - INIntentParameterType - String - - - INIntentParameterConfigurable - - INIntentParameterCustomDisambiguation - - INIntentParameterDisplayName - home - INIntentParameterDisplayNameID - X9cN9h - INIntentParameterDisplayPriority - 3 - INIntentParameterName - home - INIntentParameterObjectType - Home - INIntentParameterObjectTypeNamespace - aK4nIm - INIntentParameterPromptDialogs - - - INIntentParameterPromptDialogCustom - - INIntentParameterPromptDialogFormatString - Home name - INIntentParameterPromptDialogFormatStringID - v3hsWV - INIntentParameterPromptDialogType - Configuration - - - INIntentParameterPromptDialogCustom - - INIntentParameterPromptDialogType - Primary - - - INIntentParameterPromptDialogCustom - - INIntentParameterPromptDialogFormatString - There are ${count} configured homes with an item named '${item}'’. - INIntentParameterPromptDialogFormatStringID - YUoeAL - INIntentParameterPromptDialogType - DisambiguationIntroduction - - - INIntentParameterPromptDialogCustom - - INIntentParameterPromptDialogFormatString - For which home do you want to get the value? - INIntentParameterPromptDialogFormatStringID - h91G9C - INIntentParameterPromptDialogType - DisambiguationSelection - - - INIntentParameterPromptDialogCustom - - INIntentParameterPromptDialogFormatString - Just to confirm, you wanted ‘${home}’? - INIntentParameterPromptDialogFormatStringID - Jn2aNw - INIntentParameterPromptDialogType - Confirmation - - - INIntentParameterSupportsDynamicEnumeration - - INIntentParameterSupportsResolution - - INIntentParameterTag - 4 - INIntentParameterType - Object - - - INIntentResponse - - INIntentResponseCodes - - - INIntentResponseCodeFormatString - Sent the action of ${action} to switch ${item} - INIntentResponseCodeFormatStringID - Fb0pUB - INIntentResponseCodeName - success - INIntentResponseCodeSuccess - - - - INIntentResponseCodeName - failure - - - INIntentResponseCodeFormatString - Sorry can't find ${item} - INIntentResponseCodeFormatStringID - 80K2Dt - INIntentResponseCodeName - failureInvalidItem - - - INIntentResponseCodeFormatString - Action invalid: ${action} for ${item} - INIntentResponseCodeFormatStringID - iCUEet - INIntentResponseCodeName - failureInvalidAction - - - INIntentResponseLastParameterTag - 4 - INIntentResponseParameters - - - INIntentResponseParameterDisplayName - Item - INIntentResponseParameterDisplayNameID - KXqi2s - INIntentResponseParameterDisplayPriority - 1 - INIntentResponseParameterName - item - INIntentResponseParameterTag - 3 - INIntentResponseParameterType - String - - - INIntentResponseParameterDisplayName - Action - INIntentResponseParameterDisplayNameID - TJ1p5Y - INIntentResponseParameterDisplayPriority - 2 - INIntentResponseParameterName - action - INIntentResponseParameterTag - 4 - INIntentResponseParameterType - String - - - - INIntentTitle - Set Switch State - INIntentTitleID - XMNs7r - INIntentType - Custom - INIntentVerb - Do - - - INIntentCategory - generic - INIntentClassPrefix - OpenHAB - INIntentConfigurable - - INIntentDescription - Set the integer value of a dimmer or roller shutter - INIntentDescriptionID - pg4Bec - INIntentKeyParameter - item - INIntentLastParameterTag - 5 - INIntentManagedParameterCombinations - - item,value,home - - INIntentParameterCombinationSupportsBackgroundExecution - - INIntentParameterCombinationTitle - Set ${item} to ${value} - INIntentParameterCombinationTitleID - MGYSky - INIntentParameterCombinationUpdatesLinked - - - - INIntentName - SetDimmerRollerValue - INIntentParameterCombinations - - item,value,home - - INIntentParameterCombinationIsLinked - - INIntentParameterCombinationIsPrimary - - INIntentParameterCombinationSupportsBackgroundExecution - - INIntentParameterCombinationTitle - Set ${item} to ${value} - INIntentParameterCombinationTitleID - Ch9Akw - - - INIntentParameters - - - INIntentParameterConfigurable - - INIntentParameterDisplayName - Item - INIntentParameterDisplayNameID - 1USOx9 - INIntentParameterDisplayPriority - 1 - INIntentParameterMetadata - - INIntentParameterMetadataCapitalization - Sentences - INIntentParameterMetadataDefaultValueID - 99zKKs - - INIntentParameterName - item - INIntentParameterPromptDialogs - - - INIntentParameterPromptDialogCustom - - INIntentParameterPromptDialogFormatString - Dimmer/Roller Name - INIntentParameterPromptDialogFormatStringID - 68Y2Ya - INIntentParameterPromptDialogType - Configuration - - - INIntentParameterPromptDialogCustom - - INIntentParameterPromptDialogType - Primary - - - INIntentParameterSupportsDynamicEnumeration - - INIntentParameterTag - 1 - INIntentParameterType - String - - - INIntentParameterConfigurable - - INIntentParameterDisplayName - Value - INIntentParameterDisplayNameID - mAKsWc - INIntentParameterDisplayPriority - 2 - INIntentParameterMetadata - - INIntentParameterMetadataMaximumValue - 100 - INIntentParameterMetadataType - Field - - INIntentParameterName - value - INIntentParameterPromptDialogs - - - INIntentParameterPromptDialogCustom - - INIntentParameterPromptDialogType - Configuration - - - INIntentParameterPromptDialogCustom - - INIntentParameterPromptDialogType - Primary - - - INIntentParameterTag - 3 - INIntentParameterType - Integer - - - INIntentParameterConfigurable - - INIntentParameterCustomDisambiguation - - INIntentParameterDisplayName - home - INIntentParameterDisplayNameID - ZuBsOg - INIntentParameterDisplayPriority - 3 - INIntentParameterName - home - INIntentParameterObjectType - Home - INIntentParameterObjectTypeNamespace - aK4nIm - INIntentParameterPromptDialogs - - - INIntentParameterPromptDialogCustom - - INIntentParameterPromptDialogFormatString - Home name - INIntentParameterPromptDialogFormatStringID - rVXwR2 - INIntentParameterPromptDialogType - Configuration - - - INIntentParameterPromptDialogCustom - - INIntentParameterPromptDialogType - Primary - - - INIntentParameterPromptDialogCustom - - INIntentParameterPromptDialogFormatString - There are ${count} configured homes with an item named '${item}'’. - INIntentParameterPromptDialogFormatStringID - BaNYuS - INIntentParameterPromptDialogType - DisambiguationIntroduction - - - INIntentParameterPromptDialogCustom - - INIntentParameterPromptDialogFormatString - For which home do you want to get the value? - INIntentParameterPromptDialogFormatStringID - QATq1i - INIntentParameterPromptDialogType - DisambiguationSelection - - - INIntentParameterPromptDialogCustom - - INIntentParameterPromptDialogFormatString - Just to confirm, you wanted ‘${home}’? - INIntentParameterPromptDialogFormatStringID - exrmKW - INIntentParameterPromptDialogType - Confirmation - - - INIntentParameterSupportsDynamicEnumeration - - INIntentParameterSupportsResolution - - INIntentParameterTag - 5 - INIntentParameterType - Object - - - INIntentResponse - - INIntentResponseCodes - - - INIntentResponseCodeFormatString - Sent the value of ${value} to ${item} - INIntentResponseCodeFormatStringID - wm0fqx - INIntentResponseCodeName - success - INIntentResponseCodeSuccess - - - - INIntentResponseCodeName - failure - - - INIntentResponseCodeFormatString - Sorry can't find ${item} - INIntentResponseCodeFormatStringID - N5YsnB - INIntentResponseCodeName - failureInvalidItem - - - INIntentResponseCodeFormatString - Invalid empty value for ${item} - INIntentResponseCodeFormatStringID - iRjffC - INIntentResponseCodeName - failureEmptyValue - - - INIntentResponseCodeFormatString - Invalid value ${value} for ${item} (0-100) - INIntentResponseCodeFormatStringID - V0QsN4 - INIntentResponseCodeName - failureInvalidValue - - - INIntentResponseLastParameterTag - 3 - INIntentResponseParameters - - - INIntentResponseParameterDisplayName - Item - INIntentResponseParameterDisplayNameID - Nuhhxp - INIntentResponseParameterDisplayPriority - 1 - INIntentResponseParameterName - item - INIntentResponseParameterTag - 1 - INIntentResponseParameterType - String - - - INIntentResponseParameterDisplayName - Value - INIntentResponseParameterDisplayNameID - tuvmDw - INIntentResponseParameterDisplayPriority - 2 - INIntentResponseParameterName - value - INIntentResponseParameterTag - 3 - INIntentResponseParameterType - Integer - - - - INIntentTitle - Set Dimmer or Roller Shutter Value - INIntentTitleID - UZYwuH - INIntentType - Custom - INIntentVerb - Do - - - INIntentCategory - generic - INIntentClassPrefix - OpenHAB - INIntentConfigurable - - INIntentDescription - Set the decimal value of a number control item - INIntentDescriptionID - pptfkD - INIntentKeyParameter - item - INIntentLastParameterTag - 6 - INIntentManagedParameterCombinations - - item,value,home - - INIntentParameterCombinationSupportsBackgroundExecution - - INIntentParameterCombinationTitle - Set ${item} to ${value} - INIntentParameterCombinationTitleID - mVz5mJ - INIntentParameterCombinationUpdatesLinked - - - - INIntentName - SetNumberValue - INIntentParameterCombinations - - item,value,home - - INIntentParameterCombinationIsLinked - - INIntentParameterCombinationIsPrimary - - INIntentParameterCombinationSupportsBackgroundExecution - - INIntentParameterCombinationTitle - Set ${item} to ${value} - INIntentParameterCombinationTitleID - qYPBPC - - - INIntentParameters - - - INIntentParameterConfigurable - - INIntentParameterDisplayName - Item - INIntentParameterDisplayNameID - tclrCp - INIntentParameterDisplayPriority - 1 - INIntentParameterMetadata - - INIntentParameterMetadataCapitalization - Sentences - INIntentParameterMetadataDefaultValueID - 99zKKs - - INIntentParameterName - item - INIntentParameterPromptDialogs - - - INIntentParameterPromptDialogCustom - - INIntentParameterPromptDialogFormatString - Dimmer/Roller Name - INIntentParameterPromptDialogFormatStringID - 68Y2Ya - INIntentParameterPromptDialogType - Configuration - - - INIntentParameterPromptDialogCustom - - INIntentParameterPromptDialogType - Primary - - - INIntentParameterSupportsDynamicEnumeration - - INIntentParameterTag - 1 - INIntentParameterType - String - - - INIntentParameterConfigurable - - INIntentParameterDisplayName - Value - INIntentParameterDisplayNameID - dtRamI - INIntentParameterDisplayPriority - 2 - INIntentParameterMetadata - - INIntentParameterMetadataMaximumValue - 100 - INIntentParameterMetadataType - Field - - INIntentParameterName - value - INIntentParameterPromptDialogs - - - INIntentParameterPromptDialogCustom - - INIntentParameterPromptDialogType - Configuration - - - INIntentParameterPromptDialogCustom - - INIntentParameterPromptDialogType - Primary - - - INIntentParameterTag - 4 - INIntentParameterType - Decimal - - - INIntentParameterConfigurable - - INIntentParameterCustomDisambiguation - - INIntentParameterDisplayName - home - INIntentParameterDisplayNameID - zLlY3g - INIntentParameterDisplayPriority - 3 - INIntentParameterName - home - INIntentParameterObjectType - Home - INIntentParameterObjectTypeNamespace - aK4nIm - INIntentParameterPromptDialogs - - - INIntentParameterPromptDialogCustom - - INIntentParameterPromptDialogFormatString - Home name - INIntentParameterPromptDialogFormatStringID - IS2nO0 - INIntentParameterPromptDialogType - Configuration - - - INIntentParameterPromptDialogCustom - - INIntentParameterPromptDialogType - Primary - - - INIntentParameterPromptDialogCustom - - INIntentParameterPromptDialogFormatString - There are ${count} configured homes with an item named '${item}'’. - INIntentParameterPromptDialogFormatStringID - lLFYtM - INIntentParameterPromptDialogType - DisambiguationIntroduction - - - INIntentParameterPromptDialogCustom - - INIntentParameterPromptDialogFormatString - For which home do you want to get the value? - INIntentParameterPromptDialogFormatStringID - jbUbYw - INIntentParameterPromptDialogType - DisambiguationSelection - - - INIntentParameterPromptDialogCustom - - INIntentParameterPromptDialogFormatString - Just to confirm, you wanted ‘${home}’? - INIntentParameterPromptDialogFormatStringID - VEApie - INIntentParameterPromptDialogType - Confirmation - - - INIntentParameterSupportsDynamicEnumeration - - INIntentParameterSupportsResolution - - INIntentParameterTag - 6 - INIntentParameterType - Object - - - INIntentResponse - - INIntentResponseCodes - - - INIntentResponseCodeFormatString - Sent the number ${value} to ${item} - INIntentResponseCodeFormatStringID - CZfT5a - INIntentResponseCodeName - success - INIntentResponseCodeSuccess - - - - INIntentResponseCodeName - failure - - - INIntentResponseCodeFormatString - Sorry can't find ${item} - INIntentResponseCodeFormatStringID - Oazi8d - INIntentResponseCodeName - failureInvalidItem - - - INIntentResponseCodeFormatString - Invalid empty value for ${item} - INIntentResponseCodeFormatStringID - 8OBnvT - INIntentResponseCodeName - failureEmptyValue - - - INIntentResponseLastParameterTag - 4 - INIntentResponseParameters - - - INIntentResponseParameterDisplayName - Item - INIntentResponseParameterDisplayNameID - MCp4x6 - INIntentResponseParameterDisplayPriority - 1 - INIntentResponseParameterName - item - INIntentResponseParameterTag - 1 - INIntentResponseParameterType - String - - - INIntentResponseParameterDisplayName - Value - INIntentResponseParameterDisplayNameID - E291x3 - INIntentResponseParameterDisplayPriority - 2 - INIntentResponseParameterName - value - INIntentResponseParameterTag - 4 - INIntentResponseParameterType - Decimal - - - - INIntentTitle - Set Number Control Value - INIntentTitleID - o35DYr - INIntentType - Custom - INIntentVerb - Do - - - INIntentCategory - generic - INIntentClassPrefix - OpenHAB - INIntentConfigurable - - INIntentDescription - Set the string of a string control item - INIntentDescriptionID - 5B4e9l - INIntentKeyParameter - item - INIntentLastParameterTag - 7 - INIntentManagedParameterCombinations - - item,value,home - - INIntentParameterCombinationSupportsBackgroundExecution - - INIntentParameterCombinationTitle - Set ${item} to ${value} - INIntentParameterCombinationTitleID - SMYYBh - INIntentParameterCombinationUpdatesLinked - - - - INIntentName - SetStringValue - INIntentParameterCombinations - - item,value,home - - INIntentParameterCombinationIsLinked - - INIntentParameterCombinationIsPrimary - - INIntentParameterCombinationSupportsBackgroundExecution - - INIntentParameterCombinationTitle - Set ${item} to ${value} - INIntentParameterCombinationTitleID - 1cBKdm - - - INIntentParameters - - - INIntentParameterConfigurable - - INIntentParameterDisplayName - Item - INIntentParameterDisplayNameID - mpxNDZ - INIntentParameterDisplayPriority - 1 - INIntentParameterMetadata - - INIntentParameterMetadataCapitalization - Sentences - INIntentParameterMetadataDefaultValueID - 99zKKs - - INIntentParameterName - item - INIntentParameterPromptDialogs - - - INIntentParameterPromptDialogCustom - - INIntentParameterPromptDialogFormatString - Dimmer/Roller Name - INIntentParameterPromptDialogFormatStringID - 68Y2Ya - INIntentParameterPromptDialogType - Configuration - - - INIntentParameterPromptDialogCustom - - INIntentParameterPromptDialogType - Primary - - - INIntentParameterSupportsDynamicEnumeration - - INIntentParameterTag - 1 - INIntentParameterType - String - - - INIntentParameterConfigurable - - INIntentParameterDisplayName - Value - INIntentParameterDisplayNameID - 3evvza - INIntentParameterDisplayPriority - 2 - INIntentParameterMetadata - - INIntentParameterMetadataCapitalization - None - INIntentParameterMetadataDefaultValueID - m4EqGd - INIntentParameterMetadataDisableAutocorrect - - INIntentParameterMetadataDisableSmartDashes - - INIntentParameterMetadataDisableSmartQuotes - - - INIntentParameterName - value - INIntentParameterPromptDialogs - - - INIntentParameterPromptDialogCustom - - INIntentParameterPromptDialogType - Configuration - - - INIntentParameterPromptDialogCustom - - INIntentParameterPromptDialogType - Primary - - - INIntentParameterTag - 5 - INIntentParameterType - String - - - INIntentParameterConfigurable - - INIntentParameterCustomDisambiguation - - INIntentParameterDisplayName - home - INIntentParameterDisplayNameID - 71MWtI - INIntentParameterDisplayPriority - 3 - INIntentParameterName - home - INIntentParameterObjectType - Home - INIntentParameterObjectTypeNamespace - aK4nIm - INIntentParameterPromptDialogs - - - INIntentParameterPromptDialogCustom - - INIntentParameterPromptDialogFormatString - Home name - INIntentParameterPromptDialogFormatStringID - YFIIBn - INIntentParameterPromptDialogType - Configuration - - - INIntentParameterPromptDialogCustom - - INIntentParameterPromptDialogType - Primary - - - INIntentParameterPromptDialogCustom - - INIntentParameterPromptDialogFormatString - There are ${count} configured homes with an item named '${item}'’. - INIntentParameterPromptDialogFormatStringID - MIqlld - INIntentParameterPromptDialogType - DisambiguationIntroduction - - - INIntentParameterPromptDialogCustom - - INIntentParameterPromptDialogFormatString - For which home do you want to get the value? - INIntentParameterPromptDialogFormatStringID - 3LMXV8 - INIntentParameterPromptDialogType - DisambiguationSelection - - - INIntentParameterPromptDialogCustom - - INIntentParameterPromptDialogFormatString - Just to confirm, you wanted ‘${home}’? - INIntentParameterPromptDialogFormatStringID - gobcZi - INIntentParameterPromptDialogType - Confirmation - - - INIntentParameterSupportsDynamicEnumeration - - INIntentParameterSupportsResolution - - INIntentParameterTag - 7 - INIntentParameterType - Object - - - INIntentResponse - - INIntentResponseCodes - - - INIntentResponseCodeFormatString - Sent the string ${value} to ${item} - INIntentResponseCodeFormatStringID - JUznKl - INIntentResponseCodeName - success - INIntentResponseCodeSuccess - - - - INIntentResponseCodeName - failure - - - INIntentResponseCodeFormatString - Sorry can't find ${item} - INIntentResponseCodeFormatStringID - NjP70V - INIntentResponseCodeName - failureInvalidItem - - - INIntentResponseCodeFormatString - Invalid empty value for ${item} - INIntentResponseCodeFormatStringID - 67qFTP - INIntentResponseCodeName - failureEmptyValue - - - INIntentResponseLastParameterTag - 5 - INIntentResponseParameters - - - INIntentResponseParameterDisplayName - Item - INIntentResponseParameterDisplayNameID - skEQxZ - INIntentResponseParameterDisplayPriority - 1 - INIntentResponseParameterName - item - INIntentResponseParameterTag - 1 - INIntentResponseParameterType - String - - - INIntentResponseParameterDisplayName - Value - INIntentResponseParameterDisplayNameID - HGbUGw - INIntentResponseParameterDisplayPriority - 2 - INIntentResponseParameterName - value - INIntentResponseParameterTag - 5 - INIntentResponseParameterType - String - - - - INIntentTitle - Set String Control Value - INIntentTitleID - gBn7U6 - INIntentType - Custom - INIntentVerb - Do - - - INIntentCategory - generic - INIntentClassPrefix - OpenHAB - INIntentConfigurable - - INIntentDescription - Set the color of a color control item - INIntentDescriptionID - DxO2wk - INIntentKeyParameter - item - INIntentLastParameterTag - 7 - INIntentManagedParameterCombinations - - item,value,home - - INIntentParameterCombinationSupportsBackgroundExecution - - INIntentParameterCombinationTitle - Set ${item} to ${value} (HSB) - INIntentParameterCombinationTitleID - VmFkwo - INIntentParameterCombinationUpdatesLinked - - - - INIntentName - SetColorValue - INIntentParameterCombinations - - item,value,home - - INIntentParameterCombinationIsLinked - - INIntentParameterCombinationIsPrimary - - INIntentParameterCombinationSupportsBackgroundExecution - - INIntentParameterCombinationTitle - Set ${item} to ${value} (HSB) - INIntentParameterCombinationTitleID - E07xjp - - - INIntentParameters - - - INIntentParameterConfigurable - - INIntentParameterDisplayName - Item - INIntentParameterDisplayNameID - B2I9b5 - INIntentParameterDisplayPriority - 1 - INIntentParameterMetadata - - INIntentParameterMetadataCapitalization - Sentences - INIntentParameterMetadataDefaultValueID - 99zKKs - - INIntentParameterName - item - INIntentParameterPromptDialogs - - - INIntentParameterPromptDialogCustom - - INIntentParameterPromptDialogFormatString - Dimmer/Roller Name - INIntentParameterPromptDialogFormatStringID - 68Y2Ya - INIntentParameterPromptDialogType - Configuration - - - INIntentParameterPromptDialogCustom - - INIntentParameterPromptDialogType - Primary - - - INIntentParameterSupportsDynamicEnumeration - - INIntentParameterTag - 1 - INIntentParameterType - String - - - INIntentParameterConfigurable - - INIntentParameterDisplayName - Value - INIntentParameterDisplayNameID - llL4sP - INIntentParameterDisplayPriority - 2 - INIntentParameterMetadata - - INIntentParameterMetadataCapitalization - None - INIntentParameterMetadataDefaultValue - 240,100,100 - INIntentParameterMetadataDefaultValueID - uGsyyj - INIntentParameterMetadataDisableAutocorrect - - INIntentParameterMetadataDisableSmartDashes - - INIntentParameterMetadataDisableSmartQuotes - - - INIntentParameterName - value - INIntentParameterPromptDialogs - - - INIntentParameterPromptDialogCustom - - INIntentParameterPromptDialogType - Configuration - - - INIntentParameterPromptDialogCustom - - INIntentParameterPromptDialogType - Primary - - - INIntentParameterTag - 5 - INIntentParameterType - String - - - INIntentParameterConfigurable - - INIntentParameterCustomDisambiguation - - INIntentParameterDisplayName - home - INIntentParameterDisplayNameID - iBrALm - INIntentParameterDisplayPriority - 3 - INIntentParameterName - home - INIntentParameterObjectType - Home - INIntentParameterObjectTypeNamespace - aK4nIm - INIntentParameterPromptDialogs - - - INIntentParameterPromptDialogCustom - - INIntentParameterPromptDialogFormatString - Home name - INIntentParameterPromptDialogFormatStringID - eFNxrT - INIntentParameterPromptDialogType - Configuration - - - INIntentParameterPromptDialogCustom - - INIntentParameterPromptDialogType - Primary - - - INIntentParameterPromptDialogCustom - - INIntentParameterPromptDialogFormatString - There are ${count} configured homes with an item named '${item}'’. - INIntentParameterPromptDialogFormatStringID - jP4VSa - INIntentParameterPromptDialogType - DisambiguationIntroduction - - - INIntentParameterPromptDialogCustom - - INIntentParameterPromptDialogFormatString - For which home do you want to get the value? - INIntentParameterPromptDialogFormatStringID - mHbznY - INIntentParameterPromptDialogType - DisambiguationSelection - - - INIntentParameterPromptDialogCustom - - INIntentParameterPromptDialogFormatString - Just to confirm, you wanted ‘${home}’? - INIntentParameterPromptDialogFormatStringID - tkWfxz - INIntentParameterPromptDialogType - Confirmation - - - INIntentParameterSupportsDynamicEnumeration - - INIntentParameterSupportsResolution - - INIntentParameterTag - 7 - INIntentParameterType - Object - - - INIntentResponse - - INIntentResponseCodes - - - INIntentResponseCodeFormatString - Sent the color value of ${value} to ${item} - INIntentResponseCodeFormatStringID - TK8Rrn - INIntentResponseCodeName - success - INIntentResponseCodeSuccess - - - - INIntentResponseCodeName - failure - - - INIntentResponseCodeFormatString - Sorry can't find ${item} - INIntentResponseCodeFormatStringID - ptGHON - INIntentResponseCodeName - failureInvalidItem - - - INIntentResponseCodeFormatString - Invalid value: ${value} for ${item} must be HSB (0-360,0-100,0-100) - INIntentResponseCodeFormatStringID - raIAR6 - INIntentResponseCodeName - failureInvalidValue - - - INIntentResponseLastParameterTag - 5 - INIntentResponseParameters - - - INIntentResponseParameterDisplayName - Item - INIntentResponseParameterDisplayNameID - VTeXc1 - INIntentResponseParameterDisplayPriority - 1 - INIntentResponseParameterName - item - INIntentResponseParameterTag - 1 - INIntentResponseParameterType - String - - - INIntentResponseParameterDisplayName - Value - INIntentResponseParameterDisplayNameID - 8dhboX - INIntentResponseParameterDisplayPriority - 2 - INIntentResponseParameterName - value - INIntentResponseParameterTag - 5 - INIntentResponseParameterType - String - - - - INIntentTitle - Set Color Control Value - INIntentTitleID - 1Kf0qe - INIntentType - Custom - INIntentVerb - Do - - - INIntentCategory - generic - INIntentClassPrefix - OpenHAB - INIntentConfigurable - - INIntentDescription - Set the state of a contact open or closed - INIntentDescriptionID - eFb7vM - INIntentKeyParameter - item - INIntentLastParameterTag - 4 - INIntentManagedParameterCombinations - - item,state,home - - INIntentParameterCombinationSupportsBackgroundExecution - - INIntentParameterCombinationTitle - Set the state of ${item} to ${state} - INIntentParameterCombinationTitleID - ttiBzN - INIntentParameterCombinationUpdatesLinked - - - - INIntentName - SetContactStateValue - INIntentParameterCombinations - - item,state,home - - INIntentParameterCombinationIsLinked - - INIntentParameterCombinationIsPrimary - - INIntentParameterCombinationSupportsBackgroundExecution - - INIntentParameterCombinationTitle - Set the state of ${item} to ${state} - INIntentParameterCombinationTitleID - WsMKz2 - - - INIntentParameters - - - INIntentParameterConfigurable - - INIntentParameterDisplayName - Item - INIntentParameterDisplayNameID - gQzHiG - INIntentParameterDisplayPriority - 1 - INIntentParameterMetadata - - INIntentParameterMetadataCapitalization - Sentences - INIntentParameterMetadataDefaultValueID - h0kR74 - - INIntentParameterName - item - INIntentParameterPromptDialogs - - - INIntentParameterPromptDialogCustom - - INIntentParameterPromptDialogFormatString - Switch name - INIntentParameterPromptDialogFormatStringID - lqlrGx - INIntentParameterPromptDialogType - Configuration - - - INIntentParameterPromptDialogCustom - - INIntentParameterPromptDialogType - Primary - - - INIntentParameterSupportsDynamicEnumeration - - INIntentParameterTag - 1 - INIntentParameterType - String - - - INIntentParameterConfigurable - - INIntentParameterDisplayName - State - INIntentParameterDisplayNameID - n7cmvU - INIntentParameterDisplayPriority - 2 - INIntentParameterMetadata - - INIntentParameterMetadataCapitalization - Sentences - INIntentParameterMetadataDefaultValueID - L1xpr9 - - INIntentParameterName - state - INIntentParameterPromptDialogs - - - INIntentParameterPromptDialogCustom - - INIntentParameterPromptDialogFormatString - Action - INIntentParameterPromptDialogFormatStringID - 1GaCDV - INIntentParameterPromptDialogType - Configuration - - - INIntentParameterPromptDialogCustom - - INIntentParameterPromptDialogType - Primary - - - INIntentParameterSupportsDynamicEnumeration - - INIntentParameterTag - 2 - INIntentParameterType - String - - - INIntentParameterConfigurable - - INIntentParameterCustomDisambiguation - - INIntentParameterDisplayName - home - INIntentParameterDisplayNameID - 7GA2x6 - INIntentParameterDisplayPriority - 3 - INIntentParameterName - home - INIntentParameterObjectType - Home - INIntentParameterObjectTypeNamespace - aK4nIm - INIntentParameterPromptDialogs - - - INIntentParameterPromptDialogCustom - - INIntentParameterPromptDialogFormatString - Home name - INIntentParameterPromptDialogFormatStringID - Ncumus - INIntentParameterPromptDialogType - Configuration - - - INIntentParameterPromptDialogCustom - - INIntentParameterPromptDialogType - Primary - - - INIntentParameterPromptDialogCustom - - INIntentParameterPromptDialogFormatString - There are ${count} configured homes with an item named '${item}'’. - INIntentParameterPromptDialogFormatStringID - H2V4UV - INIntentParameterPromptDialogType - DisambiguationIntroduction - - - INIntentParameterPromptDialogCustom - - INIntentParameterPromptDialogFormatString - For which home do you want to get the value? - INIntentParameterPromptDialogFormatStringID - 9Pu17E - INIntentParameterPromptDialogType - DisambiguationSelection - - - INIntentParameterPromptDialogCustom - - INIntentParameterPromptDialogFormatString - Just to confirm, you wanted ‘${home}’? - INIntentParameterPromptDialogFormatStringID - 0hYnWM - INIntentParameterPromptDialogType - Confirmation - - - INIntentParameterSupportsDynamicEnumeration - - INIntentParameterSupportsResolution - - INIntentParameterTag - 4 - INIntentParameterType - Object - - - INIntentResponse - - INIntentResponseCodes - - - INIntentResponseCodeFormatString - The state of ${item} was set to ${state} - INIntentResponseCodeFormatStringID - H26PEQ - INIntentResponseCodeName - success - INIntentResponseCodeSuccess - - - - INIntentResponseCodeName - failure - - - INIntentResponseCodeFormatString - Sorry can't find ${item} - INIntentResponseCodeFormatStringID - 2q0TdY - INIntentResponseCodeName - failureInvalidItem - - - INIntentResponseCodeFormatString - State invalid: ${state} for ${item} - INIntentResponseCodeFormatStringID - GI0faP - INIntentResponseCodeName - failureInvalidAction - - - INIntentResponseLastParameterTag - 4 - INIntentResponseParameters - - - INIntentResponseParameterDisplayName - Item - INIntentResponseParameterDisplayNameID - k0eXWc - INIntentResponseParameterDisplayPriority - 1 - INIntentResponseParameterName - item - INIntentResponseParameterTag - 3 - INIntentResponseParameterType - String - - - INIntentResponseParameterDisplayName - State - INIntentResponseParameterDisplayNameID - Nqxuw0 - INIntentResponseParameterDisplayPriority - 2 - INIntentResponseParameterName - state - INIntentResponseParameterTag - 4 - INIntentResponseParameterType - String - - - - INIntentTitle - Set Contact State Value - INIntentTitleID - nzw2UT - INIntentType - Custom - INIntentVerb - Do - - - INTypes - - - INTypeClassPrefix - OpenHAB - INTypeDisplayName - Home - INTypeDisplayNameID - zV2TH2 - INTypeLastPropertyTag - 100 - INTypeName - Home - INTypeProperties - - - INTypePropertyDefault - - INTypePropertyDisplayPriority - 1 - INTypePropertyName - identifier - INTypePropertyTag - 1 - INTypePropertyType - String - - - INTypePropertyDefault - - INTypePropertyDisplayPriority - 2 - INTypePropertyName - displayString - INTypePropertyTag - 2 - INTypePropertyType - String - - - INTypePropertyDefault - - INTypePropertyDisplayPriority - 3 - INTypePropertyName - pronunciationHint - INTypePropertyTag - 3 - INTypePropertyType - String - - - INTypePropertyDefault - - INTypePropertyDisplayPriority - 4 - INTypePropertyName - alternativeSpeakableMatches - INTypePropertySupportsMultipleValues - - INTypePropertyTag - 4 - INTypePropertyType - SpeakableString - - - - - - diff --git a/openHABIntents/GetItemStateIntentHandler.swift b/openHABIntents/GetItemStateIntentHandler.swift deleted file mode 100644 index ddd0b70c1..000000000 --- a/openHABIntents/GetItemStateIntentHandler.swift +++ /dev/null @@ -1,63 +0,0 @@ -// 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 Foundation -import Intents -import OpenHABCore -import os.log - -class GetItemStateIntentHandler: NSObject, OpenHABGetItemStateIntentHandling { - func resolveHome(for intent: OpenHABGetItemStateIntent) async -> OpenHABHomeResolutionResult { - Logger.intentHandling.info("Resolving home for intent: \(intent)") - return await OpenHABIntentHelper.resolveHome(home: intent.home, item: intent.item) - } - - func provideHomeOptionsCollection(for intent: OpenHABGetItemStateIntent) async throws -> INObjectCollection { - await OpenHABIntentHelper.getHomeOptions() - } - - func provideItemOptionsCollection(for intent: OpenHABGetItemStateIntent, searchTerm: String?) async throws -> INObjectCollection { - await OpenHABIntentHelper.getItemOptions(home: intent.home, searchTerm: searchTerm) - } - - func provideItemOptionsCollection(for intent: OpenHABGetItemStateIntent) async throws -> INObjectCollection { - await OpenHABIntentHelper.getItemOptions(home: intent.home) - } - - func confirm(intent: OpenHABGetItemStateIntent) async -> OpenHABGetItemStateIntentResponse { - OpenHABGetItemStateIntentResponse(code: .ready, userActivity: nil) - } - - func handle(intent: OpenHABGetItemStateIntent) async -> OpenHABGetItemStateIntentResponse { - Logger.intentHandling.info("GetItemStateIntent for \(intent.item ?? "")") - - guard let itemName = intent.item, let home = intent.home else { - return .failureInvalidItem( - String(localized: "empty.itemorhome", defaultValue: "Empty Item or empty Home", comment: "Empty Item or empty Home") - ) - } - - guard let homeId = home.uuid, await Preferences.shared.storedHomes[homeId] != nil else { - return .failureInvalidItem(String(localized: "unknownHome", comment: "unknown home")) - } - - let item = await OpenHABItemCache.instance.getItemUncached(name: itemName, home: homeId) - - guard let item else { - return .failureInvalidItem(itemName) - } - - return .success( - item: itemName, - state: item.state ?? String(localized: "unknownState", comment: "unknown item state") - ) - } -} diff --git a/openHABIntents/Info.plist b/openHABIntents/Info.plist deleted file mode 100644 index 0755d22c1..000000000 --- a/openHABIntents/Info.plist +++ /dev/null @@ -1,53 +0,0 @@ - - - - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleDisplayName - openHABIntents - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - $(PRODUCT_BUNDLE_PACKAGE_TYPE) - CFBundleShortVersionString - $(MARKETING_VERSION) - CFBundleVersion - $(CURRENT_PROJECT_VERSION) - NSAppTransportSecurity - - NSAllowsArbitraryLoads - - - NSExtension - - NSExtensionAttributes - - IntentsRestrictedWhileLocked - - IntentsRestrictedWhileProtectedDataUnavailable - - IntentsSupported - - OpenHABGetItemStateIntent - OpenHABSetColorValueIntent - OpenHABSetContactStateValueIntent - OpenHABSetDimmerRollerValueIntent - OpenHABSetNumberValueIntent - OpenHABSetStringValueIntent - OpenHABSetSwitchStateIntent - - - NSExtensionPointIdentifier - com.apple.intents-service - NSExtensionPrincipalClass - $(PRODUCT_MODULE_NAME).IntentHandler - - - diff --git a/openHABIntents/IntentHandler.swift b/openHABIntents/IntentHandler.swift deleted file mode 100644 index 339e42cff..000000000 --- a/openHABIntents/IntentHandler.swift +++ /dev/null @@ -1,37 +0,0 @@ -// 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 Intents -import OpenHABCore - -class IntentHandler: INExtension { - override init() { - super.init() - - Task { @MainActor in - // Ensure Preferences initializes on the MainActor to avoid crashes - _ = Preferences.shared - await OpenHABItemCache.instance.forceCacheReload() - } - } - - override func handler(for intent: INIntent) -> Any { - switch intent { - case is OpenHABGetItemStateIntent: GetItemStateIntentHandler() - case is OpenHABSetSwitchStateIntent: SetSwitchStateIntentHandler() - case is OpenHABSetNumberValueIntent: SetNumberValueIntentHandler() - case is OpenHABSetStringValueIntent: SetStringValueIntentHandler() - case is OpenHABSetColorValueIntent: SetColorValueIntentHandler() - case is OpenHABSetContactStateValueIntent: SetContactStateValueIntentHandler() - default: SetDimmerRollerValueIntentHandler() - } - } -} diff --git a/openHABIntents/OpenHABIntentHelper.swift b/openHABIntents/OpenHABIntentHelper.swift deleted file mode 100644 index 61096da55..000000000 --- a/openHABIntents/OpenHABIntentHelper.swift +++ /dev/null @@ -1,78 +0,0 @@ -// 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 Foundation -import Intents -import OpenHABCore - -@MainActor -public enum OpenHABIntentHelper { - static func resolveHome(home: OpenHABHome?, item: String?) async -> OpenHABHomeResolutionResult { - if let home, let homeId = home.uuid { - // TODO: fuzzy matching / account for potential renaming? - // TODO: accept potential mismatches if item name is unique - let homePrefs = Preferences.shared.storedHomes.first { $0.key == homeId } - if homePrefs != nil { - return .success(with: home) - } else { - return .unsupported() // given home is not found in preferences - } - } else if let item { - // try to find the home by home-specific item selection - let allItems = await OpenHABItemCache.instance.getAllCachedItems() - let homeIdsWithMatchingItems = allItems.map(\.key).filter { uuid in - allItems[uuid]?.filtered(by: item).isEmpty != true - } - let potentialHomes = homeIdsWithMatchingItems - .compactMap { Preferences.shared.storedHomes[$0] } - .map { OpenHABHome(homeId: $0.id, homeName: $0.homeName) } - if potentialHomes.count == 1 { - return .success(with: potentialHomes[0]) - } else { - return .disambiguation(with: potentialHomes) - } - } else { - return .needsValue() - } - } - - static func getHomeOptions() -> INObjectCollection { - INObjectCollection(items: Preferences.shared.storedHomes.map { OpenHABHome(homeId: $0.value.id, homeName: $0.value.homeName) }) - } - - static func getItemOptions(home: OpenHABHome?, searchTerm: String? = nil, itemTypes: [OpenHABItem.ItemType]? = nil) async -> INObjectCollection { - let allItems = await getAllItems(home: home) - let items = allItems.filtered(by: searchTerm, for: itemTypes) - return INObjectCollection(items: items.map(\.name).map { $0 as NSString }) - } - - private static func getAllItems(home: OpenHABHome?) async -> [OpenHABItem] { - if let home, let homeId = home.uuid { - await OpenHABItemCache.instance.getCachedItems(home: homeId) ?? [] - } else { - await OpenHABItemCache.instance.getAllCachedItems().flatMap(\.value) - } - } -} - -extension OpenHABHome: @unchecked Sendable { - var uuid: UUID? { - UUID(uuidString: identifier ?? "") - } - - convenience init(homeId: UUID, homeName: String) { - self.init(identifier: homeId.uuidString, display: homeName) - } -} - -extension OpenHABHomeResolutionResult: @unchecked Sendable {} - -extension INObjectCollection: @unchecked @retroactive Sendable {} diff --git a/openHABIntents/SetColorValueIntentHandler.swift b/openHABIntents/SetColorValueIntentHandler.swift deleted file mode 100644 index e61fe08de..000000000 --- a/openHABIntents/SetColorValueIntentHandler.swift +++ /dev/null @@ -1,79 +0,0 @@ -// 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 Foundation -import Intents -import OpenHABCore -import os.log - -class SetColorValueIntentHandler: NSObject, OpenHABSetColorValueIntentHandling { - func resolveHome(for intent: OpenHABSetColorValueIntent) async -> OpenHABHomeResolutionResult { - Logger.intentHandling.info("Resolving home for intent: \(intent)") - return await OpenHABIntentHelper.resolveHome(home: intent.home, item: intent.item) - } - - func provideHomeOptionsCollection(for intent: OpenHABSetColorValueIntent) async throws -> INObjectCollection { - await OpenHABIntentHelper.getHomeOptions() - } - - func provideItemOptionsCollection(for intent: OpenHABSetColorValueIntent, searchTerm: String?) async throws -> INObjectCollection { - await OpenHABIntentHelper.getItemOptions(home: intent.home, searchTerm: searchTerm, itemTypes: [.color]) - } - - func provideItemOptionsCollection(for intent: OpenHABSetColorValueIntent) async throws -> INObjectCollection { - await OpenHABIntentHelper.getItemOptions(home: intent.home, itemTypes: [.color]) - } - - func confirm(intent: OpenHABSetColorValueIntent) async -> OpenHABSetColorValueIntentResponse { - OpenHABSetColorValueIntentResponse(code: .ready, userActivity: nil) - } - - func handle(intent: OpenHABSetColorValueIntent) async -> OpenHABSetColorValueIntentResponse { - Logger.intentHandling.info("SetColorValueIntent for \(intent.item ?? "")") - - guard let itemName = intent.item, let home = intent.home else { - return .failureInvalidItem( - String(localized: "empty.itemorhome", defaultValue: "Empty Item or empty Home", comment: "Empty Item or empty Home") - ) - } - - guard let homeId = home.uuid, await Preferences.shared.storedHomes[homeId] != nil else { - return .failureInvalidItem(String(localized: "unknownHome", comment: "unknown home")) - } - - guard var value = intent.value else { - return .failureInvalidValue( - String(localized: "empty.value", defaultValue: "Empty", comment: "Empty, with value name behind"), - item: itemName - ) - } - - let hsb = value.split(separator: ",") - guard hsb.count == 3, - let hue = Int(hsb[0]), (0 ... 360).contains(hue), - let sat = Int(hsb[1]), (0 ... 100).contains(sat), - let val = Int(hsb[2]), (0 ... 100).contains(val) else { - return .failureInvalidValue(value, item: itemName) - } - - value = "\(hue),\(sat),\(val)" - - guard let items = await OpenHABItemCache.instance.getCachedItem(name: itemName, home: homeId), !items.isEmpty else { - return .failureInvalidItem(itemName) - } - - let item = items[0] - - await OpenHABItemCache.instance.sendCommand(to: item, home: homeId, command: value) - - return .success(value: value, item: itemName) - } -} diff --git a/openHABIntents/SetContactStateValueIntentHandler.swift b/openHABIntents/SetContactStateValueIntentHandler.swift deleted file mode 100644 index 429158a08..000000000 --- a/openHABIntents/SetContactStateValueIntentHandler.swift +++ /dev/null @@ -1,86 +0,0 @@ -// 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 Foundation -import Intents -import OpenHABCore -import os.log - -class SetContactStateValueIntentHandler: NSObject, OpenHABSetContactStateValueIntentHandling { - private static let onLabel = String(localized: "on", comment: "").capitalized - private static let offLabel = String(localized: "off", comment: "").capitalized - - private static let localizedActions = [onLabel, offLabel] - private static let actionMap: [String: String] = [ - onLabel: "ON", - offLabel: "OFF" - ] - - func resolveHome(for intent: OpenHABSetContactStateValueIntent) async -> OpenHABHomeResolutionResult { - Logger.intentHandling.info("Resolving home for intent: \(intent)") - return await OpenHABIntentHelper.resolveHome(home: intent.home, item: intent.item) - } - - func provideHomeOptionsCollection(for intent: OpenHABSetContactStateValueIntent) async throws -> INObjectCollection { - await OpenHABIntentHelper.getHomeOptions() - } - - func provideStateOptionsCollection(for intent: OpenHABSetContactStateValueIntent) async throws -> INObjectCollection { - INObjectCollection(items: Self.localizedActions as [NSString]) - } - - func provideItemOptionsCollection(for intent: OpenHABSetContactStateValueIntent, searchTerm: String?) async throws -> INObjectCollection { - await OpenHABIntentHelper.getItemOptions(home: intent.home, searchTerm: searchTerm, itemTypes: [.contact]) - } - - func provideItemOptionsCollection(for intent: OpenHABSetContactStateValueIntent) async throws -> INObjectCollection { - await OpenHABIntentHelper.getItemOptions(home: intent.home, itemTypes: [.contact]) - } - - func confirm(intent: OpenHABSetContactStateValueIntent) async -> OpenHABSetContactStateValueIntentResponse { - OpenHABSetContactStateValueIntentResponse(code: .ready, userActivity: nil) - } - - func handle(intent: OpenHABSetContactStateValueIntent) async -> OpenHABSetContactStateValueIntentResponse { - Logger.intentHandling.info("SetContactStateValueIntent for \(intent.item ?? "")") - - guard let itemName = intent.item, let home = intent.home else { - return .failureInvalidItem( - String(localized: "empty.itemorhome", defaultValue: "Empty Item or empty Home", comment: "Empty Item or empty Home") - ) - } - - guard let homeId = home.uuid, await Preferences.shared.storedHomes[homeId] != nil else { - return .failureInvalidItem(String(localized: "unknownHome", comment: "unknown home")) - } - - guard let state = intent.state else { - return .failureInvalidAction( - state: String(localized: "empty.value", defaultValue: "Empty", comment: "Empty, with value name behind"), - item: itemName - ) - } - - guard let realState = Self.actionMap[state] else { - return .failureInvalidAction(state: state, item: itemName) - } - - guard let items = await OpenHABItemCache.instance.getCachedItem(name: itemName, home: homeId), !items.isEmpty else { - return .failureInvalidItem(itemName) - } - - let item = items[0] - - await OpenHABItemCache.instance.sendCommand(to: item, home: homeId, command: realState) - - return .success(item: itemName, state: state) - } -} diff --git a/openHABIntents/SetDimmerRollerValueIntentHandler.swift b/openHABIntents/SetDimmerRollerValueIntentHandler.swift deleted file mode 100644 index 38c125e07..000000000 --- a/openHABIntents/SetDimmerRollerValueIntentHandler.swift +++ /dev/null @@ -1,72 +0,0 @@ -// 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 Foundation -import Intents -import OpenHABCore -import os.log - -class SetDimmerRollerValueIntentHandler: NSObject, OpenHABSetDimmerRollerValueIntentHandling { - func resolveHome(for intent: OpenHABSetDimmerRollerValueIntent) async -> OpenHABHomeResolutionResult { - Logger.intentHandling.info("Resolving home for intent: \(intent)") - return await OpenHABIntentHelper.resolveHome(home: intent.home, item: intent.item) - } - - func provideHomeOptionsCollection(for intent: OpenHABSetDimmerRollerValueIntent) async throws -> INObjectCollection { - await OpenHABIntentHelper.getHomeOptions() - } - - func provideItemOptionsCollection(for intent: OpenHABSetDimmerRollerValueIntent, searchTerm: String?) async throws -> INObjectCollection { - await OpenHABIntentHelper.getItemOptions(home: intent.home, searchTerm: searchTerm, itemTypes: [.dimmer, .rollershutter]) - } - - func provideItemOptionsCollection(for intent: OpenHABSetDimmerRollerValueIntent) async throws -> INObjectCollection { - await OpenHABIntentHelper.getItemOptions(home: intent.home, itemTypes: [.dimmer, .rollershutter]) - } - - func confirm(intent: OpenHABSetDimmerRollerValueIntent) async -> OpenHABSetDimmerRollerValueIntentResponse { - OpenHABSetDimmerRollerValueIntentResponse(code: .ready, userActivity: nil) - } - - func handle(intent: OpenHABSetDimmerRollerValueIntent) async -> OpenHABSetDimmerRollerValueIntentResponse { - Logger.intentHandling.info("SetDimmerRollerValueIntent for \(intent.item ?? "")") - - guard let itemName = intent.item, let home = intent.home else { - return .failureInvalidItem( - String(localized: "empty.itemorhome", defaultValue: "Empty Item or empty Home", comment: "Empty Item or empty Home") - ) - } - - guard let homeId = home.uuid, await Preferences.shared.storedHomes[homeId] != nil else { - return .failureInvalidItem(String(localized: "unknownHome", comment: "unknown home")) - } - - guard let value = intent.value else { - return .failureEmptyValue(item: itemName) - } - - let number = Int(truncating: value) - - guard (0 ... 100).contains(number) else { - return .failureInvalidValue(value, item: itemName) - } - - guard let items = await OpenHABItemCache.instance.getCachedItem(name: itemName, home: homeId), !items.isEmpty else { - return .failureInvalidItem(itemName) - } - - let item = items[0] - - await OpenHABItemCache.instance.sendCommand(to: item, home: homeId, command: "\(number)") - - return .success(value: NSNumber(value: number), item: itemName) - } -} diff --git a/openHABIntents/SetNumberValueIntentHandler.swift b/openHABIntents/SetNumberValueIntentHandler.swift deleted file mode 100644 index 6b901cee2..000000000 --- a/openHABIntents/SetNumberValueIntentHandler.swift +++ /dev/null @@ -1,66 +0,0 @@ -// 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 Foundation -import Intents -import OpenHABCore -import os.log - -class SetNumberValueIntentHandler: NSObject, OpenHABSetNumberValueIntentHandling { - func resolveHome(for intent: OpenHABSetNumberValueIntent) async -> OpenHABHomeResolutionResult { - Logger.intentHandling.info("Resolving home for intent: \(intent)") - return await OpenHABIntentHelper.resolveHome(home: intent.home, item: intent.item) - } - - func provideHomeOptionsCollection(for intent: OpenHABSetNumberValueIntent) async throws -> INObjectCollection { - await OpenHABIntentHelper.getHomeOptions() - } - - func provideItemOptionsCollection(for intent: OpenHABSetNumberValueIntent, searchTerm: String?) async throws -> INObjectCollection { - await OpenHABIntentHelper.getItemOptions(home: intent.home, searchTerm: searchTerm, itemTypes: [.number]) - } - - func provideItemOptionsCollection(for intent: OpenHABSetNumberValueIntent) async throws -> INObjectCollection { - await OpenHABIntentHelper.getItemOptions(home: intent.home, itemTypes: [.number]) - } - - func confirm(intent: OpenHABSetNumberValueIntent) async -> OpenHABSetNumberValueIntentResponse { - OpenHABSetNumberValueIntentResponse(code: .ready, userActivity: nil) - } - - func handle(intent: OpenHABSetNumberValueIntent) async -> OpenHABSetNumberValueIntentResponse { - Logger.intentHandling.info("SetNumberValueIntent for \(intent.item ?? "")") - - guard let itemName = intent.item, let home = intent.home else { - return .failureInvalidItem( - String(localized: "empty.itemorhome", defaultValue: "Empty Item or empty Home", comment: "Empty Item or empty Home") - ) - } - - guard let homeId = home.uuid, await Preferences.shared.storedHomes[homeId] != nil else { - return .failureInvalidItem(String(localized: "unknownHome", comment: "unknown home")) - } - - guard let value = intent.value else { - return .failureEmptyValue(item: itemName) - } - - guard let items = await OpenHABItemCache.instance.getCachedItem(name: itemName, home: homeId), !items.isEmpty else { - return .failureInvalidItem(itemName) - } - - let item = items[0] - - await OpenHABItemCache.instance.sendCommand(to: item, home: homeId, command: value.stringValue) - - return .success(value: value, item: itemName) - } -} diff --git a/openHABIntents/SetStringValueIntentHandler.swift b/openHABIntents/SetStringValueIntentHandler.swift deleted file mode 100644 index c94d9dd88..000000000 --- a/openHABIntents/SetStringValueIntentHandler.swift +++ /dev/null @@ -1,66 +0,0 @@ -// 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 Foundation -import Intents -import OpenHABCore -import os.log - -class SetStringValueIntentHandler: NSObject, OpenHABSetStringValueIntentHandling { - func resolveHome(for intent: OpenHABSetStringValueIntent) async -> OpenHABHomeResolutionResult { - Logger.intentHandling.info("Resolving home for intent: \(intent)") - return await OpenHABIntentHelper.resolveHome(home: intent.home, item: intent.item) - } - - func provideHomeOptionsCollection(for intent: OpenHABSetStringValueIntent) async throws -> INObjectCollection { - await OpenHABIntentHelper.getHomeOptions() - } - - func provideItemOptionsCollection(for intent: OpenHABSetStringValueIntent, searchTerm: String?) async throws -> INObjectCollection { - await OpenHABIntentHelper.getItemOptions(home: intent.home, searchTerm: searchTerm, itemTypes: [.stringItem]) - } - - func provideItemOptionsCollection(for intent: OpenHABSetStringValueIntent) async throws -> INObjectCollection { - await OpenHABIntentHelper.getItemOptions(home: intent.home, itemTypes: [.stringItem]) - } - - func confirm(intent: OpenHABSetStringValueIntent) async -> OpenHABSetStringValueIntentResponse { - OpenHABSetStringValueIntentResponse(code: .ready, userActivity: nil) - } - - func handle(intent: OpenHABSetStringValueIntent) async -> OpenHABSetStringValueIntentResponse { - Logger.intentHandling.info("SetStringValueIntent for \(intent.item ?? "")") - - guard let itemName = intent.item, let home = intent.home else { - return .failureInvalidItem( - String(localized: "empty.itemorhome", defaultValue: "Empty Item or empty Home", comment: "Empty Item or empty Home") - ) - } - - guard let homeId = home.uuid, await Preferences.shared.storedHomes[homeId] != nil else { - return .failureInvalidItem(String(localized: "unknownHome", comment: "unknown home")) - } - - guard let value = intent.value else { - return .failureEmptyValue(item: itemName) - } - - guard let items = await OpenHABItemCache.instance.getCachedItem(name: itemName, home: homeId), !items.isEmpty else { - return .failureInvalidItem(itemName) - } - - let item = items[0] - - await OpenHABItemCache.instance.sendCommand(to: item, home: homeId, command: value) - - return .success(value: value, item: itemName) - } -} diff --git a/openHABIntents/SetSwitchStateIntentHandler.swift b/openHABIntents/SetSwitchStateIntentHandler.swift deleted file mode 100644 index 7d47f2b7b..000000000 --- a/openHABIntents/SetSwitchStateIntentHandler.swift +++ /dev/null @@ -1,89 +0,0 @@ -// 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 Foundation -import Intents -import OpenHABCore -import os - -final class SetSwitchStateIntentHandler: NSObject, OpenHABSetSwitchStateIntentHandling { - private static let onLabel = String(localized: "on", comment: "").capitalized - private static let offLabel = String(localized: "off", comment: "").capitalized - - private static let localizedActions = [onLabel, offLabel] - private static let actionMap: [String: String] = [ - onLabel: "ON", - offLabel: "OFF" - ] - - func resolveHome(for intent: OpenHABSetSwitchStateIntent) async -> OpenHABHomeResolutionResult { - Logger.intentHandling.info("Resolving home for intent: \(intent)") - return await OpenHABIntentHelper.resolveHome(home: intent.home, item: intent.item) - } - - func provideHomeOptionsCollection(for intent: OpenHABSetSwitchStateIntent) async throws -> INObjectCollection { - await OpenHABIntentHelper.getHomeOptions() - } - - func provideActionOptionsCollection(for intent: OpenHABSetSwitchStateIntent) async throws -> INObjectCollection { - Logger.intentHandling.info("SetSwitchStateIntentHandler provideActionOptionsCollection") - return INObjectCollection(items: Self.localizedActions as [NSString]) - } - - func provideItemOptionsCollection(for intent: OpenHABSetSwitchStateIntent, searchTerm: String?) async throws -> INObjectCollection { - Logger.intentHandling.info("SetSwitchStateIntentHandler provideItemOptionsCollection with searchTerm: \(searchTerm ?? "", privacy: .public)") - return await OpenHABIntentHelper.getItemOptions(home: intent.home, searchTerm: searchTerm, itemTypes: [.switchItem]) - } - - func provideItemOptionsCollection(for intent: OpenHABSetSwitchStateIntent) async throws -> INObjectCollection { - Logger.intentHandling.info("SetSwitchStateIntentHandler provideItemOptionsCollection") - return await OpenHABIntentHelper.getItemOptions(home: intent.home, itemTypes: [.switchItem]) - } - - func confirm(intent: OpenHABSetSwitchStateIntent) async -> OpenHABSetSwitchStateIntentResponse { - .init(code: .ready, userActivity: nil) - } - - func handle(intent: OpenHABSetSwitchStateIntent) async -> OpenHABSetSwitchStateIntentResponse { - Logger.intentHandling.info("SetSwitchStateIntent for item: \(intent.item ?? "", privacy: .public)") - - guard let itemName = intent.item, let home = intent.home else { - return .failureInvalidItem( - String(localized: "empty.itemorhome", defaultValue: "Empty Item or empty Home", comment: "Empty Item or empty Home") - ) - } - - guard let homeId = home.uuid, await Preferences.shared.storedHomes[homeId] != nil else { - return .failureInvalidItem(String(localized: "unknownHome", comment: "unknown home")) - } - - guard !itemName.isEmpty else { - return .failureInvalidItem(String(localized: "empty.itemname", defaultValue: "Empty Item name", comment: "empty item name")) - } - - guard let action = intent.action else { - return .failureInvalidAction(String(localized: "empty.action", defaultValue: "Empty action", comment: "empty action"), item: itemName) - } - - guard let command = Self.actionMap[action] else { - return .failureInvalidAction(action, item: itemName) - } - - guard let items = await OpenHABItemCache.instance.getCachedItem(name: itemName, home: homeId), !items.isEmpty else { - return .failureInvalidItem(itemName) - } - - let item = items[0] - - await OpenHABItemCache.instance.sendCommand(to: item, home: homeId, command: command) - return .success(action: action, item: itemName) - } -} diff --git a/openHABIntents/de.lproj/Intents.strings b/openHABIntents/de.lproj/Intents.strings deleted file mode 100644 index 58514a3f5..000000000 --- a/openHABIntents/de.lproj/Intents.strings +++ /dev/null @@ -1,243 +0,0 @@ -"0hYnWM" = "Zur Bestätigung, meinten Sie '${home}'?"; - -"1GaCDV" = "Aktion"; - -"1Kf0qe" = "Farbwert festlegen"; - -"1SeS3V" = "${item} auf ${value} (HSB) setzen"; - -"1USOx9" = "Item"; - -"1cBKdm" = "${item} auf ${value} setzen"; - -"2q0TdY" = "${item} kann nicht gefunden werden"; - -"3LMXV8" = "Für welches Zuhause möchten Sie den Wert abrufen?"; - -"3evvza" = "Wert"; - -"4WCzFG" = "Für welches Zuhause möchten Sie den Wert abrufen?"; - -"5B4e9l" = "Setze die Zeichenkette eines String-Kontrollelements"; - -"5Uu4bK" = "Erhalte Status eines Items"; - -"67qFTP" = "Ungültiger leerer Wert für ${item}"; - -"68Y2Ya" = "Dimmer/Rollladen Name"; - -"6hUiXZ" = "Der Status von ${item} ist ${state}"; - -"71MWtI" = "Zuhause"; - -"7BE642" = "Es gibt ${count} konfigurierte Zuhause mit einem Item namens '${item}'."; - -"7GA2x6" = "Zuhause"; - -"80K2Dt" = "${item} kann nicht gefunden werden"; - -"8OBnvT" = "Ungültiger leerer Wert für ${item}"; - -"8dhboX" = "Wert"; - -"9Pu17E" = "Für welches Zuhause möchten Sie den Wert abrufen?"; - -"B2I9b5" = "Item"; - -"BaNYuS" = "Es gibt ${count} konfigurierte Zuhause mit einem Item namens '${item}'."; - -"CZfT5a" = "Der Wert ${value} wurde an ${item} gesendet"; - -"Cdg015" = "${item} auf ${value} setzen"; - -"Ch9Akw" = "${item} auf ${value} setzen"; - -"DxO2wk" = "Farbe eines Kontrollelements festlegen"; - -"E07xjp" = "${item} auf ${value} (HSB) setzen"; - -"E291x3" = "Wert"; - -"EEj00Q" = "${item} auf ${value} setzen"; - -"Fb0pUB" = "Aktion ${action} gesendet um ${item} zu schalten"; - -"GD4RTw" = "Aktuellen Status eines Items abrufen"; - -"GI0faP" = "Status ungültig: ${state} für ${item}"; - -"Gjd9MH" = "${item} auf ${value} setzen"; - -"H26PEQ" = "Der Status von ${item} wurde auf ${state} gesetzt"; - -"H2V4UV" = "Es gibt ${count} konfigurierte Zuhause mit einem Item namens '${item}'."; - -"HGbUGw" = "Wert"; - -"IS2nO0" = "Name des Zuhauses"; - -"IpbXHW" = "${action} an ${item} senden"; - -"JUznKl" = "Der String ${value} wurde an ${item} gesendet"; - -"Jn2aNw" = "Zur Bestätigung, meinten Sie '${home}'?"; - -"KXqi2s" = "Item"; - -"M8mX5O" = "Item"; - -"MCp4x6" = "Item"; - -"MGYSky" = "${item} auf ${value} setzen"; - -"MIqlld" = "Es gibt ${count} konfigurierte Zuhause mit einem Item namens '${item}'."; - -"N5YsnB" = "${item} kann nicht gefunden werden"; - -"Ncumus" = "Name des Zuhauses"; - -"NehcMF" = "Objektname"; - -"NjP70V" = "${item} kann nicht gefunden werden"; - -"Nqxuw0" = "Status"; - -"Nuhhxp" = "Item"; - -"Oazi8d" = "${item} kann nicht gefunden werden"; - -"P9kblb" = "Name des Zuhauses"; - -"QATq1i" = "Für welches Zuhause möchten Sie den Wert abrufen?"; - -"SMYYBh" = "${item} auf ${value} setzen"; - -"TJ1p5Y" = "Aktion"; - -"TK8Rrn" = "Der Farbwert ${value} wurde an ${item} gesendet"; - -"UZYwuH" = "Wert eines Dimmers oder Rolladens festlegen"; - -"V0QsN4" = "Ungültiger Wert ${value} für ${item} (0-100)"; - -"VEApie" = "Zur Bestätigung, meinten Sie '${home}'?"; - -"VTeXc1" = "Item"; - -"VmFkwo" = "${item} auf ${value} (HSB) setzen"; - -"WsMKz2" = "Setze den Status von ${item} auf ${state}"; - -"X9cN9h" = "Zuhause"; - -"XMNs7r" = "Schalterzustand setzen"; - -"YFIIBn" = "Name des Zuhauses"; - -"YUoeAL" = "Es gibt ${count} konfigurierte Zuhause mit einem Item namens '${item}'."; - -"ZuBsOg" = "Zuhause"; - -"cBrxnz" = "Item"; - -"dtRamI" = "Wert"; - -"eFNxrT" = "Name des Zuhauses"; - -"eFb7vM" = "Status eines Kontakts öffnen oder schließen"; - -"exrmKW" = "Zur Bestätigung, meinten Sie '${home}'?"; - -"gBn7U6" = "String Kontrollwert setzen"; - -"gQzHiG" = "Item"; - -"gobcZi" = "Zur Bestätigung, meinten Sie '${home}'?"; - -"h91G9C" = "Für welches Zuhause möchten Sie den Wert abrufen?"; - -"hflKDd" = "${item} kann nicht gefunden werden"; - -"i8fP5e" = "Aktion"; - -"iBrALm" = "Zuhause"; - -"iCUEet" = "Aktion ungültig: ${action} für ${item}"; - -"iKSmqU" = "Zur Bestätigung, meinten Sie '${home}'?"; - -"iRjffC" = "Ungültiger leerer Wert für ${item}"; - -"jP4VSa" = "Es gibt ${count} konfigurierte Zuhause mit einem Item namens '${item}'."; - -"jbUbYw" = "Für welches Zuhause möchten Sie den Wert abrufen?"; - -"jyXazc" = "${action} an ${item} senden"; - -"k0eXWc" = "Item"; - -"k6pj4o" = "Zuhause"; - -"lLFYtM" = "Es gibt ${count} konfigurierte Zuhause mit einem Item namens '${item}'."; - -"llL4sP" = "Wert"; - -"lqlrGx" = "Name wechseln"; - -"m5DFXq" = "Status"; - -"m9yAKr" = "Erhalte Status von ${item}"; - -"mAKsWc" = "Wert"; - -"mHbznY" = "Für welches Zuhause möchten Sie den Wert abrufen?"; - -"mVz5mJ" = "${item} auf ${value} setzen"; - -"mpxNDZ" = "Item"; - -"n7cmvU" = "Status"; - -"nzw2UT" = "Wert des Kontaktstatus festlegen"; - -"o35DYr" = "Wert eines numerischen Kontrollelements setzen"; - -"oOjAU0" = "Setze den Status eines Schalters ein oder aus"; - -"oUolm5" = "${action} an ${item} senden"; - -"pg4Bec" = "Festlegen des Wertes eines Dimmers oder Rollladens"; - -"pptfkD" = "Dezimalwert eines numerischen Kontrollelements setzen"; - -"ptGHON" = "${item} kann nicht gefunden werden"; - -"qYPBPC" = "${item} auf ${value} setzen"; - -"qu9K9v" = "Setze den Status von ${item} auf ${state}"; - -"rVXwR2" = "Name des Zuhauses"; - -"raIAR6" = "Ungültiger Wert: ${value} für ${item} muss HSB sein (0-360,0-100,0-100)"; - -"rsrXB9" = "Name des Zuhauses"; - -"skEQxZ" = "Item"; - -"tclrCp" = "Item"; - -"tkWfxz" = "Zur Bestätigung, meinten Sie '${home}'?"; - -"ttiBzN" = "Setze den Status von ${item} auf ${state}"; - -"tuvmDw" = "Wert"; - -"uGsyyj" = "240,100,100"; - -"v3hsWV" = "Name des Zuhauses"; - -"wm0fqx" = "Der Farbwert ${value} wurde an ${item} gesendet"; - -"zLlY3g" = "Zuhause"; - -"zV2TH2" = "Zuhause"; diff --git a/openHABIntents/en.lproj/Intents.strings b/openHABIntents/en.lproj/Intents.strings deleted file mode 100644 index 45f12fe6f..000000000 --- a/openHABIntents/en.lproj/Intents.strings +++ /dev/null @@ -1,243 +0,0 @@ -"0hYnWM" = "Just to confirm, you wanted '${home}'?"; - -"1GaCDV" = "Action"; - -"1Kf0qe" = "Set Color Control Value"; - -"1SeS3V" = "Set ${item} to ${value} (HSB)"; - -"1USOx9" = "Item"; - -"1cBKdm" = "Set ${item} to ${value}"; - -"2q0TdY" = "Sorry can't find ${item}"; - -"3LMXV8" = "For which home do you want to get the value?"; - -"3evvza" = "Value"; - -"4WCzFG" = "For which home do you want to get the value?"; - -"5B4e9l" = "Set the string of a string control item"; - -"5Uu4bK" = "Get Item State"; - -"67qFTP" = "Invalid empty value for ${item}"; - -"68Y2Ya" = "Dimmer/Roller Name"; - -"6hUiXZ" = "The state of ${item} is ${state}"; - -"71MWtI" = "home"; - -"7BE642" = "There are ${count} configured homes with an item named '${item}'."; - -"7GA2x6" = "home"; - -"80K2Dt" = "Sorry can't find ${item}"; - -"8OBnvT" = "Invalid empty value for ${item}"; - -"8dhboX" = "Value"; - -"9Pu17E" = "For which home do you want to get the value?"; - -"B2I9b5" = "Item"; - -"BaNYuS" = "There are ${count} configured homes with an item named '${item}'."; - -"CZfT5a" = "Sent the number ${value} to ${item}"; - -"Cdg015" = "Set ${item} to ${value}"; - -"Ch9Akw" = "Set ${item} to ${value}"; - -"DxO2wk" = "Set the color of a color control item"; - -"E07xjp" = "Set ${item} to ${value} (HSB)"; - -"E291x3" = "Value"; - -"EEj00Q" = "Set ${item} to ${value}"; - -"Fb0pUB" = "Sent the action of ${action} to switch ${item}"; - -"GD4RTw" = "Retrieve the current state of an item"; - -"GI0faP" = "State invalid: ${state} for ${item}"; - -"Gjd9MH" = "Set ${item} to ${value}"; - -"H26PEQ" = "The state of ${item} was set to ${state}"; - -"H2V4UV" = "There are ${count} configured homes with an item named '${item}'."; - -"HGbUGw" = "Value"; - -"IS2nO0" = "Home name"; - -"IpbXHW" = "Send ${action} to ${item}"; - -"JUznKl" = "Sent the string ${value} to ${item}"; - -"Jn2aNw" = "Just to confirm, you wanted '${home}'?"; - -"KXqi2s" = "Item"; - -"M8mX5O" = "Item"; - -"MCp4x6" = "Item"; - -"MGYSky" = "Set ${item} to ${value}"; - -"MIqlld" = "There are ${count} configured homes with an item named '${item}'."; - -"N5YsnB" = "Sorry can't find ${item}"; - -"Ncumus" = "Home name"; - -"NehcMF" = "Item Name"; - -"NjP70V" = "Sorry can't find ${item}"; - -"Nqxuw0" = "State"; - -"Nuhhxp" = "Item"; - -"Oazi8d" = "Sorry can't find ${item}"; - -"P9kblb" = "Home name"; - -"QATq1i" = "For which home do you want to get the value?"; - -"SMYYBh" = "Set ${item} to ${value}"; - -"TJ1p5Y" = "Action"; - -"TK8Rrn" = "Sent the color value of ${value} to ${item}"; - -"UZYwuH" = "Set Dimmer or Roller Shutter Value"; - -"V0QsN4" = "Invalid value ${value} for ${item} (0-100)"; - -"VEApie" = "Just to confirm, you wanted '${home}'?"; - -"VTeXc1" = "Item"; - -"VmFkwo" = "Set ${item} to ${value} (HSB)"; - -"WsMKz2" = "Set the state of ${item} to ${state}"; - -"X9cN9h" = "home"; - -"XMNs7r" = "Set Switch State"; - -"YFIIBn" = "Home name"; - -"YUoeAL" = "There are ${count} configured homes with an item named '${item}'."; - -"ZuBsOg" = "home"; - -"cBrxnz" = "Item"; - -"dtRamI" = "Value"; - -"eFNxrT" = "Home name"; - -"eFb7vM" = "Set the state of a contact open or closed"; - -"exrmKW" = "Just to confirm, you wanted '${home}'?"; - -"gBn7U6" = "Set String Control Value"; - -"gQzHiG" = "Item"; - -"gobcZi" = "Just to confirm, you wanted '${home}'?"; - -"h91G9C" = "For which home do you want to get the value?"; - -"hflKDd" = "Sorry can't find ${item}"; - -"i8fP5e" = "Action"; - -"iBrALm" = "home"; - -"iCUEet" = "Action invalid: ${action} for ${item}"; - -"iKSmqU" = "Just to confirm, you wanted '${home}'?"; - -"iRjffC" = "Invalid empty value for ${item}"; - -"jP4VSa" = "There are ${count} configured homes with an item named '${item}'."; - -"jbUbYw" = "For which home do you want to get the value?"; - -"jyXazc" = "Send ${action} to ${item}"; - -"k0eXWc" = "Item"; - -"k6pj4o" = "home"; - -"lLFYtM" = "There are ${count} configured homes with an item named '${item}'."; - -"llL4sP" = "Value"; - -"lqlrGx" = "Switch name"; - -"m5DFXq" = "State"; - -"m9yAKr" = "Get ${item} State"; - -"mAKsWc" = "Value"; - -"mHbznY" = "For which home do you want to get the value?"; - -"mVz5mJ" = "Set ${item} to ${value}"; - -"mpxNDZ" = "Item"; - -"n7cmvU" = "State"; - -"nzw2UT" = "Set Contact State Value"; - -"o35DYr" = "Set Number Control Value"; - -"oOjAU0" = "Set the state of a switch on or off"; - -"oUolm5" = "Send ${action} to ${item}"; - -"pg4Bec" = "Set the integer value of a dimmer or roller shutter"; - -"pptfkD" = "Set the decimal value of a number control item"; - -"ptGHON" = "Sorry can't find ${item}"; - -"qYPBPC" = "Set ${item} to ${value}"; - -"qu9K9v" = "Set the state of ${item} to ${state}"; - -"rVXwR2" = "Home name"; - -"raIAR6" = "Invalid value: ${value} for ${item} must be HSB (0-360,0-100,0-100)"; - -"rsrXB9" = "Home name"; - -"skEQxZ" = "Item"; - -"tclrCp" = "Item"; - -"tkWfxz" = "Just to confirm, you wanted '${home}'?"; - -"ttiBzN" = "Set the state of ${item} to ${state}"; - -"tuvmDw" = "Value"; - -"uGsyyj" = "240,100,100"; - -"v3hsWV" = "Home name"; - -"wm0fqx" = "Sent the value of ${value} to ${item}"; - -"zLlY3g" = "home"; - -"zV2TH2" = "Home"; diff --git a/openHABIntents/es.lproj/Intents.strings b/openHABIntents/es.lproj/Intents.strings deleted file mode 100644 index 0206fde6e..000000000 --- a/openHABIntents/es.lproj/Intents.strings +++ /dev/null @@ -1,243 +0,0 @@ -"0hYnWM" = "Para confirmar, ¿quisiste '${home}'?"; - -"1GaCDV" = "Acción"; - -"1Kf0qe" = "Define el valor de control de color"; - -"1SeS3V" = "Establecer ${item} en ${value} (HSB)"; - -"1USOx9" = "Ítem"; - -"1cBKdm" = "Establecer ${item} en ${value}"; - -"2q0TdY" = "Lo sentimos, no se puede encontrar ${item}"; - -"3LMXV8" = "¿Para qué hogar deseas obtener el valor?"; - -"3evvza" = "Valor"; - -"4WCzFG" = "¿Para qué hogar deseas obtener el valor?"; - -"5B4e9l" = "Establecer la cadena de un ítem controlador de cadena"; - -"5Uu4bK" = "Obtener estado del ítem"; - -"67qFTP" = "Valor vacío no válido para ${item}"; - -"68Y2Ya" = "Nombre regulador/persiana"; - -"6hUiXZ" = "El estado de ${item} es ${state}"; - -"71MWtI" = "hogar"; - -"7BE642" = "Hay ${count} hogares configurados con un ítem llamado '${item}'."; - -"7GA2x6" = "hogar"; - -"80K2Dt" = "Lo sentimos, no se puede encontrar ${item}"; - -"8OBnvT" = "Valor vacío no válido para ${item}"; - -"8dhboX" = "Valor"; - -"9Pu17E" = "¿Para qué hogar deseas obtener el valor?"; - -"B2I9b5" = "Ítem"; - -"BaNYuS" = "Hay ${count} hogares configurados con un ítem llamado '${item}'."; - -"CZfT5a" = "Definir el número ${value} a ${item}"; - -"Cdg015" = "Establecer ${item} en ${value}"; - -"Ch9Akw" = "Establecer ${item} en ${value}"; - -"DxO2wk" = "Definir el color de un ítem de control de color"; - -"E07xjp" = "Establecer ${item} en ${value} (HSB)"; - -"E291x3" = "Valor"; - -"EEj00Q" = "Establecer ${item} en ${value}"; - -"Fb0pUB" = "Enviada la acción de ${action} al interruptor ${item}"; - -"GD4RTw" = "Recuperar el estado actual de un elemento"; - -"GI0faP" = "Estado no válido: ${state} para ${item}"; - -"Gjd9MH" = "Establecer ${item} en ${value}"; - -"H26PEQ" = "El estado de ${item} se ha establecido en ${state}"; - -"H2V4UV" = "Hay ${count} hogares configurados con un ítem llamado '${item}'."; - -"HGbUGw" = "Valor"; - -"IS2nO0" = "Nombre del hogar"; - -"IpbXHW" = "Enviar ${action} a ${item}"; - -"JUznKl" = "Enviada la cadena ${value} a ${item}"; - -"Jn2aNw" = "Para confirmar, ¿quisiste '${home}'?"; - -"KXqi2s" = "Ítem"; - -"M8mX5O" = "Ítem"; - -"MCp4x6" = "Ítem"; - -"MGYSky" = "Establecer ${item} en ${value}"; - -"MIqlld" = "Hay ${count} hogares configurados con un ítem llamado '${item}'."; - -"N5YsnB" = "Lo sentimos, no se puede encontrar ${item}"; - -"Ncumus" = "Nombre del hogar"; - -"NehcMF" = "Nombre del ítem"; - -"NjP70V" = "Lo sentimos, no se puede encontrar ${item}"; - -"Nqxuw0" = "Estado"; - -"Nuhhxp" = "Ítem"; - -"Oazi8d" = "Lo sentimos, no se puede encontrar ${item}"; - -"P9kblb" = "Nombre del hogar"; - -"QATq1i" = "¿Para qué hogar deseas obtener el valor?"; - -"SMYYBh" = "Establecer ${item} en ${value}"; - -"TJ1p5Y" = "Acción"; - -"TK8Rrn" = "Enviado el valor de color de ${value} a ${item}"; - -"UZYwuH" = "Definir el valor del regulador o persiana"; - -"V0QsN4" = "Valor no válido ${value} para ${item} (0-100)"; - -"VEApie" = "Para confirmar, ¿quisiste '${home}'?"; - -"VTeXc1" = "Ítem"; - -"VmFkwo" = "Establecer ${item} en ${value} (HSB)"; - -"WsMKz2" = "Establecer el estado de ${item} a ${state}"; - -"X9cN9h" = "hogar"; - -"XMNs7r" = "Definir el estado del interruptor"; - -"YFIIBn" = "Nombre del hogar"; - -"YUoeAL" = "Hay ${count} hogares configurados con un ítem llamado '${item}'."; - -"ZuBsOg" = "hogar"; - -"cBrxnz" = "Ítem"; - -"dtRamI" = "Valor"; - -"eFNxrT" = "Nombre del hogar"; - -"eFb7vM" = "Establecer el estado de un interruptor encendido o apagado"; - -"exrmKW" = "Para confirmar, ¿quisiste '${home}'?"; - -"gBn7U6" = "Establecer el valor de control de cadena"; - -"gQzHiG" = "Ítem"; - -"gobcZi" = "Para confirmar, ¿quisiste '${home}'?"; - -"h91G9C" = "¿Para qué hogar deseas obtener el valor?"; - -"hflKDd" = "Lo sentimos, no se puede encontrar ${item}"; - -"i8fP5e" = "Acción"; - -"iBrALm" = "hogar"; - -"iCUEet" = "Acción inválida: ${action} para ${item}"; - -"iKSmqU" = "Para confirmar, ¿quisiste '${home}'?"; - -"iRjffC" = "Valor vacío no válido para ${item}"; - -"jP4VSa" = "Hay ${count} hogares configurados con un ítem llamado '${item}'."; - -"jbUbYw" = "¿Para qué hogar deseas obtener el valor?"; - -"jyXazc" = "Enviar ${action} a ${item}"; - -"k0eXWc" = "Ítem"; - -"k6pj4o" = "hogar"; - -"lLFYtM" = "Hay ${count} hogares configurados con un ítem llamado '${item}'."; - -"llL4sP" = "Valor"; - -"lqlrGx" = "Nombre del interruptor"; - -"m5DFXq" = "Estado"; - -"m9yAKr" = "Obtener Estado de ${item}"; - -"mAKsWc" = "Valor"; - -"mHbznY" = "¿Para qué hogar deseas obtener el valor?"; - -"mVz5mJ" = "Establecer ${item} en ${value}"; - -"mpxNDZ" = "Ítem"; - -"n7cmvU" = "Estado"; - -"nzw2UT" = "Establecer el valor del estado del pulsador"; - -"o35DYr" = "Establecer valor de control de número"; - -"oOjAU0" = "Establecer el estado de un interruptor encendido o apagado"; - -"oUolm5" = "Enviar ${action} a ${item}"; - -"pg4Bec" = "Define el valor entero de un regulador o persiana"; - -"pptfkD" = "Establecer el valor decimal de un ítem de control numérico"; - -"ptGHON" = "Lo sentimos, no se puede encontrar ${item}"; - -"qYPBPC" = "Establecer ${item} en ${value}"; - -"qu9K9v" = "Establecer el estado de ${item} a ${state}"; - -"rVXwR2" = "Nombre del hogar"; - -"raIAR6" = "Valor no válido: ${value} para ${item} debe ser HSB (0-360,0-100,0-100)"; - -"rsrXB9" = "Nombre del hogar"; - -"skEQxZ" = "Ítem"; - -"tclrCp" = "Ítem"; - -"tkWfxz" = "Para confirmar, ¿quisiste '${home}'?"; - -"ttiBzN" = "Establecer el estado de ${item} a ${state}"; - -"tuvmDw" = "Valor"; - -"uGsyyj" = "240,100,100"; - -"v3hsWV" = "Nombre del hogar"; - -"wm0fqx" = "Enviado el valor de ${value} a ${item}"; - -"zLlY3g" = "hogar"; - -"zV2TH2" = "Hogar"; diff --git a/openHABIntents/fi.lproj/Intents.strings b/openHABIntents/fi.lproj/Intents.strings deleted file mode 100644 index 45f12fe6f..000000000 --- a/openHABIntents/fi.lproj/Intents.strings +++ /dev/null @@ -1,243 +0,0 @@ -"0hYnWM" = "Just to confirm, you wanted '${home}'?"; - -"1GaCDV" = "Action"; - -"1Kf0qe" = "Set Color Control Value"; - -"1SeS3V" = "Set ${item} to ${value} (HSB)"; - -"1USOx9" = "Item"; - -"1cBKdm" = "Set ${item} to ${value}"; - -"2q0TdY" = "Sorry can't find ${item}"; - -"3LMXV8" = "For which home do you want to get the value?"; - -"3evvza" = "Value"; - -"4WCzFG" = "For which home do you want to get the value?"; - -"5B4e9l" = "Set the string of a string control item"; - -"5Uu4bK" = "Get Item State"; - -"67qFTP" = "Invalid empty value for ${item}"; - -"68Y2Ya" = "Dimmer/Roller Name"; - -"6hUiXZ" = "The state of ${item} is ${state}"; - -"71MWtI" = "home"; - -"7BE642" = "There are ${count} configured homes with an item named '${item}'."; - -"7GA2x6" = "home"; - -"80K2Dt" = "Sorry can't find ${item}"; - -"8OBnvT" = "Invalid empty value for ${item}"; - -"8dhboX" = "Value"; - -"9Pu17E" = "For which home do you want to get the value?"; - -"B2I9b5" = "Item"; - -"BaNYuS" = "There are ${count} configured homes with an item named '${item}'."; - -"CZfT5a" = "Sent the number ${value} to ${item}"; - -"Cdg015" = "Set ${item} to ${value}"; - -"Ch9Akw" = "Set ${item} to ${value}"; - -"DxO2wk" = "Set the color of a color control item"; - -"E07xjp" = "Set ${item} to ${value} (HSB)"; - -"E291x3" = "Value"; - -"EEj00Q" = "Set ${item} to ${value}"; - -"Fb0pUB" = "Sent the action of ${action} to switch ${item}"; - -"GD4RTw" = "Retrieve the current state of an item"; - -"GI0faP" = "State invalid: ${state} for ${item}"; - -"Gjd9MH" = "Set ${item} to ${value}"; - -"H26PEQ" = "The state of ${item} was set to ${state}"; - -"H2V4UV" = "There are ${count} configured homes with an item named '${item}'."; - -"HGbUGw" = "Value"; - -"IS2nO0" = "Home name"; - -"IpbXHW" = "Send ${action} to ${item}"; - -"JUznKl" = "Sent the string ${value} to ${item}"; - -"Jn2aNw" = "Just to confirm, you wanted '${home}'?"; - -"KXqi2s" = "Item"; - -"M8mX5O" = "Item"; - -"MCp4x6" = "Item"; - -"MGYSky" = "Set ${item} to ${value}"; - -"MIqlld" = "There are ${count} configured homes with an item named '${item}'."; - -"N5YsnB" = "Sorry can't find ${item}"; - -"Ncumus" = "Home name"; - -"NehcMF" = "Item Name"; - -"NjP70V" = "Sorry can't find ${item}"; - -"Nqxuw0" = "State"; - -"Nuhhxp" = "Item"; - -"Oazi8d" = "Sorry can't find ${item}"; - -"P9kblb" = "Home name"; - -"QATq1i" = "For which home do you want to get the value?"; - -"SMYYBh" = "Set ${item} to ${value}"; - -"TJ1p5Y" = "Action"; - -"TK8Rrn" = "Sent the color value of ${value} to ${item}"; - -"UZYwuH" = "Set Dimmer or Roller Shutter Value"; - -"V0QsN4" = "Invalid value ${value} for ${item} (0-100)"; - -"VEApie" = "Just to confirm, you wanted '${home}'?"; - -"VTeXc1" = "Item"; - -"VmFkwo" = "Set ${item} to ${value} (HSB)"; - -"WsMKz2" = "Set the state of ${item} to ${state}"; - -"X9cN9h" = "home"; - -"XMNs7r" = "Set Switch State"; - -"YFIIBn" = "Home name"; - -"YUoeAL" = "There are ${count} configured homes with an item named '${item}'."; - -"ZuBsOg" = "home"; - -"cBrxnz" = "Item"; - -"dtRamI" = "Value"; - -"eFNxrT" = "Home name"; - -"eFb7vM" = "Set the state of a contact open or closed"; - -"exrmKW" = "Just to confirm, you wanted '${home}'?"; - -"gBn7U6" = "Set String Control Value"; - -"gQzHiG" = "Item"; - -"gobcZi" = "Just to confirm, you wanted '${home}'?"; - -"h91G9C" = "For which home do you want to get the value?"; - -"hflKDd" = "Sorry can't find ${item}"; - -"i8fP5e" = "Action"; - -"iBrALm" = "home"; - -"iCUEet" = "Action invalid: ${action} for ${item}"; - -"iKSmqU" = "Just to confirm, you wanted '${home}'?"; - -"iRjffC" = "Invalid empty value for ${item}"; - -"jP4VSa" = "There are ${count} configured homes with an item named '${item}'."; - -"jbUbYw" = "For which home do you want to get the value?"; - -"jyXazc" = "Send ${action} to ${item}"; - -"k0eXWc" = "Item"; - -"k6pj4o" = "home"; - -"lLFYtM" = "There are ${count} configured homes with an item named '${item}'."; - -"llL4sP" = "Value"; - -"lqlrGx" = "Switch name"; - -"m5DFXq" = "State"; - -"m9yAKr" = "Get ${item} State"; - -"mAKsWc" = "Value"; - -"mHbznY" = "For which home do you want to get the value?"; - -"mVz5mJ" = "Set ${item} to ${value}"; - -"mpxNDZ" = "Item"; - -"n7cmvU" = "State"; - -"nzw2UT" = "Set Contact State Value"; - -"o35DYr" = "Set Number Control Value"; - -"oOjAU0" = "Set the state of a switch on or off"; - -"oUolm5" = "Send ${action} to ${item}"; - -"pg4Bec" = "Set the integer value of a dimmer or roller shutter"; - -"pptfkD" = "Set the decimal value of a number control item"; - -"ptGHON" = "Sorry can't find ${item}"; - -"qYPBPC" = "Set ${item} to ${value}"; - -"qu9K9v" = "Set the state of ${item} to ${state}"; - -"rVXwR2" = "Home name"; - -"raIAR6" = "Invalid value: ${value} for ${item} must be HSB (0-360,0-100,0-100)"; - -"rsrXB9" = "Home name"; - -"skEQxZ" = "Item"; - -"tclrCp" = "Item"; - -"tkWfxz" = "Just to confirm, you wanted '${home}'?"; - -"ttiBzN" = "Set the state of ${item} to ${state}"; - -"tuvmDw" = "Value"; - -"uGsyyj" = "240,100,100"; - -"v3hsWV" = "Home name"; - -"wm0fqx" = "Sent the value of ${value} to ${item}"; - -"zLlY3g" = "home"; - -"zV2TH2" = "Home"; diff --git a/openHABIntents/fr.lproj/Intents.strings b/openHABIntents/fr.lproj/Intents.strings deleted file mode 100644 index b24db5eed..000000000 --- a/openHABIntents/fr.lproj/Intents.strings +++ /dev/null @@ -1,243 +0,0 @@ -"0hYnWM" = "Pour confirmer, vous vouliez '${home}' ?"; - -"1GaCDV" = "Action"; - -"1Kf0qe" = "Définir la valeur de contrôle de couleur"; - -"1SeS3V" = "Définir ${item} à ${value} (HSB)"; - -"1USOx9" = "Item"; - -"1cBKdm" = "Définir ${item} à ${value}"; - -"2q0TdY" = "Désolé impossible de trouver ${item}"; - -"3LMXV8" = "Pour quelle maison voulez-vous obtenir la valeur ?"; - -"3evvza" = "Valeur"; - -"4WCzFG" = "Pour quelle maison voulez-vous obtenir la valeur ?"; - -"5B4e9l" = "Définir la chaîne d'un item de contrôle de chaîne"; - -"5Uu4bK" = "Récupérer l'état de l'item"; - -"67qFTP" = "Valeur vide invalide pour ${item}"; - -"68Y2Ya" = "Nom du Dimmer/Roller"; - -"6hUiXZ" = "L'état de ${item} est ${state}"; - -"71MWtI" = "maison"; - -"7BE642" = "Il y a ${count} maisons configurées avec un élément nommé '${item}'."; - -"7GA2x6" = "maison"; - -"80K2Dt" = "Désolé impossible de trouver ${item}"; - -"8OBnvT" = "Valeur vide invalide pour ${item}"; - -"8dhboX" = "Valeur"; - -"9Pu17E" = "Pour quelle maison voulez-vous obtenir la valeur ?"; - -"B2I9b5" = "Item"; - -"BaNYuS" = "Il y a ${count} maisons configurées avec un élément nommé '${item}'."; - -"CZfT5a" = "Envoyé le numéro ${value} à ${item}"; - -"Cdg015" = "Définir ${item} à ${value}"; - -"Ch9Akw" = "Définir ${item} à ${value}"; - -"DxO2wk" = "Définir la couleur d'un élément de contrôle de couleur"; - -"E07xjp" = "Définir ${item} à ${value} (HSB)"; - -"E291x3" = "Valeur"; - -"EEj00Q" = "Définir ${item} à ${value}"; - -"Fb0pUB" = "A envoyé l'action ${action} pour changer la position de ${item}"; - -"GD4RTw" = "Récupérer l'état actuel d'un item"; - -"GI0faP" = "État invalide : ${state} pour ${item}"; - -"Gjd9MH" = "Définir ${item} à ${value}"; - -"H26PEQ" = "L'état de ${item} a été réglé sur ${state}"; - -"H2V4UV" = "Il y a ${count} maisons configurées avec un élément nommé '${item}'."; - -"HGbUGw" = "Valeur"; - -"IS2nO0" = "Nom de la maison"; - -"IpbXHW" = "Envoyer ${action} à ${item}"; - -"JUznKl" = "Envoi de la chaîne ${value} à ${item}"; - -"Jn2aNw" = "Pour confirmer, vous vouliez '${home}' ?"; - -"KXqi2s" = "Item"; - -"M8mX5O" = "Item"; - -"MCp4x6" = "Item"; - -"MGYSky" = "Définir ${item} à ${value}"; - -"MIqlld" = "Il y a ${count} maisons configurées avec un élément nommé '${item}'."; - -"N5YsnB" = "Désolé impossible de trouver ${item}"; - -"Ncumus" = "Nom de la maison"; - -"NehcMF" = "Nom d'Item"; - -"NjP70V" = "Désolé impossible de trouver ${item}"; - -"Nqxuw0" = "État"; - -"Nuhhxp" = "Item"; - -"Oazi8d" = "Désolé impossible de trouver ${item}"; - -"P9kblb" = "Nom de la maison"; - -"QATq1i" = "Pour quelle maison voulez-vous obtenir la valeur ?"; - -"SMYYBh" = "Définir ${item} à ${value}"; - -"TJ1p5Y" = "Action"; - -"TK8Rrn" = "Envoyé la valeur de couleur de ${value} à ${item}"; - -"UZYwuH" = "Régler la valeur du Dimmer ou du Volet Roulant"; - -"V0QsN4" = "Valeur ${value} invalide pour ${item} (0-100)"; - -"VEApie" = "Pour confirmer, vous vouliez '${home}' ?"; - -"VTeXc1" = "Item"; - -"VmFkwo" = "Définir ${item} à ${value} (HSB)"; - -"WsMKz2" = "Définit l'état de ${item} à ${state}"; - -"X9cN9h" = "maison"; - -"XMNs7r" = "Définir l'état du Switch"; - -"YFIIBn" = "Nom de la maison"; - -"YUoeAL" = "Il y a ${count} maisons configurées avec un élément nommé '${item}'."; - -"ZuBsOg" = "maison"; - -"cBrxnz" = "Item"; - -"dtRamI" = "Valeur"; - -"eFNxrT" = "Nom de la maison"; - -"eFb7vM" = "Définir l'état d'un contacteur ouvert ou fermé"; - -"exrmKW" = "Pour confirmer, vous vouliez '${home}' ?"; - -"gBn7U6" = "Définir la valeur de contrôle de chaîne"; - -"gQzHiG" = "Item"; - -"gobcZi" = "Pour confirmer, vous vouliez '${home}' ?"; - -"h91G9C" = "Pour quelle maison voulez-vous obtenir la valeur ?"; - -"hflKDd" = "Désolé impossible de trouver ${item}"; - -"i8fP5e" = "Action"; - -"iBrALm" = "maison"; - -"iCUEet" = "Action invalide : ${action} pour ${item}"; - -"iKSmqU" = "Pour confirmer, vous vouliez '${home}' ?"; - -"iRjffC" = "Valeur vide invalide pour ${item}"; - -"jP4VSa" = "Il y a ${count} maisons configurées avec un élément nommé '${item}'."; - -"jbUbYw" = "Pour quelle maison voulez-vous obtenir la valeur ?"; - -"jyXazc" = "Envoyer ${action} à ${item}"; - -"k0eXWc" = "Item"; - -"k6pj4o" = "maison"; - -"lLFYtM" = "Il y a ${count} maisons configurées avec un élément nommé '${item}'."; - -"llL4sP" = "Valeur"; - -"lqlrGx" = "Nom du Switch"; - -"m5DFXq" = "État"; - -"m9yAKr" = "Récupérer l'état de ${item}"; - -"mAKsWc" = "Valeur"; - -"mHbznY" = "Pour quelle maison voulez-vous obtenir la valeur ?"; - -"mVz5mJ" = "Définir ${item} à ${value}"; - -"mpxNDZ" = "Item"; - -"n7cmvU" = "État"; - -"nzw2UT" = "Définir la valeur de l'état du contacteur"; - -"o35DYr" = "Définir la valeur de contrôle du numéro"; - -"oOjAU0" = "Définir l'état d'un contacteur ouvert ou fermé"; - -"oUolm5" = "Envoyer ${action} à ${item}"; - -"pg4Bec" = "Définir la valeur entière d'un dimmer ou d'un volet roulant"; - -"pptfkD" = "Définir la valeur décimale d'un item de contrôle de nombre"; - -"ptGHON" = "Désolé impossible de trouver ${item}"; - -"qYPBPC" = "Définir ${item} à ${value}"; - -"qu9K9v" = "Définit l'état de ${item} à ${state}"; - -"rVXwR2" = "Nom de la maison"; - -"raIAR6" = "Valeur invalide : ${value} pour ${item} doit être HSB (0-360,0-100,0-100)"; - -"rsrXB9" = "Nom de la maison"; - -"skEQxZ" = "Item"; - -"tclrCp" = "Item"; - -"tkWfxz" = "Pour confirmer, vous vouliez '${home}' ?"; - -"ttiBzN" = "Définit l'état de ${item} à ${state}"; - -"tuvmDw" = "Valeur"; - -"uGsyyj" = "240,100,100"; - -"v3hsWV" = "Nom de la maison"; - -"wm0fqx" = "Envoyé la valeur ${value} à ${item}"; - -"zLlY3g" = "maison"; - -"zV2TH2" = "Maison"; diff --git a/openHABIntents/it.lproj/Intents.strings b/openHABIntents/it.lproj/Intents.strings deleted file mode 100644 index 77ca98408..000000000 --- a/openHABIntents/it.lproj/Intents.strings +++ /dev/null @@ -1,243 +0,0 @@ -"0hYnWM" = "Per confermare, volevi '${home}'?"; - -"1GaCDV" = "Azione"; - -"1Kf0qe" = "Imposta Valore Controllo Colore"; - -"1SeS3V" = "Imposta ${item} a ${value} (HSB)"; - -"1USOx9" = "Item"; - -"1cBKdm" = "Imposta ${item} a ${value}"; - -"2q0TdY" = "Spiacente non posso trovare ${item}"; - -"3LMXV8" = "Per quale casa vuoi ottenere il valore?"; - -"3evvza" = "Valore"; - -"4WCzFG" = "Per quale casa vuoi ottenere il valore?"; - -"5B4e9l" = "Imposta il valore di una item di controllo di tipo stringa"; - -"5Uu4bK" = "Leggi lo stato dell’Item"; - -"67qFTP" = "Valore vuoto non valido per ${item}"; - -"68Y2Ya" = "Nome Dimmer/Roller"; - -"6hUiXZ" = "Lo stato di ${item} è ${state}"; - -"71MWtI" = "casa"; - -"7BE642" = "Ci sono ${count} case configurate con un elemento chiamato '${item}'."; - -"7GA2x6" = "casa"; - -"80K2Dt" = "Spiacente non posso trovare ${item}"; - -"8OBnvT" = "Valore vuoto non valido per ${item}"; - -"8dhboX" = "Valore"; - -"9Pu17E" = "Per quale casa vuoi ottenere il valore?"; - -"B2I9b5" = "Item"; - -"BaNYuS" = "Ci sono ${count} case configurate con un elemento chiamato '${item}'."; - -"CZfT5a" = "Inviato il numero ${value} a ${item}"; - -"Cdg015" = "Imposta ${item} a ${value}"; - -"Ch9Akw" = "Imposta ${item} a ${value}"; - -"DxO2wk" = "Imposta il colore di un Item di controllo colore"; - -"E07xjp" = "Imposta ${item} a ${value} (HSB)"; - -"E291x3" = "Valore"; - -"EEj00Q" = "Imposta ${item} a ${value}"; - -"Fb0pUB" = "Inviata l'azione di ${action} per cambiare ${item}"; - -"GD4RTw" = "Recupera lo stato attuale di un Item"; - -"GI0faP" = "Stato non valido: ${state} per ${item}"; - -"Gjd9MH" = "Imposta ${item} a ${value}"; - -"H26PEQ" = "Lo stato di ${item} è stato impostato su ${state}"; - -"H2V4UV" = "Ci sono ${count} case configurate con un elemento chiamato '${item}'."; - -"HGbUGw" = "Valore"; - -"IS2nO0" = "Nome della casa"; - -"IpbXHW" = "Invia ${action} a ${item}"; - -"JUznKl" = "Inviata la stringa ${value} a ${item}"; - -"Jn2aNw" = "Per confermare, volevi '${home}'?"; - -"KXqi2s" = "Item"; - -"M8mX5O" = "Item"; - -"MCp4x6" = "Item"; - -"MGYSky" = "Imposta ${item} a ${value}"; - -"MIqlld" = "Ci sono ${count} case configurate con un elemento chiamato '${item}'."; - -"N5YsnB" = "Spiacente non posso trovare ${item}"; - -"Ncumus" = "Nome della casa"; - -"NehcMF" = "Nome Item"; - -"NjP70V" = "Spiacente non posso trovare ${item}"; - -"Nqxuw0" = "Stato"; - -"Nuhhxp" = "Item"; - -"Oazi8d" = "Spiacente non posso trovare ${item}"; - -"P9kblb" = "Nome della casa"; - -"QATq1i" = "Per quale casa vuoi ottenere il valore?"; - -"SMYYBh" = "Imposta ${item} a ${value}"; - -"TJ1p5Y" = "Azione"; - -"TK8Rrn" = "Inviato il valore di colore di ${value} a ${item}"; - -"UZYwuH" = "Imposta il valore di Dimmer o Tapparella"; - -"V0QsN4" = "Valore non valido ${value} per ${item} (0-100)"; - -"VEApie" = "Per confermare, volevi '${home}'?"; - -"VTeXc1" = "Item"; - -"VmFkwo" = "Imposta ${item} a ${value} (HSB)"; - -"WsMKz2" = "Imposta lo stato di ${item} a ${state}"; - -"X9cN9h" = "casa"; - -"XMNs7r" = "Imposta stato Interruttore"; - -"YFIIBn" = "Nome della casa"; - -"YUoeAL" = "Ci sono ${count} case configurate con un elemento chiamato '${item}'."; - -"ZuBsOg" = "casa"; - -"cBrxnz" = "Item"; - -"dtRamI" = "Valore"; - -"eFNxrT" = "Nome della casa"; - -"eFb7vM" = "Imposta lo stato di un contatto aperto o chiuso"; - -"exrmKW" = "Per confermare, volevi '${home}'?"; - -"gBn7U6" = "Imposta Valore della Stringa di Controllo"; - -"gQzHiG" = "Item"; - -"gobcZi" = "Per confermare, volevi '${home}'?"; - -"h91G9C" = "Per quale casa vuoi ottenere il valore?"; - -"hflKDd" = "Spiacente non posso trovare ${item}"; - -"i8fP5e" = "Azione"; - -"iBrALm" = "casa"; - -"iCUEet" = "Azione non valida: ${action} per ${item}"; - -"iKSmqU" = "Per confermare, volevi '${home}'?"; - -"iRjffC" = "Valore vuoto non valido per ${item}"; - -"jP4VSa" = "Ci sono ${count} case configurate con un elemento chiamato '${item}'."; - -"jbUbYw" = "Per quale casa vuoi ottenere il valore?"; - -"jyXazc" = "Invia ${action} a ${item}"; - -"k0eXWc" = "Item"; - -"k6pj4o" = "casa"; - -"lLFYtM" = "Ci sono ${count} case configurate con un elemento chiamato '${item}'."; - -"llL4sP" = "Valore"; - -"lqlrGx" = "Cambia nome"; - -"m5DFXq" = "Stato"; - -"m9yAKr" = "Leggi lo stato di ${item}"; - -"mAKsWc" = "Valore"; - -"mHbznY" = "Per quale casa vuoi ottenere il valore?"; - -"mVz5mJ" = "Imposta ${item} a ${value}"; - -"mpxNDZ" = "Item"; - -"n7cmvU" = "Stato"; - -"nzw2UT" = "Imposta lo Stato del Contatto"; - -"o35DYr" = "Imposta Valore del Numero di Controllo"; - -"oOjAU0" = "Imposta lo stato di un interruttore ad acceso o spento"; - -"oUolm5" = "Invia ${action} a ${item}"; - -"pg4Bec" = "Imposta il valore intero di un dimmer o una tapparella"; - -"pptfkD" = "Imposta il valore decimale di un Item di controllo numerico"; - -"ptGHON" = "Spiacente non posso trovare ${item}"; - -"qYPBPC" = "Imposta ${item} a ${value}"; - -"qu9K9v" = "Imposta lo stato di ${item} a ${state}"; - -"rVXwR2" = "Nome della casa"; - -"raIAR6" = "Valore non valido: ${value} per ${item} deve essere HSB (0-360,0-100,0-100)"; - -"rsrXB9" = "Nome della casa"; - -"skEQxZ" = "Item"; - -"tclrCp" = "Item"; - -"tkWfxz" = "Per confermare, volevi '${home}'?"; - -"ttiBzN" = "Imposta lo stato di ${item} a ${state}"; - -"tuvmDw" = "Valore"; - -"uGsyyj" = "240,100,100"; - -"v3hsWV" = "Nome della casa"; - -"wm0fqx" = "Imposta il valore di ${value} a ${item}"; - -"zLlY3g" = "casa"; - -"zV2TH2" = "Casa"; diff --git a/openHABIntents/nb.lproj/Intents.strings b/openHABIntents/nb.lproj/Intents.strings deleted file mode 100644 index 3560993b6..000000000 --- a/openHABIntents/nb.lproj/Intents.strings +++ /dev/null @@ -1,243 +0,0 @@ -"0hYnWM" = "For å bekrefte, mente du '${home}'?"; - -"1GaCDV" = "Handling"; - -"1Kf0qe" = "Angi Fargekontrollverdi"; - -"1SeS3V" = "Sett ${item} til ${value} (HSB)"; - -"1USOx9" = "Item"; - -"1cBKdm" = "Sett ${item} til ${value}"; - -"2q0TdY" = "Beklager, kan ikke finne ${item}"; - -"3LMXV8" = "For hvilket hjem vil du hente verdien?"; - -"3evvza" = "Verdi"; - -"4WCzFG" = "For hvilket hjem vil du hente verdien?"; - -"5B4e9l" = "Angi teksten til et teksterkontroll-Item"; - -"5Uu4bK" = "Få Item-tilstand"; - -"67qFTP" = "Ugyldig tom verdi for ${item}"; - -"68Y2Ya" = "Navn på Dimmer/Ruller"; - -"6hUiXZ" = "Tilstanden til ${item} er ${state}"; - -"71MWtI" = "hjem"; - -"7BE642" = "Det er ${count} konfigurerte hjem med et element kalt '${item}'."; - -"7GA2x6" = "hjem"; - -"80K2Dt" = "Beklager, kan ikke finne ${item}"; - -"8OBnvT" = "Ugyldig tom verdi for ${item}"; - -"8dhboX" = "Verdi"; - -"9Pu17E" = "For hvilket hjem vil du hente verdien?"; - -"B2I9b5" = "Item"; - -"BaNYuS" = "Det er ${count} konfigurerte hjem med et element kalt '${item}'."; - -"CZfT5a" = "Send den numeriske verdien ${value} til ${item}"; - -"Cdg015" = "Sett ${item} til ${value}"; - -"Ch9Akw" = "Sett ${item} til ${value}"; - -"DxO2wk" = "Angi fargen til et fargekontroll-Item"; - -"E07xjp" = "Sett ${item} til ${value} (HSB)"; - -"E291x3" = "Verdi"; - -"EEj00Q" = "Sett ${item} til ${value}"; - -"Fb0pUB" = "Send handlingen ${action} til bryter ${item}"; - -"GD4RTw" = "Hent nåværende tilstand for et Item"; - -"GI0faP" = "Ugyldig tilstand: ${state} for ${item}"; - -"Gjd9MH" = "Sett ${item} til ${value}"; - -"H26PEQ" = "Tilstanden til ${item} ble satt til ${state}"; - -"H2V4UV" = "Det er ${count} konfigurerte hjem med et element kalt '${item}'."; - -"HGbUGw" = "Verdi"; - -"IS2nO0" = "Hjemnavn"; - -"IpbXHW" = "Send ${action} til ${item}"; - -"JUznKl" = "Send strengverdien ${value} til ${item}"; - -"Jn2aNw" = "For å bekrefte, mente du '${home}'?"; - -"KXqi2s" = "Item"; - -"M8mX5O" = "Item"; - -"MCp4x6" = "Item"; - -"MGYSky" = "Sett ${item} til ${value}"; - -"MIqlld" = "Det er ${count} konfigurerte hjem med et element kalt '${item}'."; - -"N5YsnB" = "Beklager, kan ikke finne ${item}"; - -"Ncumus" = "Hjemnavn"; - -"NehcMF" = "Item-navn"; - -"NjP70V" = "Beklager, kan ikke finne ${item}"; - -"Nqxuw0" = "Tilstand"; - -"Nuhhxp" = "Item"; - -"Oazi8d" = "Beklager, kan ikke finne ${item}"; - -"P9kblb" = "Hjemnavn"; - -"QATq1i" = "For hvilket hjem vil du hente verdien?"; - -"SMYYBh" = "Sett ${item} til ${value}"; - -"TJ1p5Y" = "Handling"; - -"TK8Rrn" = "Sendte fargeverdien ${value} til ${item}"; - -"UZYwuH" = "Angi Verdi for Dimmer eller Rullegardin"; - -"V0QsN4" = "Ugyldig verdi ${value} for ${item} (0-100)"; - -"VEApie" = "For å bekrefte, mente du '${home}'?"; - -"VTeXc1" = "Item"; - -"VmFkwo" = "Sett ${item} til ${value} (HSB)"; - -"WsMKz2" = "Sett tilstanden til ${item} til ${state}"; - -"X9cN9h" = "hjem"; - -"XMNs7r" = "Angi Brytertilstand"; - -"YFIIBn" = "Hjemnavn"; - -"YUoeAL" = "Det er ${count} konfigurerte hjem med et element kalt '${item}'."; - -"ZuBsOg" = "hjem"; - -"cBrxnz" = "Item"; - -"dtRamI" = "Verdi"; - -"eFNxrT" = "Hjemnavn"; - -"eFb7vM" = "Sett tilstanden til en bryter til åpen eller lukket"; - -"exrmKW" = "For å bekrefte, mente du '${home}'?"; - -"gBn7U6" = "Sett Strengkontroll-Verdi"; - -"gQzHiG" = "Item"; - -"gobcZi" = "For å bekrefte, mente du '${home}'?"; - -"h91G9C" = "For hvilket hjem vil du hente verdien?"; - -"hflKDd" = "Beklager, kan ikke finne ${item}"; - -"i8fP5e" = "Handling"; - -"iBrALm" = "hjem"; - -"iCUEet" = "Ugyldig handling: ${action} for ${item}"; - -"iKSmqU" = "For å bekrefte, mente du '${home}'?"; - -"iRjffC" = "Ugyldig tom verdi for ${item}"; - -"jP4VSa" = "Det er ${count} konfigurerte hjem med et element kalt '${item}'."; - -"jbUbYw" = "For hvilket hjem vil du hente verdien?"; - -"jyXazc" = "Send ${action} til ${item}"; - -"k0eXWc" = "Item"; - -"k6pj4o" = "hjem"; - -"lLFYtM" = "Det er ${count} konfigurerte hjem med et element kalt '${item}'."; - -"llL4sP" = "Verdi"; - -"lqlrGx" = "Navn på bryter"; - -"m5DFXq" = "Tilstand"; - -"m9yAKr" = "Hent tilstand til ${item}"; - -"mAKsWc" = "Verdi"; - -"mHbznY" = "For hvilket hjem vil du hente verdien?"; - -"mVz5mJ" = "Sett ${item} til ${value}"; - -"mpxNDZ" = "Item"; - -"n7cmvU" = "Tilstand"; - -"nzw2UT" = "Sett Tilstand for Bryter"; - -"o35DYr" = "Sett Verdi for Numerisk Kontroll"; - -"oOjAU0" = "Sett tilstanden til en bryter til av eller på"; - -"oUolm5" = "Send ${action} til ${item}"; - -"pg4Bec" = "Sett heltallsverdien til en dimmer eller rullegardin"; - -"pptfkD" = "Sett desimalverdien til en numerisk kontroll-Item"; - -"ptGHON" = "Beklager, kan ikke finne ${item}"; - -"qYPBPC" = "Sett ${item} til ${value}"; - -"qu9K9v" = "Sett tilstanden til ${item} til ${state}"; - -"rVXwR2" = "Hjemnavn"; - -"raIAR6" = "Ugyldig verdi: ${value} for ${item} må være HSB (0-360,0-100,0-100)"; - -"rsrXB9" = "Hjemnavn"; - -"skEQxZ" = "Item"; - -"tclrCp" = "Item"; - -"tkWfxz" = "For å bekrefte, mente du '${home}'?"; - -"ttiBzN" = "Sett tilstanden til ${item} til ${state}"; - -"tuvmDw" = "Verdi"; - -"uGsyyj" = "240,100,100"; - -"v3hsWV" = "Hjemnavn"; - -"wm0fqx" = "Sendte verdien ${value} til ${item}"; - -"zLlY3g" = "hjem"; - -"zV2TH2" = "Hjem"; diff --git a/openHABIntents/nl.lproj/Intents.strings b/openHABIntents/nl.lproj/Intents.strings deleted file mode 100644 index f43ceb97c..000000000 --- a/openHABIntents/nl.lproj/Intents.strings +++ /dev/null @@ -1,243 +0,0 @@ -"0hYnWM" = "Ter bevestiging, u bedoelde '${home}'?"; - -"1GaCDV" = "Actie"; - -"1Kf0qe" = "Kleurwaarde instellen"; - -"1SeS3V" = "Stel ${item} in op ${value} (HSB)"; - -"1USOx9" = "Item"; - -"1cBKdm" = "Stel ${item} in op ${value}"; - -"2q0TdY" = "Sorry kan ${item} niet vinden"; - -"3LMXV8" = "Voor welk huis wilt u de waarde ophalen?"; - -"3evvza" = "Waarde"; - -"4WCzFG" = "Voor welk huis wilt u de waarde ophalen?"; - -"5B4e9l" = "De tekenreeks van een string controle item instellen"; - -"5Uu4bK" = "Haal item state op"; - -"67qFTP" = "Ongeldige lege waarde voor ${item}"; - -"68Y2Ya" = "Dimmer/Roller Naam"; - -"6hUiXZ" = "De staat van ${item} is ${state}"; - -"71MWtI" = "huis"; - -"7BE642" = "Er zijn ${count} geconfigureerde huizen met een item genaamd '${item}'."; - -"7GA2x6" = "huis"; - -"80K2Dt" = "Sorry kan ${item} niet vinden"; - -"8OBnvT" = "Ongeldige lege waarde voor ${item}"; - -"8dhboX" = "Waarde"; - -"9Pu17E" = "Voor welk huis wilt u de waarde ophalen?"; - -"B2I9b5" = "Item"; - -"BaNYuS" = "Er zijn ${count} geconfigureerde huizen met een item genaamd '${item}'."; - -"CZfT5a" = "De waarde ${value} is verzonden naar ${item}"; - -"Cdg015" = "Stel ${item} in op ${value}"; - -"Ch9Akw" = "Stel ${item} in op ${value}"; - -"DxO2wk" = "Stel de kleur van een kleurbesturingsitem in"; - -"E07xjp" = "Stel ${item} in op ${value} (HSB)"; - -"E291x3" = "Waarde"; - -"EEj00Q" = "Stel ${item} in op ${value}"; - -"Fb0pUB" = "Actie ${action} verzonden om ${item} om te zetten"; - -"GD4RTw" = "Haal de huidige status van een item op"; - -"GI0faP" = "Status ongeldig: ${state} voor ${item}"; - -"Gjd9MH" = "Stel ${item} in op ${value}"; - -"H26PEQ" = "De status van ${item} is ingesteld op ${state}"; - -"H2V4UV" = "Er zijn ${count} geconfigureerde huizen met een item genaamd '${item}'."; - -"HGbUGw" = "Waarde"; - -"IS2nO0" = "Huisnaam"; - -"IpbXHW" = "${action} verzenden naar ${item}"; - -"JUznKl" = "De waarde ${value} is verzonden naar ${item}"; - -"Jn2aNw" = "Ter bevestiging, u bedoelde '${home}'?"; - -"KXqi2s" = "Item"; - -"M8mX5O" = "Item"; - -"MCp4x6" = "Item"; - -"MGYSky" = "Stel ${item} in op ${value}"; - -"MIqlld" = "Er zijn ${count} geconfigureerde huizen met een item genaamd '${item}'."; - -"N5YsnB" = "Sorry kan ${item} niet vinden"; - -"Ncumus" = "Huisnaam"; - -"NehcMF" = "Item Naam"; - -"NjP70V" = "Sorry kan ${item} niet vinden"; - -"Nqxuw0" = "Status"; - -"Nuhhxp" = "Item"; - -"Oazi8d" = "Sorry kan ${item} niet vinden"; - -"P9kblb" = "Huisnaam"; - -"QATq1i" = "Voor welk huis wilt u de waarde ophalen?"; - -"SMYYBh" = "Stel ${item} in op ${value}"; - -"TJ1p5Y" = "Actie"; - -"TK8Rrn" = "De kleurwaarde van ${value} is verzonden naar ${item}"; - -"UZYwuH" = "Stel Dimmer of Rolluik waarde in"; - -"V0QsN4" = "Ongeldige waarde ${value} voor ${item} (0-100)"; - -"VEApie" = "Ter bevestiging, u bedoelde '${home}'?"; - -"VTeXc1" = "Item"; - -"VmFkwo" = "Stel ${item} in op ${value} (HSB)"; - -"WsMKz2" = "Zet de status van ${item} op ${state}"; - -"X9cN9h" = "huis"; - -"XMNs7r" = "Stel schakelstatus in"; - -"YFIIBn" = "Huisnaam"; - -"YUoeAL" = "Er zijn ${count} geconfigureerde huizen met een item genaamd '${item}'."; - -"ZuBsOg" = "huis"; - -"cBrxnz" = "Item"; - -"dtRamI" = "Waarde"; - -"eFNxrT" = "Huisnaam"; - -"eFb7vM" = "Zet de status van een contact open of gesloten"; - -"exrmKW" = "Ter bevestiging, u bedoelde '${home}'?"; - -"gBn7U6" = "Zet String Control Waarde in"; - -"gQzHiG" = "Item"; - -"gobcZi" = "Ter bevestiging, u bedoelde '${home}'?"; - -"h91G9C" = "Voor welk huis wilt u de waarde ophalen?"; - -"hflKDd" = "Sorry kan ${item} niet vinden"; - -"i8fP5e" = "Actie"; - -"iBrALm" = "huis"; - -"iCUEet" = "Status ongeldig: ${action} voor ${item}"; - -"iKSmqU" = "Ter bevestiging, u bedoelde '${home}'?"; - -"iRjffC" = "Ongeldige lege waarde voor ${item}"; - -"jP4VSa" = "Er zijn ${count} geconfigureerde huizen met een item genaamd '${item}'."; - -"jbUbYw" = "Voor welk huis wilt u de waarde ophalen?"; - -"jyXazc" = "${action} verzenden naar ${item}"; - -"k0eXWc" = "Item"; - -"k6pj4o" = "huis"; - -"lLFYtM" = "Er zijn ${count} geconfigureerde huizen met een item genaamd '${item}'."; - -"llL4sP" = "Waarde"; - -"lqlrGx" = "Wissel naam"; - -"m5DFXq" = "Status"; - -"m9yAKr" = "Haal ${item} state op"; - -"mAKsWc" = "Waarde"; - -"mHbznY" = "Voor welk huis wilt u de waarde ophalen?"; - -"mVz5mJ" = "Stel ${item} in op ${value}"; - -"mpxNDZ" = "Item"; - -"n7cmvU" = "Status"; - -"nzw2UT" = "Stel contact status waarde in"; - -"o35DYr" = "Zet Nummer Control Waarde"; - -"oOjAU0" = "Zet de status van een schakelaar aan of uit"; - -"oUolm5" = "${action} verzenden naar ${item}"; - -"pg4Bec" = "Zet de integerwaarde van een dimmer of rolluik"; - -"pptfkD" = "Stel de decimale waarde van een nummer control item in"; - -"ptGHON" = "Sorry kan ${item} niet vinden"; - -"qYPBPC" = "Stel ${item} in op ${value}"; - -"qu9K9v" = "Zet de status van ${item} op ${state}"; - -"rVXwR2" = "Huisnaam"; - -"raIAR6" = "Ongeldige waarde: ${value} voor ${item} moet HSB zijn (0-360,0-100,0-100)"; - -"rsrXB9" = "Huisnaam"; - -"skEQxZ" = "Item"; - -"tclrCp" = "Item"; - -"tkWfxz" = "Ter bevestiging, u bedoelde '${home}'?"; - -"ttiBzN" = "Zet de status van ${item} op ${state}"; - -"tuvmDw" = "Waarde"; - -"uGsyyj" = "240,100,100"; - -"v3hsWV" = "Huisnaam"; - -"wm0fqx" = "De waarde van ${value} is verzonden naar ${item}"; - -"zLlY3g" = "huis"; - -"zV2TH2" = "Huis"; diff --git a/openHABIntents/ru.lproj/Intents.strings b/openHABIntents/ru.lproj/Intents.strings deleted file mode 100644 index 45f12fe6f..000000000 --- a/openHABIntents/ru.lproj/Intents.strings +++ /dev/null @@ -1,243 +0,0 @@ -"0hYnWM" = "Just to confirm, you wanted '${home}'?"; - -"1GaCDV" = "Action"; - -"1Kf0qe" = "Set Color Control Value"; - -"1SeS3V" = "Set ${item} to ${value} (HSB)"; - -"1USOx9" = "Item"; - -"1cBKdm" = "Set ${item} to ${value}"; - -"2q0TdY" = "Sorry can't find ${item}"; - -"3LMXV8" = "For which home do you want to get the value?"; - -"3evvza" = "Value"; - -"4WCzFG" = "For which home do you want to get the value?"; - -"5B4e9l" = "Set the string of a string control item"; - -"5Uu4bK" = "Get Item State"; - -"67qFTP" = "Invalid empty value for ${item}"; - -"68Y2Ya" = "Dimmer/Roller Name"; - -"6hUiXZ" = "The state of ${item} is ${state}"; - -"71MWtI" = "home"; - -"7BE642" = "There are ${count} configured homes with an item named '${item}'."; - -"7GA2x6" = "home"; - -"80K2Dt" = "Sorry can't find ${item}"; - -"8OBnvT" = "Invalid empty value for ${item}"; - -"8dhboX" = "Value"; - -"9Pu17E" = "For which home do you want to get the value?"; - -"B2I9b5" = "Item"; - -"BaNYuS" = "There are ${count} configured homes with an item named '${item}'."; - -"CZfT5a" = "Sent the number ${value} to ${item}"; - -"Cdg015" = "Set ${item} to ${value}"; - -"Ch9Akw" = "Set ${item} to ${value}"; - -"DxO2wk" = "Set the color of a color control item"; - -"E07xjp" = "Set ${item} to ${value} (HSB)"; - -"E291x3" = "Value"; - -"EEj00Q" = "Set ${item} to ${value}"; - -"Fb0pUB" = "Sent the action of ${action} to switch ${item}"; - -"GD4RTw" = "Retrieve the current state of an item"; - -"GI0faP" = "State invalid: ${state} for ${item}"; - -"Gjd9MH" = "Set ${item} to ${value}"; - -"H26PEQ" = "The state of ${item} was set to ${state}"; - -"H2V4UV" = "There are ${count} configured homes with an item named '${item}'."; - -"HGbUGw" = "Value"; - -"IS2nO0" = "Home name"; - -"IpbXHW" = "Send ${action} to ${item}"; - -"JUznKl" = "Sent the string ${value} to ${item}"; - -"Jn2aNw" = "Just to confirm, you wanted '${home}'?"; - -"KXqi2s" = "Item"; - -"M8mX5O" = "Item"; - -"MCp4x6" = "Item"; - -"MGYSky" = "Set ${item} to ${value}"; - -"MIqlld" = "There are ${count} configured homes with an item named '${item}'."; - -"N5YsnB" = "Sorry can't find ${item}"; - -"Ncumus" = "Home name"; - -"NehcMF" = "Item Name"; - -"NjP70V" = "Sorry can't find ${item}"; - -"Nqxuw0" = "State"; - -"Nuhhxp" = "Item"; - -"Oazi8d" = "Sorry can't find ${item}"; - -"P9kblb" = "Home name"; - -"QATq1i" = "For which home do you want to get the value?"; - -"SMYYBh" = "Set ${item} to ${value}"; - -"TJ1p5Y" = "Action"; - -"TK8Rrn" = "Sent the color value of ${value} to ${item}"; - -"UZYwuH" = "Set Dimmer or Roller Shutter Value"; - -"V0QsN4" = "Invalid value ${value} for ${item} (0-100)"; - -"VEApie" = "Just to confirm, you wanted '${home}'?"; - -"VTeXc1" = "Item"; - -"VmFkwo" = "Set ${item} to ${value} (HSB)"; - -"WsMKz2" = "Set the state of ${item} to ${state}"; - -"X9cN9h" = "home"; - -"XMNs7r" = "Set Switch State"; - -"YFIIBn" = "Home name"; - -"YUoeAL" = "There are ${count} configured homes with an item named '${item}'."; - -"ZuBsOg" = "home"; - -"cBrxnz" = "Item"; - -"dtRamI" = "Value"; - -"eFNxrT" = "Home name"; - -"eFb7vM" = "Set the state of a contact open or closed"; - -"exrmKW" = "Just to confirm, you wanted '${home}'?"; - -"gBn7U6" = "Set String Control Value"; - -"gQzHiG" = "Item"; - -"gobcZi" = "Just to confirm, you wanted '${home}'?"; - -"h91G9C" = "For which home do you want to get the value?"; - -"hflKDd" = "Sorry can't find ${item}"; - -"i8fP5e" = "Action"; - -"iBrALm" = "home"; - -"iCUEet" = "Action invalid: ${action} for ${item}"; - -"iKSmqU" = "Just to confirm, you wanted '${home}'?"; - -"iRjffC" = "Invalid empty value for ${item}"; - -"jP4VSa" = "There are ${count} configured homes with an item named '${item}'."; - -"jbUbYw" = "For which home do you want to get the value?"; - -"jyXazc" = "Send ${action} to ${item}"; - -"k0eXWc" = "Item"; - -"k6pj4o" = "home"; - -"lLFYtM" = "There are ${count} configured homes with an item named '${item}'."; - -"llL4sP" = "Value"; - -"lqlrGx" = "Switch name"; - -"m5DFXq" = "State"; - -"m9yAKr" = "Get ${item} State"; - -"mAKsWc" = "Value"; - -"mHbznY" = "For which home do you want to get the value?"; - -"mVz5mJ" = "Set ${item} to ${value}"; - -"mpxNDZ" = "Item"; - -"n7cmvU" = "State"; - -"nzw2UT" = "Set Contact State Value"; - -"o35DYr" = "Set Number Control Value"; - -"oOjAU0" = "Set the state of a switch on or off"; - -"oUolm5" = "Send ${action} to ${item}"; - -"pg4Bec" = "Set the integer value of a dimmer or roller shutter"; - -"pptfkD" = "Set the decimal value of a number control item"; - -"ptGHON" = "Sorry can't find ${item}"; - -"qYPBPC" = "Set ${item} to ${value}"; - -"qu9K9v" = "Set the state of ${item} to ${state}"; - -"rVXwR2" = "Home name"; - -"raIAR6" = "Invalid value: ${value} for ${item} must be HSB (0-360,0-100,0-100)"; - -"rsrXB9" = "Home name"; - -"skEQxZ" = "Item"; - -"tclrCp" = "Item"; - -"tkWfxz" = "Just to confirm, you wanted '${home}'?"; - -"ttiBzN" = "Set the state of ${item} to ${state}"; - -"tuvmDw" = "Value"; - -"uGsyyj" = "240,100,100"; - -"v3hsWV" = "Home name"; - -"wm0fqx" = "Sent the value of ${value} to ${item}"; - -"zLlY3g" = "home"; - -"zV2TH2" = "Home"; diff --git a/openHABIntentsTests/SetSwitchStateIntentHandlerTests.swift b/openHABIntentsTests/SetSwitchStateIntentHandlerTests.swift deleted file mode 100644 index 5b50ee05a..000000000 --- a/openHABIntentsTests/SetSwitchStateIntentHandlerTests.swift +++ /dev/null @@ -1,79 +0,0 @@ -// 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 - -//// Copyright (c) 2010-2025 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 XCTest -// @testable import openHAB // Replace with actual module name -// import Intents -// import OpenHABCore -// -// final class SetSwitchStateIntentHandlerTests: XCTestCase { -// -// final class MockItemCache: ItemCacheProtocol { -// var lastCommand: String? -// -// func getItem(name: String) async -> OpenHABItem? { -// .mockSwitch(name: name) -// } -// -// func sendCommand(_ item: OpenHABItem, commandToSend: String) async { -// lastCommand = commandToSend -// } -// -// func getItemNames(searchTerm: String?, types: [OpenHABItem.ItemType]?) -> [String] { -// return ["MockSwitch"] -// } -// } -// -// func testHandleIntentMissingItem() { -// let intent = OpenHABSetSwitchStateIntent() -// intent.item = nil -// intent.action = "On" -// -// let handler = SetSwitchStateIntentHandler(itemCache: MockItemCache()) -// let expectation = expectation(description: "Handle failure") -// -// handler.handle(intent: intent) { response in -// XCTAssertEqual(response.code, .failureInvalidItem) -// expectation.fulfill() -// } -// -// wait(for: [expectation], timeout: 1) -// } -// } -// -// extension OpenHABItem { -// static func mockSwitch(name: String = "MockSwitch", state: String = "OFF") -> OpenHABItem { -// return OpenHABItem( -// name: name, -// type: "Switch", -// state: state, -// link: "http://mock/api/items/\(name)", -// label: name, -// groupType: nil, -// stateDescription: nil, -// commandDescription: nil, -// members: [], -// category: nil, -// options: nil -// ) -// } -// } diff --git a/openHABTestsSwift/LocalizationTests.swift b/openHABTestsSwift/LocalizationTests.swift index c7acedf03..93cfdcc95 100644 --- a/openHABTestsSwift/LocalizationTests.swift +++ b/openHABTestsSwift/LocalizationTests.swift @@ -19,11 +19,6 @@ struct LocalizationTests { Bundle.main.localizations.filter { $0 != "Base" } } - private static var intentsBundle: Bundle? { - guard let pluginsURL = Bundle.main.builtInPlugInsURL else { return nil } - return Bundle(url: pluginsURL.appendingPathComponent("openHABIntents.appex")) - } - // MARK: - Tests @Test func infoPlistLocalizations() { @@ -41,84 +36,4 @@ struct LocalizationTests { } } } - - @Test func intentsLocalizations() { - guard let bundle = LocalizationTests.intentsBundle, - let path = bundle.url(forResource: "Intents", withExtension: "strings", subdirectory: nil, localization: "en"), - let localizableStrings = NSDictionary(contentsOf: path) as? [String: String], - !localizableStrings.isEmpty - else { - Issue.record("Failed to load Intents.strings.") - return - } - - let localizations = bundle.localizations.filter { $0 != "Base" } - - for language in localizations { - print("Testing language: '\(language)'.") - - for localizableString in localizableStrings { - let translation = localizableString.key.localized(for: language, with: "Intents", in: bundle) - #expect(translation != nil, "Failed to get translation for key '\(localizableString.key)' in language '\(language)'.") - #expect(translation != "__MISSING__", "Missing translation for key '\(localizableString.key)' in language '\(language)'.") - #expect(translation?.isEmpty == false, "Translation for key '\(localizableString.key)' in language '\(language)' is empty.") - print("Translation: \(localizableString.key) = \(translation ?? "FAILED")") - } - } - } - - @Test func intentsPlaceholders() { - let regex = #/\$\{([a-z0-9]*)\}/#.ignoresCase() - - guard let bundle = LocalizationTests.intentsBundle, - let path = bundle.url(forResource: "Intents", withExtension: "strings", subdirectory: nil, localization: "en"), - let placeholderTuples = (NSDictionary(contentsOf: path) as? [String: String])?.filter({ $0.value.contains("${") }), - !placeholderTuples.isEmpty - else { - Issue.record("Failed to load Intents.strings.") - return - } - - let localizations = bundle.localizations.filter { $0 != "Base" } - - for language in localizations { - print("Testing language: '\(language)'.") - - guard let path = bundle.url(forResource: "Intents", withExtension: "strings", subdirectory: nil, localization: language), - let languageTuples = (NSDictionary(contentsOf: path) as? [String: String])?.filter({ $0.value.contains("${") }), - !languageTuples.isEmpty - else { - Issue.record("Failed to load Intents.strings for language '\(language)'.") - continue - } - - #expect(placeholderTuples.count == languageTuples.count, "Number of strings with placeholders in language '\(language)' doesn't match. Translations to check: \(languageTuples.filter { !placeholderTuples.keys.contains($0.key) }).") - - for placeholderTuple in placeholderTuples { - let placeholderString = placeholderTuple.value - guard let translation = placeholderTuple.key.localized(for: language, with: "Intents", in: bundle) else { - continue - } - - let numberOfOccurrencesInPlaceholder = placeholderString.matches(of: regex).count - let numberOfOccurrencesInTranslation = translation.matches(of: regex).count - #expect(numberOfOccurrencesInPlaceholder == numberOfOccurrencesInTranslation, "Number of placeholders for key '\(placeholderTuple.key)' in language '\(language)' does not match.") - - let matchesPlaceholder = placeholderString.matches(of: regex).map { String($0.0) } - let matchesTranslation = translation.matches(of: regex).map { String($0.0) } - #expect(matchesPlaceholder.elementsEqual(matchesTranslation), "Placeholders do not match for key '\(placeholderTuple.key)' in language '\(language)'.") - print("Placeholders: \(matchesPlaceholder) == \(matchesTranslation)") - } - } - } -} - -private extension String { - func localized(for language: String, with table: String? = nil, in bundle: Bundle = .main) -> String? { - guard let path = bundle.path(forResource: language, ofType: "lproj") else { - return nil - } - - return Bundle(path: path)?.localizedString(forKey: self, value: "__MISSING__", table: table) - } } diff --git a/openHABWidget/Assets.xcassets/AccentColor.colorset/Contents.json b/openHABWidget/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 000000000..eb8789700 --- /dev/null +++ b/openHABWidget/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/openHABWidget/Assets.xcassets/AppIcon.appiconset/Contents.json b/openHABWidget/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 000000000..230588010 --- /dev/null +++ b/openHABWidget/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,35 @@ +{ + "images" : [ + { + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "tinted" + } + ], + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/openHABWidget/Assets.xcassets/Contents.json b/openHABWidget/Assets.xcassets/Contents.json new file mode 100644 index 000000000..73c00596a --- /dev/null +++ b/openHABWidget/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/openHABWidget/Assets.xcassets/WidgetBackground.colorset/Contents.json b/openHABWidget/Assets.xcassets/WidgetBackground.colorset/Contents.json new file mode 100644 index 000000000..eb8789700 --- /dev/null +++ b/openHABWidget/Assets.xcassets/WidgetBackground.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/openHABWidget/ConfigurationAppIntent.swift b/openHABWidget/ConfigurationAppIntent.swift new file mode 100644 index 000000000..e4fd19548 --- /dev/null +++ b/openHABWidget/ConfigurationAppIntent.swift @@ -0,0 +1,33 @@ +// 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 +import WidgetKit + +struct ConfigurationAppIntent: WidgetConfigurationIntent { + struct ItemOptionsProvider: DynamicOptionsProvider { + func results() async throws -> [String] { + let allItems = await OpenHABItemCache.instance.getAllCachedItems() + return allItems.flatMap { $0.value.map(\.name) } + } + } + + static var title: LocalizedStringResource = "Widget Configuration" + static var description = IntentDescription("Configure which openHAB item to display in the widget.") + + @Parameter(title: "Item", optionsProvider: ItemOptionsProvider()) + var item: String? + + @Parameter(title: "Home") + var home: Home? +} diff --git a/openHABWidget/Info.plist b/openHABWidget/Info.plist new file mode 100644 index 000000000..0f118fb75 --- /dev/null +++ b/openHABWidget/Info.plist @@ -0,0 +1,11 @@ + + + + + NSExtension + + NSExtensionPointIdentifier + com.apple.widgetkit-extension + + + diff --git a/openHABIntents/openHABIntents.entitlements b/openHABWidget/OpenHABWidget.entitlements similarity index 100% rename from openHABIntents/openHABIntents.entitlements rename to openHABWidget/OpenHABWidget.entitlements diff --git a/openHABWidget/OpenHABWidgetBundle.swift b/openHABWidget/OpenHABWidgetBundle.swift new file mode 100644 index 000000000..c4fce5e43 --- /dev/null +++ b/openHABWidget/OpenHABWidgetBundle.swift @@ -0,0 +1,21 @@ +// 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 SwiftUI +import WidgetKit + +@main +struct OpenHABWidgetBundle: WidgetBundle { + var body: some Widget { + SwitchWidgetView() + SensorWidgetView() + } +} diff --git a/openHABWidget/OpenHABWidgetEntryView.swift b/openHABWidget/OpenHABWidgetEntryView.swift new file mode 100644 index 000000000..6d2839ba2 --- /dev/null +++ b/openHABWidget/OpenHABWidgetEntryView.swift @@ -0,0 +1,452 @@ +// 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 +internal import SFSafeSymbols +import SwiftUI +import WidgetKit + +// MARK: - Timeline Entry + +struct SimpleEntry: TimelineEntry { + let date: Date + let configuration: ConfigurationAppIntent + let itemName: String? + let itemLabel: String? + let itemState: String? + let itemType: OpenHABItem.ItemType? + let item: OpenHABItem? // Store full item for creating intents + let homeUUID: UUID? // Store home UUID for creating entities +} + +// MARK: - Helper Functions + +@available(iOS 17.0, *) +private func createToggleIntent(item: OpenHABItem, homeUUID: UUID, home: Home) -> SetSwitchItemIntent { + let intent = SetSwitchItemIntent() + intent.itemEntity = SwitchItemEntity(item, homeId: homeUUID, homeName: home.displayString) + intent.action = .toggle + intent.home = home + return intent +} + +// MARK: - Timeline Provider + +struct Provider: AppIntentTimelineProvider { + func placeholder(in context: Context) -> SimpleEntry { + SimpleEntry( + date: Date(), + configuration: ConfigurationAppIntent(), + itemName: "Example Item", + itemLabel: "Example Item", + itemState: "ON", + itemType: .switchItem, + item: nil, + homeUUID: nil + ) + } + + func snapshot(for configuration: ConfigurationAppIntent, in context: Context) async -> SimpleEntry { + // Show placeholder data in widget gallery + if context.isPreview { + return placeholder(in: context) + } + return await createEntry(for: configuration) + } + + func timeline(for configuration: ConfigurationAppIntent, in context: Context) async -> Timeline { + let entry = await createEntry(for: configuration) + + // Refresh every 5 minutes + let nextUpdate = Calendar.current.date(byAdding: .minute, value: 5, to: Date())! + return Timeline(entries: [entry], policy: .after(nextUpdate)) + } + + private func createEntry(for configuration: ConfigurationAppIntent) async -> SimpleEntry { + guard let itemName = configuration.item, + !itemName.isEmpty, + let homeId = configuration.home?.id, + let homeUUID = UUID(uuidString: homeId) else { + return SimpleEntry( + date: Date(), + configuration: configuration, + itemName: nil, + itemLabel: nil, + itemState: nil, + itemType: nil, + item: nil, + homeUUID: nil + ) + } + + // Fetch the item from cache + let item = await OpenHABItemCache.instance.getItemUncached(name: itemName, home: homeUUID) + + return SimpleEntry( + date: Date(), + configuration: configuration, + itemName: item?.name ?? itemName, + itemLabel: item?.label.isEmpty == false ? item?.label : item?.name ?? itemName, + itemState: item?.state, + itemType: item?.type, + item: item, + homeUUID: homeUUID + ) + } +} + +// MARK: - Small Widget + +struct SmallWidgetView: View { + let entry: SimpleEntry + + var body: some View { + ZStack(alignment: .topLeading) { + VStack(spacing: 8) { + if let itemLabel = entry.itemLabel { + Text(itemLabel) + .font(.headline) + .lineLimit(1) + .minimumScaleFactor(0.7) + + if let itemState = entry.itemState { + Text(itemState) + .font(.title2) + .fontWeight(.bold) + .lineLimit(1) + .minimumScaleFactor(0.5) + } else { + Text("—") + .font(.title2) + .foregroundColor(.secondary) + } + + Spacer() + + // Interactive toggle for switch items + if entry.itemType == .switchItem, let item = entry.item, let homeUUID = entry.homeUUID, let home = entry.configuration.home { + Toggle(isOn: entry.itemState == "ON", intent: createToggleIntent(item: item, homeUUID: homeUUID, home: home)) { + Text(entry.itemState ?? "Unknown") + .font(.caption) + } + .tint(.green) + } + } else { + VStack { + Image(systemSymbol: .gear) + .font(.largeTitle) + .foregroundColor(.secondary) + Text("Configure Widget") + .font(.caption) + .foregroundColor(.secondary) + .multilineTextAlignment(.center) + } + } + } + .frame(maxHeight: .infinity) + .padding() + + // Widget indicator icon + Image(systemSymbol: .switch2) + .font(.system(size: 16)) + .foregroundStyle(.secondary) + .opacity(0.5) + .padding(8) + } + } +} + +// MARK: - Medium Widget + +struct MediumWidgetView: View { + let entry: SimpleEntry + + var body: some View { + ZStack(alignment: .topLeading) { + HStack(spacing: 16) { + VStack(alignment: .leading, spacing: 8) { + if let itemLabel = entry.itemLabel { + Text(itemLabel) + .font(.title3) + .fontWeight(.semibold) + .lineLimit(2) + + if let itemState = entry.itemState { + Text(itemState) + .font(.title) + .fontWeight(.bold) + .foregroundColor(.primary) + } else { + Text("No State") + .font(.title) + .foregroundColor(.secondary) + } + + if let itemType = entry.itemType { + Text(itemType.rawValue) + .font(.caption) + .foregroundColor(.secondary) + } + } else { + VStack(alignment: .leading) { + Image(systemSymbol: .gear) + .font(.largeTitle) + .foregroundColor(.secondary) + Text("Configure Widget") + .font(.subheadline) + .foregroundColor(.secondary) + } + } + + Spacer() + } + + Spacer() + + // Interactive toggle for switch items + if entry.itemType == .switchItem, let item = entry.item, let homeUUID = entry.homeUUID, let home = entry.configuration.home { + VStack(spacing: 8) { + Toggle(isOn: entry.itemState == "ON", intent: createToggleIntent(item: item, homeUUID: homeUUID, home: home)) { + VStack { + Image(systemSymbol: .power) + .font(.title2) + Text(entry.itemState ?? "Unknown") + .font(.caption) + } + } + .tint(.green) + } + } + } + .frame(maxHeight: .infinity) + .padding() + + // Widget indicator icon + Image(systemSymbol: .switch2) + .font(.system(size: 18)) + .foregroundStyle(.secondary) + .opacity(0.5) + .padding(8) + } + } +} + +// MARK: - Large Widget + +struct LargeWidgetView: View { + let entry: SimpleEntry + + var body: some View { + ZStack(alignment: .topLeading) { + VStack(alignment: .leading, spacing: 16) { + if let itemLabel = entry.itemLabel { + Text(itemLabel) + .font(.title) + .fontWeight(.bold) + + HStack { + VStack(alignment: .leading, spacing: 8) { + Text("Current State") + .font(.caption) + .foregroundColor(.secondary) + + if let itemState = entry.itemState { + Text(itemState) + .font(.system(size: 48, weight: .bold)) + .foregroundColor(.primary) + } else { + Text("—") + .font(.system(size: 48)) + .foregroundColor(.secondary) + } + } + + Spacer() + } + + if let itemType = entry.itemType { + HStack { + Image(systemSymbol: itemTypeIcon(for: itemType)) + .font(.caption) + Text(itemType.rawValue) + .font(.caption) + } + .foregroundColor(.secondary) + } + + Spacer() + + // Interactive toggle for switch items + if entry.itemType == .switchItem, let item = entry.item, let homeUUID = entry.homeUUID, let home = entry.configuration.home { + Toggle(isOn: entry.itemState == "ON", intent: createToggleIntent(item: item, homeUUID: homeUUID, home: home)) { + HStack { + Image(systemSymbol: .power) + Text(entry.itemState ?? "Unknown") + } + .font(.headline) + } + .tint(.green) + } + } else { + VStack(alignment: .center, spacing: 16) { + Image(systemSymbol: .gear) + .font(.system(size: 60)) + .foregroundColor(.secondary) + Text("Configure Widget") + .font(.title2) + .foregroundColor(.secondary) + Text("Long press the widget to configure which openHAB item to display") + .font(.caption) + .foregroundColor(.secondary) + .multilineTextAlignment(.center) + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + } + } + .frame(maxHeight: .infinity) + .padding() + + // Widget indicator icon + Image(systemSymbol: .switch2) + .font(.system(size: 20)) + .foregroundStyle(.secondary) + .opacity(0.5) + .padding(12) + } + } +} + +// MARK: - Accessory Views + +struct AccessoryCircularView: View { + let entry: SimpleEntry + + var body: some View { + if let itemState = entry.itemState { + ZStack { + AccessoryWidgetBackground() + VStack(spacing: 2) { + Image(systemSymbol: itemTypeIcon(for: entry.itemType)) + .font(.caption) + Text(itemState) + .font(.caption2) + .fontWeight(.bold) + .lineLimit(1) + .minimumScaleFactor(0.5) + } + } + } else { + ZStack { + AccessoryWidgetBackground() + Image(systemSymbol: .gear) + .font(.title3) + } + } + } +} + +struct AccessoryRectangularView: View { + let entry: SimpleEntry + + var body: some View { + if let itemLabel = entry.itemLabel { + VStack(alignment: .leading, spacing: 2) { + Text(itemLabel) + .font(.headline) + .lineLimit(1) + .minimumScaleFactor(0.7) + + if let itemState = entry.itemState { + Text(itemState) + .font(.body) + .fontWeight(.semibold) + .lineLimit(1) + } else { + Text("No State") + .font(.caption) + .foregroundColor(.secondary) + } + } + } else { + Text("Configure") + .font(.caption) + } + } +} + +struct AccessoryInlineView: View { + let entry: SimpleEntry + + var body: some View { + if let itemLabel = entry.itemLabel, let itemState = entry.itemState { + Text("\(itemLabel): \(itemState)") + } else if let itemLabel = entry.itemLabel { + Text(itemLabel) + } else { + Text("Configure Widget") + } + } +} + +// MARK: - Widget View + +struct OpenHABWidgetEntryView: View { + var entry: SimpleEntry + @Environment(\.widgetFamily) var family + + var body: some View { + switch family { + case .systemSmall: + SmallWidgetView(entry: entry) + case .systemMedium: + MediumWidgetView(entry: entry) + case .systemLarge: + LargeWidgetView(entry: entry) + case .accessoryCircular: + AccessoryCircularView(entry: entry) + case .accessoryRectangular: + AccessoryRectangularView(entry: entry) + case .accessoryInline: + AccessoryInlineView(entry: entry) + default: + SmallWidgetView(entry: entry) + } + } +} + +// MARK: - Preview + +#Preview(as: .systemSmall) { + OpenHABWidgetView() +} timeline: { + SimpleEntry( + date: .now, + configuration: ConfigurationAppIntent(), + itemName: "Living Room Light", + itemLabel: "Living Room Light", + itemState: "ON", + itemType: .switchItem, + item: nil, + homeUUID: nil + ) + SimpleEntry( + date: .now, + configuration: ConfigurationAppIntent(), + itemName: "Temperature", + itemLabel: "Temperature", + itemState: "22.5 °C", + itemType: .number, + item: nil, + homeUUID: nil + ) +} diff --git a/openHABWidget/OpenHABWidgetHelpers.swift b/openHABWidget/OpenHABWidgetHelpers.swift new file mode 100644 index 000000000..00618d5ed --- /dev/null +++ b/openHABWidget/OpenHABWidgetHelpers.swift @@ -0,0 +1,31 @@ +// 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 OpenHABCore +internal import SFSafeSymbols +import SwiftUI + +// MARK: - Item Type Icons + +/// Returns an SF Symbol icon for the given openHAB item type +func itemTypeIcon(for type: OpenHABItem.ItemType?) -> SFSymbol { + guard let type else { return .circleFill } + switch type { + case .switchItem: return .power + case .color: return .paintpalette + case .dimmer: return .lightMax + case .number: return .number + case .stringItem: return .textformat + case .contact: return .doorLeftHandOpen + case .rollershutter: return .blindsVerticalClosed + default: return .circleFill + } +} diff --git a/openHABWidget/OpenHABWidgetLiveActivity.swift b/openHABWidget/OpenHABWidgetLiveActivity.swift new file mode 100644 index 000000000..f075b3c6e --- /dev/null +++ b/openHABWidget/OpenHABWidgetLiveActivity.swift @@ -0,0 +1,222 @@ +// 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 ActivityKit +import OpenHABCore +internal import SFSafeSymbols +import SwiftUI +import WidgetKit + +struct OpenHABWidgetAttributes: ActivityAttributes { + struct ContentState: Codable, Hashable { + // Dynamic stateful properties about your activity go here! + var itemState: String? + var lastUpdated: Date + var iconColor: String? // Color from sitemap widget + } + + // Fixed non-changing properties about your activity go here! + var itemName: String + var itemLabel: String + var itemType: String // Using String for ItemType to ensure Codable +} + +// MARK: - Helper Functions + +private func parseColor(from colorString: String?) -> Color? { + guard let colorString, !colorString.isEmpty else { return nil } + let uiColor = UIColor(fromString: colorString) + return Color(uiColor) +} + +private func stateColor(for typeString: String, state: String?, iconColor: String?) -> Color { + // If iconColor is provided from sitemap, use it + if let color = parseColor(from: iconColor) { + return color + } + + // Otherwise fall back to state-based colors + guard let state else { return .secondary } + guard let type = OpenHABItem.ItemType(rawValue: typeString) else { return .primary } + + if type == .switchItem { + return state.uppercased() == "ON" ? .green : .red + } else if type == .contact { + return state.uppercased() == "OPEN" ? .orange : .green + } + return .primary +} + +struct OpenHABWidgetLiveActivity: Widget { + var body: some WidgetConfiguration { + ActivityConfiguration(for: OpenHABWidgetAttributes.self) { context in + // Lock screen/banner UI goes here + HStack(spacing: 16) { + // Icon + Image(systemSymbol: itemTypeIcon(for: OpenHABItem.ItemType(rawValue: context.attributes.itemType))) + .font(.title2) + .foregroundColor(stateColor(for: context.attributes.itemType, state: context.state.itemState, iconColor: context.state.iconColor)) + + VStack(alignment: .leading, spacing: 4) { + Text(context.attributes.itemLabel) + .font(.headline) + .lineLimit(1) + + if let state = context.state.itemState { + Text(state) + .font(.title3) + .fontWeight(.semibold) + .foregroundColor(stateColor(for: context.attributes.itemType, state: context.state.itemState, iconColor: context.state.iconColor)) + } else { + Text("No State") + .font(.subheadline) + .foregroundColor(.secondary) + } + } + + Spacer() + + Text(context.state.lastUpdated, style: .relative) + .font(.caption) + .foregroundColor(.secondary) + } + .padding() + .activityBackgroundTint(Color.gray.opacity(0.2)) + .activitySystemActionForegroundColor(Color.primary) + + } dynamicIsland: { context in + DynamicIsland { + // Expanded UI goes here + DynamicIslandExpandedRegion(.leading) { + Image(systemSymbol: itemTypeIcon(for: OpenHABItem.ItemType(rawValue: context.attributes.itemType))) + .font(.title2) + .foregroundColor(stateColor(for: context.attributes.itemType, state: context.state.itemState, iconColor: context.state.iconColor)) + } + DynamicIslandExpandedRegion(.trailing) { + VStack(alignment: .trailing, spacing: 2) { + if let state = context.state.itemState { + Text(state) + .font(.title3) + .fontWeight(.bold) + .foregroundColor(stateColor(for: context.attributes.itemType, state: context.state.itemState, iconColor: context.state.iconColor)) + } else { + Text("—") + .font(.title3) + .foregroundColor(.secondary) + } + Text(context.state.lastUpdated, style: .relative) + .font(.caption2) + .foregroundColor(.secondary) + } + } + DynamicIslandExpandedRegion(.bottom) { + HStack { + Text(context.attributes.itemLabel) + .font(.subheadline) + .lineLimit(1) + Spacer() + Text(context.attributes.itemType) + .font(.caption) + .foregroundColor(.secondary) + } + } + } compactLeading: { + Image(systemSymbol: itemTypeIcon(for: OpenHABItem.ItemType(rawValue: context.attributes.itemType))) + .foregroundColor(stateColor(for: context.attributes.itemType, state: context.state.itemState, iconColor: context.state.iconColor)) + } compactTrailing: { + if let state = context.state.itemState { + Text(state) + .font(.caption2) + .fontWeight(.semibold) + .lineLimit(1) + .minimumScaleFactor(0.6) + .foregroundColor(stateColor(for: context.attributes.itemType, state: context.state.itemState, iconColor: context.state.iconColor)) + } else { + Text("—") + .font(.caption2) + .foregroundColor(.secondary) + } + } minimal: { + Image(systemSymbol: itemTypeIcon(for: OpenHABItem.ItemType(rawValue: context.attributes.itemType))) + .foregroundColor(stateColor(for: context.attributes.itemType, state: context.state.itemState, iconColor: context.state.iconColor)) + } + .keylineTint(stateColor(for: context.attributes.itemType, state: context.state.itemState, iconColor: context.state.iconColor)) + } + } +} + +private extension OpenHABWidgetAttributes { + static var switchPreview: OpenHABWidgetAttributes { + OpenHABWidgetAttributes( + itemName: "LivingRoom_Light", + itemLabel: "Living Room Light", + itemType: "Switch" + ) + } + + static var temperaturePreview: OpenHABWidgetAttributes { + OpenHABWidgetAttributes( + itemName: "Kitchen_Temperature", + itemLabel: "Kitchen Temperature", + itemType: "Number" + ) + } + + static var contactPreview: OpenHABWidgetAttributes { + OpenHABWidgetAttributes( + itemName: "FrontDoor", + itemLabel: "Front Door", + itemType: "Contact" + ) + } +} + +private extension OpenHABWidgetAttributes.ContentState { + static var switchOn: OpenHABWidgetAttributes.ContentState { + OpenHABWidgetAttributes.ContentState(itemState: "ON", lastUpdated: Date(), iconColor: "green") + } + + static var switchOff: OpenHABWidgetAttributes.ContentState { + OpenHABWidgetAttributes.ContentState(itemState: "OFF", lastUpdated: Date(), iconColor: "red") + } + + static var temperature: OpenHABWidgetAttributes.ContentState { + OpenHABWidgetAttributes.ContentState(itemState: "22.5 °C", lastUpdated: Date(), iconColor: "blue") + } + + static var doorOpen: OpenHABWidgetAttributes.ContentState { + OpenHABWidgetAttributes.ContentState(itemState: "OPEN", lastUpdated: Date(), iconColor: "orange") + } + + static var doorClosed: OpenHABWidgetAttributes.ContentState { + OpenHABWidgetAttributes.ContentState(itemState: "CLOSED", lastUpdated: Date(), iconColor: "green") + } +} + +#Preview("Switch Item", as: .content, using: OpenHABWidgetAttributes.switchPreview) { + OpenHABWidgetLiveActivity() +} contentStates: { + OpenHABWidgetAttributes.ContentState.switchOn + OpenHABWidgetAttributes.ContentState.switchOff +} + +#Preview("Temperature", as: .content, using: OpenHABWidgetAttributes.temperaturePreview) { + OpenHABWidgetLiveActivity() +} contentStates: { + OpenHABWidgetAttributes.ContentState.temperature +} + +#Preview("Door Sensor", as: .content, using: OpenHABWidgetAttributes.contactPreview) { + OpenHABWidgetLiveActivity() +} contentStates: { + OpenHABWidgetAttributes.ContentState.doorOpen + OpenHABWidgetAttributes.ContentState.doorClosed +} diff --git a/openHABWidget/OpenHABWidgetView.swift b/openHABWidget/OpenHABWidgetView.swift new file mode 100644 index 000000000..55ec82463 --- /dev/null +++ b/openHABWidget/OpenHABWidgetView.swift @@ -0,0 +1,39 @@ +// 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 SwiftUI +import WidgetKit + +struct OpenHABWidgetView: Widget { + let kind = "OpenHABWidget" + + var body: some WidgetConfiguration { + AppIntentConfiguration( + kind: kind, + intent: ConfigurationAppIntent.self, + provider: Provider() + ) { entry in + OpenHABWidgetEntryView(entry: entry) + .containerBackground(.fill.tertiary, for: .widget) + } + .configurationDisplayName("openHAB Item") + .description("Display and control your openHAB items") + .supportedFamilies([ + .systemSmall, + .systemMedium, + .systemLarge, + .accessoryCircular, + .accessoryRectangular, + .accessoryInline + ]) + .widgetContentMarginsDisabled() + } +} diff --git a/openHABWidget/SensorConfigurationAppIntent.swift b/openHABWidget/SensorConfigurationAppIntent.swift new file mode 100644 index 000000000..07429ea54 --- /dev/null +++ b/openHABWidget/SensorConfigurationAppIntent.swift @@ -0,0 +1,24 @@ +// 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 + +struct SensorConfigurationAppIntent: WidgetConfigurationIntent { + static var title: LocalizedStringResource = "Sensor Widget Configuration" + static var description = IntentDescription("Configure which sensor item to display in the widget.") + + @Parameter(title: "Home") + var home: Home? + + @Parameter(title: "Sensor Item") + var itemEntity: SensorWidgetItemEntity? +} diff --git a/openHABWidget/SensorWidgetEntryView.swift b/openHABWidget/SensorWidgetEntryView.swift new file mode 100644 index 000000000..d62cf6158 --- /dev/null +++ b/openHABWidget/SensorWidgetEntryView.swift @@ -0,0 +1,426 @@ +// 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 +internal import os.log +internal import SFSafeSymbols +import SwiftUI +import WidgetKit + +// MARK: - Timeline Entry + +struct SensorEntry: TimelineEntry { + let date: Date + let configuration: SensorConfigurationAppIntent + let item: OpenHABItem? + let homeUUID: UUID? +} + +// MARK: - Timeline Provider + +struct SensorProvider: AppIntentTimelineProvider { + func placeholder(in context: Context) -> SensorEntry { + // Create a sample sensor item for placeholder + let sampleItem = OpenHABItem( + name: "LivingRoomTemperature", + type: "Number:Temperature", + state: "22.5 °C", + link: "", + label: "Living Room Temperature", + groupType: nil, + stateDescription: nil, + commandDescription: nil, + members: [], + category: "temperature", + options: nil + ) + + return SensorEntry( + date: Date(), + configuration: SensorConfigurationAppIntent(), + item: sampleItem, + homeUUID: nil + ) + } + + func snapshot(for configuration: SensorConfigurationAppIntent, in context: Context) async -> SensorEntry { + // Show placeholder data in widget gallery + if context.isPreview { + return placeholder(in: context) + } + return await createEntry(for: configuration) + } + + func timeline(for configuration: SensorConfigurationAppIntent, in context: Context) async -> Timeline { + let entry = await createEntry(for: configuration) + + // Refresh every 5 minutes + let nextUpdate = Calendar.current.date(byAdding: .minute, value: 5, to: Date())! + return Timeline(entries: [entry], policy: .after(nextUpdate)) + } + + private func createEntry(for configuration: SensorConfigurationAppIntent) async -> SensorEntry { + guard let itemEntity = configuration.itemEntity else { + return SensorEntry( + date: Date(), + configuration: configuration, + item: nil, + homeUUID: nil + ) + } + + // Get item from entity + let item = itemEntity.item + let homeUUID = itemEntity.homeId + + // Register this item for monitoring in the main app + await WidgetItemRegistry.shared.registerItem(name: item.name, homeId: homeUUID) + + // Refresh the item state from cache + let refreshedItem = await OpenHABItemCache.instance.getItemUncached(name: item.name, home: homeUUID) + + return SensorEntry( + date: Date(), + configuration: configuration, + item: refreshedItem ?? item, + homeUUID: homeUUID + ) + } +} + +// MARK: - Small Widget + +struct SensorSmallWidgetView: View { + let entry: SensorEntry + + var body: some View { + ZStack(alignment: .topLeading) { + VStack(spacing: 8) { + if let item = entry.item { + let itemLabel = item.label.isEmpty ? item.name : item.label + Text(itemLabel) + .font(.headline) + .lineLimit(1) + .minimumScaleFactor(0.7) + + Spacer() + + if let itemState = item.state { + Text(itemState) + .font(.title2) + .fontWeight(.bold) + .lineLimit(2) + .minimumScaleFactor(0.5) + .multilineTextAlignment(.center) + } else { + Text("—") + .font(.title2) + .foregroundColor(.secondary) + } + + Spacer() + } else { + VStack { + Image(systemSymbol: .gear) + .font(.largeTitle) + .foregroundColor(.secondary) + Text("Configure Widget") + .font(.caption) + .foregroundColor(.secondary) + .multilineTextAlignment(.center) + } + } + } + .frame(maxHeight: .infinity) + .padding() + + // Widget indicator icon + Image(systemSymbol: .gaugeWithDotsNeedleBottom50percent) + .font(.system(size: 16)) + .foregroundStyle(.secondary) + .opacity(0.5) + .padding(8) + } + } +} + +// MARK: - Medium Widget + +struct SensorMediumWidgetView: View { + let entry: SensorEntry + + var body: some View { + ZStack(alignment: .topLeading) { + HStack(spacing: 16) { + VStack(alignment: .leading, spacing: 8) { + if let item = entry.item { + let itemLabel = item.label.isEmpty ? item.name : item.label + Text(itemLabel) + .font(.title3) + .fontWeight(.semibold) + .lineLimit(2) + + Spacer() + + if let itemState = item.state { + Text(itemState) + .font(.system(size: 36, weight: .bold)) + .foregroundColor(.primary) + .lineLimit(2) + } else { + Text("No Data") + .font(.title) + .foregroundColor(.secondary) + } + + Text(item.type?.rawValue ?? "Sensor") + .font(.caption) + .foregroundColor(.secondary) + } else { + VStack(alignment: .leading) { + Image(systemSymbol: .gear) + .font(.largeTitle) + .foregroundColor(.secondary) + Text("Configure Widget") + .font(.subheadline) + .foregroundColor(.secondary) + } + } + } + + Spacer() + } + .frame(maxHeight: .infinity) + .padding() + + // Widget indicator icon + Image(systemSymbol: .gaugeWithDotsNeedleBottom50percent) + .font(.system(size: 18)) + .foregroundStyle(.secondary) + .opacity(0.5) + .padding(8) + } + } +} + +// MARK: - Large Widget + +struct SensorLargeWidgetView: View { + let entry: SensorEntry + + var body: some View { + ZStack(alignment: .topLeading) { + VStack(alignment: .leading, spacing: 16) { + if let item = entry.item { + let itemLabel = item.label.isEmpty ? item.name : item.label + Text(itemLabel) + .font(.title) + .fontWeight(.bold) + + HStack { + VStack(alignment: .leading, spacing: 8) { + Text("Current Value") + .font(.caption) + .foregroundColor(.secondary) + + if let itemState = item.state { + Text(itemState) + .font(.system(size: 48, weight: .bold)) + .foregroundColor(.primary) + .lineLimit(2) + } else { + Text("—") + .font(.system(size: 48)) + .foregroundColor(.secondary) + } + } + + Spacer() + } + + HStack { + Image(systemSymbol: sensorIcon(for: item.type)) + .font(.caption) + Text(item.type?.rawValue ?? "Sensor") + .font(.caption) + } + .foregroundColor(.secondary) + + Spacer() + } else { + VStack(alignment: .center, spacing: 16) { + Image(systemSymbol: .gear) + .font(.system(size: 60)) + .foregroundColor(.secondary) + Text("Configure Widget") + .font(.title2) + .foregroundColor(.secondary) + Text("Long press the widget to configure which sensor to display") + .font(.caption) + .foregroundColor(.secondary) + .multilineTextAlignment(.center) + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + } + } + .frame(maxHeight: .infinity) + .padding() + + // Widget indicator icon + Image(systemSymbol: .gaugeWithDotsNeedleBottom50percent) + .font(.system(size: 20)) + .foregroundStyle(.secondary) + .opacity(0.5) + .padding(12) + } + } + + private func sensorIcon(for type: OpenHABItem.ItemType?) -> SFSymbol { + guard let type else { return .gaugeWithDotsNeedleBottom50percent } + switch type { + case .number, .numberWithDimension: + return .gaugeWithDotsNeedleBottom50percent + case .stringItem: + return .textQuote + default: + return .gaugeWithDotsNeedleBottom50percent + } + } +} + +// MARK: - Accessory Views + +struct SensorAccessoryCircularView: View { + let entry: SensorEntry + + var body: some View { + if let item = entry.item, let itemState = item.state { + ZStack { + AccessoryWidgetBackground() + VStack(spacing: 2) { + Image(systemSymbol: .gaugeWithDotsNeedleBottom50percent) + .font(.caption) + Text(itemState) + .font(.caption2) + .fontWeight(.bold) + .lineLimit(1) + .minimumScaleFactor(0.5) + } + } + } else { + ZStack { + AccessoryWidgetBackground() + Image(systemSymbol: .gear) + .font(.title3) + } + } + } +} + +struct SensorAccessoryRectangularView: View { + let entry: SensorEntry + + var body: some View { + if let item = entry.item { + let itemLabel = item.label.isEmpty ? item.name : item.label + VStack(alignment: .leading, spacing: 2) { + Text(itemLabel) + .font(.headline) + .lineLimit(1) + .minimumScaleFactor(0.7) + + if let itemState = item.state { + Text(itemState) + .font(.body) + .fontWeight(.semibold) + .lineLimit(1) + } else { + Text("No Data") + .font(.caption) + .foregroundColor(.secondary) + } + } + } else { + Text("Configure") + .font(.caption) + } + } +} + +struct SensorAccessoryInlineView: View { + let entry: SensorEntry + + var body: some View { + if let item = entry.item { + let itemLabel = item.label.isEmpty ? item.name : item.label + if let itemState = item.state { + Text("\(itemLabel): \(itemState)") + } else { + Text(itemLabel) + } + } else { + Text("Configure Widget") + } + } +} + +// MARK: - Widget View + +struct SensorWidgetEntryView: View { + var entry: SensorEntry + @Environment(\.widgetFamily) var family + + var body: some View { + switch family { + case .systemSmall: + SensorSmallWidgetView(entry: entry) + case .systemMedium: + SensorMediumWidgetView(entry: entry) + case .systemLarge: + SensorLargeWidgetView(entry: entry) + case .accessoryCircular: + SensorAccessoryCircularView(entry: entry) + case .accessoryRectangular: + SensorAccessoryRectangularView(entry: entry) + case .accessoryInline: + SensorAccessoryInlineView(entry: entry) + default: + SensorSmallWidgetView(entry: entry) + } + } +} + +// MARK: - Preview + +#Preview(as: .systemSmall) { + SensorWidgetView() +} timeline: { + SensorEntry( + date: .now, + configuration: SensorConfigurationAppIntent(), + item: OpenHABItem( + name: "LivingRoomTemperature", + type: "Number:Temperature", + state: "22.5 °C", + link: "", + label: "Living Room Temperature", + groupType: nil, + stateDescription: nil, + commandDescription: nil, + members: [], + category: "temperature", + options: nil + ), + homeUUID: nil + ) +} diff --git a/openHABWidget/SensorWidgetItemEntity.swift b/openHABWidget/SensorWidgetItemEntity.swift new file mode 100644 index 000000000..9f3ed419f --- /dev/null +++ b/openHABWidget/SensorWidgetItemEntity.swift @@ -0,0 +1,39 @@ +// 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 + +@available(iOS 17.0, macOS 14.0, watchOS 10.0, *) +struct SensorWidgetItemEntity: ItemEntity { + struct SensorWidgetItemQuery: ItemEntityQuery { + typealias EntityType = SensorWidgetItemEntity + + @IntentParameterDependency(\.$home) + var intent + + var allowedTypes: [OpenHABItem.ItemType] = [.number, .numberWithDimension, .stringItem] + var selectedHomeId: UUID? { UUID(uuidString: intent?.home.id ?? "") } + } + + static let typeDisplayRepresentation = TypeDisplayRepresentation(name: "Sensor Item") + static let defaultQuery = SensorWidgetItemQuery() + + var id: ItemIdentifier + var item: OpenHABItem + var homeName: String? + + init(id: ItemIdentifier, item: OpenHABItem, homeName: String? = nil) { + self.id = id + self.item = item + self.homeName = homeName + } +} diff --git a/openHABWidget/SensorWidgetView.swift b/openHABWidget/SensorWidgetView.swift new file mode 100644 index 000000000..d4360de54 --- /dev/null +++ b/openHABWidget/SensorWidgetView.swift @@ -0,0 +1,39 @@ +// 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 SwiftUI +import WidgetKit + +struct SensorWidgetView: Widget { + let kind = "OpenHABSensorWidget" + + var body: some WidgetConfiguration { + AppIntentConfiguration( + kind: kind, + intent: SensorConfigurationAppIntent.self, + provider: SensorProvider() + ) { entry in + SensorWidgetEntryView(entry: entry) + .containerBackground(.fill.tertiary, for: .widget) + } + .configurationDisplayName("openHAB Sensor") + .description("Display your openHAB sensor values") + .supportedFamilies([ + .systemSmall, + .systemMedium, + .systemLarge, + .accessoryCircular, + .accessoryRectangular, + .accessoryInline + ]) + .widgetContentMarginsDisabled() + } +} diff --git a/openHABWidget/SwitchConfigurationAppIntent.swift b/openHABWidget/SwitchConfigurationAppIntent.swift new file mode 100644 index 000000000..305dabf06 --- /dev/null +++ b/openHABWidget/SwitchConfigurationAppIntent.swift @@ -0,0 +1,24 @@ +// 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 + +struct SwitchConfigurationAppIntent: WidgetConfigurationIntent { + static var title: LocalizedStringResource = "Switch Widget Configuration" + static var description = IntentDescription("Configure which switch item to control in the widget.") + + @Parameter(title: "Home") + var home: Home? + + @Parameter(title: "Switch Item") + var itemEntity: SwitchWidgetItemEntity? +} diff --git a/openHABWidget/SwitchWidgetEntryView.swift b/openHABWidget/SwitchWidgetEntryView.swift new file mode 100644 index 000000000..391414aa5 --- /dev/null +++ b/openHABWidget/SwitchWidgetEntryView.swift @@ -0,0 +1,460 @@ +// 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 +internal import os.log +internal import SFSafeSymbols +import SwiftUI +import WidgetKit + +// MARK: - Timeline Entry + +struct SwitchEntry: TimelineEntry { + let date: Date + let configuration: SwitchConfigurationAppIntent + let item: OpenHABItem? + let homeUUID: UUID? +} + +// MARK: - Helper Functions + +@available(iOS 17.0, *) +private func createToggleIntent(item: OpenHABItem, homeUUID: UUID, home: Home) -> SetSwitchItemIntent { + let intent = SetSwitchItemIntent() + intent.itemEntity = SwitchItemEntity(item, homeId: homeUUID, homeName: home.displayString) + intent.action = .toggle + intent.home = home + return intent +} + +// MARK: - Timeline Provider + +struct SwitchProvider: AppIntentTimelineProvider { + func placeholder(in context: Context) -> SwitchEntry { + // Create a sample switch item for placeholder + let sampleItem = OpenHABItem( + name: "LivingRoomLight", + type: "Switch", + state: "ON", + link: "", + label: "Living Room Light", + groupType: nil, + stateDescription: nil, + commandDescription: nil, + members: [], + category: "light", + options: nil + ) + + return SwitchEntry( + date: Date(), + configuration: SwitchConfigurationAppIntent(), + item: sampleItem, + homeUUID: nil + ) + } + + func snapshot(for configuration: SwitchConfigurationAppIntent, in context: Context) async -> SwitchEntry { + // Show placeholder data in widget gallery + if context.isPreview { + return placeholder(in: context) + } + return await createEntry(for: configuration) + } + + func timeline(for configuration: SwitchConfigurationAppIntent, in context: Context) async -> Timeline { + let entry = await createEntry(for: configuration) + + // Refresh every 5 minutes + let nextUpdate = Calendar.current.date(byAdding: .minute, value: 5, to: Date())! + return Timeline(entries: [entry], policy: .after(nextUpdate)) + } + + private func createEntry(for configuration: SwitchConfigurationAppIntent) async -> SwitchEntry { + guard let itemEntity = configuration.itemEntity else { + return SwitchEntry( + date: Date(), + configuration: configuration, + item: nil, + homeUUID: nil + ) + } + + // Get item from entity + let item = itemEntity.item + let homeUUID = itemEntity.homeId + + // Register this item for monitoring in the main app + await WidgetItemRegistry.shared.registerItem(name: item.name, homeId: homeUUID) + + // Refresh the item state from cache + let refreshedItem = await OpenHABItemCache.instance.getItemUncached(name: item.name, home: homeUUID) + + return SwitchEntry( + date: Date(), + configuration: configuration, + item: refreshedItem ?? item, + homeUUID: homeUUID + ) + } +} + +// MARK: - Small Widget + +struct SwitchSmallWidgetView: View { + let entry: SwitchEntry + + var body: some View { + ZStack(alignment: .topLeading) { + VStack(spacing: 8) { + if let item = entry.item { + let itemLabel = item.label.isEmpty ? item.name : item.label + Text(itemLabel) + .font(.headline) + .lineLimit(1) + .minimumScaleFactor(0.7) + + if let itemState = item.state { + Text(itemState) + .font(.title2) + .fontWeight(.bold) + .lineLimit(1) + .minimumScaleFactor(0.5) + } else { + Text("—") + .font(.title2) + .foregroundColor(.secondary) + } + + Spacer() + + // Interactive toggle for switch items + if let homeUUID = entry.homeUUID, let home = entry.configuration.home { + Toggle( + isOn: item.state == "ON", + intent: createToggleIntent(item: item, homeUUID: homeUUID, home: home) + ) { + Text(item.state ?? "Unknown") + .font(.caption) + } + .tint(.green) + } + } else { + VStack { + Image(systemSymbol: .gear) + .font(.largeTitle) + .foregroundColor(.secondary) + Text("Configure Widget") + .font(.caption) + .foregroundColor(.secondary) + .multilineTextAlignment(.center) + } + } + } + .frame(maxHeight: .infinity) + .padding() + + // Widget indicator icon + Image(systemSymbol: .switch2) + .font(.system(size: 16)) + .foregroundStyle(.secondary) + .opacity(0.5) + .padding(8) + } + } +} + +// MARK: - Medium Widget + +struct SwitchMediumWidgetView: View { + let entry: SwitchEntry + + var body: some View { + ZStack(alignment: .topLeading) { + HStack(spacing: 16) { + VStack(alignment: .leading, spacing: 8) { + if let item = entry.item { + let itemLabel = item.label.isEmpty ? item.name : item.label + Text(itemLabel) + .font(.title3) + .fontWeight(.semibold) + .lineLimit(2) + + if let itemState = item.state { + Text(itemState) + .font(.title) + .fontWeight(.bold) + .foregroundColor(.primary) + } else { + Text("No State") + .font(.title) + .foregroundColor(.secondary) + } + + Text("Switch") + .font(.caption) + .foregroundColor(.secondary) + } else { + VStack(alignment: .leading) { + Image(systemSymbol: .gear) + .font(.largeTitle) + .foregroundColor(.secondary) + Text("Configure Widget") + .font(.subheadline) + .foregroundColor(.secondary) + } + } + + Spacer() + } + + Spacer() + + // Interactive toggle for switch items + if let item = entry.item, let homeUUID = entry.homeUUID, let home = entry.configuration.home { + VStack(spacing: 8) { + Toggle(isOn: item.state == "ON", intent: createToggleIntent(item: item, homeUUID: homeUUID, home: home)) { + VStack { + Image(systemSymbol: .power) + .font(.title2) + Text(item.state ?? "Unknown") + .font(.caption) + } + } + .tint(.green) + } + } + } + .frame(maxHeight: .infinity) + .padding() + + // Widget indicator icon + Image(systemSymbol: .switch2) + .font(.system(size: 18)) + .foregroundStyle(.secondary) + .opacity(0.5) + .padding(8) + } + } +} + +// MARK: - Large Widget + +struct SwitchLargeWidgetView: View { + let entry: SwitchEntry + + var body: some View { + ZStack(alignment: .topLeading) { + VStack(alignment: .leading, spacing: 16) { + if let item = entry.item { + let itemLabel = item.label.isEmpty ? item.name : item.label + Text(itemLabel) + .font(.title) + .fontWeight(.bold) + + HStack { + VStack(alignment: .leading, spacing: 8) { + Text("Current State") + .font(.caption) + .foregroundColor(.secondary) + + if let itemState = item.state { + Text(itemState) + .font(.system(size: 48, weight: .bold)) + .foregroundColor(.primary) + } else { + Text("—") + .font(.system(size: 48)) + .foregroundColor(.secondary) + } + } + + Spacer() + } + + HStack { + Image(systemSymbol: .switch2) + .font(.caption) + Text("Switch") + .font(.caption) + } + .foregroundColor(.secondary) + + Spacer() + + // Interactive toggle for switch items + if let homeUUID = entry.homeUUID, let home = entry.configuration.home { + Toggle(isOn: item.state == "ON", intent: createToggleIntent(item: item, homeUUID: homeUUID, home: home)) { + HStack { + Image(systemSymbol: .power) + Text(item.state ?? "Unknown") + } + .font(.headline) + } + .tint(.green) + } + } else { + VStack(alignment: .center, spacing: 16) { + Image(systemSymbol: .gear) + .font(.system(size: 60)) + .foregroundColor(.secondary) + Text("Configure Widget") + .font(.title2) + .foregroundColor(.secondary) + Text("Long press the widget to configure which switch to control") + .font(.caption) + .foregroundColor(.secondary) + .multilineTextAlignment(.center) + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + } + } + .frame(maxHeight: .infinity) + .padding() + + // Widget indicator icon + Image(systemSymbol: .switch2) + .font(.system(size: 20)) + .foregroundStyle(.secondary) + .opacity(0.5) + .padding(12) + } + } +} + +// MARK: - Accessory Views + +struct SwitchAccessoryCircularView: View { + let entry: SwitchEntry + + var body: some View { + if let item = entry.item, let itemState = item.state { + ZStack { + AccessoryWidgetBackground() + VStack(spacing: 2) { + Image(systemSymbol: .switch2) + .font(.caption) + Text(itemState) + .font(.caption2) + .fontWeight(.bold) + .lineLimit(1) + .minimumScaleFactor(0.5) + } + } + } else { + ZStack { + AccessoryWidgetBackground() + Image(systemSymbol: .gear) + .font(.title3) + } + } + } +} + +struct SwitchAccessoryRectangularView: View { + let entry: SwitchEntry + + var body: some View { + if let item = entry.item { + let itemLabel = item.label.isEmpty ? item.name : item.label + VStack(alignment: .leading, spacing: 2) { + Text(itemLabel) + .font(.headline) + .lineLimit(1) + .minimumScaleFactor(0.7) + + if let itemState = item.state { + Text(itemState) + .font(.body) + .fontWeight(.semibold) + .lineLimit(1) + } else { + Text("No State") + .font(.caption) + .foregroundColor(.secondary) + } + } + } else { + Text("Configure") + .font(.caption) + } + } +} + +struct SwitchAccessoryInlineView: View { + let entry: SwitchEntry + + var body: some View { + if let item = entry.item { + let itemLabel = item.label.isEmpty ? item.name : item.label + if let itemState = item.state { + Text("\(itemLabel): \(itemState)") + } else { + Text(itemLabel) + } + } else { + Text("Configure Widget") + } + } +} + +// MARK: - Widget View + +struct SwitchWidgetEntryView: View { + var entry: SwitchEntry + @Environment(\.widgetFamily) var family + + var body: some View { + switch family { + case .systemSmall: + SwitchSmallWidgetView(entry: entry) + case .systemMedium: + SwitchMediumWidgetView(entry: entry) + case .systemLarge: + SwitchLargeWidgetView(entry: entry) + case .accessoryCircular: + SwitchAccessoryCircularView(entry: entry) + case .accessoryRectangular: + SwitchAccessoryRectangularView(entry: entry) + case .accessoryInline: + SwitchAccessoryInlineView(entry: entry) + default: + SwitchSmallWidgetView(entry: entry) + } + } +} + +// MARK: - Preview + +#Preview(as: .systemSmall) { + SwitchWidgetView() +} timeline: { + SwitchEntry( + date: .now, + configuration: SwitchConfigurationAppIntent(), + item: OpenHABItem( + name: "LivingRoomLight", + type: "Switch", + state: "ON", + link: "", + label: "Living Room Light", + groupType: nil, + stateDescription: nil, + commandDescription: nil, + members: [], + category: "light", + options: nil + ), + homeUUID: nil + ) +} diff --git a/openHABWidget/SwitchWidgetItemEntity.swift b/openHABWidget/SwitchWidgetItemEntity.swift new file mode 100644 index 000000000..ef972b56d --- /dev/null +++ b/openHABWidget/SwitchWidgetItemEntity.swift @@ -0,0 +1,39 @@ +// 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 + +@available(iOS 17.0, macOS 14.0, watchOS 10.0, *) +struct SwitchWidgetItemEntity: ItemEntity { + struct SwitchWidgetItemQuery: ItemEntityQuery { + typealias EntityType = SwitchWidgetItemEntity + + @IntentParameterDependency(\.$home) + var intent + + var allowedTypes: [OpenHABItem.ItemType] = [.switchItem] + var selectedHomeId: UUID? { UUID(uuidString: intent?.home.id ?? "") } + } + + static let typeDisplayRepresentation = TypeDisplayRepresentation(name: "Switch Item") + static let defaultQuery = SwitchWidgetItemQuery() + + var id: ItemIdentifier + var item: OpenHABItem + var homeName: String? + + init(id: ItemIdentifier, item: OpenHABItem, homeName: String? = nil) { + self.id = id + self.item = item + self.homeName = homeName + } +} diff --git a/openHABWidget/SwitchWidgetView.swift b/openHABWidget/SwitchWidgetView.swift new file mode 100644 index 000000000..c82daa340 --- /dev/null +++ b/openHABWidget/SwitchWidgetView.swift @@ -0,0 +1,39 @@ +// 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 SwiftUI +import WidgetKit + +struct SwitchWidgetView: Widget { + let kind = "OpenHABSwitchWidget" + + var body: some WidgetConfiguration { + AppIntentConfiguration( + kind: kind, + intent: SwitchConfigurationAppIntent.self, + provider: SwitchProvider() + ) { entry in + SwitchWidgetEntryView(entry: entry) + .containerBackground(.fill.tertiary, for: .widget) + } + .configurationDisplayName("openHAB Switch") + .description("Control your openHAB switch items") + .supportedFamilies([ + .systemSmall, + .systemMedium, + .systemLarge, + .accessoryCircular, + .accessoryRectangular, + .accessoryInline + ]) + .widgetContentMarginsDisabled() + } +} diff --git a/openHABWidget/WidgetConfigurationExtension.swift b/openHABWidget/WidgetConfigurationExtension.swift new file mode 100644 index 000000000..3e9649b23 --- /dev/null +++ b/openHABWidget/WidgetConfigurationExtension.swift @@ -0,0 +1,24 @@ +// 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 Foundation +import SwiftUI +import WidgetKit + +extension WidgetConfiguration { + func widgetContentMarginsDisabled() -> some WidgetConfiguration { + if #available(iOS 17.0, *) { + return contentMarginsDisabled() + } else { + return self + } + } +} diff --git a/openapi-source.patch b/openapi-source.patch deleted file mode 100644 index c0cd92e28..000000000 --- a/openapi-source.patch +++ /dev/null @@ -1,84 +0,0 @@ -diff --git a/OpenHABCore/Sources/OpenHABCore/openapi/openapi.json b/OpenHABCore/Sources/OpenHABCore/openapi/openapi.json -index a0b77670..da93fcb2 100644 ---- a/OpenHABCore/Sources/OpenHABCore/openapi/openapi.json -+++ b/OpenHABCore/Sources/OpenHABCore/openapi/openapi.json -@@ -2962,6 +2962,14 @@ - "summary": "Sends a command to an item.", - "operationId": "sendItemCommand", - "parameters": [ -+ { -+ "name": "X-OpenHAB-Source", -+ "in": "header", -+ "description": "the source of the command; takes priority over the query parameter or JSON body if multiple are set", -+ "schema": { -+ "type": "string" -+ } -+ }, - { - "name": "itemname", - "in": "path", -@@ -2971,14 +2979,32 @@ - "pattern": "[a-zA-Z_0-9]+", - "type": "string" - } -+ }, -+ { -+ "name": "source", -+ "in": "query", -+ "description": "the source of the command", -+ "schema": { -+ "type": "string" -+ } - } - ], - "requestBody": { -- "description": "valid item command (e.g. ON, OFF, UP, DOWN, REFRESH)", -+ "description": "Valid item command (e.g., ON, OFF) either as plain text or JSON", - "content": { - "text/plain": { - "schema": { -- "type": "string" -+ "type": "string", -+ "example": "ON" -+ } -+ }, -+ "application/json": { -+ "schema": { -+ "type": "string", -+ "example": { -+ "value": "ON", -+ "source": "org.openhab.ios" -+ } - } - } - }, -@@ -3209,6 +3235,14 @@ - "type": "string" - } - }, -+ { -+ "name": "X-OpenHAB-Source", -+ "in": "header", -+ "description": "the source of the event; takes priority over the query parameter or JSON body if multiple are set", -+ "schema": { -+ "type": "string" -+ } -+ }, - { - "name": "itemname", - "in": "path", -@@ -3218,6 +3252,14 @@ - "pattern": "[a-zA-Z_0-9]+", - "type": "string" - } -+ }, -+ { -+ "name": "source", -+ "in": "query", -+ "description": "the source of the event", -+ "schema": { -+ "type": "string" -+ } - } - ], - "requestBody": {