Skip to content

Commit

Permalink
Got the swift-openapi-generator working on openHAB iOS app:
Browse files Browse the repository at this point in the history
-Properly gets data every 30s
-Able to send commands
-Making use of structured concurrency, ie async/await, actors
-Still a lot to do

Renamed OpenHABSitemapPage into OpenHABPage to avoid confusion
Reworked OpenHABSitemap to properly handle embedded OpenHABPage
Created convenience initializers for OpenHAB models to map from openAPI generated models

Properly decoding widgets within a widget
Manually modifying the OpenHAB's openAPI schema
Manually adding X-Atmosphere-Transport in header parameters for pollDataPage

Transferred code to package - requires workaround to invoke the CLI manually: https://swiftpackageindex.com/apple/swift-openapi-generator/1.2.1/documentation/swift-openapi-generator/manually-invoking-the-generator-cli
:
- clone the generator package locally
- run locally  swift run swift-openapi-generator generate --config ../Sources/OpenHABCore/openapi/openapi-generator-config.yml --output-directory ../GeneratedSources/openapi ../Sources/OpenHABCore/openapi/openapi.json

Exclude the package and the generated code from swiftlint

Async update for actor APIActor and initialiser with URL about:blank
Using APIActor throughout the app
Upgrade target to iOS 16

Helper function openHABpollPage(sitemapname: String, longPolling: Bool) for access without

Making use internal accesModifier to properly isolate the internals in OpenHABCore
Using openAPI generated interface to send command
Support for basic authorization
Making use of os logger
  • Loading branch information
timbms committed Aug 12, 2024
1 parent 111e39b commit 237940e
Show file tree
Hide file tree
Showing 26 changed files with 20,350 additions and 629 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,6 @@ openHAB.ipa
build/
BuildTools/.build
OpenHABCore/Package.resolved

OpenHABCore/Sources/OpenHABCore/GeneratedSources
OpenHABCore/swift-openapi-generator
3 changes: 2 additions & 1 deletion BuildTools/.swiftlint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ excluded:
- ../fastlane
- ../OpenHABCore/.build
- .build

- ../OpenHABCore/Sources/OpenHABCore/GeneratedSources/*
- ../OpenHABCore/swift-openapi-generator
nesting:
type_level: 2

Expand Down
48 changes: 24 additions & 24 deletions NotificationService/NotificationService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -56,12 +56,12 @@ 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
var updatedCategories = existingCategories
os_log("handleNotification adding category %{PUBLIC}@", log: .default, type: .info, category)
Expand All @@ -70,13 +70,13 @@ class NotificationService: UNNotificationServiceExtension {
}
}
}

// 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)
Expand All @@ -86,7 +86,7 @@ class NotificationService: UNNotificationServiceExtension {
}
contentHandler(bestAttemptContent)
}

if isItem {
downloadAndAttachItemImage(itemURI: attachmentURLString, completion: downloadCompletionHandler)
} else {
Expand All @@ -97,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.
Expand All @@ -106,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) {
Expand All @@ -120,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],
Expand All @@ -129,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")
Expand All @@ -147,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 {
Expand Down Expand Up @@ -199,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) {
Expand Down
14 changes: 9 additions & 5 deletions OpenHABCore/Package.swift
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
// swift-tools-version:5.5
// swift-tools-version:5.9
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
name: "OpenHABCore",
platforms: [.iOS(.v12), .watchOS(.v6)],
platforms: [.iOS(.v16), .watchOS(.v10), .macOS(.v14)],
products: [
// Products define the executables and libraries a package produces, and make them visible to other packages.
.library(
Expand All @@ -15,8 +15,10 @@ let package = Package(
],
dependencies: [
// Dependencies declare other packages that this package depends on.
.package(name: "Alamofire", url: "https://github.com/Alamofire/Alamofire.git", from: "5.0.0"),
.package(name: "Kingfisher", url: "https://github.com/onevcat/Kingfisher.git", from: "7.0.0")
.package(url: "https://github.com/Alamofire/Alamofire.git", from: "5.0.0"),
.package(url: "https://github.com/onevcat/Kingfisher.git", from: "7.0.0"),
.package(url: "https://github.com/apple/swift-openapi-runtime", from: "1.0.0"),
.package(url: "https://github.com/apple/swift-openapi-urlsession", from: "1.0.0")
],
targets: [
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
Expand All @@ -25,7 +27,9 @@ let package = Package(
name: "OpenHABCore",
dependencies: [
.product(name: "Alamofire", package: "Alamofire", condition: .when(platforms: [.iOS, .watchOS])),
.product(name: "Kingfisher", package: "Kingfisher", condition: .when(platforms: [.iOS, .watchOS]))
.product(name: "Kingfisher", package: "Kingfisher", condition: .when(platforms: [.iOS, .watchOS])),
.product(name: "OpenAPIRuntime", package: "swift-openapi-runtime"),
.product(name: "OpenAPIURLSession", package: "swift-openapi-urlsession")
]
),
.testTarget(
Expand Down
Loading

0 comments on commit 237940e

Please sign in to comment.