Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Andriod update #14

Open
wants to merge 23 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
a0e3b4b
Added CocoaPods Support (#5)
stonehouse Aug 29, 2018
6681102
Added basic theme support
stonehouse Aug 9, 2018
2d50d38
Updated supported status code for apply theme
stonehouse Aug 9, 2018
2a4130d
Added query string init for Color
stonehouse Sep 6, 2018
ca124d8
Fix to parsing of color query string
stonehouse Sep 7, 2018
a50e929
Simplified Theme JSON parsing
stonehouse Sep 8, 2018
c2c70f8
Added invocation phrase to theme model
stonehouse Sep 8, 2018
1318a6c
Made HTTPOperation public
stonehouse Sep 9, 2018
54625e1
Codable models + HTTPSession updates
megan-lifx Oct 5, 2018
29c097a
Adding comments and logs
megan-lifx Oct 7, 2018
4ae5222
Merge pull request #7 from LIFX/feature/themes
stonehouse Oct 8, 2018
be278e2
Merge pull request #6 from LIFX/feature/codable
megan-lifx Oct 8, 2018
2c32746
Activate scene duration optional (#8)
megan-lifx Oct 9, 2018
ed761e3
Feature/state tracking (#9)
stonehouse Oct 15, 2018
138d0a4
Added optional brightness parameter to color
stonehouse Jan 21, 2019
c754f2f
Adding variable color temp capability to product (#10)
megan-lifx Oct 10, 2019
93d481d
Fixed decoding on Scene state
stonehouse Nov 13, 2019
f718093
Added convenience fetch method on LightTarget
stonehouse Mar 26, 2020
dde6c26
Fixed issue with deriveBrightness that was using the wrong denominato…
stonehouse Mar 29, 2020
0e650a8
Switch to async API calls for those that support it.
Jun 4, 2020
59105e9
Feature/toggle power result (#14)
jasonlifx Jul 13, 2020
8f54f9c
Feature/clean cherrypicked (#15)
jasonlifx Jan 6, 2021
7bfb380
Add filter function as a param for HEV
jasonlifx Jan 11, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ DerivedData
*.ipa
*.xcuserstate
.idea/
.DS_Store

# Configuration
Tests/Secrets.plist
29 changes: 29 additions & 0 deletions LIFXHTTPKit.podspec
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
#
# Be sure to run `pod spec lint LIFXHTTPKit.podspec' to ensure this is a
# valid spec and to remove all comments including this before submitting the spec.
#
# To learn more about Podspec attributes see http://docs.cocoapods.org/specification.html
# To see working Podspecs in the CocoaPods repo see https://github.com/CocoaPods/Specs/
#

Pod::Spec.new do |s|

s.name = "LIFXHTTPKit"
s.version = "3.0.1"
s.summary = "A framework for interacting with the LIFX HTTP API that has no external dependencies. Suitable for use inside extensions."

s.license = { :type => 'MIT', :file => 'LICENSE.txt' }
s.homepage = "https://github.com/tatey/LIFXHTTPKit"
s.author = { "Alex Stonehouse" => "[email protected]" }
s.source = { :git => "https://github.com/LIFX/LIFXHTTPKit.git", :tag => "#{s.version}" }

# Version
s.platform = :ios
s.swift_version = "4.0"
s.ios.deployment_target = "8.2"
s.osx.deployment_target = "10.10"
s.watchos.deployment_target = "2.0"

s.source_files = "Source/**/*"

end
8 changes: 8 additions & 0 deletions LIFXHTTPKit.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,9 @@
DD1E1B4B1DF488D68F6F302D /* ProductInformation.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9F4B5781E950F4800D0ED01 /* ProductInformation.swift */; };
DD1E1ED9D4663020C0323F96 /* ProductInformation.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9F4B5781E950F4800D0ED01 /* ProductInformation.swift */; };
E9F4B5791E950F4800D0ED01 /* ProductInformation.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9F4B5781E950F4800D0ED01 /* ProductInformation.swift */; };
FAC2688E211C2042005F778B /* Theme.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAC2688D211C2042005F778B /* Theme.swift */; };
FAC2688F211C2042005F778B /* Theme.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAC2688D211C2042005F778B /* Theme.swift */; };
FAC26890211C2042005F778B /* Theme.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAC2688D211C2042005F778B /* Theme.swift */; };
/* End PBXBuildFile section */

