Improve Home Assistant connection reliability

This commit is contained in:
2026-07-27 16:23:40 -07:00
parent b7ec774efd
commit f3341ee064
16 changed files with 1696 additions and 697 deletions
File diff suppressed because it is too large Load Diff
+55 -28
View File
@@ -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
}
+16 -14
View File
@@ -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