2025-03-22 12:59:23 +01:00
//
// R e m o t e . s w i f t
// S t a t s
//
// C r e a t e d b y S e r h i y M y t r o v t s i y o n 1 6 / 0 3 / 2 0 2 5
// U s i n g S w i f t 6 . 0
// R u n n i n g o n m a c O S 1 5 . 3
//
// C o p y r i g h t © 2 0 2 5 S e r h i y M y t r o v t s i y . A l l r i g h t s r e s e r v e d .
//
import Foundation
import Cocoa
2025-08-23 22:13:22 +02:00
import CoreAudio
2025-03-22 12:59:23 +01:00
2025-04-05 22:54:01 +02:00
public protocol RemoteType {
func remote ( ) -> Data ?
}
2025-03-22 12:59:23 +01:00
public class Remote {
public static let shared = Remote ( )
2025-11-11 17:45:56 +01:00
static public var host = URL ( string : " https://api.system-stats.com " ) !
static public var authHost = URL ( string : " https://oauth.system-stats.com " ) !
2025-08-23 22:13:22 +02:00
static public var brokerHost = URL ( string : " wss://broker.system-stats.com:8084/mqtt " ) !
2026-04-03 21:35:28 +02:00
2025-04-03 18:53:31 +02:00
public var monitoring : Bool {
get { Store . shared . bool ( key : " remote_monitoring " , defaultValue : false ) }
2025-03-22 12:59:23 +01:00
set {
2025-04-03 18:53:31 +02:00
Store . shared . set ( key : " remote_monitoring " , value : newValue )
2025-03-22 12:59:23 +01:00
if newValue {
self . start ( )
2025-08-23 22:13:22 +02:00
self . registerDevice ( )
2025-04-03 18:53:31 +02:00
} else if ! self . control {
self . stop ( )
}
}
}
public var control : Bool {
get { Store . shared . bool ( key : " remote_control " , defaultValue : false ) }
set {
Store . shared . set ( key : " remote_control " , value : newValue )
if newValue {
self . start ( )
2025-08-23 22:13:22 +02:00
self . registerDevice ( )
2025-04-03 18:53:31 +02:00
} else if ! self . monitoring {
2025-03-22 12:59:23 +01:00
self . stop ( )
}
}
}
public let id : UUID
public var isAuthorized : Bool = false
public var auth : RemoteAuth = RemoteAuth ( )
2026-04-03 21:35:28 +02:00
2025-04-03 18:53:31 +02:00
private let log : NextLog
2025-08-23 22:13:22 +02:00
private var mqtt : MQTTManager = MQTTManager ( )
2025-03-22 12:59:23 +01:00
private var isConnecting = false
2026-04-03 21:35:28 +02:00
private let session : URLSession = {
let config = URLSessionConfiguration . default
config . timeoutIntervalForRequest = 30
return URLSession ( configuration : config )
} ( )
2025-08-23 22:13:22 +02:00
private var lastSleepTime : Date ?
2025-08-31 17:50:30 +02:00
private var lastRegisterTime : Date ?
2026-04-03 21:35:28 +02:00
2025-08-23 22:13:22 +02:00
struct Details : Codable {
let client : Client
let system : System
let hardware : Hardware
}
struct Client : Codable {
let version : String
let control : Bool
}
struct OS : Codable {
let name : String ?
let version : String ?
let build : String ?
}
struct System : Codable {
let platform : String
let vendor : String ?
let model : String ?
let modelID : String ?
let os : OS
let arch : String ?
}
struct Hardware : Codable {
let cpu : cpu_s ?
let gpu : [ gpu_s ] ?
let ram : [ dimm_s ] ?
let disk : [ disk_s ] ?
}
2026-04-03 21:35:28 +02:00
2025-03-22 12:59:23 +01:00
public init ( ) {
2025-04-03 18:53:31 +02:00
self . log = NextLog . shared . copy ( category : " Remote " )
2026-04-03 21:35:28 +02:00
2025-11-30 14:09:10 +01:00
var id = UUID ( uuidString : Store . shared . string ( key : " remote_id " , defaultValue : UUID ( ) . uuidString ) ) ? ? UUID ( )
if Store . shared . exist ( key : " telemetry_id " ) {
id = UUID ( uuidString : Store . shared . string ( key : " telemetry_id " , defaultValue : UUID ( ) . uuidString ) ) ? ? UUID ( )
Store . shared . remove ( " telemetry_id " )
}
if ! Store . shared . exist ( key : " remote_id " ) {
Store . shared . set ( key : " remote_id " , value : id . uuidString )
}
self . id = id
2026-04-03 21:35:28 +02:00
2025-08-23 22:13:22 +02:00
self . mqtt . commandCallback = { [ weak self ] cmd , payload in
self ? . command ( cmd : cmd , payload : payload )
}
self . mqtt . registerCallback = { [ weak self ] in
self ? . registerDevice ( )
}
2025-09-02 17:13:39 +02:00
self . mqtt . unregisterHandler = { [ weak self ] in
guard let self else { return }
info ( " Unregistered from MQTT broker, stopping Remote... " , log : self . log )
self . logout ( )
}
2026-04-03 21:35:28 +02:00
2025-04-03 18:53:31 +02:00
if self . auth . hasCredentials ( ) {
info ( " Found auth credentials for remote monitoring, starting Remote... " , log : self . log )
self . start ( )
2025-03-22 12:59:23 +01:00
}
2026-04-03 21:35:28 +02:00
2025-03-22 12:59:23 +01:00
NotificationCenter . default . addObserver ( self , selector : #selector ( self . successLogin ) , name : . remoteLoginSuccess , object : nil )
}
2026-04-03 21:35:28 +02:00
2025-03-22 12:59:23 +01:00
deinit {
2025-08-23 22:13:22 +02:00
self . mqtt . disconnect ( )
2025-03-22 12:59:23 +01:00
NotificationCenter . default . removeObserver ( self , name : . remoteLoginSuccess , object : nil )
}
2026-04-03 21:35:28 +02:00
2025-04-01 19:13:38 +02:00
public func login ( ) {
self . auth . login { url in
2025-04-03 18:53:31 +02:00
guard let url else {
error ( " Empty url when try to login " , log : self . log )
return
}
debug ( " Open \( url ) to login to Stats Remote " , log : self . log )
2025-04-01 19:13:38 +02:00
NSWorkspace . shared . open ( url )
}
}
2026-04-03 21:35:28 +02:00
2025-03-22 12:59:23 +01:00
public func logout ( ) {
2025-09-02 17:13:39 +02:00
self . mqtt . disconnect ( )
2025-03-22 12:59:23 +01:00
self . auth . logout ( )
self . isAuthorized = false
2025-04-03 18:53:31 +02:00
debug ( " Logout successfully from Stats Remote " , log : self . log )
NotificationCenter . default . post ( name : . remoteState , object : nil , userInfo : [ " auth " : self . isAuthorized ] )
2025-03-22 12:59:23 +01:00
}
2026-04-03 21:35:28 +02:00
2025-04-05 22:54:01 +02:00
public func send ( key : String , value : Any ) {
guard self . monitoring && self . isAuthorized , let v = value as ? RemoteType , let data = v . remote ( ) else { return }
2025-08-23 22:13:22 +02:00
let topic = " stats/ \( self . id . uuidString ) /metrics/ \( key ) "
self . mqtt . publish ( topic : topic , data : data )
2025-03-22 12:59:23 +01:00
}
2026-04-03 21:35:28 +02:00
2025-03-22 12:59:23 +01:00
@objc private func successLogin ( ) {
self . isAuthorized = true
2025-04-03 18:53:31 +02:00
NotificationCenter . default . post ( name : . remoteState , object : nil , userInfo : [ " auth " : self . isAuthorized ] )
2025-08-23 22:13:22 +02:00
self . mqtt . connect ( )
2025-04-03 18:53:31 +02:00
debug ( " Login successfully on Stats Remote " , log : self . log )
2025-03-22 12:59:23 +01:00
}
2026-04-03 21:35:28 +02:00
2025-03-22 12:59:23 +01:00
public func start ( ) {
self . auth . isAuthorized { [ weak self ] status in
guard let self else { return }
2026-04-03 21:35:28 +02:00
2025-03-22 12:59:23 +01:00
self . isAuthorized = status
2025-04-03 18:53:31 +02:00
NotificationCenter . default . post ( name : . remoteState , object : nil , userInfo : [ " auth " : self . isAuthorized ] )
2026-04-03 21:35:28 +02:00
2025-03-22 12:59:23 +01:00
if status {
2025-08-23 22:13:22 +02:00
self . mqtt . connect ( )
2025-03-22 12:59:23 +01:00
}
}
}
2026-04-03 21:35:28 +02:00
2025-03-22 12:59:23 +01:00
private func stop ( ) {
2025-08-23 22:13:22 +02:00
self . mqtt . disconnect ( )
2025-04-03 18:53:31 +02:00
NotificationCenter . default . post ( name : . remoteState , object : nil , userInfo : [ " auth " : self . isAuthorized ] )
2025-03-22 12:59:23 +01:00
}
2026-04-03 21:35:28 +02:00
2025-08-23 22:13:22 +02:00
public func terminate ( ) {
self . mqtt . disconnect ( )
}
2026-04-03 21:35:28 +02:00
2025-08-23 22:13:22 +02:00
private func registerDevice ( ) {
2025-08-31 17:50:30 +02:00
let oneHour : TimeInterval = 3600
let now = Date ( )
if let lastTime = self . lastRegisterTime , now . timeIntervalSince ( lastTime ) < oneHour {
debug ( " Device registration skipped: cooldown period not met " , log : self . log )
return
}
self . lastRegisterTime = now
2026-04-03 21:35:28 +02:00
2025-08-23 22:13:22 +02:00
guard let url = URL ( string : " \( Remote . host ) /remote/device " ) else { return }
2026-04-03 21:35:28 +02:00
2025-08-23 22:13:22 +02:00
var request = URLRequest ( url : url )
request . httpMethod = " POST "
request . setValue ( " application/json " , forHTTPHeaderField : " Content-Type " )
request . setValue ( " Bearer \( Remote . shared . auth . accessToken ) " , forHTTPHeaderField : " Authorization " )
2026-04-03 21:35:28 +02:00
2025-08-23 22:13:22 +02:00
struct RegisterPayload : Codable {
let id : String
let details : Remote . Details
}
2026-04-03 21:35:28 +02:00
2025-08-23 22:13:22 +02:00
let payload = RegisterPayload (
id : Remote . shared . id . uuidString ,
details : Remote . Details (
client : Client (
version : Bundle . main . infoDictionary ? [ " CFBundleShortVersionString " ] as ? String ? ? " Unknown " ,
control : Remote . shared . control
) ,
system : Remote . System (
platform : " macOS " ,
vendor : " Apple " ,
model : SystemKit . shared . device . model . name ,
modelID : SystemKit . shared . device . model . id ,
os : Remote . OS (
name : SystemKit . shared . device . os ? . name ,
version : SystemKit . shared . device . os ? . version . getFullVersion ( ) ,
build : SystemKit . shared . device . os ? . build
) ,
arch : SystemKit . shared . device . arch
) ,
hardware : Remote . Hardware (
cpu : SystemKit . shared . device . info . cpu ,
gpu : SystemKit . shared . device . info . gpu ,
ram : SystemKit . shared . device . info . ram ? . dimms ,
disk : SystemKit . shared . device . info . disk
)
)
)
2026-04-03 21:35:28 +02:00
2025-08-23 22:13:22 +02:00
guard let body = try ? JSONEncoder ( ) . encode ( payload ) else { return }
request . httpBody = body
2026-04-03 21:35:28 +02:00
self . session . dataTask ( with : request ) { data , response , _ in
2025-08-23 22:13:22 +02:00
guard let httpResponse = response as ? HTTPURLResponse else { return }
if httpResponse . statusCode = = 200 {
debug ( " Registered device: \( Remote . shared . id . uuidString ) " , log : self . log )
} else {
let bodyString = data . flatMap { String ( data : $0 , encoding : . utf8 ) } ? ? " "
debug ( " Register remote failed ( \( httpResponse . statusCode ) ): \( bodyString ) " , log : self . log )
}
} . resume ( )
}
2026-04-03 21:35:28 +02:00
2025-08-23 22:13:22 +02:00
private func command ( cmd : String , payload : Data ? ) {
guard self . control else { return }
2026-04-03 21:35:28 +02:00
2025-08-23 22:13:22 +02:00
debug ( " received command ' \( cmd ) ' with payload: \( String ( data : payload ? ? Data ( ) , encoding : . utf8 ) ? ? " " ) " , log : self . log )
2026-04-03 21:35:28 +02:00
2025-08-23 22:13:22 +02:00
switch cmd {
2026-04-03 21:35:28 +02:00
case " disable " :
self . disableControl ( )
case " sleep " :
self . sleep ( )
2025-08-23 22:13:22 +02:00
case " volume " :
guard let payload else { return }
let value = String ( data : payload , encoding : . utf8 )
let step : Float32 = 0.0625
switch value {
case " up " :
if let current = self . getSystemVolume ( ) {
if self . isSystemMuted ( ) {
self . setSystemMute ( false )
} else {
self . setSystemVolume ( min ( current + step , 1.0 ) )
}
}
case " down " :
if let current = self . getSystemVolume ( ) {
if self . isSystemMuted ( ) {
self . setSystemMute ( false )
} else {
self . setSystemVolume ( max ( current - step , 0.0 ) )
}
}
case " mute " :
self . setSystemMute ( true )
case " unmute " :
self . setSystemMute ( false )
2026-04-03 21:35:28 +02:00
default : return
2025-08-23 22:13:22 +02:00
}
2026-04-03 21:35:28 +02:00
default : return
2025-08-23 22:13:22 +02:00
}
2026-04-03 21:35:28 +02:00
self . mqtt . controlAck ( cmd )
2025-08-23 22:13:22 +02:00
}
}
2026-04-03 21:35:28 +02:00
// MARK: - A u d i o h e l p e r s
2025-08-23 22:13:22 +02:00
extension Remote {
func disableControl ( ) {
self . control = false
}
2026-04-03 21:35:28 +02:00
2025-08-23 22:13:22 +02:00
func sleep ( ) {
let minInterval : TimeInterval = 300
let now = Date ( )
if let last = self . lastSleepTime , now . timeIntervalSince ( last ) < minInterval {
debug ( " Sleep command ignored due to cooldown " , log : self . log )
return
}
self . lastSleepTime = now
let process = Process ( )
process . launchPath = " /usr/bin/pmset "
process . arguments = [ " sleepnow " ]
process . launch ( )
}
2026-04-03 21:35:28 +02:00
private func getDefaultOutputDevice ( ) -> AudioDeviceID ? {
var deviceID = AudioDeviceID ( 0 )
2025-08-23 22:13:22 +02:00
var propertyAddress = AudioObjectPropertyAddress (
mSelector : kAudioHardwarePropertyDefaultOutputDevice ,
mScope : kAudioObjectPropertyScopeGlobal ,
mElement : kAudioObjectPropertyElementMain
)
var size = UInt32 ( MemoryLayout < AudioDeviceID > . size )
let status = AudioObjectGetPropertyData (
AudioObjectID ( kAudioObjectSystemObject ) ,
& propertyAddress ,
0 ,
nil ,
& size ,
2026-04-03 21:35:28 +02:00
& deviceID
2025-08-23 22:13:22 +02:00
)
2026-04-03 21:35:28 +02:00
return status = = noErr ? deviceID : nil
}
func isSystemMuted ( ) -> Bool {
guard let deviceID = self . getDefaultOutputDevice ( ) else { return false }
2025-08-23 22:13:22 +02:00
2026-04-03 21:35:28 +02:00
var propertyAddress = AudioObjectPropertyAddress (
2025-08-23 22:13:22 +02:00
mSelector : kAudioDevicePropertyMute ,
mScope : kAudioDevicePropertyScopeOutput ,
mElement : 0
)
var muteValue : UInt32 = 0
2026-04-03 21:35:28 +02:00
var size = UInt32 ( MemoryLayout < UInt32 > . size )
let status = AudioObjectGetPropertyData ( deviceID , & propertyAddress , 0 , nil , & size , & muteValue )
return status = = noErr && muteValue = = 1
2025-08-23 22:13:22 +02:00
}
2026-04-03 21:35:28 +02:00
2025-08-23 22:13:22 +02:00
func setSystemMute ( _ mute : Bool ) {
2026-04-03 21:35:28 +02:00
guard let deviceID = self . getDefaultOutputDevice ( ) else { return }
2025-08-23 22:13:22 +02:00
2026-04-03 21:35:28 +02:00
var propertyAddress = AudioObjectPropertyAddress (
2025-08-23 22:13:22 +02:00
mSelector : kAudioDevicePropertyMute ,
mScope : kAudioDevicePropertyScopeOutput ,
mElement : 0
)
var muteValue : UInt32 = mute ? 1 : 0
2026-04-03 21:35:28 +02:00
AudioObjectSetPropertyData ( deviceID , & propertyAddress , 0 , nil , UInt32 ( MemoryLayout < UInt32 > . size ) , & muteValue )
2025-08-23 22:13:22 +02:00
}
2026-04-03 21:35:28 +02:00
2025-08-23 22:13:22 +02:00
func getSystemVolume ( ) -> Float32 ? {
2026-04-03 21:35:28 +02:00
guard let deviceID = self . getDefaultOutputDevice ( ) else { return nil }
2025-08-23 22:13:22 +02:00
2026-04-03 21:35:28 +02:00
var propertyAddress = AudioObjectPropertyAddress (
2025-08-23 22:13:22 +02:00
mSelector : kAudioDevicePropertyVolumeScalar ,
mScope : kAudioDevicePropertyScopeOutput ,
mElement : 0
)
var volume : Float32 = 0
2026-04-03 21:35:28 +02:00
var size = UInt32 ( MemoryLayout < Float32 > . size )
let status = AudioObjectGetPropertyData ( deviceID , & propertyAddress , 0 , nil , & size , & volume )
return status = = noErr ? volume : nil
2025-08-23 22:13:22 +02:00
}
func setSystemVolume ( _ volume : Float32 ) {
2026-04-03 21:35:28 +02:00
guard let deviceID = self . getDefaultOutputDevice ( ) else { return }
2025-08-23 22:13:22 +02:00
2026-04-03 21:35:28 +02:00
var propertyAddress = AudioObjectPropertyAddress (
2025-08-23 22:13:22 +02:00
mSelector : kAudioDevicePropertyVolumeScalar ,
mScope : kAudioDevicePropertyScopeOutput ,
mElement : 0
)
var vol = max ( 0.0 , min ( 1.0 , volume ) )
2026-04-03 21:35:28 +02:00
AudioObjectSetPropertyData ( deviceID , & propertyAddress , 0 , nil , UInt32 ( MemoryLayout < Float32 > . size ) , & vol )
2025-08-23 22:13:22 +02:00
}
2025-03-22 12:59:23 +01:00
}
2026-04-03 21:35:28 +02:00
// MARK: - A u t h
2025-03-22 12:59:23 +01:00
public class RemoteAuth {
public var accessToken : String {
get { Store . shared . string ( key : " access_token " , defaultValue : " " ) }
set { Store . shared . set ( key : " access_token " , value : newValue ) }
}
private var refreshToken : String {
get { Store . shared . string ( key : " refresh_token " , defaultValue : " " ) }
set { Store . shared . set ( key : " refresh_token " , value : newValue ) }
}
2025-04-01 19:13:38 +02:00
private var clientID : String = " stats "
2026-04-03 21:35:28 +02:00
2025-04-01 19:13:38 +02:00
private var deviceCode : String = " "
private var userCode : String = " "
private var interval : Int = 5
private var repeater : Repeater ?
2026-04-03 21:35:28 +02:00
2025-08-25 18:44:42 +02:00
private var lastValidationTime : Date ?
private var validationAttempts : Int = 0
2026-04-03 21:35:28 +02:00
private let baseCooldown : TimeInterval = 2.0
private let maxCooldown : TimeInterval = 60.0
private var isRefreshing = false
private var refreshCompletions : [ ( Bool ? ) -> Void ] = [ ]
private let session : URLSession = {
let config = URLSessionConfiguration . default
config . timeoutIntervalForRequest = 30
return URLSession ( configuration : config )
} ( )
2025-03-22 12:59:23 +01:00
public init ( ) {
NotificationCenter . default . addObserver ( self , selector : #selector ( self . successLogin ) , name : . remoteLoginSuccess , object : nil )
}
2026-04-03 21:35:28 +02:00
2025-03-22 12:59:23 +01:00
deinit {
2026-04-03 21:35:28 +02:00
self . session . invalidateAndCancel ( )
2025-03-22 12:59:23 +01:00
NotificationCenter . default . removeObserver ( self , name : . remoteLoginSuccess , object : nil )
}
2026-04-03 21:35:28 +02:00
2025-03-22 12:59:23 +01:00
public func isAuthorized ( completion : @ escaping ( Bool ) -> Void ) {
2025-04-03 18:53:31 +02:00
if ! self . hasCredentials ( ) {
completion ( false )
return
}
2026-04-03 21:35:28 +02:00
2025-08-25 18:44:42 +02:00
if ! self . accessToken . isEmpty && ! self . isTokenExpired ( ) {
DispatchQueue . main . async {
completion ( true )
}
return
}
2026-04-03 21:35:28 +02:00
2025-03-22 12:59:23 +01:00
self . validate ( completion )
}
2025-04-03 18:53:31 +02:00
public func hasCredentials ( ) -> Bool {
return ! self . accessToken . isEmpty && ! self . refreshToken . isEmpty
}
2026-04-03 21:35:28 +02:00
2025-04-01 19:13:38 +02:00
public func login ( completion : @ escaping ( URL ? ) -> Void ) {
self . registerDevice { device in
guard let device else {
completion ( nil )
return
}
completion ( device . verification_uri_complete )
2026-04-03 21:35:28 +02:00
2025-04-01 19:13:38 +02:00
self . deviceCode = device . device_code
self . userCode = device . user_code
self . interval = device . interval ? ? 5
2026-04-03 21:35:28 +02:00
2025-04-01 19:13:38 +02:00
self . repeater = Repeater ( seconds : self . interval ) {
self . pollForToken { error in
guard error = = nil else {
print ( error ? . localizedDescription ? ? " error pooling for token " )
self . repeater ? . pause ( )
self . repeater = nil
return
}
if ! self . accessToken . isEmpty {
self . repeater ? . pause ( )
self . repeater = nil
}
}
}
self . repeater ? . start ( )
}
}
2026-04-03 21:35:28 +02:00
2025-03-22 12:59:23 +01:00
public func logout ( ) {
self . accessToken = " "
self . refreshToken = " "
}
2026-04-03 21:35:28 +02:00
2025-03-22 12:59:23 +01:00
private func validate ( _ completion : @ escaping ( Bool ) -> Void ) {
2025-11-11 17:45:56 +01:00
guard ! self . accessToken . isEmpty && ! self . refreshToken . isEmpty , let url = URL ( string : " \( Remote . authHost ) /me " ) else {
2025-03-22 12:59:23 +01:00
completion ( false )
return
}
2026-04-03 21:35:28 +02:00
2025-08-25 18:44:42 +02:00
let now = Date ( )
let dynamicCooldown = min ( self . baseCooldown * pow ( 2.0 , Double ( self . validationAttempts ) ) , self . maxCooldown )
if let lastTime = self . lastValidationTime , now . timeIntervalSince ( lastTime ) < dynamicCooldown {
let remainingTime = dynamicCooldown - now . timeIntervalSince ( lastTime )
DispatchQueue . main . asyncAfter ( deadline : . now ( ) + remainingTime ) {
self . validate ( completion )
}
return
}
2026-04-03 21:35:28 +02:00
2025-08-25 18:44:42 +02:00
self . lastValidationTime = now
self . validationAttempts += 1
2026-04-03 21:35:28 +02:00
2025-03-22 12:59:23 +01:00
var request = URLRequest ( url : url )
request . httpMethod = " GET "
request . setValue ( " Bearer \( self . accessToken ) " , forHTTPHeaderField : " Authorization " )
2026-04-03 21:35:28 +02:00
self . session . dataTask ( with : request ) { [ weak self ] _ , response , error in
2025-03-22 12:59:23 +01:00
guard let self = self , error = = nil , let httpResponse = response as ? HTTPURLResponse else {
completion ( false )
return
}
2026-04-03 21:35:28 +02:00
2025-03-22 12:59:23 +01:00
if httpResponse . statusCode = = 401 {
self . refreshTokenFunc { ok in
2025-08-25 18:44:42 +02:00
if ok = = true {
self . validationAttempts = 0
self . lastValidationTime = nil
}
2025-03-22 12:59:23 +01:00
completion ( ok ? ? false )
}
} else if httpResponse . statusCode = = 200 {
2025-08-25 18:44:42 +02:00
self . validationAttempts = 0
self . lastValidationTime = nil
2025-03-22 12:59:23 +01:00
completion ( true )
2025-08-25 18:44:42 +02:00
} else {
completion ( false )
2025-03-22 12:59:23 +01:00
}
} . resume ( )
}
2026-04-03 21:35:28 +02:00
2025-03-22 12:59:23 +01:00
private func refreshTokenFunc ( completion : @ escaping ( Bool ? ) -> Void ) {
2026-04-03 21:35:28 +02:00
self . refreshCompletions . append ( completion )
guard ! self . isRefreshing else { return }
self . isRefreshing = true
2025-11-11 17:45:56 +01:00
guard let url = URL ( string : " \( Remote . authHost ) /token " ) else {
2026-04-03 21:35:28 +02:00
self . completeRefresh ( nil )
2025-03-22 12:59:23 +01:00
return
}
2026-04-03 21:35:28 +02:00
2025-03-22 12:59:23 +01:00
var request = URLRequest ( url : url )
request . httpMethod = " POST "
request . setValue ( " application/x-www-form-urlencoded " , forHTTPHeaderField : " Content-Type " )
2026-04-03 21:35:28 +02:00
2025-03-22 12:59:23 +01:00
let body = " grant_type=refresh_token&refresh_token= \( self . refreshToken ) "
. addingPercentEncoding ( withAllowedCharacters : . urlQueryAllowed )
request . httpBody = body ? . data ( using : . utf8 )
2026-04-03 21:35:28 +02:00
self . session . dataTask ( with : request ) { [ weak self ] data , response , error in
guard let self else { return }
2025-03-22 12:59:23 +01:00
guard error = = nil , let httpResponse = response as ? HTTPURLResponse , httpResponse . statusCode = = 200 ,
let data = data , let token = try ? JSONDecoder ( ) . decode ( TokenResponse . self , from : data ) else {
2026-04-03 21:35:28 +02:00
self . completeRefresh ( nil )
2025-03-22 12:59:23 +01:00
return
}
self . accessToken = token . access_token
self . refreshToken = token . refresh_token
2026-04-03 21:35:28 +02:00
self . completeRefresh ( true )
2025-03-22 12:59:23 +01:00
} . resume ( )
}
2026-04-03 21:35:28 +02:00
private func completeRefresh ( _ result : Bool ? ) {
let completions = self . refreshCompletions
self . refreshCompletions . removeAll ( )
self . isRefreshing = false
for completion in completions {
completion ( result )
}
}
2025-04-01 19:13:38 +02:00
private func registerDevice ( completion : @ escaping ( DeviceResponse ? ) -> Void ) {
2025-11-11 17:45:56 +01:00
guard let url = URL ( string : " \( Remote . authHost ) /device " ) else {
2025-04-01 19:13:38 +02:00
completion ( nil )
return
}
2026-04-03 21:35:28 +02:00
2025-04-01 19:13:38 +02:00
var request = URLRequest ( url : url )
request . httpMethod = " POST "
request . setValue ( " application/x-www-form-urlencoded " , forHTTPHeaderField : " Content-Type " )
2026-04-03 21:35:28 +02:00
2025-04-01 19:13:38 +02:00
let body = " client_id= \( self . clientID ) "
. addingPercentEncoding ( withAllowedCharacters : . urlQueryAllowed )
request . httpBody = body ? . data ( using : . utf8 )
2026-04-03 21:35:28 +02:00
self . session . dataTask ( with : request ) { data , response , error in
2025-04-01 19:13:38 +02:00
guard error = = nil , let httpResponse = response as ? HTTPURLResponse , httpResponse . statusCode = = 200 ,
let data = data , let resp = try ? JSONDecoder ( ) . decode ( DeviceResponse . self , from : data ) else {
completion ( nil )
return
}
completion ( resp )
} . resume ( )
}
2026-04-03 21:35:28 +02:00
2025-04-01 19:13:38 +02:00
private func pollForToken ( completion : @ escaping ( Error ? ) -> Void ) {
2025-11-11 17:45:56 +01:00
guard let url = URL ( string : " \( Remote . authHost ) /token " ) else {
2025-04-01 19:13:38 +02:00
completion ( nil )
return
}
2026-04-03 21:35:28 +02:00
2025-04-01 19:13:38 +02:00
var request = URLRequest ( url : url )
request . httpMethod = " POST "
request . setValue ( " application/x-www-form-urlencoded " , forHTTPHeaderField : " Content-Type " )
2026-04-03 21:35:28 +02:00
2025-04-01 19:13:38 +02:00
let body = " client_id= \( self . clientID ) &device_code= \( self . deviceCode ) &grant_type=urn:ietf:params:oauth:grant-type:device_code "
. addingPercentEncoding ( withAllowedCharacters : . urlQueryAllowed )
request . httpBody = body ? . data ( using : . utf8 )
2026-04-03 21:35:28 +02:00
self . session . dataTask ( with : request ) { data , response , error in
2025-04-01 19:13:38 +02:00
if let error = error {
completion ( error )
return
}
2026-04-03 21:35:28 +02:00
2025-04-01 19:13:38 +02:00
guard let httpResponse = response as ? HTTPURLResponse else {
completion ( NSError ( domain : " " , code : - 1 , userInfo : [ NSLocalizedDescriptionKey : " Invalid response " ] ) )
return
}
2026-04-03 21:35:28 +02:00
2025-04-01 19:13:38 +02:00
if httpResponse . statusCode = = 200 {
guard let data = data else {
completion ( NSError ( domain : " " , code : - 1 , userInfo : [ NSLocalizedDescriptionKey : " No data returned " ] ) )
return
}
2026-04-03 21:35:28 +02:00
2025-04-01 19:13:38 +02:00
do {
let result = try JSONDecoder ( ) . decode ( TokenResponse . self , from : data )
NotificationCenter . default . post ( name : . remoteLoginSuccess , object : nil , userInfo : [
" access_token " : result . access_token ,
" refresh_token " : result . refresh_token
] )
completion ( nil )
} catch {
completion ( error )
}
} else if httpResponse . statusCode = = 400 {
guard let data = data , let responseString = String ( data : data , encoding : . utf8 ) else {
completion ( NSError ( domain : " " , code : httpResponse . statusCode , userInfo : [ NSLocalizedDescriptionKey : " Bad request " ] ) )
return
}
2026-04-03 21:35:28 +02:00
2025-04-01 19:13:38 +02:00
if responseString . contains ( " authorization_pending " ) {
completion ( nil )
} else if responseString . contains ( " expired_token " ) {
completion ( NSError ( domain : " " , code : httpResponse . statusCode , userInfo : [ NSLocalizedDescriptionKey : " Device code expired, please re-register " ] ) )
} else if responseString . contains ( " slow_down " ) {
DispatchQueue . global ( ) . asyncAfter ( deadline : . now ( ) + Double ( self . interval ) ) {
completion ( nil )
}
} else {
completion ( NSError ( domain : " " , code : httpResponse . statusCode , userInfo : [ NSLocalizedDescriptionKey : responseString ] ) )
}
} else {
let errorMessage = data . flatMap { String ( data : $0 , encoding : . utf8 ) } ? ? " Unknown error "
completion ( NSError ( domain : " " , code : httpResponse . statusCode , userInfo : [ NSLocalizedDescriptionKey : " Failed to get token ( \( httpResponse . statusCode ) ): \( errorMessage ) " ] ) )
}
} . resume ( )
}
2026-04-03 21:35:28 +02:00
2025-03-22 12:59:23 +01:00
@objc private func successLogin ( _ notification : Notification ) {
guard let userInfo = notification . userInfo ,
let accessToken = userInfo [ " access_token " ] as ? String ,
let refreshToken = userInfo [ " refresh_token " ] as ? String else { return }
2026-04-03 21:35:28 +02:00
2025-03-22 12:59:23 +01:00
self . accessToken = accessToken
self . refreshToken = refreshToken
}
2026-04-03 21:35:28 +02:00
2025-08-25 18:44:42 +02:00
private func isTokenExpired ( ) -> Bool {
let parts = self . accessToken . components ( separatedBy : " . " )
guard parts . count = = 3 else { return true }
2026-04-03 21:35:28 +02:00
2025-08-25 18:44:42 +02:00
let payload = parts [ 1 ]
var base64 = payload
. replacingOccurrences ( of : " - " , with : " + " )
. replacingOccurrences ( of : " _ " , with : " / " )
2026-04-03 21:35:28 +02:00
2025-08-25 18:44:42 +02:00
while base64 . count % 4 != 0 {
base64 += " = "
}
2026-04-03 21:35:28 +02:00
2025-08-25 18:44:42 +02:00
guard let data = Data ( base64Encoded : base64 ) ,
let json = try ? JSONSerialization . jsonObject ( with : data ) as ? [ String : Any ] ,
let exp = json [ " exp " ] as ? TimeInterval else {
return true
}
2026-04-03 21:35:28 +02:00
2025-08-25 18:44:42 +02:00
return Date ( ) . timeIntervalSince1970 >= exp
}
2025-03-22 12:59:23 +01:00
}
2026-04-03 21:35:28 +02:00
// MARK: - M Q T T
2025-08-23 22:13:22 +02:00
struct MQTTMessage {
let topic : String
let payload : Data
let qos : UInt8
let retain : Bool
}
enum MQTTPacketType : UInt8 {
case connect = 1
case connack = 2
case publish = 3
case puback = 4
case subscribe = 8
case suback = 9
case pingreq = 12
case pingresp = 13
case disconnect = 14
2025-03-22 12:59:23 +01:00
}
2025-08-23 22:13:22 +02:00
class MQTTManager : NSObject {
public var registerCallback : ( ( ) -> Void ) ? = nil
public var commandCallback : ( ( String , Data ? ) -> Void ) ? = nil
2025-09-02 17:13:39 +02:00
public var unregisterHandler : ( ( ) -> Void ) ? = nil
2026-04-03 21:35:28 +02:00
2025-03-22 12:59:23 +01:00
private var webSocket : URLSessionWebSocketTask ?
private var session : URLSession ?
private var isConnected = false
2026-04-03 21:35:28 +02:00
private var isConnecting = false
2025-03-22 12:59:23 +01:00
private var isDisconnected = false
2025-08-25 18:44:42 +02:00
private var isReconnecting = false
private var reconnectAttempts = 0
private var maxReconnectDelay : TimeInterval = 60.0
2026-04-03 21:35:28 +02:00
private var pingTimer : DispatchSourceTimer ?
2025-03-22 12:59:23 +01:00
private var reachability : Reachability = Reachability ( start : true )
2025-04-03 18:53:31 +02:00
private let log : NextLog
2025-08-23 22:13:22 +02:00
private var packetIdentifier : UInt16 = 1
2026-04-03 21:35:28 +02:00
2025-03-22 12:59:23 +01:00
override init ( ) {
2025-08-23 22:13:22 +02:00
self . log = NextLog . shared . copy ( category : " Remote MQTT " )
2026-04-03 21:35:28 +02:00
2025-03-22 12:59:23 +01:00
super . init ( )
2026-04-03 21:35:28 +02:00
2025-03-22 12:59:23 +01:00
self . session = URLSession ( configuration : . default , delegate : self , delegateQueue : . main )
2026-04-03 21:35:28 +02:00
self . reachability . reachable = { [ weak self ] in
2025-04-03 18:53:31 +02:00
if Remote . shared . isAuthorized {
2026-04-03 21:35:28 +02:00
self ? . connect ( )
2025-03-22 12:59:23 +01:00
}
}
2026-04-03 21:35:28 +02:00
self . reachability . unreachable = { [ weak self ] in
guard let self else { return }
2025-03-22 12:59:23 +01:00
if self . isConnected {
self . disconnect ( )
}
}
}
2026-04-03 21:35:28 +02:00
2025-03-22 12:59:23 +01:00
public func connect ( ) {
2026-04-03 21:35:28 +02:00
guard ! self . isConnected && ! self . isConnecting else { return }
self . isConnecting = true
2025-03-22 12:59:23 +01:00
Remote . shared . auth . isAuthorized { [ weak self ] status in
2025-08-31 17:50:30 +02:00
guard let self else { return }
2026-04-03 21:35:28 +02:00
2025-08-31 17:50:30 +02:00
if status {
2026-04-03 21:35:28 +02:00
self . webSocket ? . cancel ( with : . normalClosure , reason : nil )
2025-08-31 17:50:30 +02:00
self . webSocket = self . session ? . webSocketTask ( with : Remote . brokerHost , protocols : [ " mqtt " ] )
self . webSocket ? . resume ( )
self . receiveMessage ( )
self . isDisconnected = false
debug ( " MQTT WebSocket connecting... " , log : self . log )
} else {
2026-04-03 21:35:28 +02:00
self . isConnecting = false
2025-08-31 17:50:30 +02:00
debug ( " Authorization failed, retrying connection... " , log : self . log )
self . reconnect ( )
}
2025-03-22 12:59:23 +01:00
}
}
2026-04-03 21:35:28 +02:00
2025-03-22 12:59:23 +01:00
public func disconnect ( ) {
2025-04-03 18:53:31 +02:00
if self . webSocket = = nil && ! self . isConnected { return }
2025-03-22 12:59:23 +01:00
self . isDisconnected = true
2026-04-03 21:35:28 +02:00
self . isConnecting = false
2025-09-02 17:13:39 +02:00
self . sendStatus ( false )
self . sendDisconnect ( )
2026-04-03 21:35:28 +02:00
2025-03-22 12:59:23 +01:00
self . webSocket ? . cancel ( with : . normalClosure , reason : nil )
self . webSocket = nil
self . isConnected = false
2025-08-23 22:13:22 +02:00
self . stopPingTimer ( )
debug ( " MQTT disconnected gracefully " , log : self . log )
2025-03-22 12:59:23 +01:00
}
2026-04-03 21:35:28 +02:00
2025-03-22 12:59:23 +01:00
private func reconnect ( ) {
2025-08-25 18:44:42 +02:00
guard ! self . isDisconnected && ! self . isReconnecting else { return }
2026-04-03 21:35:28 +02:00
2025-08-25 18:44:42 +02:00
self . isReconnecting = true
2026-04-03 21:35:28 +02:00
2025-08-25 18:44:42 +02:00
let delays : [ TimeInterval ] = [ 1 , 3 , 5 , 10 , 20 , 40 ]
let delayIndex = min ( self . reconnectAttempts , delays . count - 1 )
let delay = self . reconnectAttempts >= delays . count ? self . maxReconnectDelay : delays [ delayIndex ]
2026-04-03 21:35:28 +02:00
2025-08-25 18:44:42 +02:00
debug ( " Waiting \( delay ) seconds before next MQTT reconnection attempt... " , log : self . log )
2026-04-03 21:35:28 +02:00
2025-08-25 18:44:42 +02:00
DispatchQueue . main . asyncAfter ( deadline : . now ( ) + delay ) { [ weak self ] in
guard let self else { return }
2026-04-03 21:35:28 +02:00
2025-08-25 18:44:42 +02:00
self . isReconnecting = false
2026-04-03 21:35:28 +02:00
2025-08-25 18:44:42 +02:00
guard ! self . isDisconnected && ! self . isConnected else {
self . reconnectAttempts = 0
return
2025-04-03 18:53:31 +02:00
}
2026-04-03 21:35:28 +02:00
2025-08-25 18:44:42 +02:00
self . reconnectAttempts += 1
debug ( " Attempting MQTT reconnection # \( self . reconnectAttempts ) " , log : self . log )
self . connect ( )
2025-03-22 12:59:23 +01:00
}
}
2026-04-03 21:35:28 +02:00
2025-08-23 22:13:22 +02:00
public func sendStatus ( _ value : Bool ) {
let status = value ? " online " : " offline "
let topic = " stats/ \( Remote . shared . id . uuidString ) /status "
2026-04-03 21:35:28 +02:00
if let payload = status . data ( using : . utf8 ) {
2025-08-23 22:13:22 +02:00
self . publish ( topic : topic , data : payload )
2025-03-22 12:59:23 +01:00
}
2025-08-23 22:13:22 +02:00
}
2026-04-03 21:35:28 +02:00
2025-08-23 22:13:22 +02:00
private func sendConnect ( ) {
let connectPacket = createConnectPacket ( username : Remote . shared . id . uuidString , password : Remote . shared . auth . accessToken )
self . webSocket ? . send ( . data ( connectPacket ) ) { error in
if let error = error {
print ( " Error sending MQTT CONNECT: \( error ) " )
}
2025-03-22 12:59:23 +01:00
}
2025-08-23 22:13:22 +02:00
}
2026-04-03 21:35:28 +02:00
2025-08-23 22:13:22 +02:00
private func sendDisconnect ( ) {
let disconnectPacket = Data ( [ MQTTPacketType . disconnect . rawValue << 4 , 0 ] )
self . webSocket ? . send ( . data ( disconnectPacket ) ) { _ in }
}
2026-04-03 21:35:28 +02:00
2025-08-23 22:13:22 +02:00
private func sendPingRequest ( ) {
let pingPacket = Data ( [ MQTTPacketType . pingreq . rawValue << 4 , 0 ] )
self . webSocket ? . send ( . data ( pingPacket ) ) { error in
if let error = error {
print ( " Error sending MQTT PINGREQ: \( error ) " )
}
}
}
2026-04-03 21:35:28 +02:00
public func controlAck ( _ cmd : String ) {
let topic = " stats/ \( Remote . shared . id . uuidString ) /control-ack "
if let payload = cmd . data ( using : . utf8 ) {
self . publish ( topic : topic , data : payload )
}
}
2025-08-23 22:13:22 +02:00
public func publish ( topic : String , data : Data ) {
guard self . isConnected else { return }
2026-04-03 21:35:28 +02:00
2025-08-23 22:13:22 +02:00
let publishPacket = createPublishPacket ( topic : topic , payload : data )
self . webSocket ? . send ( . data ( publishPacket ) ) { error in
if let error = error {
print ( " Error publishing MQTT message: \( error ) " )
}
2025-03-22 12:59:23 +01:00
}
2025-08-23 22:13:22 +02:00
}
2026-04-03 21:35:28 +02:00
2025-08-23 22:13:22 +02:00
private func subscribe ( to topic : String ) {
guard self . isConnected else { return }
2026-04-03 21:35:28 +02:00
2025-08-23 22:13:22 +02:00
let subscribePacket = createSubscribePacket ( topic : topic )
self . webSocket ? . send ( . data ( subscribePacket ) ) { error in
if let error = error {
print ( " Error subscribing to MQTT topic: \( error ) " )
}
2025-03-22 12:59:23 +01:00
}
2025-08-23 22:13:22 +02:00
}
2026-04-03 21:35:28 +02:00
2025-08-23 22:13:22 +02:00
private func createConnectPacket ( username : String , password : String ) -> Data {
var packet = Data ( )
2026-04-03 21:35:28 +02:00
2025-08-23 22:13:22 +02:00
let fixedHeaderByte = MQTTPacketType . connect . rawValue << 4
2026-04-03 21:35:28 +02:00
2025-08-23 22:13:22 +02:00
var variableHeader = Data ( )
variableHeader . append ( contentsOf : encodeString ( " MQTT " ) )
variableHeader . append ( 4 )
2026-04-03 21:35:28 +02:00
var connectFlags : UInt8 = 0x00
connectFlags |= 0x80
connectFlags |= 0x40
2025-08-23 22:13:22 +02:00
variableHeader . append ( connectFlags )
variableHeader . append ( contentsOf : [ 0x03 , 0x84 ] )
2026-04-03 21:35:28 +02:00
2025-08-23 22:13:22 +02:00
var payload = Data ( )
payload . append ( contentsOf : encodeString ( " stats- \( username ) " ) )
payload . append ( contentsOf : encodeString ( username ) )
payload . append ( contentsOf : encodeString ( password ) )
2026-04-03 21:35:28 +02:00
2025-08-23 22:13:22 +02:00
let remainingLength = variableHeader . count + payload . count
packet . append ( fixedHeaderByte )
packet . append ( contentsOf : encodeRemainingLength ( remainingLength ) )
packet . append ( variableHeader )
packet . append ( payload )
2026-04-03 21:35:28 +02:00
2025-08-23 22:13:22 +02:00
return packet
2025-03-22 12:59:23 +01:00
}
2026-04-03 21:35:28 +02:00
2025-08-23 22:13:22 +02:00
private func createPublishPacket ( topic : String , payload : Data ) -> Data {
var packet = Data ( )
2026-04-03 21:35:28 +02:00
let fixedHeaderByte = ( MQTTPacketType . publish . rawValue << 4 ) | 0x00
2025-08-23 22:13:22 +02:00
var variableHeader = Data ( )
variableHeader . append ( contentsOf : encodeString ( topic ) )
2026-04-03 21:35:28 +02:00
2025-08-23 22:13:22 +02:00
let remainingLength = variableHeader . count + payload . count
2026-04-03 21:35:28 +02:00
2025-08-23 22:13:22 +02:00
packet . append ( fixedHeaderByte )
packet . append ( contentsOf : encodeRemainingLength ( remainingLength ) )
packet . append ( variableHeader )
packet . append ( payload )
2026-04-03 21:35:28 +02:00
2025-08-23 22:13:22 +02:00
return packet
}
2026-04-03 21:35:28 +02:00
2025-08-23 22:13:22 +02:00
private func createSubscribePacket ( topic : String ) -> Data {
var packet = Data ( )
2026-04-03 21:35:28 +02:00
2025-08-23 22:13:22 +02:00
let fixedHeaderByte = ( MQTTPacketType . subscribe . rawValue << 4 ) | 0x02
2026-04-03 21:35:28 +02:00
2025-08-23 22:13:22 +02:00
var variableHeader = Data ( )
2026-04-03 21:35:28 +02:00
2025-08-23 22:13:22 +02:00
let packetId = self . getNextPacketId ( )
variableHeader . append ( contentsOf : [ UInt8 ( packetId >> 8 ) , UInt8 ( packetId & 0xFF ) ] )
2026-04-03 21:35:28 +02:00
2025-08-23 22:13:22 +02:00
var payload = Data ( )
payload . append ( contentsOf : encodeString ( topic ) )
2026-04-03 21:35:28 +02:00
payload . append ( 0x00 )
2025-08-23 22:13:22 +02:00
let remainingLength = variableHeader . count + payload . count
2026-04-03 21:35:28 +02:00
2025-08-23 22:13:22 +02:00
packet . append ( fixedHeaderByte )
packet . append ( contentsOf : encodeRemainingLength ( remainingLength ) )
packet . append ( variableHeader )
packet . append ( payload )
2026-04-03 21:35:28 +02:00
2025-08-23 22:13:22 +02:00
return packet
}
2026-04-03 21:35:28 +02:00
2025-08-23 22:13:22 +02:00
private func encodeString ( _ string : String ) -> [ UInt8 ] {
let data = string . data ( using : . utf8 ) ? ? Data ( )
let length = data . count
return [ UInt8 ( length >> 8 ) , UInt8 ( length & 0xFF ) ] + Array ( data )
}
2026-04-03 21:35:28 +02:00
2025-08-23 22:13:22 +02:00
private func encodeRemainingLength ( _ length : Int ) -> [ UInt8 ] {
var bytes : [ UInt8 ] = [ ]
var remainingLength = length
2026-04-03 21:35:28 +02:00
2025-08-23 22:13:22 +02:00
repeat {
var byte = UInt8 ( remainingLength % 128 )
remainingLength /= 128
if remainingLength > 0 {
byte |= 128
2025-03-22 12:59:23 +01:00
}
2025-08-23 22:13:22 +02:00
bytes . append ( byte )
} while remainingLength > 0
2026-04-03 21:35:28 +02:00
2025-08-23 22:13:22 +02:00
return bytes
}
2026-04-03 21:35:28 +02:00
2025-08-23 22:13:22 +02:00
private func getNextPacketId ( ) -> UInt16 {
self . packetIdentifier += 1
if self . packetIdentifier = = 0 {
self . packetIdentifier = 1
}
return self . packetIdentifier
}
2026-04-03 21:35:28 +02:00
2025-08-23 22:13:22 +02:00
private func handleMQTTPacket ( _ data : Data ) {
guard data . count >= 2 else { return }
2026-04-03 21:35:28 +02:00
2025-08-23 22:13:22 +02:00
let packetType = MQTTPacketType ( rawValue : ( data [ 0 ] >> 4 ) & 0x0F )
2026-04-03 21:35:28 +02:00
2025-08-23 22:13:22 +02:00
switch packetType {
case . connack :
self . handleConnAck ( data )
case . pingresp :
break
case . suback :
break
case . publish :
2025-09-02 17:13:39 +02:00
self . handlePublish ( data )
2025-08-23 22:13:22 +02:00
default :
break
}
}
2026-04-03 21:35:28 +02:00
2025-08-23 22:13:22 +02:00
private func handleConnAck ( _ data : Data ) {
guard data . count >= 4 else { return }
2026-04-03 21:35:28 +02:00
self . isConnecting = false
2025-08-23 22:13:22 +02:00
let returnCode = data [ 3 ]
if returnCode = = 0 {
self . isConnected = true
2025-08-25 18:44:42 +02:00
self . isReconnecting = false
self . reconnectAttempts = 0
2025-08-23 22:13:22 +02:00
self . startPingTimer ( )
2025-09-02 17:13:39 +02:00
self . subscribeToTopics ( )
2025-08-23 22:13:22 +02:00
self . sendStatus ( true )
debug ( " MQTT connected successfully " , log : self . log )
self . registerCallback ? ( )
} else {
debug ( " MQTT connection failed with code: \( returnCode ) " , log : self . log )
2025-03-22 12:59:23 +01:00
}
}
2026-04-03 21:35:28 +02:00
2025-09-02 17:13:39 +02:00
private func subscribeToTopics ( ) {
self . subscribe ( to : " stats/ \( Remote . shared . id . uuidString ) /control/+ " )
self . subscribe ( to : " stats/ \( Remote . shared . id . uuidString ) /unregister " )
2025-08-23 22:13:22 +02:00
}
2026-04-03 21:35:28 +02:00
2025-03-22 12:59:23 +01:00
private func receiveMessage ( ) {
self . webSocket ? . receive { [ weak self ] result in
switch result {
case . failure ( let error ) :
self ? . isConnected = false
2026-04-03 21:35:28 +02:00
self ? . isConnecting = false
2025-03-22 12:59:23 +01:00
self ? . handleWebSocketError ( error )
2025-08-23 22:13:22 +02:00
case . success ( let message ) :
switch message {
case . data ( let data ) :
self ? . handleMQTTPacket ( data )
case . string :
break
@ unknown default :
break
}
2025-03-22 12:59:23 +01:00
self ? . receiveMessage ( )
}
}
}
2026-04-03 21:35:28 +02:00
2025-03-22 12:59:23 +01:00
private func startPingTimer ( ) {
self . stopPingTimer ( )
2026-04-03 21:35:28 +02:00
let timer = DispatchSource . makeTimerSource ( queue : . global ( qos : . utility ) )
timer . schedule ( deadline : . now ( ) + 450 , repeating : 450 )
timer . setEventHandler { [ weak self ] in
2025-08-23 22:13:22 +02:00
self ? . sendPingRequest ( )
2025-03-22 12:59:23 +01:00
}
2026-04-03 21:35:28 +02:00
timer . resume ( )
self . pingTimer = timer
2025-03-22 12:59:23 +01:00
}
2026-04-03 21:35:28 +02:00
2025-03-22 12:59:23 +01:00
private func stopPingTimer ( ) {
2026-04-03 21:35:28 +02:00
self . pingTimer ? . cancel ( )
2025-03-22 12:59:23 +01:00
self . pingTimer = nil
}
2026-04-03 21:35:28 +02:00
2025-03-22 12:59:23 +01:00
private func handleWebSocketError ( _ error : Error ) {
if let urlError = error as ? URLError , urlError . code . rawValue = = 401 {
Remote . shared . start ( )
} else {
self . reconnect ( )
}
}
2026-04-03 21:35:28 +02:00
2025-09-02 17:13:39 +02:00
private func handlePublish ( _ data : Data ) {
2025-08-23 22:13:22 +02:00
var offset = 1
2026-04-03 21:35:28 +02:00
while offset < data . count && data [ offset ] & 0x80 != 0 { offset += 1 }
guard offset < data . count else { return }
2025-08-23 22:13:22 +02:00
offset += 1
2026-04-03 21:35:28 +02:00
2025-08-23 22:13:22 +02:00
guard data . count > offset + 1 else { return }
let topicLength = Int ( data [ offset ] ) << 8 | Int ( data [ offset + 1 ] )
offset += 2
2026-04-03 21:35:28 +02:00
2025-08-23 22:13:22 +02:00
guard data . count >= offset + topicLength else { return }
let topicData = data . subdata ( in : offset . . < ( offset + topicLength ) )
let topic = String ( data : topicData , encoding : . utf8 ) ? ? " <invalid topic> "
offset += topicLength
2026-04-03 21:35:28 +02:00
2025-09-02 17:13:39 +02:00
if topic . hasSuffix ( " unregister " ) {
self . unregisterHandler ? ( )
return
}
2026-04-03 21:35:28 +02:00
2025-08-23 22:13:22 +02:00
let prefix = " stats/ \( Remote . shared . id . uuidString ) /control/ "
let commandName = topic . hasPrefix ( prefix ) ? String ( topic . dropFirst ( prefix . count ) ) : topic
let payload = data . subdata ( in : offset . . < data . count )
self . commandCallback ? ( commandName , payload )
}
2025-03-22 12:59:23 +01:00
}
2025-08-23 22:13:22 +02:00
extension MQTTManager : URLSessionWebSocketDelegate {
2025-03-22 12:59:23 +01:00
func urlSession ( _ session : URLSession , webSocketTask : URLSessionWebSocketTask , didOpenWithProtocol protocol : String ? ) {
2025-08-23 22:13:22 +02:00
debug ( " MQTT WebSocket opened, sending CONNECT " , log : self . log )
self . sendConnect ( )
2025-03-22 12:59:23 +01:00
}
2026-04-03 21:35:28 +02:00
2025-03-22 12:59:23 +01:00
func urlSession ( _ session : URLSession , webSocketTask : URLSessionWebSocketTask , didCloseWith closeCode : URLSessionWebSocketTask . CloseCode , reason : Data ? ) {
2025-08-23 22:13:22 +02:00
self . stopPingTimer ( )
self . sendStatus ( false )
2026-04-03 21:35:28 +02:00
self . isConnected = false
self . isConnecting = false
2025-08-23 22:13:22 +02:00
debug ( " MQTT WebSocket closed " , log : self . log )
2025-03-22 12:59:23 +01:00
self . reconnect ( )
}
2026-04-03 21:35:28 +02:00
2025-08-23 22:13:22 +02:00
func urlSession ( _ session : URLSession , task : URLSessionTask , didCompleteWithError error : Error ? ) {
if let error = error {
if let response = task . response as ? HTTPURLResponse {
let statusCode = response . statusCode
let headers = response . allHeaderFields
debug ( " MQTT WebSocket failed: \( error . localizedDescription ) , status: \( statusCode ) , headers: \( headers ) " , log : self . log )
} else {
debug ( " MQTT WebSocket failed: \( error . localizedDescription ) " , log : self . log )
}
}
}
2025-03-22 12:59:23 +01:00
}