Improve Home Assistant connection reliability
This commit is contained in:
@@ -17,7 +17,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate
|
||||
func application(_ application: UIApplication,
|
||||
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool
|
||||
{
|
||||
self.window = UIWindow(frame: UIScreen.main.bounds)
|
||||
self.window = UIWindow()
|
||||
self.window?.rootViewController = self.mainViewController
|
||||
self.window?.makeKeyAndVisible()
|
||||
|
||||
@@ -26,12 +26,17 @@ class AppDelegate: UIResponder, UIApplicationDelegate
|
||||
|
||||
func applicationDidBecomeActive(_ application: UIApplication)
|
||||
{
|
||||
self.mainViewController.viewDidAppear(false)
|
||||
mainViewController.applicationDidBecomeActive()
|
||||
}
|
||||
|
||||
func applicationWillResignActive(_ application: UIApplication)
|
||||
{
|
||||
mainViewController.applicationWillResignActive()
|
||||
}
|
||||
|
||||
func applicationDidEnterBackground(_ application: UIApplication)
|
||||
{
|
||||
self.mainViewController.viewDidDisappear(false)
|
||||
mainViewController.applicationWillResignActive()
|
||||
}
|
||||
|
||||
func application(_ application: UIApplication,
|
||||
|
||||
@@ -13,7 +13,9 @@ class MainViewController: UIViewController, SwitchesViewControllerDelegate, Serv
|
||||
fileprivate var _serverMultiplex: ServerMultiplex = ServerMultiplex()
|
||||
fileprivate var _visualizationController: VisualizationViewController = VisualizationViewController()
|
||||
fileprivate var _headerView: HeaderView = HeaderView()
|
||||
fileprivate var _updateDevices: Bool = false
|
||||
fileprivate var _refreshTimer: Timer?
|
||||
fileprivate var _isApplicationActive: Bool = false
|
||||
fileprivate var _isViewVisible: Bool = false
|
||||
|
||||
fileprivate var _switchesController: SwitchesViewController = SwitchesViewController()
|
||||
fileprivate var _lightsController: SwitchesViewController = SwitchesViewController()
|
||||
@@ -58,6 +60,7 @@ class MainViewController: UIViewController, SwitchesViewControllerDelegate, Serv
|
||||
self.view.addSubview(_headerView)
|
||||
|
||||
_updateConnectivityStatus(.disconnected)
|
||||
_updateSizeClassPresentation()
|
||||
}
|
||||
|
||||
override func viewDidLayoutSubviews()
|
||||
@@ -80,61 +83,68 @@ class MainViewController: UIViewController, SwitchesViewControllerDelegate, Serv
|
||||
|
||||
_headerView.frame = headerBounds
|
||||
|
||||
let visualizationFrame = CGRect(
|
||||
x: bodyBounds.origin.x,
|
||||
y: bodyBounds.origin.y,
|
||||
width: rint(0.5 * bodyBounds.size.width),
|
||||
height: bodyBounds.size.height / 1.5
|
||||
)
|
||||
_visualizationController.view.frame = visualizationFrame
|
||||
|
||||
let lightsControllerFrame = CGRect(
|
||||
x: bodyBounds.origin.x,
|
||||
y: visualizationFrame.maxY,
|
||||
width: visualizationFrame.width,
|
||||
height: bodyBounds.height - visualizationFrame.height
|
||||
)
|
||||
_lightsController.view.frame = lightsControllerFrame.insetBy(dx: 18.0, dy: 0.0)
|
||||
|
||||
var switchesOriginX: CGFloat = 0.0
|
||||
var switchesWidth: CGFloat = 0.0
|
||||
if (_visualizationShouldBeVisible()) {
|
||||
switchesOriginX = visualizationFrame.maxX
|
||||
switchesWidth = bodyBounds.size.width - visualizationFrame.size.width
|
||||
if _visualizationShouldBeVisible() {
|
||||
let visualizationFrame = CGRect(
|
||||
x: bodyBounds.origin.x,
|
||||
y: bodyBounds.origin.y,
|
||||
width: rint(0.5 * bodyBounds.size.width),
|
||||
height: bodyBounds.size.height / 1.5
|
||||
)
|
||||
_visualizationController.view.frame = visualizationFrame
|
||||
|
||||
let lightsControllerFrame = CGRect(
|
||||
x: bodyBounds.origin.x,
|
||||
y: visualizationFrame.maxY,
|
||||
width: visualizationFrame.width,
|
||||
height: bodyBounds.height - visualizationFrame.height
|
||||
)
|
||||
_lightsController.view.frame = lightsControllerFrame.insetBy(
|
||||
dx: 18.0,
|
||||
dy: 0.0
|
||||
)
|
||||
|
||||
_switchesController.view.frame = CGRect(
|
||||
x: visualizationFrame.maxX,
|
||||
y: bodyBounds.origin.y,
|
||||
width: bodyBounds.width - visualizationFrame.width,
|
||||
height: bodyBounds.height
|
||||
)
|
||||
} else {
|
||||
switchesOriginX = 0.0
|
||||
switchesWidth = bodyBounds.size.width
|
||||
let sectionSpacing: CGFloat = 8
|
||||
let switchesHeight = rint(bodyBounds.height * 0.68)
|
||||
|
||||
_visualizationController.view.frame = .zero
|
||||
_switchesController.view.frame = CGRect(
|
||||
x: bodyBounds.minX,
|
||||
y: bodyBounds.minY,
|
||||
width: bodyBounds.width,
|
||||
height: switchesHeight
|
||||
)
|
||||
_lightsController.view.frame = CGRect(
|
||||
x: bodyBounds.minX + sectionSpacing,
|
||||
y: bodyBounds.minY + switchesHeight + sectionSpacing,
|
||||
width: bodyBounds.width - (sectionSpacing * 2),
|
||||
height: bodyBounds.height - switchesHeight - sectionSpacing
|
||||
)
|
||||
}
|
||||
|
||||
let switchesControllerFrame = CGRect(
|
||||
x: switchesOriginX,
|
||||
y: bodyBounds.origin.y,
|
||||
width: switchesWidth,
|
||||
height: bodyBounds.size.height
|
||||
)
|
||||
_switchesController.view.frame = switchesControllerFrame
|
||||
|
||||
_updateSizeClassPresentation()
|
||||
}
|
||||
|
||||
override func viewDidAppear(_ animated: Bool)
|
||||
{
|
||||
super.viewDidAppear(animated)
|
||||
|
||||
UIApplication.shared.isIdleTimerDisabled = true
|
||||
|
||||
_headerView.xionLogoView.beginAnimating()
|
||||
|
||||
if _serverMultiplex.devices.count == 0 {
|
||||
// Do initial refresh
|
||||
_updateConnectivityStatus(.connecting)
|
||||
_serverMultiplex.refreshDevices()
|
||||
_startUpdatingDevices()
|
||||
}
|
||||
|
||||
_isViewVisible = true
|
||||
_updateActiveState()
|
||||
}
|
||||
|
||||
override func viewDidDisappear(_ animated: Bool)
|
||||
{
|
||||
super.viewDidDisappear(animated)
|
||||
UIApplication.shared.isIdleTimerDisabled = false
|
||||
|
||||
_isViewVisible = false
|
||||
_updateActiveState()
|
||||
}
|
||||
|
||||
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator)
|
||||
@@ -155,7 +165,21 @@ class MainViewController: UIViewController, SwitchesViewControllerDelegate, Serv
|
||||
_updateVisualization(true)
|
||||
|
||||
for device in devices {
|
||||
_serverMultiplex.toggleDeviceState(device, state: device.state, completion: { (error: Error?) -> Void in })
|
||||
_serverMultiplex.toggleDeviceState(
|
||||
device,
|
||||
state: device.state
|
||||
) { [weak self] error in
|
||||
guard let error else { return }
|
||||
|
||||
DispatchQueue.main.async {
|
||||
guard let self else { return }
|
||||
self._updateConnectivityStatus(.error)
|
||||
self._serverMultiplex.refreshDevices()
|
||||
|
||||
var stderr = StandardErrorOutputStream()
|
||||
print(error.localizedDescription, to: &stderr)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -200,22 +224,41 @@ class MainViewController: UIViewController, SwitchesViewControllerDelegate, Serv
|
||||
}
|
||||
}
|
||||
|
||||
internal func _startUpdatingDevices()
|
||||
internal func applicationDidBecomeActive()
|
||||
{
|
||||
_updateDevices = true
|
||||
|
||||
let interval = DispatchTime.now() + Double(Int64(10 * Double(NSEC_PER_SEC))) / Double(NSEC_PER_SEC)
|
||||
DispatchQueue.main.asyncAfter(deadline: interval) { () -> Void in
|
||||
if (self._updateDevices) {
|
||||
self._serverMultiplex.refreshDevices()
|
||||
self._startUpdatingDevices()
|
||||
}
|
||||
}
|
||||
_isApplicationActive = true
|
||||
_updateActiveState()
|
||||
}
|
||||
|
||||
internal func _stopUpdatingDevices()
|
||||
|
||||
internal func applicationWillResignActive()
|
||||
{
|
||||
_updateDevices = false
|
||||
_isApplicationActive = false
|
||||
_updateActiveState()
|
||||
}
|
||||
|
||||
private func _updateActiveState()
|
||||
{
|
||||
let shouldBeActive = _isApplicationActive && _isViewVisible
|
||||
|
||||
if shouldBeActive, _refreshTimer == nil {
|
||||
UIApplication.shared.isIdleTimerDisabled = true
|
||||
_headerView.xionLogoView.beginAnimating()
|
||||
_updateConnectivityStatus(.connecting)
|
||||
_serverMultiplex.refreshDevices()
|
||||
|
||||
let timer = Timer(timeInterval: 10, repeats: true) { [weak self] _ in
|
||||
self?._serverMultiplex.refreshDevices()
|
||||
}
|
||||
RunLoop.main.add(timer, forMode: .common)
|
||||
_refreshTimer = timer
|
||||
} else if !shouldBeActive, _refreshTimer != nil {
|
||||
_refreshTimer?.invalidate()
|
||||
_refreshTimer = nil
|
||||
_serverMultiplex.disconnect()
|
||||
_updateConnectivityStatus(.disconnected)
|
||||
_headerView.xionLogoView.stopAnimating()
|
||||
UIApplication.shared.isIdleTimerDisabled = false
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Server Multiplex Delegate
|
||||
@@ -232,6 +275,7 @@ class MainViewController: UIViewController, SwitchesViewControllerDelegate, Serv
|
||||
{
|
||||
_switchesController.devicesStateChanged(devices.switches)
|
||||
_lightsController.devicesStateChanged(devices.lights)
|
||||
_updateVisualization(true)
|
||||
}
|
||||
|
||||
func serverMultiplex(_ multiplex: ServerMultiplex, didReceiveAcknowledgementFromServer server: Server)
|
||||
@@ -240,6 +284,11 @@ class MainViewController: UIViewController, SwitchesViewControllerDelegate, Serv
|
||||
let groupConnectionStatus = multiplex.groupConnectionStatus()
|
||||
_updateConnectivityStatus(groupConnectionStatus)
|
||||
}
|
||||
|
||||
func serverMultiplexConnectionStatusDidChange(_ multiplex: ServerMultiplex)
|
||||
{
|
||||
_updateConnectivityStatus(multiplex.groupConnectionStatus())
|
||||
}
|
||||
|
||||
func serverMultiplex(_ multiplex: ServerMultiplex, didEncounterError error: Error)
|
||||
{
|
||||
|
||||
@@ -10,7 +10,7 @@ import Darwin
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
protocol SwitchesViewControllerDelegate: class
|
||||
protocol SwitchesViewControllerDelegate: AnyObject
|
||||
{
|
||||
func switchesViewControllerDidToggleDevices(_ controller: SwitchesViewController, devices: [AnyDevice])
|
||||
}
|
||||
@@ -29,9 +29,8 @@ class SwitchesViewController: UIViewController,
|
||||
fileprivate var _collectionView: UICollectionView = UICollectionView(frame: CGRect.zero,
|
||||
collectionViewLayout: UICollectionViewFlowLayout())
|
||||
|
||||
fileprivate var _collectionViewDataSource: UICollectionViewDiffableDataSource<Int, Int>!
|
||||
|
||||
fileprivate var _currentDevicesHash: Int = 0
|
||||
fileprivate var _collectionViewDataSource:
|
||||
UICollectionViewDiffableDataSource<Int, ItemIdentifier>!
|
||||
|
||||
fileprivate let _label = UILabel(frame: .zero)
|
||||
|
||||
@@ -42,7 +41,7 @@ class SwitchesViewController: UIViewController,
|
||||
static fileprivate let actionCellsSectionIdentifier = 0
|
||||
static fileprivate let switchCellsSectionIdentifier = 1
|
||||
|
||||
fileprivate enum ActionCell: Int, CaseIterable
|
||||
fileprivate enum ActionCell: Int, CaseIterable, Hashable
|
||||
{
|
||||
case allOn
|
||||
case allOff
|
||||
@@ -59,6 +58,12 @@ class SwitchesViewController: UIViewController,
|
||||
|
||||
static let count: Int = { return ActionCell.allCases.count }()
|
||||
}
|
||||
|
||||
fileprivate enum ItemIdentifier: Hashable
|
||||
{
|
||||
case action(ActionCell)
|
||||
case device(String)
|
||||
}
|
||||
|
||||
public enum ActionCellLayout
|
||||
{
|
||||
@@ -85,8 +90,8 @@ class SwitchesViewController: UIViewController,
|
||||
|
||||
// Action cells
|
||||
snapshot.appendItems([
|
||||
SwitchesViewController.ActionCell.allOn.rawValue,
|
||||
SwitchesViewController.ActionCell.allOff.rawValue
|
||||
.action(.allOn),
|
||||
.action(.allOff)
|
||||
], toSection: SwitchesViewController.actionCellsSectionIdentifier)
|
||||
|
||||
_collectionViewDataSource.apply(snapshot, animatingDifferences: false)
|
||||
@@ -158,7 +163,10 @@ class SwitchesViewController: UIViewController,
|
||||
|
||||
var snapshot = _collectionViewDataSource.snapshot()
|
||||
snapshot.deleteItems(snapshot.itemIdentifiers(inSection: SwitchesViewController.switchCellsSectionIdentifier))
|
||||
snapshot.appendItems(self.devices.map { $0.hashValue }, toSection: SwitchesViewController.switchCellsSectionIdentifier)
|
||||
snapshot.appendItems(
|
||||
self.devices.map { .device($0.serial) },
|
||||
toSection: SwitchesViewController.switchCellsSectionIdentifier
|
||||
)
|
||||
snapshot.reloadSections([Self.actionCellsSectionIdentifier])
|
||||
_collectionViewDataSource.apply(snapshot, animatingDifferences: false)
|
||||
}
|
||||
@@ -168,9 +176,14 @@ class SwitchesViewController: UIViewController,
|
||||
{
|
||||
var snapshot = _collectionViewDataSource.snapshot()
|
||||
changedDevices.forEach { changedDevice in
|
||||
if let existingDevice = (self.devices.first { $0.hashValue == changedDevice.hashValue }) {
|
||||
if let existingDevice = self.devices.first(where: {
|
||||
$0.serial == changedDevice.serial
|
||||
}) {
|
||||
existingDevice.state = changedDevice.state
|
||||
snapshot.reloadItems([ existingDevice.hashValue ])
|
||||
let identifier = ItemIdentifier.device(existingDevice.serial)
|
||||
if snapshot.indexOfItem(identifier) != nil {
|
||||
snapshot.reloadItems([identifier])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,29 +192,36 @@ class SwitchesViewController: UIViewController,
|
||||
|
||||
// MARK: UICollectionView
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath, identifier: Int) -> UICollectionViewCell?
|
||||
fileprivate func collectionView(
|
||||
_ collectionView: UICollectionView,
|
||||
cellForItemAt indexPath: IndexPath,
|
||||
identifier: ItemIdentifier
|
||||
) -> UICollectionViewCell?
|
||||
{
|
||||
if (indexPath.section == SwitchesViewController.actionCellsSectionIdentifier) {
|
||||
switch identifier {
|
||||
case .action(let action):
|
||||
let reuseID = SwitchesViewController.collectionViewActionCellReuseIdentifier
|
||||
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseID, for: indexPath) as! WemoActionCellView
|
||||
cell.textLabel.text = ActionCell(rawValue: identifier)?.name().uppercased()
|
||||
cell.textLabel.text = action.name().uppercased()
|
||||
cell.accessibilityIdentifier = "action.\(action.rawValue)"
|
||||
cell.accessibilityLabel = action.name()
|
||||
cell.enabled = (self.devices.count > 0)
|
||||
|
||||
return cell
|
||||
} else if (indexPath.section == SwitchesViewController.switchCellsSectionIdentifier) {
|
||||
|
||||
case .device(let serial):
|
||||
let reuseID = SwitchesViewController.collectionViewDeviceSwitchCellReuseIdentifier
|
||||
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseID, for: indexPath) as! WemoDeviceCellView
|
||||
|
||||
if let device = (self.devices.first { $0.hashValue == identifier }) {
|
||||
if let device = self.devices.first(where: { $0.serial == serial }) {
|
||||
cell.deviceName = device.name
|
||||
cell.toggled = (device.state == .on)
|
||||
cell.ordinal = indexPath.row
|
||||
cell.accessibilityIdentifier = "device.\(device.serial)"
|
||||
}
|
||||
|
||||
return cell
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView,
|
||||
@@ -252,9 +272,12 @@ class SwitchesViewController: UIViewController,
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath)
|
||||
{
|
||||
if (indexPath.section == SwitchesViewController.actionCellsSectionIdentifier) {
|
||||
let tappedActionCell = ActionCell(rawValue: indexPath.item)
|
||||
|
||||
guard let identifier = _collectionViewDataSource.itemIdentifier(for: indexPath) else {
|
||||
return
|
||||
}
|
||||
|
||||
switch identifier {
|
||||
case .action(let tappedActionCell):
|
||||
for cell in collectionView.visibleCells {
|
||||
if collectionView.indexPath(for: cell)?.section == SwitchesViewController.switchCellsSectionIdentifier {
|
||||
let switchCell = cell as! WemoDeviceCellView
|
||||
@@ -272,13 +295,17 @@ class SwitchesViewController: UIViewController,
|
||||
}
|
||||
|
||||
self.delegate?.switchesViewControllerDidToggleDevices(self, devices: self.devices)
|
||||
} else if (indexPath.section == SwitchesViewController.switchCellsSectionIdentifier) {
|
||||
let cell = collectionView.cellForItem(at: indexPath) as! WemoDeviceCellView
|
||||
|
||||
case .device(let serial):
|
||||
guard
|
||||
let cell = collectionView.cellForItem(at: indexPath) as? WemoDeviceCellView,
|
||||
let device = self.devices.first(where: { $0.serial == serial })
|
||||
else {
|
||||
return
|
||||
}
|
||||
|
||||
cell.toggled = !cell.toggled
|
||||
|
||||
let device = self.devices[indexPath.row]
|
||||
device.state = (cell.toggled ? .on : .off)
|
||||
|
||||
self.delegate?.switchesViewControllerDidToggleDevices(self, devices: [device])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ import Foundation
|
||||
import GLKit
|
||||
import SceneKit
|
||||
import UIKit
|
||||
import SceneKit
|
||||
|
||||
let π = CGFloat(Double.pi)
|
||||
|
||||
@@ -39,7 +38,7 @@ class VisualizationViewController: UIViewController, SCNSceneRendererDelegate
|
||||
override func loadView()
|
||||
{
|
||||
let opts = [SCNView.Option.preferredRenderingAPI.rawValue : SCNRenderingAPI.openGLES2.rawValue]
|
||||
let view = SCNView(frame: UIScreen.main.bounds, options: opts)
|
||||
let view = SCNView(frame: .zero, options: opts)
|
||||
view.backgroundColor = UIColor.black
|
||||
view.scene = _scene
|
||||
view.allowsCameraControl = false
|
||||
|
||||
@@ -35,14 +35,12 @@ extension Device where Self: Hashable
|
||||
{
|
||||
func hash(into hasher: inout Hasher)
|
||||
{
|
||||
hasher.combine(self.name)
|
||||
hasher.combine(self.type)
|
||||
hasher.combine(self.serial)
|
||||
}
|
||||
|
||||
static func == (lhs: Self, rhs: Self) -> Bool
|
||||
{
|
||||
return lhs.name == rhs.name && lhs.serial == rhs.serial
|
||||
return lhs.serial == rhs.serial
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -43,7 +43,10 @@ extension HubitatDevice : Decodable
|
||||
|
||||
name = try values.decode(String.self, forKey: .name)
|
||||
|
||||
let attributes = try values.nestedContainer(keyedBy: Self.AttributesKeys, forKey: .attributes)
|
||||
let attributes = try values.nestedContainer(
|
||||
keyedBy: Self.AttributesKeys.self,
|
||||
forKey: .attributes
|
||||
)
|
||||
state = try attributes.decode(DeviceState.self, forKey: .switch)
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -8,11 +8,12 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
protocol ServerMultiplexDelegate
|
||||
protocol ServerMultiplexDelegate: AnyObject
|
||||
{
|
||||
func serverMultiplex(_ multiplex: ServerMultiplex, didAddDevices devices: [AnyDevice])
|
||||
func serverMultiplex(_ multiplex: ServerMultiplex, devicesStateChanged devices: [AnyDevice])
|
||||
func serverMultiplex(_ multiplex: ServerMultiplex, didReceiveAcknowledgementFromServer server: Server)
|
||||
func serverMultiplexConnectionStatusDidChange(_ multiplex: ServerMultiplex)
|
||||
func serverMultiplex(_ multiplex: ServerMultiplex, didEncounterError error: Error)
|
||||
}
|
||||
|
||||
@@ -23,10 +24,11 @@ enum ServerMultiplexError : Error
|
||||
|
||||
class ServerMultiplex
|
||||
{
|
||||
public var delegate: ServerMultiplexDelegate?
|
||||
public weak var delegate: ServerMultiplexDelegate?
|
||||
public private(set) var devices = Set<AnyDevice>()
|
||||
|
||||
private var servers: [Server] = []
|
||||
private var devicesByServer: [ObjectIdentifier: Set<AnyDevice>] = [:]
|
||||
public var numServers: Int { return servers.count }
|
||||
|
||||
public func addServer(_ server: Server)
|
||||
@@ -73,31 +75,43 @@ class ServerMultiplex
|
||||
|
||||
public func refreshDevices()
|
||||
{
|
||||
self.servers.forEach { server in
|
||||
servers.forEach { server in
|
||||
server.connect { error in
|
||||
if let error {
|
||||
print("Multiplexer: error connecting server \(server): \(error)")
|
||||
self.handleError(forServer: server, error: error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.servers.forEach { (server: Server) in
|
||||
|
||||
server.fetchDevices { (result: Result<[AnyDevice], Error>) in
|
||||
self.handleServerFetchResult(forServer: server, result: result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func disconnect()
|
||||
{
|
||||
servers.forEach { server in
|
||||
server.disconnect { error in
|
||||
if let error {
|
||||
self.handleError(forServer: server, error: error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension ServerMultiplex
|
||||
{
|
||||
private func handleServerFetchResult(forServer server: Server, result: Result<[AnyDevice], Error>)
|
||||
{
|
||||
switch result {
|
||||
case .success(let devices):
|
||||
handleDevicesChanged(forServer: server, devicesChanged: devices)
|
||||
case .failure(let error):
|
||||
handleError(forServer: server, error: error)
|
||||
DispatchQueue.main.async {
|
||||
switch result {
|
||||
case .success(let devices):
|
||||
self.handleDevicesChanged(forServer: server, devicesChanged: devices)
|
||||
case .failure(let error):
|
||||
self.handleError(forServer: server, error: error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,28 +121,29 @@ extension ServerMultiplex
|
||||
self.delegate?.serverMultiplex(self, didReceiveAcknowledgementFromServer: server)
|
||||
|
||||
// Then, optionally notify about new devices or device state changes
|
||||
let serverID = ObjectIdentifier(server)
|
||||
let previousDevices = devicesByServer[serverID] ?? []
|
||||
let newDevicesSet = Set<AnyDevice>(devicesChanged)
|
||||
let additions = newDevicesSet.subtracting(self.devices)
|
||||
let additions = newDevicesSet.subtracting(previousDevices)
|
||||
let removals = previousDevices.subtracting(newDevicesSet)
|
||||
let changed = newDevicesSet.filter { (device: AnyDevice) in
|
||||
if let existing = (devices.first { $0.hashValue == device.hashValue }) {
|
||||
if let existing = previousDevices.first(where: { $0.serial == device.serial }) {
|
||||
return existing.state != device.state
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
self.devices = self.devices.union(newDevicesSet)
|
||||
|
||||
if additions.count > 0 {
|
||||
DispatchQueue.main.async {
|
||||
self.delegate?.serverMultiplex(self, didAddDevices: Array(additions))
|
||||
}
|
||||
|
||||
devicesByServer[serverID] = newDevicesSet
|
||||
devices.subtract(previousDevices)
|
||||
devices.formUnion(newDevicesSet)
|
||||
|
||||
if !additions.isEmpty || !removals.isEmpty {
|
||||
delegate?.serverMultiplex(self, didAddDevices: Array(additions))
|
||||
}
|
||||
|
||||
if changed.count > 0 {
|
||||
DispatchQueue.main.async {
|
||||
self.delegate?.serverMultiplex(self, devicesStateChanged: Array(changed))
|
||||
}
|
||||
if !changed.isEmpty {
|
||||
delegate?.serverMultiplex(self, devicesStateChanged: Array(changed))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -143,9 +158,21 @@ extension ServerMultiplex
|
||||
extension ServerMultiplex: ServerDelegate
|
||||
{
|
||||
func server(_ server: any Server, deviceChangedState subjectDevice: AnyDevice) {
|
||||
guard let device = devices.first(where: { $0.serial == subjectDevice.serial }) else { return }
|
||||
device.state = subjectDevice.state
|
||||
|
||||
delegate?.serverMultiplex(self, devicesStateChanged: [device])
|
||||
DispatchQueue.main.async {
|
||||
guard let device = self.devices.first(where: {
|
||||
$0.serial == subjectDevice.serial
|
||||
}) else {
|
||||
return
|
||||
}
|
||||
|
||||
device.state = subjectDevice.state
|
||||
self.delegate?.serverMultiplex(self, devicesStateChanged: [device])
|
||||
}
|
||||
}
|
||||
|
||||
func server(_ server: any Server, connectionStatusChanged status: ConnectionStatus) {
|
||||
DispatchQueue.main.async {
|
||||
self.delegate?.serverMultiplexConnectionStatusDidChange(self)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
enum ConnectionStatus
|
||||
enum ConnectionStatus: Equatable
|
||||
{
|
||||
case disconnected
|
||||
case connecting
|
||||
@@ -19,6 +19,7 @@ enum ConnectionStatus
|
||||
protocol ServerDelegate: AnyObject
|
||||
{
|
||||
func server(_ server: Server, deviceChangedState: AnyDevice)
|
||||
func server(_ server: Server, connectionStatusChanged status: ConnectionStatus)
|
||||
}
|
||||
|
||||
protocol Server: AnyObject
|
||||
@@ -44,4 +45,3 @@ protocol Server: AnyObject
|
||||
/// Returns true if this is a device this server is responsible for
|
||||
func responsibleForDevice(_ device: AnyDevice) -> Bool
|
||||
}
|
||||
|
||||
|
||||
@@ -42,9 +42,8 @@ class WemoServer : Server
|
||||
self.connectionStatus = .connecting
|
||||
|
||||
let op = ConnectOperation(baseURL: self.baseURL, session: _urlSession)
|
||||
weak var weakOp = op
|
||||
op.completionBlock = {
|
||||
guard let strongOp = weakOp else { completion(nil) ; return }
|
||||
op.completionBlock = { [weak op] in
|
||||
guard let strongOp = op else { completion(nil) ; return }
|
||||
if let error = strongOp.error {
|
||||
self._logError("Error connecting to server", error: error)
|
||||
self.connectionStatus = .disconnected
|
||||
@@ -69,14 +68,18 @@ class WemoServer : Server
|
||||
func fetchDevices(_ completion: @escaping (Result<[AnyDevice], Error>) -> Void)
|
||||
{
|
||||
let op = FetchDevicesOperation(baseURL: self.baseURL, session: _urlSession)
|
||||
op.completionBlock = { [unowned op] in
|
||||
op.completionBlock = { [weak op] in
|
||||
guard let op else {
|
||||
completion(.failure(ConnectionError.unknown))
|
||||
return
|
||||
}
|
||||
|
||||
if let error = op.error {
|
||||
self._logError("Error fetching devices", error: error)
|
||||
completion(.failure(error))
|
||||
} else {
|
||||
self.devices = op.devices
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
self.devices = op.devices
|
||||
completion(.success(self.devices.map { AnyDevice($0) }))
|
||||
}
|
||||
@@ -98,9 +101,8 @@ class WemoServer : Server
|
||||
{
|
||||
if connectionStatus == .connected, let device = findDevice(device) {
|
||||
let op = ToggleDeviceOperation(baseURL: self.baseURL, session: _urlSession, device: device, state: state)
|
||||
weak var weakOp = op
|
||||
op.completionBlock = {
|
||||
guard let strongOp = weakOp else { completion(nil) ; return }
|
||||
op.completionBlock = { [weak op] in
|
||||
guard let strongOp = op else { completion(nil) ; return }
|
||||
if let error = strongOp.error {
|
||||
self._logError("Error toggling device", error: error)
|
||||
}
|
||||
@@ -132,7 +134,7 @@ class WemoServer : Server
|
||||
}
|
||||
}
|
||||
|
||||
internal class WemoOperation : Operation
|
||||
internal class WemoOperation : Operation, @unchecked Sendable
|
||||
{
|
||||
var baseURL: URL
|
||||
var session: URLSession
|
||||
@@ -146,7 +148,7 @@ internal class WemoOperation : Operation
|
||||
}
|
||||
}
|
||||
|
||||
internal class ConnectOperation : WemoOperation
|
||||
internal class ConnectOperation : WemoOperation, @unchecked Sendable
|
||||
{
|
||||
override func main()
|
||||
{
|
||||
@@ -164,7 +166,7 @@ internal class ConnectOperation : WemoOperation
|
||||
}
|
||||
}
|
||||
|
||||
internal class FetchDevicesOperation : WemoOperation
|
||||
internal class FetchDevicesOperation : WemoOperation, @unchecked Sendable
|
||||
{
|
||||
private(set) var devices: [WemoDevice] = []
|
||||
|
||||
@@ -201,7 +203,7 @@ internal class FetchDevicesOperation : WemoOperation
|
||||
}
|
||||
}
|
||||
|
||||
internal class ToggleDeviceOperation : WemoOperation
|
||||
internal class ToggleDeviceOperation : WemoOperation, @unchecked Sendable
|
||||
{
|
||||
var device: WemoDevice
|
||||
var state: DeviceState
|
||||
|
||||
@@ -20,6 +20,8 @@
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>HomeAssistantAccessToken</key>
|
||||
<string>$(HOME_ASSISTANT_ACCESS_TOKEN)</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
|
||||
@@ -19,6 +19,7 @@ open class WemoCellView: UICollectionViewCell
|
||||
{
|
||||
super.init(frame: frame)
|
||||
|
||||
isAccessibilityElement = true
|
||||
self.contentView.backgroundColor = WemoCellView.disabledBackgroundColor
|
||||
|
||||
_selectionOverlayView.backgroundColor = UIColor.clear
|
||||
@@ -130,6 +131,7 @@ open class WemoDeviceCellView: WemoCellView
|
||||
didSet
|
||||
{
|
||||
_nameLabel.text = self.deviceName.uppercased()
|
||||
accessibilityLabel = deviceName
|
||||
self.setNeedsLayout()
|
||||
}
|
||||
}
|
||||
@@ -154,6 +156,8 @@ open class WemoDeviceCellView: WemoCellView
|
||||
didSet
|
||||
{
|
||||
_indicator.status = toggled
|
||||
accessibilityValue = toggled ? "On" : "Off"
|
||||
accessibilityTraits = toggled ? [.button, .selected] : .button
|
||||
|
||||
if (toggled) {
|
||||
self.contentView.backgroundColor = WemoCellView.enabledBackgroundColor
|
||||
@@ -176,6 +180,7 @@ open class WemoActionCellView: WemoCellView
|
||||
{
|
||||
super.init(frame: frame)
|
||||
|
||||
accessibilityTraits = .button
|
||||
self.textLabel.font = UIFont(name: "Orbitron-Medium", size: 21.0)
|
||||
self.textLabel.textColor = UIColor.white
|
||||
self.textLabel.textAlignment = .center
|
||||
|
||||
Reference in New Issue
Block a user