From 04fc7f251ff8e082241be0205df438afde1674ff Mon Sep 17 00:00:00 2001 From: TheMisfit68 Date: Sat, 17 Feb 2024 21:59:03 +0100 Subject: [PATCH] Add MQTT client to exchange events with external services like the Shortcuts-app --- .../LeafAccessoryDelegate.swift | 226 ++++++++++-------- HAPiNest/HAPiNestApp.swift | 11 +- HAPiNest/MQTTClient.swift | 17 ++ .../PreferencesWindow/PreferencesView.swift | 10 +- MainConfiguration.swift | 29 ++- 5 files changed, 173 insertions(+), 120 deletions(-) create mode 100644 HAPiNest/MQTTClient.swift diff --git a/Accessory Delegates/LeafAccessoryDelegate.swift b/Accessory Delegates/LeafAccessoryDelegate.swift index 61a77a3f..3d98692f 100644 --- a/Accessory Delegates/LeafAccessoryDelegate.swift +++ b/Accessory Delegates/LeafAccessoryDelegate.swift @@ -11,109 +11,131 @@ import HAP import JVSwift import LeafDriver import OSLog +import JVNetworking class LeafAccessoryDelegate:LeafDriver, AccessoryDelegate, AccessorySource{ - - var name: String{ - return String(localized:"Electric Car") - } - - typealias AccessorySubclass = Accessory.ElectricCar - - var characteristicChanged: Bool = false - - let lowChargeLimit = 15 - - var getBatteryStatusTimer:Timer! - - public override init(leafProtocol: LeafProtocol) { - - super.init(leafProtocol: leafProtocol) - - getBatteryStatusTimer = Timer.scheduledTimer(withTimeInterval: 1800, repeats: true) { timer in super.batteryChecker.getNewBatteryStatus()} - getBatteryStatusTimer.tolerance = getBatteryStatusTimer.timeInterval/10.0 // Give the processor some slack with a 10% tolerance on the timeInterval - - } - - var startCharging:Bool = false{ - didSet{ - if startCharging{ - self.charger.startCharging() - } - } - } - - var setAirco:Bool = false{ - didSet{ - if setAirco{ - self.acController.setAirCo(to: .on) - }else{ - self.acController.setAirCo(to: .off) - - } - } - } - - func handleCharacteristicChange(accessory: AccessorySubclass, service: Service, characteristic: GenericCharacteristic, to value: T?) where T : CharacteristicValueType { - let logger = Logger(subsystem: "be.oneclick.HAPiNest", category: "LeafAccessoryDelegate") - - switch service { - case accessory.chargerService: - - switch characteristic.type{ - case CharacteristicType.powerState: - - startCharging = characteristic.value as! Bool - - default: - logger.warning( "Unhandled characteristic change for accessory \(accessory.info.name.value ?? "")") - } - -// case accessory.aircoService: -// -// switch characteristic.type{ -// case CharacteristicType.powerState: -// -// setAirco = characteristic.value as! Bool -// -// default: -// logger.warning( "Unhandled characteristic change for accessory \(accessory.info.name.value ?? "")") -// } - - default: - logger.warning( "Unhandled characteristic change for accessory \(accessory.info.name.value ?? "")") - } - - - - } - - var hardwareFeedbackChanged:Bool = false - - - func pollCycle() { - - if let percentageRemaining = batteryChecker.percentageRemaining, let _ = batteryChecker.rangeRemaining{ - - accessory.primaryService.batteryLevel?.value = UInt8(percentageRemaining) - if (percentageRemaining >= lowChargeLimit){ - accessory.primaryService.statusLowBattery.value = .batteryNormal - }else{ - accessory.primaryService.statusLowBattery.value = .batteryLow - } - - - } - - if let chargingState = batteryChecker.chargingState{ - accessory.primaryService.chargingState?.value = .charging - } - - } - - private func sendSMS(phoneNumber:String, content:String){ - - } - + let logger = Logger(subsystem: "be.oneclick.HAPiNest", category: "LeafAccessoryDelegate") + + var name: String{ + return String(localized:"Electric Car") + } + + typealias AccessorySubclass = Accessory.ElectricCar + + var characteristicChanged: Bool = false + + let lowChargeLimit = 15 + + + public override init(leafProtocol: LeafProtocol) { + + super.init(leafProtocol: leafProtocol) + + } + + var startCharging:Bool = false{ + didSet{ + if startCharging{ + self.charger.startCharging() + } + } + } + + var setAirco:Bool = false{ + didSet{ + if setAirco{ + self.acController.setAirCo(to: .on) + }else{ + self.acController.setAirCo(to: .off) + + } + } + } + + var mqttMessage:LeafMQTTMessage? = nil{ + didSet{ + guard mqttMessage != nil else {return} + if mqttMessage != oldValue{ + MQTTClient.shared.publish(topic: "HomeKit/ExternalEvents/FromServer/NissanLeaf", type: mqttMessage!, retained: true) + } + } + } + + func handleCharacteristicChange(accessory: AccessorySubclass, service: Service, characteristic: GenericCharacteristic, to value: T?) where T : CharacteristicValueType { + + switch service { + case accessory.chargerService: + + switch characteristic.type{ + case CharacteristicType.powerState: + + startCharging = characteristic.value as! Bool + + default: + logger.warning( "Unhandled characteristic change for accessory \(accessory.info.name.value ?? "")") + } + + case accessory.aircoService: + + switch characteristic.type{ + case CharacteristicType.powerState: + + setAirco = characteristic.value as! Bool + + default: + logger.warning( "Unhandled characteristic change for accessory \(accessory.info.name.value ?? "")") + } + + default: + logger.warning( "Unhandled characteristic change for accessory \(accessory.info.name.value ?? "")") + } + + + + } + + + var hardwareFeedbackChanged:Bool = false + + + func pollCycle() { + + if let percentageRemaining = batteryChecker.percentageRemaining, + let rangeRemaining = batteryChecker.rangeRemaining, + let isConnected = batteryChecker.connectionStatus, + let isCharging = batteryChecker.chargingStatus + { + + // Send an MQTT-payload + let timeStamp = batteryChecker.updateTimeStamp! + self.mqttMessage = LeafMQTTMessage(timeStamp: timeStamp.localDateTimeString(), percentage: percentageRemaining, range: rangeRemaining, isConnected: isConnected, isCharging: isCharging) + + // Chang the state of the accessory + accessory.primaryService.batteryLevel?.value = UInt8(percentageRemaining) + if (percentageRemaining >= lowChargeLimit){ + accessory.primaryService.statusLowBattery.value = .batteryNormal + }else{ + accessory.primaryService.statusLowBattery.value = .batteryLow + } + if isConnected{ + accessory.primaryService.chargingState?.value = isCharging ? .charging : .notCharging + }else{ + accessory.primaryService.chargingState?.value = .notChargeable + } + + } + + + } + } +public struct LeafMQTTMessage:Codable, Equatable{ + + let timeStamp: String + let percentage: Int + let range: Int + let isConnected: Bool + let isCharging: Bool + +} diff --git a/HAPiNest/HAPiNestApp.swift b/HAPiNest/HAPiNestApp.swift index 373b54c5..d380cc40 100644 --- a/HAPiNest/HAPiNestApp.swift +++ b/HAPiNest/HAPiNestApp.swift @@ -30,7 +30,7 @@ struct HAPiNestApp: App { let homekitServer:HomeKitServer = HomeKitServer.shared - let mqttCLient = MQTTClient() + let mqttCLient = MQTTClient.shared let plc:SoftPLC = SoftPLC(hardwareConfig:MainConfiguration.PLC.HardwareConfig, ioList: MainConfiguration.PLC.IOList, simulator:ModbusSimulator()) let cyclicPoller:CyclicPoller = CyclicPoller(timeInterval: 1.0) @@ -47,7 +47,6 @@ struct HAPiNestApp: App { accessories: MainConfiguration.Accessories.map{accessory, delegate in return accessory}, configfileName: configFile ) - mqttCLient.connect() plc.plcObjects = MainConfiguration.PLC.PLCobjects #if DEBUG @@ -73,8 +72,12 @@ struct HAPiNestApp: App { ) .padding() .background(Color.Neumorphic.main) - .onAppear(perform: { - }) + .onAppear(perform: + { mqttCLient.connect() } + ) + .onDisappear(perform: + { mqttCLient.disconnect() } + ) } .onChange(of: scenePhase) { let logger = Logger(subsystem: "be.oneclick.HAPiNest", category:.lifeCycle) diff --git a/HAPiNest/MQTTClient.swift b/HAPiNest/MQTTClient.swift new file mode 100644 index 00000000..eeee3ab6 --- /dev/null +++ b/HAPiNest/MQTTClient.swift @@ -0,0 +1,17 @@ +// +// MQTTClient.swift +// HAPiNest +// +// Created by Jan Verrept on 29/01/2024. +// Copyright © 2024 Jan Verrept. All rights reserved. +// + +import Foundation +import JVSwiftCore +import JVNetworking + +extension MQTTClient:Singleton{ + + public static var shared: MQTTClient = MQTTClient(autoSubscribeTo: ["HomeKit/ExternalEvents/ToServer"], autoPublishTo: ["HomeKit/ExternalEvents/FromServer"]) + +} diff --git a/HAPiNest/PreferencesWindow/PreferencesView.swift b/HAPiNest/PreferencesWindow/PreferencesView.swift index 4cd8ef13..3901b04d 100644 --- a/HAPiNest/PreferencesWindow/PreferencesView.swift +++ b/HAPiNest/PreferencesWindow/PreferencesView.swift @@ -7,9 +7,13 @@ // import SwiftUI -import LeafDriver import WeatherKit import JVWeather +import JVNetworking + +// Struct specifier needed because compiler can't keep module LeafDriver apart from the Class with the same name +import struct LeafDriver.LeafSettingsView + struct PreferencesView: View { @@ -19,6 +23,10 @@ struct PreferencesView: View { .tabItem { Label("General", systemImage: "gearshape") } + JVNetworking.MQTTClientSettingsView() + .tabItem { + Label("MQTT", systemImage: "info.bubble") + } TizenSettingsView() .tabItem { Label("Samsung Tizen", systemImage: "tv") diff --git a/MainConfiguration.swift b/MainConfiguration.swift index 3e82963a..6751edf3 100644 --- a/MainConfiguration.swift +++ b/MainConfiguration.swift @@ -154,19 +154,22 @@ struct MainConfiguration{ ]) : TizenAccessoryDelegate(tvName:String(localized:"TV Upstairs", table:"AccessoryNames"), macAddress: "7C:64:56:80:4E:90", ipAddress: "192.168.0.116", port: 8002, deviceName: "HAPiNestServer"), // MARK: - Other -// Accessory.ElectricCar(info: Service.Info(name: String(localized:"Electric Car", table:"AccessoryNames"), serialNumber: "30003", manufacturer: "Nissan")) : LeafAccessoryDelegate(leafProtocol: LeafProtocolV2()) -// - - - // (Accessory.init(info: Service.Info(name:String(localized:"Zonnepanelen", table:"AccessoryNames"), serialNumber: "30001", manufacturer: "SMA"), - // type: .other, - // services: [ - // // TODO: - Insert a Service.EnergyMeter and Service.PowerMeter, - // // once it gets supported by Apples 'Home'-App - // .SwitchBase(characteristics:[.name("Opbrengst opvragen")] ), - // ] ), - // YASDIDriver.InstallDrivers().first! - // ) + Accessory.ElectricCar(info: Service.Info(name: String(localized:"Electric Car", table:"AccessoryNames"), serialNumber: "30003", manufacturer: "Nissan")) : LeafAccessoryDelegate(leafProtocol: LeafProtocolV2()) + + + +// (Accessory.init(info: Service.Info(name:String(localized:"Zonnepanelen", table:"AccessoryNames"), serialNumber: "30001", manufacturer: "SMA"), +// type: .other, +// services: [ +// // TODO: - Insert a Service.EnergyMeter and Service.PowerMeter, +// // once it gets supported by Apples 'Home'-App +// .SwitchBase(characteristics:[.name("Opbrengst opvragen")] ), +// ] ), +// YASDIDriver.InstallDrivers().first! +// ) + + + ] }