/* Begin PBXContainerItemProxy section */
Expand Down Expand Up @@ -127,6 +130,7 @@
903A12B71CE050C70071D8F0 /* LIFXHTTPKit.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LIFXHTTPKit.h; sourceTree = "<group>"; };
908631CD1CDC7629006D9E47 /* LIFXHTTPKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = LIFXHTTPKit.framework; sourceTree = BUILT_PRODUCTS_DIR; };
E9F4B5781E950F4800D0ED01 /* ProductInformation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProductInformation.swift; sourceTree = "<group>"; };
FAC2688D211C2042005F778B /* Theme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Theme.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
Expand Down Expand Up @@ -205,6 +209,7 @@
5DA7AB431B377ACA008A8130 /* Color.swift */,
5DB41F4B1B2BD5DD0006F2A5 /* Light.swift */,
5D50E0581BC3A21800AED146 /* Scene.swift */,
FAC2688D211C2042005F778B /* Theme.swift */,
5D50E05A1BC3A26A00AED146 /* State.swift */,
E9F4B5781E950F4800D0ED01 /* ProductInformation.swift */,
5D94C6491B5671C300C0BCD2 /* Group.swift */,
Expand Down Expand Up @@ -494,6 +499,7 @@
5D94C64C1B56726200C0BCD2 /* Location.swift in Sources */,
5DB41F461B2BD55C0006F2A5 /* LightTargetObserver.swift in Sources */,
DD1E1B4B1DF488D68F6F302D /* ProductInformation.swift in Sources */,
FAC2688F211C2042005F778B /* Theme.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down Expand Up @@ -531,6 +537,7 @@
5D8BF53E1BB5257900A5575C /* Light.swift in Sources */,
5D8BF53D1BB5257600A5575C /* HTTPSession.swift in Sources */,
5D8BF53F1BB5257B00A5575C /* Color.swift in Sources */,
FAC2688E211C2042005F778B /* Theme.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand All @@ -554,6 +561,7 @@
903A12CA1CE064070071D8F0 /* Scene.swift in Sources */,
903A12CB1CE064090071D8F0 /* State.swift in Sources */,
DD1E132F81EB962FED0C262F /* ProductInformation.swift in Sources */,
FAC26890211C2042005F778B /* Theme.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down
94 changes: 65 additions & 29 deletions Source/Client.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,15 @@ import Foundation

public class Client {
public let session: HTTPSession
public private(set) var lights: [Light]
public private(set) var scenes: [Scene]
public internal(set) var lights: [Light]
public internal(set) var scenes: [Scene]
private var observers: [ClientObserver]

public convenience init(accessToken: String, lights: [Light]? = nil, scenes: [Scene]? = nil) {
self.init(session: HTTPSession(accessToken: accessToken), lights: lights, scenes: scenes)
public convenience init(accessToken: String, lights: [Light]? = nil, scenes: [Scene]? = nil) {
self.init(session: HTTPSession(accessToken: accessToken), lights: lights, scenes: scenes)
}

public init(session: HTTPSession, lights: [Light]? = nil, scenes: [Scene]? = nil) {
public init(session: HTTPSession, lights: [Light]? = nil, scenes: [Scene]? = nil) {
self.session = session
self.lights = lights ?? []
self.scenes = scenes ?? []
Expand Down Expand Up @@ -48,27 +48,49 @@ public class Client {
}

public func fetchLights(completionHandler: ((_ error: Error?) -> Void)? = nil) {
let requestedAt = Date()
session.lights("all") { [weak self] (request, response, lights, error) in
if error != nil {
guard let `self` = self, error == nil else {
completionHandler?(error)
return
}

if let strongSelf = self {
let oldLights = strongSelf.lights
let newLights = lights
if oldLights != newLights {
strongSelf.lights = newLights
for observer in strongSelf.observers {
observer.lightsDidUpdateHandler(lights)
}
}

}

self.handleUpdated(lights: lights, requestedAt: requestedAt)
completionHandler?(nil)
}
}

public func fetchLight(_ selector: LightTargetSelector, completionHandler: ((_ error: Error?) -> Void)? = nil) {
guard selector.type != .SceneID else {
completionHandler?(nil)
return
}
let requestedAt = Date()
session.lights(selector.toQueryStringValue()) { [weak self] (request, response, lights, error) in
guard let `self` = self, error == nil else {
completionHandler?(error)
return
}

self.handleUpdated(lights: lights, requestedAt: requestedAt)
completionHandler?(nil)
}
}

private func handleUpdated(lights: [Light], requestedAt: Date) {
let oldLights = self.lights
var newLights = lights
if oldLights != newLights {
newLights = newLights.map { newLight in
if let oldLight = oldLights.first(where: { $0.id == newLight.id }), oldLight.isDirty {
return oldLight.light(withUpdatedLight: newLight, requestedAt: requestedAt)
} else {
return newLight
}
}
updateLights(newLights)
}
}

public func fetchScenes(completionHandler: ((_ error: Error?) -> Void)? = nil) {
session.scenes { [weak self] (request, response, scenes, error) in
Expand All @@ -87,7 +109,20 @@ public class Client {
return lightTargetWithSelector(LightTargetSelector(type: .All))
}

/// Creates a target for API requests with the given selector. If an ID selector is specified and the Light is not already
/// contained in the cache, then a placeholder light will be created so that events can be subscribed to.
///
/// - Parameter selector: Selector referring to a Scene/Group/Light etc.
/// - Returns: LightTarget which can be used to trigger API requests against the specified Selector
public func lightTargetWithSelector(_ selector: LightTargetSelector) -> LightTarget {
switch selector.type {
case .ID:
// Add light to cache if not already present
if !lights.contains(where: { $0.id == selector.value }) {
updateLights([Light(id: selector.value, power: false, brightness: 0, color: Color(hue: 0, saturation: 0, kelvin: 3500), product: nil, label: "", connected: true, inFlightProperties: [], dirtyProperties: [])])
}
default: break
}
return LightTarget(client: self, selector: selector, filter: selectorToFilter(selector))
}

Expand All @@ -108,26 +143,27 @@ public class Client {

func updateLights(_ lights: [Light]) {
let oldLights = self.lights
var newLights: [Light] = []
var newLights: [Light] = lights

for light in lights {
if !newLights.contains(where: { $0.id == light.id }) {
newLights.append(light)
}
}
for light in oldLights {
if !newLights.contains(where: { $0.id == light.id }) {
newLights.append(light)
for oldLight in oldLights {
if !newLights.contains(where: { $0.id == oldLight.id }) {
newLights.append(oldLight)
}
}

newLights.sort(by: { $0.id < $1.id })

if oldLights != newLights {
self.lights = newLights
for observer in observers {
observer.lightsDidUpdateHandler(newLights)
}
self.lights = newLights
}
}

func updateScenes(_ scenes: [Scene]) {
self.scenes = scenes
}

private func selectorToFilter(_ selector: LightTargetSelector) -> LightTargetFilter {
switch selector.type {
Expand All @@ -141,7 +177,7 @@ public class Client {
return { (light) in return light.location?.id == selector.value }
case .SceneID:
return { [weak self] (light) in
if let strongSelf = self, let index = strongSelf.scenes.index(where: { $0.toSelector() == selector }) {
if let strongSelf = self, let index = strongSelf.scenes.firstIndex(where: { $0.toSelector() == selector }) {
let scene = strongSelf.scenes[index]
return scene.states.contains { (state) in
let filter = strongSelf.selectorToFilter(state.selector)
Expand Down
20 changes: 20 additions & 0 deletions Source/Errors.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,25 @@ struct HTTPKitError: Error {
self.code = code
self.message = message
}

/// Returns an `HTTPKitError` based on the HTTP status code from a response.
init?(statusCode: Int) {
switch statusCode {
case 401:
self.code = .unauthorized
self.message = "Bad access token"
case 403:
self.code = .forbidden
self.message = "Permission denied"
case 429:
self.code = .tooManyRequests
self.message = "Rate limit exceeded"
case 500, 502, 503, 523:
self.code = .unauthorized
self.message = "Server error"
default:
return nil
}
}
}

28 changes: 14 additions & 14 deletions Source/HTTPOperation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,30 +5,30 @@

import Foundation

class HTTPOperationState {
var cancelled: Bool
var executing: Bool
var finished: Bool
public class HTTPOperationState {
public var cancelled: Bool
public var executing: Bool
public var finished: Bool

init() {
public init() {
cancelled = false
executing = false
finished = false
}
}

class HTTPOperation: Operation {
public class HTTPOperation: Operation {
private let state: HTTPOperationState
private let delegateQueue: DispatchQueue
private var task: URLSessionDataTask?

init(URLSession: Foundation.URLSession, delegateQueue: DispatchQueue, request: URLRequest, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) {
public init(session: URLSession, delegateQueue: DispatchQueue, request: URLRequest, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) {
state = HTTPOperationState()
self.delegateQueue = delegateQueue

super.init()

task = URLSession.dataTask(with: request, completionHandler: { [weak self] (data, response, error) in
task = session.dataTask(with: request, completionHandler: { [weak self] (data, response, error) in
if let strongSelf = self {
strongSelf.isExecuting = false
strongSelf.isFinished = true
Expand All @@ -39,11 +39,11 @@ class HTTPOperation: Operation {
})
}

override var isAsynchronous: Bool {
override public var isAsynchronous: Bool {
return true
}

override private(set) var isCancelled: Bool {
override private(set) public var isCancelled: Bool {
get { return state.cancelled }
set {
willChangeValue(forKey: "isCancelled")
Expand All @@ -52,7 +52,7 @@ class HTTPOperation: Operation {
}
}

override private(set) var isExecuting: Bool {
override private(set) public var isExecuting: Bool {
get { return state.executing }
set {
willChangeValue(forKey: "isExecuting")
Expand All @@ -61,7 +61,7 @@ class HTTPOperation: Operation {
}
}

override private(set) var isFinished: Bool {
override private(set) public var isFinished: Bool {
get { return state.finished }
set {
willChangeValue(forKey: "isFinished")
Expand All @@ -70,7 +70,7 @@ class HTTPOperation: Operation {
}
}

override func start() {
override public func start() {
if isCancelled {
return
}
Expand All @@ -79,7 +79,7 @@ class HTTPOperation: Operation {
isExecuting = true
}

override func cancel() {
override public func cancel() {
task?.cancel()
isCancelled = true
}
Expand Down
Loading