Adds Home Assistant support for Xaibatsu
This commit is contained in:
@@ -41,6 +41,8 @@
|
|||||||
CD815B92241DA28B00E2FA97 /* ServerMultiplex.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD815B91241DA28B00E2FA97 /* ServerMultiplex.swift */; };
|
CD815B92241DA28B00E2FA97 /* ServerMultiplex.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD815B91241DA28B00E2FA97 /* ServerMultiplex.swift */; };
|
||||||
CD815B94241DC16700E2FA97 /* HubitatServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD815B93241DC16700E2FA97 /* HubitatServer.swift */; };
|
CD815B94241DC16700E2FA97 /* HubitatServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD815B93241DC16700E2FA97 /* HubitatServer.swift */; };
|
||||||
CD815B96241DC44D00E2FA97 /* HubitatDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD815B95241DC44D00E2FA97 /* HubitatDevice.swift */; };
|
CD815B96241DC44D00E2FA97 /* HubitatDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD815B95241DC44D00E2FA97 /* HubitatDevice.swift */; };
|
||||||
|
CDA64C242E03AE6400E9B07E /* HomeAssistantServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDA64C232E03AE6100E9B07E /* HomeAssistantServer.swift */; };
|
||||||
|
CDA64C262E03EA1400E9B07E /* HomeAssistantDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDA64C252E03EA1000E9B07E /* HomeAssistantDevice.swift */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXFileReference section */
|
/* Begin PBXFileReference section */
|
||||||
@@ -83,6 +85,8 @@
|
|||||||
CD815B91241DA28B00E2FA97 /* ServerMultiplex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerMultiplex.swift; sourceTree = "<group>"; };
|
CD815B91241DA28B00E2FA97 /* ServerMultiplex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerMultiplex.swift; sourceTree = "<group>"; };
|
||||||
CD815B93241DC16700E2FA97 /* HubitatServer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HubitatServer.swift; sourceTree = "<group>"; };
|
CD815B93241DC16700E2FA97 /* HubitatServer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HubitatServer.swift; sourceTree = "<group>"; };
|
||||||
CD815B95241DC44D00E2FA97 /* HubitatDevice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HubitatDevice.swift; sourceTree = "<group>"; };
|
CD815B95241DC44D00E2FA97 /* HubitatDevice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HubitatDevice.swift; sourceTree = "<group>"; };
|
||||||
|
CDA64C232E03AE6100E9B07E /* HomeAssistantServer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeAssistantServer.swift; sourceTree = "<group>"; };
|
||||||
|
CDA64C252E03EA1000E9B07E /* HomeAssistantDevice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeAssistantDevice.swift; sourceTree = "<group>"; };
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXFrameworksBuildPhase section */
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
@@ -131,6 +135,7 @@
|
|||||||
children = (
|
children = (
|
||||||
CD815B8F241C7CAC00E2FA97 /* DeviceProtocol.swift */,
|
CD815B8F241C7CAC00E2FA97 /* DeviceProtocol.swift */,
|
||||||
CD815B95241DC44D00E2FA97 /* HubitatDevice.swift */,
|
CD815B95241DC44D00E2FA97 /* HubitatDevice.swift */,
|
||||||
|
CDA64C252E03EA1000E9B07E /* HomeAssistantDevice.swift */,
|
||||||
0C4314FF1C35B1E700164559 /* WemoDevice.swift */,
|
0C4314FF1C35B1E700164559 /* WemoDevice.swift */,
|
||||||
);
|
);
|
||||||
path = Models;
|
path = Models;
|
||||||
@@ -232,6 +237,7 @@
|
|||||||
children = (
|
children = (
|
||||||
CD815B91241DA28B00E2FA97 /* ServerMultiplex.swift */,
|
CD815B91241DA28B00E2FA97 /* ServerMultiplex.swift */,
|
||||||
CD815B8D241C6F4000E2FA97 /* ServerProtocol.swift */,
|
CD815B8D241C6F4000E2FA97 /* ServerProtocol.swift */,
|
||||||
|
CDA64C232E03AE6100E9B07E /* HomeAssistantServer.swift */,
|
||||||
CD815B93241DC16700E2FA97 /* HubitatServer.swift */,
|
CD815B93241DC16700E2FA97 /* HubitatServer.swift */,
|
||||||
0CAA32781C361A2400B353CF /* WemoServer.swift */,
|
0CAA32781C361A2400B353CF /* WemoServer.swift */,
|
||||||
);
|
);
|
||||||
@@ -324,10 +330,12 @@
|
|||||||
files = (
|
files = (
|
||||||
0C41EF4E1C34B81F00AF847F /* KSIRingView.m in Sources */,
|
0C41EF4E1C34B81F00AF847F /* KSIRingView.m in Sources */,
|
||||||
0CAA327B1C361CBE00B353CF /* NSErrorAdditions.swift in Sources */,
|
0CAA327B1C361CBE00B353CF /* NSErrorAdditions.swift in Sources */,
|
||||||
|
CDA64C242E03AE6400E9B07E /* HomeAssistantServer.swift in Sources */,
|
||||||
0CF0F8961C362D8F00FE7752 /* NSURLAdditions.swift in Sources */,
|
0CF0F8961C362D8F00FE7752 /* NSURLAdditions.swift in Sources */,
|
||||||
CD815B94241DC16700E2FA97 /* HubitatServer.swift in Sources */,
|
CD815B94241DC16700E2FA97 /* HubitatServer.swift in Sources */,
|
||||||
0CAA32791C361A2400B353CF /* WemoServer.swift in Sources */,
|
0CAA32791C361A2400B353CF /* WemoServer.swift in Sources */,
|
||||||
0C41EF561C35ABCD00AF847F /* ConnectionStatusView.swift in Sources */,
|
0C41EF561C35ABCD00AF847F /* ConnectionStatusView.swift in Sources */,
|
||||||
|
CDA64C262E03EA1400E9B07E /* HomeAssistantDevice.swift in Sources */,
|
||||||
0C63A87D1C33C655007E4B52 /* NSValue+XIONAdditions.m in Sources */,
|
0C63A87D1C33C655007E4B52 /* NSValue+XIONAdditions.m in Sources */,
|
||||||
CD815B96241DC44D00E2FA97 /* HubitatDevice.swift in Sources */,
|
CD815B96241DC44D00E2FA97 /* HubitatDevice.swift in Sources */,
|
||||||
0C20E2BD1C33534F00BA789B /* VisualizationViewController.swift in Sources */,
|
0C20E2BD1C33534F00BA789B /* VisualizationViewController.swift in Sources */,
|
||||||
|
|||||||
@@ -21,8 +21,9 @@ class MainViewController: UIViewController, SwitchesViewControllerDelegate, Serv
|
|||||||
super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
|
super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
|
||||||
|
|
||||||
_serverMultiplex.delegate = self
|
_serverMultiplex.delegate = self
|
||||||
_serverMultiplex.addServer(HubitatServer(URL(string: "https://midna.xionsf.com:6000/apps/api/1/")!))
|
_serverMultiplex.addServer(HomeAssistantServer(URL(string: "https://ha.xaibatsu.com/api")!))
|
||||||
_serverMultiplex.addServer(WemoServer(URL(string: "http://midna.xionsf.com:5000")!))
|
// _serverMultiplex.addServer(HubitatServer(URL(string: "https://midna.xionsf.com:6000/apps/api/1/")!))
|
||||||
|
// _serverMultiplex.addServer(WemoServer(URL(string: "http://midna.xionsf.com:5000")!))
|
||||||
}
|
}
|
||||||
|
|
||||||
required init?(coder aDecoder: NSCoder)
|
required init?(coder aDecoder: NSCoder)
|
||||||
|
|||||||
@@ -124,6 +124,7 @@ class SwitchesViewController: UIViewController,
|
|||||||
var snapshot = _collectionViewDataSource.snapshot()
|
var snapshot = _collectionViewDataSource.snapshot()
|
||||||
snapshot.deleteItems(snapshot.itemIdentifiers(inSection: SwitchesViewController.switchCellsSectionIdentifier))
|
snapshot.deleteItems(snapshot.itemIdentifiers(inSection: SwitchesViewController.switchCellsSectionIdentifier))
|
||||||
snapshot.appendItems(self.devices.map { $0.hashValue }, toSection: SwitchesViewController.switchCellsSectionIdentifier)
|
snapshot.appendItems(self.devices.map { $0.hashValue }, toSection: SwitchesViewController.switchCellsSectionIdentifier)
|
||||||
|
snapshot.reloadSections([Self.actionCellsSectionIdentifier])
|
||||||
_collectionViewDataSource.apply(snapshot, animatingDifferences: false)
|
_collectionViewDataSource.apply(snapshot, animatingDifferences: false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,3 +70,9 @@ class AnyDevice : Device
|
|||||||
}
|
}
|
||||||
|
|
||||||
extension AnyDevice : Hashable {}
|
extension AnyDevice : Hashable {}
|
||||||
|
|
||||||
|
extension Device {
|
||||||
|
func eraseToAnyDevice() -> AnyDevice {
|
||||||
|
AnyDevice(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
24
XIONControlPanel/Models/HomeAssistantDevice.swift
Normal file
24
XIONControlPanel/Models/HomeAssistantDevice.swift
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
//
|
||||||
|
// HomeAssistantDevice.swift
|
||||||
|
// XIONControlPanel
|
||||||
|
//
|
||||||
|
// Created by James Magahern on 6/18/25.
|
||||||
|
// Copyright © 2025 XION. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
class HomeAssistantDevice: Device
|
||||||
|
{
|
||||||
|
var name: String
|
||||||
|
var serial: String
|
||||||
|
var type: DeviceType
|
||||||
|
var state: DeviceState
|
||||||
|
|
||||||
|
init(name: String, serial: String, type: DeviceType, state: DeviceState) {
|
||||||
|
self.name = name
|
||||||
|
self.serial = serial
|
||||||
|
self.type = type
|
||||||
|
self.state = state
|
||||||
|
}
|
||||||
|
}
|
||||||
389
XIONControlPanel/Servers/HomeAssistantServer.swift
Normal file
389
XIONControlPanel/Servers/HomeAssistantServer.swift
Normal file
@@ -0,0 +1,389 @@
|
|||||||
|
//
|
||||||
|
// HomeAssistantServer.swift
|
||||||
|
// XIONControlPanel
|
||||||
|
//
|
||||||
|
// Created by James Magahern on 6/18/25.
|
||||||
|
// Copyright © 2025 XION. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
class HomeAssistantServer: Server
|
||||||
|
{
|
||||||
|
var connectionStatus: ConnectionStatus = .disconnected
|
||||||
|
|
||||||
|
private typealias Reply = (Data) -> Void
|
||||||
|
|
||||||
|
// Configure:
|
||||||
|
private let authToken: String = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiI2ZTAwODI1N2Y4N2Q0OWIxYWRkN2ExNTBhOWRmNGZiZCIsImlhdCI6MTc1MDMwMDg5NiwiZXhwIjoyMDY1NjYwODk2fQ.KRf9GpRZdpW9Z_vkbL3sl74rgKS7eAyc8kO0a5jLTgg"
|
||||||
|
|
||||||
|
// Configure:
|
||||||
|
private static let filterLabel = "control_panel"
|
||||||
|
|
||||||
|
private let url: URL
|
||||||
|
private var websocketTask: URLSessionWebSocketTask? = nil
|
||||||
|
private var pendingMessages: [MessageID: Reply] = [:]
|
||||||
|
private var connectionContinuations: [CheckedContinuation<Bool, Never>] = []
|
||||||
|
|
||||||
|
required init(_ url: URL) {
|
||||||
|
self.url = url
|
||||||
|
}
|
||||||
|
|
||||||
|
func connect(_ completion: @escaping ((any Error)?) -> Void) {
|
||||||
|
guard websocketTask == nil else { return }
|
||||||
|
|
||||||
|
let url = url.appending(component: "websocket").websocket()
|
||||||
|
let websocketTask = URLSession.shared.webSocketTask(with: url)
|
||||||
|
websocketTask.resume()
|
||||||
|
|
||||||
|
self.websocketTask = websocketTask
|
||||||
|
Task {
|
||||||
|
do {
|
||||||
|
while websocketTask.state == .running {
|
||||||
|
switch try await websocketTask.receive() {
|
||||||
|
case .string(let string):
|
||||||
|
await decodeEvent(string.data(using: .utf8)!)
|
||||||
|
case .data(let data):
|
||||||
|
await decodeEvent(data)
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
print("Websocket not running: \(String(describing: websocketTask.error))")
|
||||||
|
} catch {
|
||||||
|
print("Websocket Error: \(error)")
|
||||||
|
self.connectionStatus = .error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
completion(nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func disconnect(_ completion: ((any Error)?) -> Void) {
|
||||||
|
websocketTask?.cancel()
|
||||||
|
websocketTask = nil
|
||||||
|
completion(nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchDevices(_ completion: @escaping (Result<[AnyDevice], any Error>) -> Void) {
|
||||||
|
Task {
|
||||||
|
await waitConnected()
|
||||||
|
|
||||||
|
do {
|
||||||
|
let registryEntries: [RegistryEntry] = try await sendMessage(RegistryEntriesMessage())
|
||||||
|
let filteredEntries = registryEntries.filter { $0.labels.contains(Self.filterLabel) }
|
||||||
|
|
||||||
|
let states: [EntityState] = try await sendMessage(GetStatesMessage())
|
||||||
|
let stateMap = states.reduce(into: [String: DeviceState]()) { partialResult, state in
|
||||||
|
partialResult[state.entityId] = switch state.state {
|
||||||
|
case .on: .on
|
||||||
|
case .off: .off
|
||||||
|
case .other(_): .off
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let devices = filteredEntries.map { entry in
|
||||||
|
HomeAssistantDevice(entity: entry, state: stateMap[entry.entityId] ?? .on)
|
||||||
|
.eraseToAnyDevice()
|
||||||
|
}
|
||||||
|
|
||||||
|
Task { @MainActor in completion(.success(devices)) }
|
||||||
|
} catch {
|
||||||
|
Task { @MainActor in completion(.failure(error)) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func toggleDevice(_ device: AnyDevice, state: DeviceState, completion: @escaping ((any Error)?) -> Void) {
|
||||||
|
Task {
|
||||||
|
await waitConnected()
|
||||||
|
|
||||||
|
do {
|
||||||
|
let entityID = device.serial
|
||||||
|
let service: CallServiceMessage.Service = switch state {
|
||||||
|
case .off: .turnOff
|
||||||
|
case .on: .turnOn
|
||||||
|
}
|
||||||
|
|
||||||
|
let _: Bool = try await sendMessage(CallServiceMessage(entityID: entityID, service: service))
|
||||||
|
Task { @MainActor in completion(nil) }
|
||||||
|
} catch {
|
||||||
|
Task { @MainActor in completion(error) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func responsibleForDevice(_ device: AnyDevice) -> Bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: -
|
||||||
|
|
||||||
|
@discardableResult
|
||||||
|
private func waitConnected() async -> Bool {
|
||||||
|
if connectionStatus == .connecting || connectionStatus == .disconnected {
|
||||||
|
return await withCheckedContinuation { continuation in
|
||||||
|
self.connectionContinuations.append(continuation)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
@discardableResult
|
||||||
|
private func sendMessage<R: Decodable>(_ message: Message) async throws -> R {
|
||||||
|
return try await withCheckedThrowingContinuation { continuation in
|
||||||
|
Task { @MainActor in
|
||||||
|
self.pendingMessages[message.id] = { response in
|
||||||
|
do {
|
||||||
|
let decodedResponse = try JSONDecoder().decode(Event<R>.self, from: response)
|
||||||
|
if let result = decodedResponse.result {
|
||||||
|
continuation.resume(returning: result)
|
||||||
|
} else {
|
||||||
|
let error = decodedResponse.error ?? ErrorResult(code: "unknown", message: "unknown")
|
||||||
|
continuation.resume(throwing: error)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
print("Response decoding error: \(error)")
|
||||||
|
continuation.resume(throwing: error)
|
||||||
|
}
|
||||||
|
|
||||||
|
self.pendingMessages.removeValue(forKey: message.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
let encodedMessage = try JSONEncoder().encode(message)
|
||||||
|
let jsonString = String(data: encodedMessage, encoding: .utf8)!
|
||||||
|
websocketTask?.send(.string(jsonString)) { error in
|
||||||
|
guard let error else { return }
|
||||||
|
print("Websocket send error: \(error)")
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
print("Error sending message: \(error)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func decodeEvent(_ data: Data) async {
|
||||||
|
do {
|
||||||
|
let decoder = JSONDecoder()
|
||||||
|
let partialEvent = try decoder.decode(PartialEvent.self, from: data)
|
||||||
|
try await handlePartialEvent(partialEvent, data: data)
|
||||||
|
} catch {
|
||||||
|
print("HA Decoding error: \(error)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func handlePartialEvent(_ partialEvent: PartialEvent, data: Data) async throws {
|
||||||
|
Task {
|
||||||
|
switch partialEvent.type {
|
||||||
|
case .authRequired:
|
||||||
|
await authenticate()
|
||||||
|
case .authOK:
|
||||||
|
didConnectToWebsocket()
|
||||||
|
case .result:
|
||||||
|
guard let id = partialEvent.id else { return }
|
||||||
|
Task { @MainActor in
|
||||||
|
self.pendingMessages[id]?(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func didConnectToWebsocket() {
|
||||||
|
self.connectionStatus = .connected
|
||||||
|
|
||||||
|
connectionContinuations.forEach { continuation in
|
||||||
|
continuation.resume(returning: true)
|
||||||
|
}
|
||||||
|
connectionContinuations.removeAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func authenticate() async {
|
||||||
|
do {
|
||||||
|
let _: Bool = try await sendMessage(AuthMessage(token: authToken))
|
||||||
|
} catch {
|
||||||
|
print("Error authenticating: \(error)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Types
|
||||||
|
|
||||||
|
struct PartialEvent: Codable
|
||||||
|
{
|
||||||
|
let id: Int64?
|
||||||
|
let type: EventType
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Event<Result: Decodable>: Decodable
|
||||||
|
{
|
||||||
|
let id: Int?
|
||||||
|
let type: EventType
|
||||||
|
let result: Result?
|
||||||
|
let error: ErrorResult?
|
||||||
|
}
|
||||||
|
|
||||||
|
enum EventType: String, Codable
|
||||||
|
{
|
||||||
|
case authRequired = "auth_required"
|
||||||
|
case authOK = "auth_ok"
|
||||||
|
case result = "result"
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ErrorResult: Codable, Error
|
||||||
|
{
|
||||||
|
let code: String
|
||||||
|
let message: String
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Result Types
|
||||||
|
|
||||||
|
struct RegistryEntry: Codable
|
||||||
|
{
|
||||||
|
let id: String
|
||||||
|
let entityId: String
|
||||||
|
let name: String?
|
||||||
|
let labels: [String]
|
||||||
|
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case id
|
||||||
|
case name
|
||||||
|
case labels
|
||||||
|
case entityId = "entity_id"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct EntityState: Decodable
|
||||||
|
{
|
||||||
|
let entityId: String
|
||||||
|
let state: State
|
||||||
|
|
||||||
|
enum State: Decodable {
|
||||||
|
case on
|
||||||
|
case off
|
||||||
|
case other(String)
|
||||||
|
|
||||||
|
init(from decoder: Decoder) throws {
|
||||||
|
let container = try decoder.singleValueContainer()
|
||||||
|
let stringValue = try container.decode(String.self)
|
||||||
|
|
||||||
|
switch stringValue.lowercased() {
|
||||||
|
case "on":
|
||||||
|
self = .on
|
||||||
|
case "off":
|
||||||
|
self = .off
|
||||||
|
default:
|
||||||
|
self = .other(stringValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Attributes: Decodable {
|
||||||
|
let friendlyName: String?
|
||||||
|
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case friendlyName = "friendly_name"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case entityId = "entity_id"
|
||||||
|
case state
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension HomeAssistantDevice
|
||||||
|
{
|
||||||
|
convenience init(entity: HomeAssistantServer.RegistryEntry, state: DeviceState) {
|
||||||
|
self.init(
|
||||||
|
name: entity.name?.replacingOccurrences(of: " Switch", with: "") ?? entity.entityId,
|
||||||
|
serial: entity.entityId,
|
||||||
|
type: .switch,
|
||||||
|
state: state
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
typealias MessageID = Int64?
|
||||||
|
|
||||||
|
extension MessageID
|
||||||
|
{
|
||||||
|
private static var counter: Int64 = 0
|
||||||
|
static func next() -> Self {
|
||||||
|
Self.counter += 1
|
||||||
|
return Self.counter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Message Types
|
||||||
|
|
||||||
|
fileprivate protocol Message: Encodable
|
||||||
|
{
|
||||||
|
var id: MessageID { get }
|
||||||
|
var type: String { get }
|
||||||
|
}
|
||||||
|
|
||||||
|
fileprivate struct RegistryEntriesMessage: Message
|
||||||
|
{
|
||||||
|
let id: MessageID = .next()
|
||||||
|
let type: String = "config/entity_registry/list"
|
||||||
|
}
|
||||||
|
|
||||||
|
fileprivate struct GetStatesMessage: Message
|
||||||
|
{
|
||||||
|
let id: MessageID = .next()
|
||||||
|
let type: String = "get_states"
|
||||||
|
}
|
||||||
|
|
||||||
|
fileprivate struct AuthMessage: Message
|
||||||
|
{
|
||||||
|
let id: MessageID = nil
|
||||||
|
let type: String = "auth"
|
||||||
|
let token: String
|
||||||
|
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case id
|
||||||
|
case type
|
||||||
|
case token = "access_token"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fileprivate struct CallServiceMessage: Message
|
||||||
|
{
|
||||||
|
let id: MessageID = .next()
|
||||||
|
let type: String = "call_service"
|
||||||
|
|
||||||
|
let service: Service
|
||||||
|
let domain: ServiceDomain
|
||||||
|
let target: Target
|
||||||
|
|
||||||
|
init(entityID: String, service: Service, domain: ServiceDomain = .switch) {
|
||||||
|
self.service = service
|
||||||
|
self.domain = domain
|
||||||
|
self.target = Target(entityID: entityID)
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ServiceDomain: String, Codable
|
||||||
|
{
|
||||||
|
case `switch`
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Service: String, Codable
|
||||||
|
{
|
||||||
|
case toggle
|
||||||
|
case turnOn = "turn_on"
|
||||||
|
case turnOff = "turn_off"
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Target: Codable
|
||||||
|
{
|
||||||
|
let entityID: String
|
||||||
|
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case entityID = "entity_id"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -72,6 +72,14 @@ class ServerMultiplex
|
|||||||
|
|
||||||
public func refreshDevices()
|
public func refreshDevices()
|
||||||
{
|
{
|
||||||
|
self.servers.forEach { server in
|
||||||
|
server.connect { error in
|
||||||
|
if let error {
|
||||||
|
print("Multiplexer: error connecting server \(server): \(error)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
self.servers.forEach { (server: Server) in
|
self.servers.forEach { (server: Server) in
|
||||||
server.fetchDevices { (result: Result<[AnyDevice], Error>) in
|
server.fetchDevices { (result: Result<[AnyDevice], Error>) in
|
||||||
self.handleServerFetchResult(forServer: server, result: result)
|
self.handleServerFetchResult(forServer: server, result: result)
|
||||||
|
|||||||
@@ -53,4 +53,10 @@ extension URL
|
|||||||
|
|
||||||
return URL(string: absoluteString)
|
return URL(string: absoluteString)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func websocket() -> URL {
|
||||||
|
guard var components = URLComponents(url: self, resolvingAgainstBaseURL: false) else { fatalError() }
|
||||||
|
components.scheme = components.scheme == "https" ? "wss" : "ws"
|
||||||
|
return components.url!
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user