From 9bff3dda4885d6cc25095490e83ecdda97ae5d81 Mon Sep 17 00:00:00 2001 From: James Magahern Date: Sat, 14 Mar 2020 19:53:18 -0700 Subject: [PATCH] HubitatServer: Adds HubitatServer type and adds it to the multiplexer --- XIONControlPanel.xcodeproj/project.pbxproj | 8 ++ .../Controllers/MainViewController.swift | 3 +- .../Controllers/SwitchesViewController.swift | 1 + XIONControlPanel/Models/DeviceProtocol.swift | 10 +-- XIONControlPanel/Models/HubitatDevice.swift | 47 +++++++++++ XIONControlPanel/Servers/HubitatServer.swift | 82 +++++++++++++++++++ .../Servers/ServerMultiplex.swift | 17 ++-- 7 files changed, 153 insertions(+), 15 deletions(-) create mode 100644 XIONControlPanel/Models/HubitatDevice.swift create mode 100644 XIONControlPanel/Servers/HubitatServer.swift diff --git a/XIONControlPanel.xcodeproj/project.pbxproj b/XIONControlPanel.xcodeproj/project.pbxproj index a2dc1ec..5c2018d 100644 --- a/XIONControlPanel.xcodeproj/project.pbxproj +++ b/XIONControlPanel.xcodeproj/project.pbxproj @@ -39,6 +39,8 @@ CD815B8E241C6F4000E2FA97 /* ServerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD815B8D241C6F4000E2FA97 /* ServerProtocol.swift */; }; CD815B90241C7CAC00E2FA97 /* DeviceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD815B8F241C7CAC00E2FA97 /* DeviceProtocol.swift */; }; CD815B92241DA28B00E2FA97 /* ServerMultiplex.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD815B91241DA28B00E2FA97 /* ServerMultiplex.swift */; }; + CD815B94241DC16700E2FA97 /* HubitatServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD815B93241DC16700E2FA97 /* HubitatServer.swift */; }; + CD815B96241DC44D00E2FA97 /* HubitatDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD815B95241DC44D00E2FA97 /* HubitatDevice.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -79,6 +81,8 @@ CD815B8D241C6F4000E2FA97 /* ServerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerProtocol.swift; sourceTree = ""; }; CD815B8F241C7CAC00E2FA97 /* DeviceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceProtocol.swift; sourceTree = ""; }; CD815B91241DA28B00E2FA97 /* ServerMultiplex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerMultiplex.swift; sourceTree = ""; }; + CD815B93241DC16700E2FA97 /* HubitatServer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HubitatServer.swift; sourceTree = ""; }; + CD815B95241DC44D00E2FA97 /* HubitatDevice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HubitatDevice.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -126,6 +130,7 @@ isa = PBXGroup; children = ( CD815B8F241C7CAC00E2FA97 /* DeviceProtocol.swift */, + CD815B95241DC44D00E2FA97 /* HubitatDevice.swift */, 0C4314FF1C35B1E700164559 /* WemoDevice.swift */, ); path = Models; @@ -227,6 +232,7 @@ children = ( CD815B91241DA28B00E2FA97 /* ServerMultiplex.swift */, CD815B8D241C6F4000E2FA97 /* ServerProtocol.swift */, + CD815B93241DC16700E2FA97 /* HubitatServer.swift */, 0CAA32781C361A2400B353CF /* WemoServer.swift */, ); path = Servers; @@ -318,9 +324,11 @@ 0C41EF4E1C34B81F00AF847F /* KSIRingView.m in Sources */, 0CAA327B1C361CBE00B353CF /* NSErrorAdditions.swift in Sources */, 0CF0F8961C362D8F00FE7752 /* NSURLAdditions.swift in Sources */, + CD815B94241DC16700E2FA97 /* HubitatServer.swift in Sources */, 0CAA32791C361A2400B353CF /* WemoServer.swift in Sources */, 0C41EF561C35ABCD00AF847F /* ConnectionStatusView.swift in Sources */, 0C63A87D1C33C655007E4B52 /* NSValue+XIONAdditions.m in Sources */, + CD815B96241DC44D00E2FA97 /* HubitatDevice.swift in Sources */, 0C20E2BD1C33534F00BA789B /* VisualizationViewController.swift in Sources */, CD815B90241C7CAC00E2FA97 /* DeviceProtocol.swift in Sources */, 0C41EF4A1C34B51500AF847F /* SwitchesViewController.swift in Sources */, diff --git a/XIONControlPanel/Controllers/MainViewController.swift b/XIONControlPanel/Controllers/MainViewController.swift index 4c07bbb..467ab0d 100644 --- a/XIONControlPanel/Controllers/MainViewController.swift +++ b/XIONControlPanel/Controllers/MainViewController.swift @@ -21,6 +21,7 @@ class MainViewController: UIViewController, SwitchesViewControllerDelegate, Serv super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) _serverMultiplex.delegate = self + _serverMultiplex.addServer(HubitatServer(URL(string: "https://midna.xionsf.com:6000/apps/api/1/")!)) _serverMultiplex.addServer(WemoServer(URL(string: "http://midna.xionsf.com:5000")!)) } @@ -203,7 +204,7 @@ class MainViewController: UIViewController, SwitchesViewControllerDelegate, Serv func serverMultiplex(_ multiplex: ServerMultiplex, didAddDevices devices: [AnyDevice]) { - _switchesController.devices = devices + _switchesController.devices = Array(multiplex.devices) _updateVisualization(false) // Update connection status too, if applicable diff --git a/XIONControlPanel/Controllers/SwitchesViewController.swift b/XIONControlPanel/Controllers/SwitchesViewController.swift index 61d5e0b..b831789 100644 --- a/XIONControlPanel/Controllers/SwitchesViewController.swift +++ b/XIONControlPanel/Controllers/SwitchesViewController.swift @@ -122,6 +122,7 @@ class SwitchesViewController: UIViewController, }) var snapshot = _collectionViewDataSource.snapshot() + snapshot.deleteItems(snapshot.itemIdentifiers(inSection: SwitchesViewController.switchCellsSectionIdentifier)) snapshot.appendItems(self.devices.map { $0.hashValue }, toSection: SwitchesViewController.switchCellsSectionIdentifier) _collectionViewDataSource.apply(snapshot, animatingDifferences: false) } diff --git a/XIONControlPanel/Models/DeviceProtocol.swift b/XIONControlPanel/Models/DeviceProtocol.swift index 515d655..bbdfd0e 100644 --- a/XIONControlPanel/Models/DeviceProtocol.swift +++ b/XIONControlPanel/Models/DeviceProtocol.swift @@ -8,15 +8,15 @@ import Foundation -enum DeviceState +enum DeviceState : String, Codable { - case off - case on + case off = "off" + case on = "on" } -enum DeviceType +enum DeviceType : String, Codable { - case `switch` + case `switch` = "switch" } protocol Device diff --git a/XIONControlPanel/Models/HubitatDevice.swift b/XIONControlPanel/Models/HubitatDevice.swift new file mode 100644 index 0000000..076e4d0 --- /dev/null +++ b/XIONControlPanel/Models/HubitatDevice.swift @@ -0,0 +1,47 @@ +// +// HubitatDevice.swift +// XIONControlPanel +// +// Created by James Magahern on 3/14/20. +// Copyright © 2020 XION. All rights reserved. +// + +import Foundation + +public struct HubitatDevice : Device +{ + var name: String = "" + var serial: String = "" + var type: DeviceType = .switch + var state: DeviceState = .off + + var id: Int = -1 + + enum CodingKeys: String, CodingKey { + case name = "label" + case id = "id" + case attributes = "attributes" + } + + enum AttributesKeys: String, CodingKey { + case power + case dataType + case `switch` + } +} + +extension HubitatDevice : Decodable +{ + public init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + + let idStr = try values.decode(String.self, forKey: .id) + id = Int(idStr)! + serial = String(format: "hubitat:%d", id) + + name = try values.decode(String.self, forKey: .name) + + let attributes = try values.nestedContainer(keyedBy: Self.AttributesKeys, forKey: .attributes) + state = try attributes.decode(DeviceState.self, forKey: .switch) + } +} diff --git a/XIONControlPanel/Servers/HubitatServer.swift b/XIONControlPanel/Servers/HubitatServer.swift new file mode 100644 index 0000000..33fd94e --- /dev/null +++ b/XIONControlPanel/Servers/HubitatServer.swift @@ -0,0 +1,82 @@ +// +// HubitatServer.swift +// XIONControlPanel +// +// Created by James Magahern on 3/14/20. +// Copyright © 2020 XION. All rights reserved. +// + +import Foundation +import Combine + +class HubitatServer : Server +{ + var connected: Bool = false + + public fileprivate(set) var devices: [HubitatDevice] = [] + + private let baseURL: URL + private let accessToken = "fa861583-71f0-466f-8add-f29b2278af6a" + private var cancellable: AnyCancellable? + + required init(_ url: URL) + { + self.baseURL = url + } + + func connect(_ completion: @escaping (Error?) -> Void) + { + // RESTful, assume we're connected unless we get an error + self.connected = true + completion(nil) + } + + func disconnect(_ completion: (Error?) -> Void) + { + self.connected = false + } + + func fetchDevices(_ fetchCompletion: @escaping (Result<[AnyDevice], Error>) -> Void) + { + let url = baseURL.appendingPathComponent("devices/all").authenticatedURLWithAccessToken(accessToken) + + self.cancellable = URLSession.shared.dataTaskPublisher(for: url) + .mapError { $0 as Error } + .map { $0.data } + .decode(type: [HubitatDevice].self, decoder: JSONDecoder()) + .eraseToAnyPublisher() + .sink(receiveCompletion: { completion in + if case let Subscribers.Completion.failure(error) = completion { + fetchCompletion(.failure(error)) + } + }) { (devices: [HubitatDevice]) in + self.devices = devices + fetchCompletion(.success(devices.map { AnyDevice($0) })) + } + } + + func toggleDevice(_ device: AnyDevice, state: DeviceState, completion: @escaping (Error?) -> Void) + { + if let hubitatDevice = (self.devices.first { $0.serial == device.serial }) { + print("HUBITAT: Toggle: ", hubitatDevice.id) + + // TODO... + } + } + + func responsibleForDevice(_ device: AnyDevice) -> Bool + { + return self.devices.contains { $0.serial == device.serial } + } +} + +extension URL +{ + func authenticatedURLWithAccessToken(_ token: String) -> URL + { + guard var components = URLComponents(url: self, resolvingAgainstBaseURL: false) else { fatalError() } + components.queryItems = [ URLQueryItem(name: "access_token", value: token) ] + + return components.url! + } +} diff --git a/XIONControlPanel/Servers/ServerMultiplex.swift b/XIONControlPanel/Servers/ServerMultiplex.swift index 95d9874..7366866 100644 --- a/XIONControlPanel/Servers/ServerMultiplex.swift +++ b/XIONControlPanel/Servers/ServerMultiplex.swift @@ -102,14 +102,7 @@ extension ServerMultiplex private func handleDevicesChanged(_ devicesChanged: [AnyDevice]) { let newDevicesSet = Set(devicesChanged) - let additions = newDevicesSet.subtracting(self.devices) - if additions.count > 0 { - DispatchQueue.main.async { - self.delegate?.serverMultiplex(self, didAddDevices: Array(additions)) - } - } - let changed = newDevicesSet.filter { (device: AnyDevice) in if let existing = (devices.first { $0.hashValue == device.hashValue }) { return existing.state != device.state @@ -118,13 +111,19 @@ extension ServerMultiplex return false } + self.devices = self.devices.union(newDevicesSet) + + if additions.count > 0 { + DispatchQueue.main.async { + self.delegate?.serverMultiplex(self, didAddDevices: Array(additions)) + } + } + if changed.count > 0 { DispatchQueue.main.async { self.delegate?.serverMultiplex(self, devicesStateChanged: Array(changed)) } } - - self.devices = self.devices.union(newDevicesSet) } private func handleError(_ error: Error)