diff --git a/prefab-client/Client/Client+Accessories.swift b/prefab-client/Client/Client+Accessories.swift index eb773b1..15563d9 100644 --- a/prefab-client/Client/Client+Accessories.swift +++ b/prefab-client/Client/Client+Accessories.swift @@ -15,5 +15,10 @@ extension Client { public func getAccessory(name: String, home: String, room: String) async throws -> String { return try await get(path: "/accessories/\(home)/\(room)/\(name)") } + + public func updateAccessory(name: String, home: String, room: String, serviceId: String, characteristicId: String, value: Any) async throws -> String { + let updateAccessoryInput: UpdateAccessoryInput = UpdateAccessoryInput(home: home, room: room, accessory: name, serviceId: serviceId, characteristicId: characteristicId, value: value as! String) + return try await put(path: "/accessories", data: updateAccessoryInput) + } } diff --git a/prefab-client/Client/Client.swift b/prefab-client/Client/Client.swift index 65922c1..d43136a 100644 --- a/prefab-client/Client/Client.swift +++ b/prefab-client/Client/Client.swift @@ -74,4 +74,27 @@ public class Client { } return body } + + func put(path: String, data: Codable) async throws -> String { + let json = try JSONEncoder().encode(data) + + let request = HTTPRequest(method: .put, scheme: self.scheme, authority: "\(self.host):\(self.port)", path: path) + let (data, response) = try await URLSession.shared.upload(for: URLRequest(httpRequest: request)!, from: json) + let body = String(decoding: data, as: UTF8.self) + if let httpResponse = response as? HTTPURLResponse { + guard httpResponse.httpResponse!.status == .ok else { + switch httpResponse.httpResponse!.status { + case .notFound: + throw HTTPResponseError.notFound(response: body) + case .tooManyRequests: + throw HTTPResponseError.tooManyRequests(response: body) + case .forbidden: + throw HTTPResponseError.forbidden(response: body) + default: + throw HTTPResponseError.unexpected(response: body, code: httpResponse.httpResponse!.status.code) + } + } + } + return body + } } diff --git a/prefab-client/Command/Prefab+Accessories.swift b/prefab-client/Command/Prefab+Accessories.swift index d25087a..e7452bf 100644 --- a/prefab-client/Command/Prefab+Accessories.swift +++ b/prefab-client/Command/Prefab+Accessories.swift @@ -51,7 +51,7 @@ extension Prefab { prefab get-accessory """, discussion: """ - Returns an accessories from HomeKit. + Returns an accessory from HomeKit. """) @Option(name: .shortAndLong, help: "The name of the home you would like accessories for.") var home: String @@ -78,3 +78,51 @@ extension Prefab { } } } + + +extension Prefab { + struct UpdateAccessory: AsyncParsableCommand { + static var configuration = CommandConfiguration( + commandName: "update-accessory", + abstract: "Update the characteristic of an accessory from a room in your home.", + usage: """ + prefab update-accessory + """, + discussion: """ + Update the characteristic of an accessory from a room in your home. + """) + @Option(name: .shortAndLong, help: "The name of the home you would like accessories for.") + var home: String + + @Option(name: .shortAndLong, help: "The name of the room you would like accessories for.") + var room: String + + @Option(name: .shortAndLong, help: "The name of the accessory you would like to update.") + var accessory: String + + @Option(name: .shortAndLong, help: "The id of the service you would like to update.") + var serviceId: String + + @Option(name: .shortAndLong, help: "The id of the characteristic you would like to update.") + var characteristicId: String + + @Option(name: .shortAndLong, help: "The value of the characteristic you would like to update.") + var value: String + +// 0D84EB02-914A-5B90-BF23-EF27764F9438 B16D61DB-EFC7-5BA3-BBF0-101860D06D60 + mutating func run() async { + do{ + let accData = try await client.updateAccessory(name: accessory, home: home, room: room, serviceId: serviceId, characteristicId: characteristicId, value: value) + print(accData) + } catch UninitializeClientError.propertyIsNotSet(let property) { + print("Attempting to use client without setting \(property)") + } catch HTTPResponseError.notFound(let response), HTTPResponseError.tooManyRequests(let response), HTTPResponseError.forbidden(let response) { + print("\(response)") + } catch HTTPResponseError.unexpected(let response, let code) { + print("\(response), \(code)") + } catch { + print("An unknown error occured: \(error)") + } + } + } +} diff --git a/prefab-client/Root.swift b/prefab-client/Root.swift index 5e94aac..729b4a8 100644 --- a/prefab-client/Root.swift +++ b/prefab-client/Root.swift @@ -19,7 +19,7 @@ struct Prefab: AsyncParsableCommand { discussion: """ Prints to stdout forever, or until you halt the program. """, - subcommands: [GetHomes.self, GetHome.self, GetRooms.self, GetRoom.self, GetAccessory.self, GetAccessories.self]) + subcommands: [GetHomes.self, GetHome.self, GetRooms.self, GetRoom.self, GetAccessory.self, GetAccessories.self, UpdateAccessory.self]) static let client: Client = Client.initShared(host: "localhost", port: "8080", scheme: "http") diff --git a/prefab.xcodeproj/xcuserdata/kellyplummer.xcuserdatad/xcschemes/xcschememanagement.plist b/prefab.xcodeproj/xcuserdata/kellyplummer.xcuserdatad/xcschemes/xcschememanagement.plist index c84084a..8b0080c 100644 --- a/prefab.xcodeproj/xcuserdata/kellyplummer.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/prefab.xcodeproj/xcuserdata/kellyplummer.xcuserdatad/xcschemes/xcschememanagement.plist @@ -7,7 +7,7 @@ Prefab.xcscheme_^#shared#^_ orderHint - 1 + 0 prefab-client.xcscheme_^#shared#^_ @@ -17,7 +17,7 @@ prefab.xcscheme_^#shared#^_ orderHint - 0 + 1 SuppressBuildableAutocreation diff --git a/prefab/http/Data.swift b/prefab/http/Data.swift index dea48e9..28d0892 100644 --- a/prefab/http/Data.swift +++ b/prefab/http/Data.swift @@ -7,7 +7,6 @@ import Foundation - struct Home: Encodable, Decodable { var name: String } @@ -35,6 +34,7 @@ struct Accessory: Encodable, Decodable { } struct Service: Encodable, Decodable { + var uniqueIdentifier: UUID var name: String var type: String var isPrimary: Bool @@ -45,8 +45,40 @@ struct Service: Encodable, Decodable { } struct Characteristic: Encodable, Decodable { + var uniqueIdentifier: UUID var description: String var properties: [String] var type: String + var metadata: CharacteristicMetadata? var value: String? } + +struct CharacteristicMetadata: Encodable, Decodable { + init(manufacturerDescription: String? = nil, validValues: [String]? = nil, minimumValue: String? = nil, maximumValue: String? = nil, stepValue: String? = nil, maxLength: String? = nil, format: String? = nil, units: String? = nil) { + self.manufacturerDescription = manufacturerDescription + self.validValues = validValues + self.minimumValue = minimumValue + self.maximumValue = maximumValue + self.stepValue = stepValue + self.maxLength = maxLength + self.format = format + self.units = units + } + var manufacturerDescription: String? + var validValues: [String]? + var minimumValue: (String)? + var maximumValue: (String)? + var stepValue: (String)? + var maxLength: (String)? + var format: String? + var units: String? +} + +struct UpdateAccessoryInput: Encodable, Decodable { + var home: String + var room: String + var accessory: String + var serviceId: String + var characteristicId: String + var value: String +} diff --git a/prefab/http/Routes.swift b/prefab/http/Routes.swift index e0720ca..c768bce 100644 --- a/prefab/http/Routes.swift +++ b/prefab/http/Routes.swift @@ -157,8 +157,8 @@ extension Server { } group.wait() - let accessory: Accessory = Accessory( - home: home!.name, room: room!.name, name: hkAccessory!.name, category: hkAccessory!.category.categoryType, isReachable: hkAccessory!.isReachable, supportsIdentify: hkAccessory!.supportsIdentify, isBridged: hkAccessory!.isBridged, services: hkAccessory!.services.map{ (service: HMService) -> Service in Service(name: service.name, type: service.serviceType, isPrimary: service.isPrimaryService, isUserInteractive: service.isUserInteractive, associatedType: service.associatedServiceType, characteristics: service.characteristics.map{ (char: HMCharacteristic) -> Characteristic in Characteristic(description: char.localizedDescription, properties: char.properties, type: char.characteristicType, value: "\(char.value ?? "")" ) }) }, firmwareVersion: hkAccessory!.firmwareVersion, manufacturer: hkAccessory!.manufacturer, model: hkAccessory!.model ) + let accessory = Accessory( + home: home!.name, room: room!.name, name: hkAccessory!.name, category: hkAccessory!.category.categoryType, isReachable: hkAccessory!.isReachable, supportsIdentify: hkAccessory!.supportsIdentify, isBridged: hkAccessory!.isBridged, services: hkAccessory!.services.map{ (service: HMService) -> Service in Service(uniqueIdentifier: service.uniqueIdentifier, name: service.name, type: service.serviceType, isPrimary: service.isPrimaryService, isUserInteractive: service.isUserInteractive, associatedType: service.associatedServiceType, characteristics: service.characteristics.map{ (char: HMCharacteristic) -> Characteristic in Characteristic(uniqueIdentifier: char.uniqueIdentifier, description: char.localizedDescription, properties: char.properties, type: char.characteristicType, metadata: CharacteristicMetadata(manufacturerDescription: char.metadata?.manufacturerDescription, validValues: char.metadata?.validValues?.map{ (number: NSNumber) -> String in return number.stringValue}, minimumValue: char.metadata?.minimumValue?.stringValue, maximumValue: char.metadata?.maximumValue?.stringValue, stepValue: char.metadata?.stepValue?.stringValue, maxLength: char.metadata?.maxLength?.stringValue, format: char.metadata?.format, units: char.metadata?.units), value: "\(char.value ?? "")" )}) }, firmwareVersion: hkAccessory!.firmwareVersion, manufacturer: hkAccessory!.manufacturer, model: hkAccessory!.model ) let jsonEncoder = JSONEncoder() let jsonData = try jsonEncoder.encode(accessory) @@ -166,4 +166,83 @@ extension Server { return json! } + + + + func updateAccessoryCharacteristic(_ request: HBRequest) throws -> String { + var updateAccessoryInput: UpdateAccessoryInput + do { + updateAccessoryInput = try JSONDecoder().decode(UpdateAccessoryInput.self, from: request.body.buffer!) + } catch { + throw HBHTTPError( + .badRequest, + message: "Invalid update object." + ) + } + +// guard let homeName = request.parameters["home"] else { +// throw HBHTTPError( +// .badRequest, +// message: "Invalid name parameter." +// ) +// } +// guard let roomName = request.parameters["room"] else { +// throw HBHTTPError( +// .badRequest, +// message: "Invalid name parameter." +// ) +// } +// guard let accessoryName = request.parameters["accessory"] else { +// throw HBHTTPError( +// .badRequest, +// message: "Invalid name parameter." +// ) +// } + let home = homeBase.homes.first(where: {$0.name == updateAccessoryInput.home.removingPercentEncoding}) + if (home == nil) { + throw HBHTTPError(.notFound) + } + let room = home?.rooms.first(where: {$0.name == updateAccessoryInput.room.removingPercentEncoding}) + if (room == nil) { + throw HBHTTPError(.notFound) + } + let hkAccessory = room?.accessories.first(where: { (hmAccessory: HMAccessory) -> Bool in hmAccessory.name == updateAccessoryInput.accessory.removingPercentEncoding}) + if (hkAccessory == nil) { + throw HBHTTPError(.notFound) + } + + let hkService = hkAccessory?.services.first(where: { (hmService: HMService) -> Bool in hmService.uniqueIdentifier.uuidString == updateAccessoryInput.serviceId}) + if (hkAccessory == nil) { + Logger().debug("Service not found \(updateAccessoryInput.serviceId)") + throw HBHTTPError(.notFound) + } + + let hkChar = hkService?.characteristics.first(where: { (hmChar: HMCharacteristic) -> Bool in hmChar.uniqueIdentifier.uuidString == updateAccessoryInput.characteristicId}) + if (hkAccessory == nil) { + Logger().debug("Characteristic not found \(updateAccessoryInput.characteristicId)") + throw HBHTTPError(.notFound) + } + + + Logger().debug("Writing \(updateAccessoryInput.value) to \(hkChar)") + + var anyValue: Any + switch hkChar?.metadata?.format { + case "bool": + anyValue = updateAccessoryInput.value.boolValue + default: + anyValue = updateAccessoryInput.value + } + + let group = DispatchGroup() + group.enter() + hkChar?.writeValue(anyValue, completionHandler: { (error: Error?) -> Void in defer {group.leave()}; Logger().error("\(String(describing: error))") }) + group.wait() + + return "" //json! + } } +extension String { +var boolValue: Bool { + return (self as NSString).boolValue +}} diff --git a/prefab/http/Server.swift b/prefab/http/Server.swift index fa69084..7411b4e 100644 --- a/prefab/http/Server.swift +++ b/prefab/http/Server.swift @@ -46,6 +46,7 @@ class Server { app.router.get("accessories/:home/:room", use: self.getAccessories) app.router.get("accessories/:home/:room/:accessory", use: self.getAccessory) + app.router.put("accessories", use: self.updateAccessoryCharacteristic) try app.start()