├── .gitignore ├── Tests ├── LinuxMain.swift └── HueTests │ ├── XCTestManifests.swift │ └── HueTests.swift ├── README.md ├── Sources └── Hue │ ├── Requests.swift │ ├── Utilities.swift │ ├── Hue+API.swift │ ├── Hue.swift │ └── Responses.swift ├── Package.swift └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | .swiftpm 7 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | import HueTests 4 | 5 | var tests = [XCTestCaseEntry]() 6 | tests += HueTests.allTests() 7 | XCTMain(tests) 8 | -------------------------------------------------------------------------------- /Tests/HueTests/XCTestManifests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | #if !canImport(ObjectiveC) 4 | public func allTests() -> [XCTestCaseEntry] { 5 | return [ 6 | testCase(HueTests.allTests), 7 | ] 8 | } 9 | #endif 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Hue 2 | 3 | A Philips Hue client library written in Swift. 4 | 5 | ## Usage 6 | 7 | Example: 8 | 9 | ```swift 10 | let hue = Hue(bridgeURL: "https://...", username: "A1B2C3...") 11 | let subscription = hue.lights().sink { lights in 12 | // Do something with lights 13 | } 14 | ``` 15 | 16 | See [docs](https://jnewc.github.io/Hue/docs/) for other APIs. 17 | -------------------------------------------------------------------------------- /Sources/Hue/Requests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Jack Newcombe on 04/02/2020. 6 | // 7 | 8 | import Foundation 9 | 10 | struct RequestBody { 11 | let data: Data 12 | } 13 | 14 | extension RequestBody { 15 | static func from(_ encodable: T) -> Self? where T: Encodable { 16 | guard let data = try? JSONEncoder().encode(encodable) else { return nil } 17 | return RequestBody(data: data) 18 | } 19 | } 20 | 21 | extension RequestBody { 22 | static func link(deviceType: String) -> Self? { 23 | return .from(["devicetype": deviceType]) 24 | } 25 | 26 | static func on(state: Bool) -> Self? { 27 | let dict = ["on": state] 28 | return .from(dict) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Sources/Hue/Utilities.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Jack Newcombe on 02/02/2020. 6 | // 7 | 8 | import Foundation 9 | import Combine 10 | 11 | @available(iOS 13.0, macOS 10.15, *) 12 | extension Publisher { 13 | 14 | func autoMapErrorType(_ errorType: T.Type, default: T) -> Publishers.MapError where T: Error { 15 | return self.mapError { 16 | switch $0 { 17 | case let error as T: 18 | return error 19 | default: 20 | return `default` 21 | } 22 | } 23 | } 24 | 25 | func autoMapDecodingError() -> Publishers.MapError { 26 | return self.mapError { error -> Swift.Error in 27 | switch error { 28 | case is DecodingError: 29 | return Hue.Error.parsingFailure(errorMessage: error.localizedDescription) 30 | default: 31 | return error 32 | } 33 | } 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.1 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "Hue", 8 | products: [ 9 | // Products define the executables and libraries produced by a package, and make them visible to other packages. 10 | .library( 11 | name: "Hue", 12 | targets: ["Hue"]), 13 | ], 14 | dependencies: [ 15 | // Dependencies declare other packages that this package depends on. 16 | // .package(url: /* package url */, from: "1.0.0"), 17 | ], 18 | targets: [ 19 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 20 | // Targets can depend on other targets in this package, and on products in packages which this package depends on. 21 | .target( 22 | name: "Hue", 23 | dependencies: []), 24 | .testTarget( 25 | name: "HueTests", 26 | dependencies: ["Hue"]), 27 | ] 28 | ) 29 | -------------------------------------------------------------------------------- /Tests/HueTests/HueTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import Combine 3 | @testable import Hue 4 | 5 | @available(iOS 13.0, macOS 10.15, *) 6 | final class HueTests: XCTestCase { 7 | 8 | let hue = Hue(bridgeURL: "YOUR_HUE_HUB", username: "YOUR_HUE_USER_ID") 9 | 10 | var subscriptions = Set() 11 | 12 | // MARK: Collection Tests 13 | 14 | func testLights() { 15 | test(with: hue.lights()) 16 | } 17 | 18 | func testGroups() { 19 | test(with: hue.groups()) 20 | } 21 | 22 | func testSchedules() { 23 | test(with: hue.schedules()) 24 | } 25 | 26 | func testScenes() { 27 | test(with: hue.scenes()) 28 | } 29 | 30 | func testSensors() { 31 | test(with: hue.sensors()) 32 | } 33 | 34 | // MARK: Element Tests 35 | 36 | func testLight() { 37 | test(with: hue.light(id: "1")) 38 | } 39 | 40 | func testGroup() { 41 | test(with: hue.group(id: "1")) 42 | } 43 | 44 | func testScene() { 45 | test(with: hue.scene(id: "Vkj3pJUJwTIWp9T")) 46 | } 47 | 48 | // MARK: Utils 49 | 50 | private func test(with publisher: AnyPublisher, _ completion: ((T) -> Void)? = nil) { 51 | let expect = self.expectation(description: "Lights are returned") 52 | 53 | publisher.sink(receiveCompletion: { result in 54 | if case .failure(let error) = result { 55 | if case .parsingFailure(let message) = error { 56 | XCTFail(message) 57 | } else { 58 | XCTFail(error.localizedDescription) 59 | } 60 | } 61 | }, receiveValue: { lights in 62 | expect.fulfill() 63 | }).store(in: &subscriptions) 64 | 65 | waitForExpectations(timeout: 5.0, handler: nil) 66 | } 67 | 68 | // MARK: XCTest 69 | 70 | static var allTests = [ 71 | ("testLights", testLights), 72 | ("testGroups", testGroups), 73 | ("testSchedules", testSchedules), 74 | ("testScenes", testScenes), 75 | ("testSensors", testSensors), 76 | 77 | ("testLight", testLight), 78 | ("testGroup", testGroup), 79 | ("testScene", testScene), 80 | ] 81 | } 82 | -------------------------------------------------------------------------------- /Sources/Hue/Hue+API.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Hue+API.swift 3 | // 4 | // 5 | // Created by Jack Newcombe on 02/02/2020. 6 | // 7 | 8 | import Foundation 9 | import Combine 10 | 11 | 12 | @available(iOS 13.0, macOS 10.15, *) 13 | extension Hue { 14 | 15 | // MARK: Collections 16 | 17 | /// Publishes all available lights for the hub 18 | public func lights() -> AnyPublisher { 19 | execute(with: .lights, type: Lights.self) 20 | } 21 | 22 | /// Publishes all available groups for the hub 23 | public func groups() -> AnyPublisher { 24 | execute(with: .groups, type: Groups.self) 25 | } 26 | 27 | /// Publishes the full config for the hub 28 | public func config() -> AnyPublisher { 29 | execute(with: .config, type: Config.self) 30 | } 31 | 32 | /// Publishes all available schedules for the hub 33 | public func schedules() -> AnyPublisher { 34 | execute(with: .schedules, type: Schedules.self) 35 | } 36 | 37 | /// Publishes all available scenes for the hub 38 | public func scenes() -> AnyPublisher { 39 | execute(with: .scenes, type: Scenes.self) 40 | } 41 | 42 | /// Publishes all available sensors for the hub 43 | public func sensors() -> AnyPublisher { 44 | execute(with: .sensors, type: Sensors.self) 45 | } 46 | 47 | // MARK: Elements 48 | 49 | /// Publishes the `Light` with the given ID 50 | /// - Parameter id: The ID of the `Light` to publish 51 | public func light(id: String) -> AnyPublisher { 52 | execute(with: .light(id: id), type: Lights.Light.self) 53 | } 54 | 55 | /// Sets the state of a light and publishes the result 56 | /// - Parameters: 57 | /// - id: The ID of the light 58 | /// - state: The new state of the light 59 | public func light(id: String, state: Bool) -> AnyPublisher<[LightState], Hue.Error> { 60 | execute(with: .lightOn(id: id), type: [LightState].self, body: .on(state: state)) 61 | } 62 | 63 | /// Publishes the `Group` with the given ID 64 | /// - Parameter id: The ID of the `Group` to publish 65 | public func group(id: String) -> AnyPublisher { 66 | execute(with: .group(id: id), type: Groups.Group.self) 67 | } 68 | 69 | /// Sets the state of a group and publishes the result 70 | /// - Parameters: 71 | /// - id: The ID of the group 72 | /// - state: The new state of the group 73 | public func group(id: String, state: Bool) -> AnyPublisher<[GroupState], Hue.Error> { 74 | execute(with: .groupOn(id: id), type: [GroupState].self) 75 | } 76 | 77 | /// Publishes the `Schedule` with the given ID 78 | /// - Parameter id: The ID of the `Schedule` to publish 79 | public func schedule(id: String) -> AnyPublisher { 80 | execute(with: .schedule(id: id), type: Schedules.Schedule.self) 81 | } 82 | 83 | /// Publishes the `Scene` with the given ID 84 | /// - Parameter id: The ID of the `Scene` to publish 85 | public func scene(id: String) -> AnyPublisher { 86 | execute(with: .scene(id: id), type: Scenes.Scene.self) 87 | } 88 | 89 | /// Publishes the `Sensor` with the given ID 90 | /// - Parameter id: The ID of the `Sensor` to publish 91 | public func sensor(id: String) -> AnyPublisher { 92 | execute(with: .sensor(id: id), type: Sensors.Sensor.self) 93 | } 94 | 95 | // MARK: Helpers 96 | 97 | private func execute(with endpoint: Endpoint, type: T.Type, body: RequestBody? = nil) -> AnyPublisher where T: Decodable { 98 | execute(endpoint: endpoint, method: body == nil ? .get : .put, body: body) 99 | .decode(type: type, decoder: JSONDecoder()) 100 | .autoMapDecodingError() 101 | .autoMapErrorType(Hue.Error.self, default: .unknown) 102 | .eraseToAnyPublisher() 103 | } 104 | 105 | } 106 | -------------------------------------------------------------------------------- /Sources/Hue/Hue.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Hue.swift 3 | // Automa 4 | // 5 | // Created by Jack Newcombe on 04/01/2020. 6 | // Copyright © 2020 Jack Newcombe. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Combine 11 | 12 | @available(iOS 13.0, macOS 10.15, *) 13 | 14 | /// Describes a Phillips Hue bridge and provides various method for accessing its devices, groups, schedules, scenes, and other data. 15 | public class Hue { 16 | 17 | enum HTTPMethod: String { 18 | case get 19 | case post 20 | case put 21 | } 22 | 23 | /// Describes a Hue domain error 24 | public enum Error: Swift.Error { 25 | case unknown 26 | case unsupportedVersion 27 | case urlParseFailure 28 | case networkError 29 | case httpError(statusCode: Int) 30 | case parsingFailure(errorMessage: String) 31 | 32 | var localizedDescription: String { 33 | switch self { 34 | case .unknown: 35 | return "Unknown error" 36 | case .unsupportedVersion: 37 | return "SDK version of iOS 13.0 or above required" 38 | case .urlParseFailure: 39 | return "Failed to parse endpoint URL" 40 | case .networkError: 41 | return "Network request error" 42 | case .httpError(let statusCode): 43 | return "HTTP Error: \(statusCode)" 44 | case .parsingFailure: 45 | return "Failed to parse API response" 46 | } 47 | } 48 | } 49 | 50 | /// Describes a response given while attempting to whitelist a new username. 51 | public enum ConnectResponse { 52 | /// Indicates that a link is required (i.e. the link button should be pressed on the hub) 53 | case linkRequired 54 | /// Indicates that a link was successful. `username` contains the whitelisted user ID. 55 | case linked(username: String) 56 | } 57 | 58 | /// The host URL of the bridge, including scheme. 59 | public let bridgeURL: String 60 | 61 | /// The whitelisted username, if present. 62 | public var username: String? 63 | 64 | /// Create an instance of the 65 | /// - Parameters: 66 | /// - bridgeURL: The host URL of the bridge, including scheme, e.g. https://10.1.2.3/ 67 | /// - username: Optional username. If not specified, use `connect()` to generate a new one 68 | public init(bridgeURL: String, username: String? = nil) { 69 | self.bridgeURL = bridgeURL 70 | self.username = username 71 | } 72 | 73 | func url(forEndpoint endpoint: Endpoint) -> URL? { 74 | guard let username = username else { 75 | return URL(string: "\(bridgeURL)/api/\(endpoint.path)") 76 | } 77 | return URL(string: "\(bridgeURL)/api/\(username)/\(endpoint.path)") 78 | } 79 | 80 | /// Requests a new whitelisted user ID from the bridge. 81 | /// - Parameter deviceType: A name for the device making the link. e.g. the name of your app. 82 | public func link(deviceType: String) -> AnyPublisher { 83 | return execute(endpoint: .login, method: .post, body: .link(deviceType: deviceType)) 84 | .tryMap { data throws in 85 | let decoder = JSONDecoder() 86 | if let response = try? decoder.decode([LinkErrorResponse].self, from: data), response.isLinkRequest { 87 | return .linkRequired 88 | } 89 | do { 90 | let response = try decoder.decode([LinkSuccessResponse].self, from: data) 91 | guard let username = response.first?.success.username else { throw Error.unknown } 92 | self.username = username 93 | return .linked(username: username) 94 | } catch let error as DecodingError { 95 | throw Error.parsingFailure(errorMessage: error.localizedDescription) 96 | } catch let error { 97 | throw error 98 | } 99 | } 100 | .autoMapErrorType(Hue.Error.self, default: .unknown) 101 | .eraseToAnyPublisher() 102 | } 103 | 104 | func execute(endpoint: Endpoint, method: HTTPMethod = .get, body: RequestBody? = nil) -> AnyPublisher { 105 | guard let url = url(forEndpoint: endpoint) else { 106 | return Fail(outputType: Data.self, failure: Error.urlParseFailure).eraseToAnyPublisher() 107 | } 108 | 109 | var urlRequest = URLRequest(url: url) 110 | urlRequest.httpMethod = method.rawValue 111 | urlRequest.httpBody = body?.data 112 | 113 | return URLSession.shared.dataTaskPublisher(for: urlRequest) 114 | .tryMap { 115 | let (data, response) = $0 116 | guard let httpResponse = response as? HTTPURLResponse else { throw Error.networkError } 117 | guard httpResponse.statusCode == 200 else { throw Error.httpError(statusCode: httpResponse.statusCode) } 118 | return data 119 | } 120 | .autoMapErrorType(Hue.Error.self, default: .unknown) 121 | .eraseToAnyPublisher() 122 | } 123 | 124 | } 125 | 126 | struct Endpoint { 127 | let path: String 128 | } 129 | 130 | extension Endpoint { 131 | 132 | // MARK: Collection Endpoints 133 | 134 | static var lights: Self { 135 | Endpoint(path: "lights") 136 | } 137 | 138 | static var groups: Self { 139 | Endpoint(path:"groups") 140 | } 141 | 142 | static var config: Self { 143 | Endpoint(path:"config") 144 | } 145 | 146 | static var schedules: Self { 147 | Endpoint(path:"schedules") 148 | } 149 | 150 | static var scenes: Self { 151 | Endpoint(path:"scenes") 152 | } 153 | 154 | static var sensors: Self { 155 | Endpoint(path:"sensors") 156 | } 157 | 158 | static var rules: Self { 159 | Endpoint(path:"rules") 160 | } 161 | 162 | // MARK: Element Endpoints 163 | 164 | static func light(id: String) -> Self { 165 | Endpoint(path: "lights/\(id)") 166 | } 167 | 168 | static func lightOn(id: String) -> Self { 169 | Endpoint(path: "lights/\(id)/state") 170 | } 171 | 172 | static func group(id: String) -> Self { 173 | Endpoint(path: "groups/\(id)") 174 | } 175 | 176 | static func groupOn(id: String) -> Self { 177 | Endpoint(path: "groups/\(id)/action") 178 | } 179 | 180 | static func schedule(id: String) -> Self { 181 | Endpoint(path: "schedules/\(id)") 182 | } 183 | 184 | static func scene(id: String) -> Self { 185 | Endpoint(path: "scenes/\(id)") 186 | } 187 | 188 | static func sensor(id: String) -> Self { 189 | Endpoint(path: "sensor/\(id)") 190 | } 191 | 192 | // MARK: Login 193 | 194 | static var login: Self { 195 | Endpoint(path: "") 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | Copyright 2020 Jack Newcombe 180 | 181 | Licensed under the Apache License, Version 2.0 (the "License"); 182 | you may not use this file except in compliance with the License. 183 | You may obtain a copy of the License at 184 | 185 | http://www.apache.org/licenses/LICENSE-2.0 186 | 187 | Unless required by applicable law or agreed to in writing, software 188 | distributed under the License is distributed on an "AS IS" BASIS, 189 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 190 | See the License for the specific language governing permissions and 191 | limitations under the License. 192 | -------------------------------------------------------------------------------- /Sources/Hue/Responses.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Jack Newcombe on 02/02/2020. 6 | // 7 | 8 | import Foundation 9 | 10 | // MARK: Linking 11 | 12 | /// Describes an error response for a link attempt. 13 | public struct LinkErrorResponse: Decodable { 14 | 15 | /// Describes the content of an error response. 16 | public struct ErrorContent: Decodable { 17 | /// An error code indicating the type of the error. 18 | public let type: Int 19 | /// A textual description of the error. 20 | public let description: String 21 | } 22 | 23 | /// Error details. 24 | public let error: ErrorContent 25 | 26 | /// Indicates whether the error type implied that the Link button should be pressed. 27 | public var isLinkRequest: Bool { 28 | return error.type == 101 29 | } 30 | } 31 | 32 | /// Describes a success response for a link attempt. 33 | public struct LinkSuccessResponse: Decodable { 34 | 35 | /// Describes the content of a success response. 36 | public struct SuccessContent: Decodable { 37 | /// The username of a newly linked user. 38 | public let username: String 39 | } 40 | 41 | /// Describes the success state of the link 42 | public let success: SuccessContent 43 | } 44 | 45 | extension Array where Element == LinkErrorResponse { 46 | var isLinkRequest: Bool { 47 | return count == 1 && self.first?.isLinkRequest ?? false 48 | } 49 | } 50 | 51 | // MARK: Lights 52 | 53 | /// Describes all light elements for a bridge. 54 | public struct Lights: DynamicDecodable { 55 | /// Describes a light and its configuration. 56 | public struct Light: Decodable { 57 | enum CodingKeys: String, CodingKey { 58 | case state 59 | case capabilities 60 | case name 61 | case modelID = "modelid" 62 | case manufacturerName = "manufacturername" 63 | case productName = "productname" 64 | } 65 | 66 | /// Describes the configuration state of a `Light`. 67 | public struct State: Decodable { 68 | enum CodingKeys: String, CodingKey { 69 | case isOn = "on" 70 | case isReachable = "reachable" 71 | case brightness = "bri" 72 | } 73 | /// Indicates if a light can be reached by the bridge. 74 | public let isReachable: Bool 75 | /// On/Off state of the light. On=true, Off=false 76 | public let isOn: Bool 77 | /// Brightness of the light. This is a scale from the minimum brightness the light is capable of, 1, to the maximum capable brightness, 254. 78 | public let brightness: Int 79 | } 80 | 81 | /// Describes the capabilities of a `Light` 82 | public struct Capabilities: Decodable { 83 | enum CodingKeys: String, CodingKey { 84 | case isCertified = "certified" 85 | case control 86 | } 87 | public struct Control: Decodable { 88 | enum CodingKeys: String, CodingKey { 89 | case minDimLevel = "mindimlevel" 90 | case maxLumen = "maxlumen" 91 | } 92 | // 93 | public let minDimLevel: Int? 94 | public let maxLumen: Int? 95 | } 96 | public let isCertified: Bool 97 | public let control: Control 98 | } 99 | 100 | /// Details the state of the light, see the `State` struct for more details. 101 | public let state: State 102 | /// The capabilities of the light. 103 | public let capabilities: Capabilities 104 | /// A unique, editable name given to the light. 105 | public let name: String 106 | /// The hardware model of the light. 107 | public let modelID: String 108 | /// The manufacturer name. 109 | public let manufacturerName: String 110 | /// The product name. 111 | public let productName: String 112 | } 113 | 114 | /// Lights items, keyed by ID 115 | public var results: [String : Lights.Light] 116 | 117 | init(results: [String: Lights.Light]) { 118 | self.results = results 119 | } 120 | } 121 | 122 | // MARK: Groups 123 | 124 | /// Describes all group elements for a bridge. 125 | public struct Groups: DynamicDecodable { 126 | 127 | public struct Group: Decodable { 128 | /// Describes the configuration state of a `Group` 129 | public struct State: Decodable { 130 | enum CodingKeys: String, CodingKey { 131 | case isAllOn = "all_on" 132 | case isAnyOn = "any_on" 133 | } 134 | /// “all_on” indicates all lights within the group are ON (true) or OFF (false). 135 | public let isAllOn: Bool 136 | /// “any_on” is true when one or more lights within the group is ON. 137 | public let isAnyOn: Bool 138 | } 139 | 140 | /// Describes the action state of a `Group` 141 | public struct Action: Decodable { 142 | enum CodingKeys: String, CodingKey { 143 | case isOn = "on" 144 | case brightness = "bri" 145 | } 146 | public let isOn: Bool 147 | public let brightness: Int 148 | } 149 | 150 | /// A unique, editable name given to the group. 151 | public let name: String 152 | /// If not provided upon creation “LightGroup” is used. Can be “LightGroup”, “Room” or either “Luminaire” or “LightSource” if a Multisource Luminaire is present in the system. 153 | public let type: String 154 | /// The IDs of the lights that are in the group. 155 | public let lights: [String] 156 | /// The ordered set of sensor ids from the sensors which are in the group. The array can be empty. 157 | public let sensors: [String] 158 | /// Contains a state representation of the group 159 | public let state: State 160 | /// The light state of one of the lamps in the group. 161 | public let action: Action 162 | } 163 | 164 | /// Groups, keyed by ID 165 | public var results: [String : Groups.Group] 166 | 167 | init(results: [String: Groups.Group]) { 168 | self.results = results 169 | } 170 | } 171 | 172 | // MARK: Config 173 | 174 | 175 | /// Describes all configuration elements for a bridge. Note all times are stored in UTC. 176 | public struct Config: Decodable { 177 | enum CodingKeys: String, CodingKey { 178 | case name 179 | case zigbeeChannel = "zigbeechannel" 180 | case bridgeID = "bridgeid" 181 | case isDHCP = "dhcp" 182 | case ipAddress = "ipaddress" 183 | case modelID = "modelid" 184 | case apiVersion = "apiversion" 185 | case whitelist = "whitelist" 186 | } 187 | 188 | /// Describes whitelisted user IDs. 189 | public struct Whitelist: DynamicDecodable { 190 | /// Describesa whitelist item. 191 | public struct WhitelistItem: Decodable { 192 | enum CodingKeys: String, CodingKey { 193 | case lastUseDate = "last use date" 194 | case createDate = "create date" 195 | case name 196 | } 197 | /// The datetime of the last request made by this device. 198 | public let lastUseDate: String 199 | /// The datetime of creation 200 | public let createDate: String 201 | /// The name of the whitelisted user or device. 202 | public let name: String 203 | } 204 | 205 | /// Whitelist items, keyed by UUID 206 | public var results: [String: WhitelistItem] 207 | 208 | init(results: [String: WhitelistItem]) { 209 | self.results = results 210 | } 211 | } 212 | 213 | /// Name of the bridge. This is also its uPnP name, so will reflect the actual uPnP name after any conflicts have been resolved. 214 | public let name: String 215 | /// The current wireless frequency channel used by the bridge. It can take values of 11, 15, 20,25 or 0 if undefined (factory new). 216 | public let zigbeeChannel: String 217 | /// The unique bridge id. This is currently generated from the bridge Ethernet mac address. 218 | public let bridgeID: String 219 | /// Whether the IP address of the bridge is obtained with DHCP. 220 | public let isDHCP: Bool 221 | /// IP address of the bridge. 222 | public let ipAddress: String 223 | /// This parameter uniquely identifies the hardware model of the bridge (BSB001, BSB002). 224 | public let modelID: String 225 | /// The version of the hue API in the format .., for example 1.2.1 226 | public let apiVersion: String 227 | /// A list of whitelisted user IDs. 228 | public let whitelist: Whitelist 229 | 230 | } 231 | 232 | // MARK: Schedules 233 | 234 | /// Describe all schedule elements for a bridge. 235 | public struct Schedules: DynamicDecodable { 236 | /// Describes a schedule and it's configuration 237 | public struct Schedule: Decodable { 238 | enum CodingKeys: String, CodingKey { 239 | case name 240 | case description 241 | case time 242 | case created 243 | case status 244 | case command 245 | case recycle 246 | case localTime = "localtime" 247 | } 248 | 249 | /// Describes the command to execute when the scheduled event occurs. 250 | public struct Command: Decodable { 251 | /// A command configuration specific to the source schedule 252 | public struct Body: Decodable { 253 | enum CodingKeys: String, CodingKey { 254 | case scene 255 | case transitionTime = "transitiontime" 256 | case brightnessIncrement = "bri_inc" 257 | } 258 | /// If present, describes the scene targeted by this command. 259 | public let scene: String? 260 | /// If present, describes the amount of time this command should take to execute. 261 | public let transitionTime: Double? 262 | /// If present, describes the amount by which the brightness shoudl increment during execution. 263 | public let brightnessIncrement: Double? 264 | } 265 | /// Path to a light resource, a group resource or any other bridge resource (including “/api//”) 266 | public let address: String 267 | /// The HTTPS method used to send the body to the given address. Either “POST”, “PUT”, “DELETE” for local addresses. 268 | public let method: String 269 | /// The command configuration. 270 | public let body: Body 271 | } 272 | 273 | 274 | /// Name for the new schedule. If a name is not specified then the default name, “schedule”, is used. 275 | /// If the name is already taken a space and number will be appended by the bridge, e.g. “schedule 1”. 276 | public let name: String 277 | /// Description of the new schedule. If the description is not specified it will be empty. 278 | public let description: String 279 | /// Time when the scheduled event will occur. Time is measured in the bridge in UTC time. Either time or localtime has to be provided. 280 | /// DEPRECATED: This attribute will be removed in the future. Please use localtime instead. 281 | /// + The following time patterns are allowed: 282 | /// + Absolute time 283 | /// + Randomized time 284 | /// + Recurring times 285 | /// + Recurring randomized times 286 | /// + Timers 287 | public let time: String 288 | public let created: String 289 | /// Application is only allowed to set “enabled” or “disabled”. 290 | /// Disabled causes a timer to reset when activated (i.e. stop & reset). “enabled” when not provided on creation. 291 | public let status: String 292 | /// Command to execute when the scheduled event occurs. If the command is not valid then an error of type 7 will be raised. 293 | public let command: Command 294 | /// When true: Resource is automatically deleted when not referenced anymore in any resource link. 295 | /// Only on creation of resource. “false” when omitted. 296 | public let recycle: Bool 297 | /// Local time when the scheduled event will occur.Either time or localtime has to be provided. 298 | /// A schedule configured with localtime will operate on localtime and is returned along with the time attribute (UTC) for backwards compatibility. 299 | public let localTime: String 300 | } 301 | 302 | 303 | /// Schedule items, keyed by ID 304 | public var results: [String: Schedule] 305 | 306 | init(results: [String: Schedule]) { 307 | self.results = results 308 | } 309 | } 310 | 311 | // MARK: Scenes 312 | 313 | /// Describe all scene elements for a bridge. 314 | public struct Scenes: DynamicDecodable { 315 | public struct Scene: Decodable { 316 | /// Human readable name of the scene. Is set to if omitted on creation. 317 | public let name: String 318 | /// If not provided on creation a “LightScene” is created. Supported types: 319 | /// + 'LightScene': Default. Represents a scene which links to a specific group. While creating a new GroupScene, the group attribute shall be provided. 320 | /// The lights array is a read-only attribute, it cannot be modified, and shall not be provided upon GroupScene creation. 321 | /// + 'GroupScene': When lights in a group is changed, the GroupScenes associated to this group will be automatically updated with the new list of lights in the group. 322 | /// The new lights added to the group will be assigned with default states for associated GroupScenes. 323 | public let type: String 324 | /// group ID that a scene is linked to. 325 | public let group: String? 326 | /// The light ids which are in the scene. This array can be empty. 327 | public let lights: [String] 328 | /// Whitelist user that created or modified the content of the scene. Note that changing name does not change the owner. 329 | public let owner: String 330 | /// Indicates whether the scene can be automatically deleted by the bridge. 331 | /// Only available by `POST` Set to ‘false’ when omitted. 332 | /// Legacy scenes created by `PUT` are defaulted to true. 333 | /// When set to ‘false’ the bridge keeps the scene until deleted by an application. 334 | public let recycle: Bool 335 | /// Indicates that the scene is locked by a rule or a schedule and cannot be deleted until all resources requiring or that reference the scene are deleted. 336 | public let locked: Bool 337 | /// Only available on a GET of an individual scene resource (/api//scenes/). 338 | /// Not available for scenes created via a PUT. Reserved for future use. 339 | public let picture: String 340 | } 341 | 342 | /// Scene items, keyed by ID 343 | public var results: [String: Scene] 344 | 345 | init(results: [String: Scene]) { 346 | self.results = results 347 | } 348 | } 349 | 350 | // MARK: Sensors 351 | 352 | 353 | /// Describes all sensor elements for a bridge. 354 | public struct Sensors: DynamicDecodable { 355 | /// Describes a sensor and its configuration. 356 | public struct Sensor: Decodable { 357 | /// Describes the state of a sensor. 358 | public struct State: Decodable { 359 | /// The on/off state of the sensor. 360 | public let flag: Bool 361 | } 362 | /// Describes the configuration state of the sensor. 363 | public struct Config: Decodable { 364 | enum CodingKeys: String, CodingKey { 365 | case isOn = "on" 366 | case isConfigured = "configured" 367 | case isReachable = "reachable" 368 | } 369 | /// Indicates whether or not the sensor has been configured. 370 | public let isConfigured: Bool? 371 | /// Indicates whether or not the sensor is currently reachable. 372 | public let isReachable: Bool? 373 | /// Indicates whether or not the sensor is currently activated. 374 | public let isOn: Bool 375 | } 376 | } 377 | 378 | /// Sensor items, keyed by ID. 379 | public var results: [String: Sensor] 380 | 381 | init(results: [String: Sensor]) { 382 | self.results = results 383 | } 384 | } 385 | 386 | // MARK: LightState 387 | 388 | /// Describes the modified state of a `Light` 389 | public struct LightState: Decodable { 390 | } 391 | 392 | /// Describes the modified state of a `Group` 393 | public struct GroupState: Decodable { 394 | } 395 | 396 | // MARK: Dynamic Decodable Helpers 397 | 398 | protocol DynamicDecodable: Decodable { 399 | associatedtype InnerDecodable: Decodable 400 | 401 | var results: [String: InnerDecodable] { get set } 402 | 403 | init(results: [String: InnerDecodable]) 404 | } 405 | 406 | extension DynamicDecodable { 407 | 408 | public init(from decoder: Decoder) throws { 409 | let container = try decoder.container(keyedBy: CustomCodingKeys.self) 410 | 411 | var results = [String: InnerDecodable]() 412 | for key in container.allKeys { 413 | let value = try container.decode(InnerDecodable.self, forKey: CustomCodingKeys(stringValue: key.stringValue)!) 414 | results[key.stringValue] = value 415 | } 416 | self = .init(results: results) 417 | } 418 | } 419 | 420 | struct CustomCodingKeys: CodingKey { 421 | var stringValue: String 422 | init?(stringValue: String) { 423 | self.stringValue = stringValue 424 | } 425 | var intValue: Int? 426 | init?(intValue: Int) { 427 | return nil 428 | } 429 | } 430 | --------------------------------------------------------------------------------