diff --git a/NotificationService/NotificationService.swift b/NotificationService/NotificationService.swift
index cf785e5a..038aacdc 100644
--- a/NotificationService/NotificationService.swift
+++ b/NotificationService/NotificationService.swift
@@ -18,23 +18,23 @@ import UserNotifications
class NotificationService: UNNotificationServiceExtension {
var contentHandler: ((UNNotificationContent) -> Void)?
var bestAttemptContent: UNMutableNotificationContent?
-
+
override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
self.contentHandler = contentHandler
bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent)
if let bestAttemptContent {
var notificationActions: [UNNotificationAction] = []
let userInfo = bestAttemptContent.userInfo
-
+
os_log("didReceive userInfo %{PUBLIC}@", log: .default, type: .info, userInfo)
-
+
if let title = userInfo["title"] as? String {
bestAttemptContent.title = title
}
if let message = userInfo["message"] as? String {
bestAttemptContent.body = message
}
-
+
// Check if the user has defined custom actions in the payload
if let actionsArray = parseActions(userInfo), let category = parseCategory(userInfo) {
for actionDict in actionsArray {
@@ -42,7 +42,7 @@ class NotificationService: UNNotificationServiceExtension {
let title = actionDict["title"] {
var options: UNNotificationActionOptions = []
// navigate/browser options need to bring the app to the foreground
- if action.hasPrefix("ui") || action.hasPrefix("http") {
+ if action.hasPrefix("ui") || action.hasPrefix("http") || action.hasPrefix("app") {
options = [.foreground]
}
let notificationAction = UNNotificationAction(
@@ -56,31 +56,27 @@ class NotificationService: UNNotificationServiceExtension {
if !notificationActions.isEmpty {
os_log("didReceive registering %{PUBLIC}@ for category %{PUBLIC}@", log: .default, type: .info, notificationActions, category)
let notificationCategory =
- UNNotificationCategory(
- identifier: category,
- actions: notificationActions,
- intentIdentifiers: [],
- options: .customDismissAction
- )
+ UNNotificationCategory(
+ identifier: category,
+ actions: notificationActions,
+ intentIdentifiers: [],
+ options: .customDismissAction
+ )
UNUserNotificationCenter.current().getNotificationCategories { existingCategories in
- // Check if the new category already exists, this is a hash of the actions string done by the cloud service
- let existingCategoryIdentifiers = existingCategories.map(\.identifier)
- if !existingCategoryIdentifiers.contains(category) {
- var updatedCategories = existingCategories
- os_log("handleNotification adding category %{PUBLIC}@", log: .default, type: .info, category)
- updatedCategories.insert(notificationCategory)
- UNUserNotificationCenter.current().setNotificationCategories(updatedCategories)
- }
+ var updatedCategories = existingCategories
+ os_log("handleNotification adding category %{PUBLIC}@", log: .default, type: .info, category)
+ updatedCategories.insert(notificationCategory)
+ UNUserNotificationCenter.current().setNotificationCategories(updatedCategories)
}
}
}
-
+
// check if there is an attachment to put on the notification
// this should be last as we need to wait for media
// TODO: we should support relative paths and try the user's openHAB (local,remote) for content
if let attachmentURLString = userInfo["media-attachment-url"] as? String {
let isItem = attachmentURLString.starts(with: "item:")
-
+
let downloadCompletionHandler: @Sendable (UNNotificationAttachment?) -> Void = { attachment in
if let attachment {
os_log("handleNotification attaching %{PUBLIC}@", log: .default, type: .info, attachmentURLString)
@@ -90,7 +86,7 @@ class NotificationService: UNNotificationServiceExtension {
}
contentHandler(bestAttemptContent)
}
-
+
if isItem {
downloadAndAttachItemImage(itemURI: attachmentURLString, completion: downloadCompletionHandler)
} else {
@@ -101,7 +97,7 @@ class NotificationService: UNNotificationServiceExtension {
}
}
}
-
+
override func serviceExtensionTimeWillExpire() {
// Called just before the extension will be terminated by the system.
// Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used.
@@ -110,7 +106,7 @@ class NotificationService: UNNotificationServiceExtension {
contentHandler(bestAttemptContent)
}
}
-
+
private func parseActions(_ userInfo: [AnyHashable: Any]) -> [[String: String]]? {
// Extract actions and convert it from JSON string to an array of dictionaries
if let actionsString = userInfo["actions"] as? String, let actionsData = actionsString.data(using: .utf8) {
@@ -124,7 +120,7 @@ class NotificationService: UNNotificationServiceExtension {
}
return nil
}
-
+
private func parseCategory(_ userInfo: [AnyHashable: Any]) -> String? {
// Extract category from aps dictionary
if let aps = userInfo["aps"] as? [String: Any],
@@ -133,10 +129,10 @@ class NotificationService: UNNotificationServiceExtension {
}
return nil
}
-
+
private func downloadAndAttachMedia(url: String, completion: @escaping (UNNotificationAttachment?) -> Void) {
let client = HTTPClient(username: Preferences.username, password: Preferences.username, alwaysSendBasicAuth: Preferences.alwaysSendCreds)
-
+
let downloadCompletionHandler: @Sendable (URL?, URLResponse?, Error?) -> Void = { (localURL, response, error) in
guard let localURL else {
os_log("Error downloading media %{PUBLIC}@", log: .default, type: .error, error?.localizedDescription ?? "Unknown error")
@@ -151,16 +147,16 @@ class NotificationService: UNNotificationServiceExtension {
client.downloadFile(url: url, completionHandler: downloadCompletionHandler)
}
}
-
+
func downloadAndAttachItemImage(itemURI: String, completion: @escaping (UNNotificationAttachment?) -> Void) {
guard let itemURI = URL(string: itemURI), let scheme = itemURI.scheme else {
os_log("Could not find scheme %{PUBLIC}@", log: .default, type: .info)
completion(nil)
return
}
-
+
let itemName = String(itemURI.absoluteString.dropFirst(scheme.count + 1))
-
+
let client = HTTPClient(username: Preferences.username, password: Preferences.password, alwaysSendBasicAuth: Preferences.alwaysSendCreds)
client.getItem(baseURLs: [Preferences.localUrl, Preferences.remoteUrl], itemName: itemName) { item, error in
guard let item else {
@@ -203,16 +199,16 @@ class NotificationService: UNNotificationServiceExtension {
completion(nil)
}
}
-
+
func attachFile(localURL: URL, mimeType: String?, completion: @escaping (UNNotificationAttachment?) -> Void) {
do {
let fileManager = FileManager.default
let tempDirectory = NSTemporaryDirectory()
let tempFile = URL(fileURLWithPath: tempDirectory).appendingPathComponent(UUID().uuidString)
-
+
try fileManager.moveItem(at: localURL, to: tempFile)
let attachment: UNNotificationAttachment?
-
+
if let mimeType,
let utType = UTType(mimeType: mimeType),
utType.conforms(to: .data) {
diff --git a/OpenHABCore/Sources/OpenHABCore/Util/HTTPClient.swift b/OpenHABCore/Sources/OpenHABCore/Util/HTTPClient.swift
index 425f62b4..9bd82403 100644
--- a/OpenHABCore/Sources/OpenHABCore/Util/HTTPClient.swift
+++ b/OpenHABCore/Sources/OpenHABCore/Util/HTTPClient.swift
@@ -282,7 +282,7 @@ public class HTTPClient: NSObject, URLSessionDelegate, URLSessionTaskDelegate {
}
// MARK: - URLSessionDelegate for Client Certificates and Basic Auth
-
+
public func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
urlSessionInternal(session, task: nil, didReceive: challenge, completionHandler: completionHandler)
}
diff --git a/openHAB/OpenHABRootViewController.swift b/openHAB/OpenHABRootViewController.swift
index f390dc6b..44ae076e 100644
--- a/openHAB/OpenHABRootViewController.swift
+++ b/openHAB/OpenHABRootViewController.swift
@@ -202,6 +202,10 @@ class OpenHABRootViewController: UIViewController {
sendCommandAction(cmd)
} else if action.hasPrefix("http") {
httpCommandAction(action)
+ } else if action.hasPrefix("app") {
+ appCommandAction(action)
+ } else if action.hasPrefix("rule") {
+ ruleCommandAction(action)
}
}
}
@@ -265,6 +269,65 @@ class OpenHABRootViewController: UIViewController {
}
}
+ private func appCommandAction(_ command: String) {
+ let content = command.dropFirst(4) // Remove "app:"
+ let pairs = content.split(separator: ",")
+ for pair in pairs {
+ let keyValue = pair.split(separator: "=", maxSplits: 1)
+ guard keyValue.count == 2 else { continue }
+ if keyValue[0] == "ios" {
+ if let url = URL(string: String(keyValue[1])) {
+ os_log("appCommandAction opening %{public}@ %{public}@", log: .default, type: .error, String(keyValue[0]), String(keyValue[1]))
+ UIApplication.shared.open(url)
+ return
+ }
+ }
+ }
+ }
+
+ private func ruleCommandAction(_ command: String) {
+ let components = command.split(separator: ":", maxSplits: 2)
+
+ guard components.count == 3,
+ components[0] == "rule" else {
+ return
+ }
+
+ let uuid = String(components[1])
+ let propertiesString = String(components[2])
+
+ let propertyPairs = propertiesString.split(separator: ",")
+ var properties: [String: String] = [:]
+
+ for pair in propertyPairs {
+ let keyValue = pair.split(separator: "=", maxSplits: 1)
+ if keyValue.count == 2 {
+ let key = String(keyValue[0])
+ let value = String(keyValue[1])
+ properties[key] = value
+ }
+ }
+
+ var jsonString = ""
+ do {
+ let jsonData = try JSONSerialization.data(withJSONObject: properties, options: [.prettyPrinted])
+ jsonString = String(data: jsonData, encoding: .utf8)!
+ } catch {
+ // nothing
+ }
+ let client = HTTPClient(username: Preferences.username, password: Preferences.username)
+ client.doPost(baseURLs: [Preferences.localUrl, Preferences.remoteUrl], path: "/rest/rules/rules/\(uuid)/runnow", body: jsonString) { data, _, error in
+ if let error {
+ os_log("Could not send data %{public}@", log: .default, type: .error, error.localizedDescription)
+ } else {
+ os_log("Request succeeded", log: .default, type: .info)
+ if let data {
+ os_log("Data: %{public}@", log: .default, type: .debug, String(data: data, encoding: .utf8) ?? "")
+ }
+ }
+ }
+ }
+
func showSideMenu() {
os_log("OpenHABRootViewController showSideMenu", log: .viewCycle, type: .info)
if let menu = SideMenuManager.default.rightMenuNavigationController {
diff --git a/openHABIntents/openHABIntents.entitlements b/openHABIntents/openHABIntents.entitlements
index b6063e1b..029463f3 100644
--- a/openHABIntents/openHABIntents.entitlements
+++ b/openHABIntents/openHABIntents.entitlements
@@ -6,5 +6,9 @@
group.org.openhab.app
+ keychain-access-groups
+
+ $(AppIdentifierPrefix)org.openhab.app
+
diff --git a/openHABWatch Extension/openHABWatch Extension.entitlements b/openHABWatch Extension/openHABWatch Extension.entitlements
index b6063e1b..e38182d1 100644
--- a/openHABWatch Extension/openHABWatch Extension.entitlements
+++ b/openHABWatch Extension/openHABWatch Extension.entitlements
@@ -6,5 +6,9 @@
group.org.openhab.app
+ keychain-access-groups
+
+ $(AppIdentifierPrefix)org.openhab.app.watchkitapp
+