diff --git a/LoopFollow.xcodeproj/project.pbxproj b/LoopFollow.xcodeproj/project.pbxproj index 9a11022d6..d06c4b829 100644 --- a/LoopFollow.xcodeproj/project.pbxproj +++ b/LoopFollow.xcodeproj/project.pbxproj @@ -425,6 +425,9 @@ FCFEEC9E2486E68E00402A7F /* WebKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FCFEEC9D2486E68E00402A7F /* WebKit.framework */; }; FCFEECA02488157B00402A7F /* Chart.swift in Sources */ = {isa = PBXBuildFile; fileRef = FCFEEC9F2488157B00402A7F /* Chart.swift */; }; FCFEECA2248857A600402A7F /* SettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FCFEECA1248857A600402A7F /* SettingsViewController.swift */; }; + DD50C10A2F60A00000000001 /* SocketIO in Frameworks */ = {isa = PBXBuildFile; productRef = DD50C10A2F60A00000000003 /* SocketIO */; }; + DD50C10A2F60B00000000002 /* NightscoutSocketManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD50C10A2F60B00000000001 /* NightscoutSocketManager.swift */; }; + DD50C10A2F60B00000000004 /* NightscoutSocketDataHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD50C10A2F60B00000000003 /* NightscoutSocketDataHandler.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -574,6 +577,8 @@ DD493AE42ACF2383009A6922 /* Treatments.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Treatments.swift; sourceTree = ""; }; DD493AE62ACF23CF009A6922 /* DeviceStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceStatus.swift; sourceTree = ""; }; DD493AE82ACF2445009A6922 /* BGData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BGData.swift; sourceTree = ""; }; + DD50C10A2F60B00000000001 /* NightscoutSocketManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightscoutSocketManager.swift; sourceTree = ""; }; + DD50C10A2F60B00000000003 /* NightscoutSocketDataHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightscoutSocketDataHandler.swift; sourceTree = ""; }; DD4A407D2E6AFEE6007B318B /* AuthService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthService.swift; sourceTree = ""; }; DD4AFB3A2DB55CB600BB593F /* TimeOfDay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimeOfDay.swift; sourceTree = ""; }; DD4AFB3C2DB55D2900BB593F /* AlarmConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmConfiguration.swift; sourceTree = ""; }; @@ -911,6 +916,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + DD50C10A2F60A00000000001 /* SocketIO in Frameworks */, FCFEEC9E2486E68E00402A7F /* WebKit.framework in Frameworks */, 3F1335F351590E573D8E6962 /* Pods_LoopFollow.framework in Frameworks */, ); @@ -1155,6 +1161,8 @@ DD0C0C5F2C415B9D00DBADDF /* ProfileManager.swift */, DD0C0C612C4175FD00DBADDF /* NSProfile.swift */, DD5334222C60ED3600062F9D /* IAge.swift */, + DD50C10A2F60B00000000001 /* NightscoutSocketManager.swift */, + DD50C10A2F60B00000000003 /* NightscoutSocketDataHandler.swift */, ); path = Nightscout; sourceTree = ""; @@ -1783,6 +1791,7 @@ ); name = LoopFollow; packageProductDependencies = ( + DD50C10A2F60A00000000003 /* SocketIO */, ); productName = LoopFollow; productReference = FC9788142485969B00A7906C /* Loop Follow.app */; @@ -1820,6 +1829,7 @@ ); mainGroup = FC97880B2485969B00A7906C; packageReferences = ( + DD50C10A2F60A00000000002 /* XCRemoteSwiftPackageReference "socket.io-client-swift" */, ); productRefGroup = FC9788152485969B00A7906C /* Products */; projectDirPath = ""; @@ -2190,6 +2200,8 @@ DD026E592EA2C8A200A39CB5 /* InsulinPrecisionManager.swift in Sources */, 374A77992F5BD8B200E96858 /* APNSClient.swift in Sources */, DD493AE72ACF23CF009A6922 /* DeviceStatus.swift in Sources */, + DD50C10A2F60B00000000002 /* NightscoutSocketManager.swift in Sources */, + DD50C10A2F60B00000000004 /* NightscoutSocketDataHandler.swift in Sources */, DDC7E5162DBCFA7F00EB1127 /* SnoozerView.swift in Sources */, FCFEECA2248857A600402A7F /* SettingsViewController.swift in Sources */, DD7F4C232DD7A62200D449E9 /* AlarmType+SortDirection.swift in Sources */, @@ -2802,6 +2814,25 @@ versionGroupType = wrapper.xcdatamodel; }; /* End XCVersionGroup section */ + +/* Begin XCRemoteSwiftPackageReference section */ + DD50C10A2F60A00000000002 /* XCRemoteSwiftPackageReference "socket.io-client-swift" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/socketio/socket.io-client-swift"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 16.1.1; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + DD50C10A2F60A00000000003 /* SocketIO */ = { + isa = XCSwiftPackageProductDependency; + package = DD50C10A2F60A00000000002 /* XCRemoteSwiftPackageReference "socket.io-client-swift" */; + productName = SocketIO; + }; +/* End XCSwiftPackageProductDependency section */ }; rootObject = FC97880C2485969B00A7906C /* Project object */; } diff --git a/LoopFollow/Controllers/Nightscout/BGData.swift b/LoopFollow/Controllers/Nightscout/BGData.swift index d97aba24c..f7f1eda52 100644 --- a/LoopFollow/Controllers/Nightscout/BGData.swift +++ b/LoopFollow/Controllers/Nightscout/BGData.swift @@ -176,6 +176,10 @@ extension MainViewController { TaskScheduler.shared.rescheduleTask(id: .alarmCheck, to: Date().addingTimeInterval(3)) } + if NightscoutSocketManager.shared.connectionState == .authenticated { + delayToSchedule = max(delayToSchedule * 3, 60) + } + TaskScheduler.shared.rescheduleTask(id: .fetchBG, to: Date().addingTimeInterval(delayToSchedule)) // Evaluate speak conditions if there is a previous value. diff --git a/LoopFollow/Controllers/Nightscout/DeviceStatus.swift b/LoopFollow/Controllers/Nightscout/DeviceStatus.swift index ff2b13a78..fce835b54 100644 --- a/LoopFollow/Controllers/Nightscout/DeviceStatus.swift +++ b/LoopFollow/Controllers/Nightscout/DeviceStatus.swift @@ -213,37 +213,28 @@ extension MainViewController { let secondsAgo = now - (Observable.shared.alertLastLoopTime.value ?? 0) DispatchQueue.main.async { + var interval: Double if secondsAgo >= (20 * 60) { - TaskScheduler.shared.rescheduleTask( - id: .deviceStatus, - to: Date().addingTimeInterval(5 * 60) - ) - + interval = 5 * 60 } else if secondsAgo >= (10 * 60) { - TaskScheduler.shared.rescheduleTask( - id: .deviceStatus, - to: Date().addingTimeInterval(60) - ) - + interval = 60 } else if secondsAgo >= (7 * 60) { - TaskScheduler.shared.rescheduleTask( - id: .deviceStatus, - to: Date().addingTimeInterval(30) - ) - + interval = 30 } else if secondsAgo >= (5 * 60) { - TaskScheduler.shared.rescheduleTask( - id: .deviceStatus, - to: Date().addingTimeInterval(10) - ) + interval = 10 } else { - let interval = (310 - secondsAgo) - TaskScheduler.shared.rescheduleTask( - id: .deviceStatus, - to: Date().addingTimeInterval(interval) - ) + interval = 310 - secondsAgo TaskScheduler.shared.rescheduleTask(id: .alarmCheck, to: Date().addingTimeInterval(3)) } + + if NightscoutSocketManager.shared.connectionState == .authenticated { + interval = max(interval * 3, 60) + } + + TaskScheduler.shared.rescheduleTask( + id: .deviceStatus, + to: Date().addingTimeInterval(interval) + ) } evaluateNotLooping() diff --git a/LoopFollow/Controllers/Nightscout/NightscoutSocketDataHandler.swift b/LoopFollow/Controllers/Nightscout/NightscoutSocketDataHandler.swift new file mode 100644 index 000000000..054491c68 --- /dev/null +++ b/LoopFollow/Controllers/Nightscout/NightscoutSocketDataHandler.swift @@ -0,0 +1,54 @@ +// LoopFollow +// NightscoutSocketDataHandler.swift + +import Foundation + +extension MainViewController { + func setupNightscoutSocket() { + NightscoutSocketManager.shared.onDataUpdate = { [weak self] data in + self?.handleSocketDataUpdate(data) + } + NightscoutSocketManager.shared.connectIfNeeded() + } + + func handleSocketDataUpdate(_ data: [String: Any]) { + let isDelta = data["delta"] as? Bool ?? false + + if !isDelta { + // Full data on initial connect — trigger all fetches + LogManager.shared.log(category: .websocket, message: "Full data received, triggering all fetches") + TaskScheduler.shared.rescheduleTask(id: .fetchBG, to: Date()) + TaskScheduler.shared.rescheduleTask(id: .deviceStatus, to: Date()) + TaskScheduler.shared.rescheduleTask(id: .treatments, to: Date()) + TaskScheduler.shared.rescheduleTask(id: .profile, to: Date()) + return + } + + // Selective: only fetch data types present in the delta + var triggered: [String] = [] + + if data["sgvs"] != nil || data["mbgs"] != nil { + TaskScheduler.shared.rescheduleTask(id: .fetchBG, to: Date()) + triggered.append("BG") + } + + if data["devicestatus"] != nil { + TaskScheduler.shared.rescheduleTask(id: .deviceStatus, to: Date()) + triggered.append("DeviceStatus") + } + + if data["treatments"] != nil { + TaskScheduler.shared.rescheduleTask(id: .treatments, to: Date()) + triggered.append("Treatments") + } + + if data["profiles"] != nil { + TaskScheduler.shared.rescheduleTask(id: .profile, to: Date()) + triggered.append("Profile") + } + + if !triggered.isEmpty { + LogManager.shared.log(category: .websocket, message: "Delta triggered: \(triggered.joined(separator: ", "))", isDebug: true) + } + } +} diff --git a/LoopFollow/Controllers/Nightscout/NightscoutSocketManager.swift b/LoopFollow/Controllers/Nightscout/NightscoutSocketManager.swift new file mode 100644 index 000000000..5bc18d61a --- /dev/null +++ b/LoopFollow/Controllers/Nightscout/NightscoutSocketManager.swift @@ -0,0 +1,179 @@ +// LoopFollow +// NightscoutSocketManager.swift + +import Foundation +import SocketIO + +class NightscoutSocketManager { + static let shared = NightscoutSocketManager() + + enum ConnectionState: String { + case disconnected + case connecting + case connected + case authenticated + case error + } + + private(set) var connectionState: ConnectionState = .disconnected { + didSet { + DispatchQueue.main.async { + NotificationCenter.default.post(name: .nightscoutSocketStateChanged, object: nil) + } + } + } + + private var manager: SocketManager? + private var socket: SocketIOClient? + private var currentURL: String = "" + private var currentToken: String = "" + + var onDataUpdate: (([String: Any]) -> Void)? + + private init() {} + + // MARK: - Public API + + func connectIfNeeded() { + guard Storage.shared.webSocketEnabled.value else { + disconnect() + return + } + + let url = Storage.shared.url.value + let token = Storage.shared.token.value + + guard !url.isEmpty else { + disconnect() + return + } + + // Already connected to the same URL + if connectionState == .authenticated || connectionState == .connecting || connectionState == .connected { + if url == currentURL, token == currentToken { + return + } + // URL or token changed, reconnect + disconnect() + } + + currentURL = url + currentToken = token + connect() + } + + func disconnect() { + socket?.removeAllHandlers() + socket?.disconnect() + manager?.disconnect() + manager = nil + socket = nil + connectionState = .disconnected + currentURL = "" + currentToken = "" + } + + // MARK: - Private + + private func connect() { + guard let url = URL(string: currentURL) else { + LogManager.shared.log(category: .websocket, message: "Invalid Nightscout URL for WebSocket") + connectionState = .error + return + } + + connectionState = .connecting + + var config: SocketIOClientConfiguration = [ + .log(false), + .compress, + .forceWebsockets(false), + .reconnects(true), + .reconnectWait(5), + .reconnectWaitMax(30), + ] + + if !currentToken.isEmpty { + config.insert(.connectParams(["token": currentToken])) + } + + manager = SocketManager(socketURL: url, config: config) + + guard let mgr = manager else { return } + socket = mgr.defaultSocket + + setupEventHandlers() + socket?.connect() + + LogManager.shared.log(category: .websocket, message: "Connecting to Nightscout WebSocket at \(currentURL)") + } + + private func setupEventHandlers() { + guard let socket = socket else { return } + + socket.on(clientEvent: .connect) { [weak self] _, _ in + guard let self = self else { return } + LogManager.shared.log(category: .websocket, message: "Socket connected, authorizing...") + self.connectionState = .connected + self.authorize() + } + + socket.on(clientEvent: .disconnect) { [weak self] data, _ in + guard let self = self else { return } + let reason = (data.first as? String) ?? "unknown" + LogManager.shared.log(category: .websocket, message: "Socket disconnected: \(reason)") + self.connectionState = .disconnected + // Immediately restore normal polling intervals + NotificationCenter.default.post(name: NSNotification.Name("refresh"), object: nil) + } + + socket.on(clientEvent: .reconnect) { _, _ in + LogManager.shared.log(category: .websocket, message: "Socket reconnecting...") + } + + socket.on(clientEvent: .error) { [weak self] data, _ in + let errorMsg = (data.first as? String) ?? "unknown error" + LogManager.shared.log(category: .websocket, message: "Socket error: \(errorMsg)") + self?.connectionState = .error + } + + socket.on("connected") { [weak self] _, _ in + guard let self = self else { return } + LogManager.shared.log(category: .websocket, message: "Authorized and receiving data") + self.connectionState = .authenticated + } + + socket.on("dataUpdate") { [weak self] data, _ in + guard let self = self, + let payload = data.first as? [String: Any] + else { return } + + LogManager.shared.log(category: .websocket, message: "Received dataUpdate (delta: \(payload["delta"] as? Bool ?? false))", isDebug: true) + + DispatchQueue.main.async { + self.onDataUpdate?(payload) + } + } + } + + private func authorize() { + var authPayload: [String: Any] = [ + "client": "LoopFollow", + "history": 1, + ] + + // Nightscout's authorization.resolve() expects: + // - "token" field for JWT tokens (verified via verifyJWT) + // - "secret" field for access tokens (checked via doesAccessTokenExist) + // LoopFollow uses access tokens (e.g. "readable-xxxx"), so pass as "secret". + if !currentToken.isEmpty { + authPayload["secret"] = currentToken + } + + socket?.emit("authorize", authPayload) + } +} + +extension Notification.Name { + static let nightscoutSocketStateChanged = Notification.Name("nightscoutSocketStateChanged") +} diff --git a/LoopFollow/Log/LogManager.swift b/LoopFollow/Log/LogManager.swift index 22403cd0f..c31747edf 100644 --- a/LoopFollow/Log/LogManager.swift +++ b/LoopFollow/Log/LogManager.swift @@ -29,6 +29,7 @@ class LogManager { case calendar = "Calendar" case deviceStatus = "Device Status" case remote = "Remote" + case websocket = "WebSocket" } init() { diff --git a/LoopFollow/Nightscout/NightscoutSettingsView.swift b/LoopFollow/Nightscout/NightscoutSettingsView.swift index 7c08e756f..11fb42d3a 100644 --- a/LoopFollow/Nightscout/NightscoutSettingsView.swift +++ b/LoopFollow/Nightscout/NightscoutSettingsView.swift @@ -12,6 +12,7 @@ struct NightscoutSettingsView: View { urlSection tokenSection statusSection + webSocketSection importSection } .onDisappear { @@ -56,6 +57,59 @@ struct NightscoutSettingsView: View { } } + @State private var showWebSocketInfo = false + + private var webSocketSection: some View { + Section(header: webSocketSectionHeader) { + Toggle("Enable WebSocket", isOn: $viewModel.webSocketEnabled) + if viewModel.webSocketEnabled { + HStack { + Text("Status") + Spacer() + Text(viewModel.webSocketStatus) + .foregroundColor(viewModel.webSocketStatusColor) + } + } + } + .sheet(isPresented: $showWebSocketInfo) { + NavigationStack { + ScrollView { + Text(""" + When enabled, LoopFollow maintains a live connection to your Nightscout server using WebSocket while the app is in the foreground. Data updates (new glucose readings, treatments, device status) arrive within seconds instead of waiting for the next polling cycle. + + The WebSocket disconnects when LoopFollow moves to the background and reconnects when you return to the app. Polling continues to handle updates while the app is in the background. + + In the foreground, polling continues at a reduced frequency as a safety net. If the WebSocket connection drops, normal polling resumes immediately. + """) + .padding() + .frame(maxWidth: .infinity, alignment: .leading) + } + .navigationTitle("Real-time Updates") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .confirmationAction) { + Button("Done") { showWebSocketInfo = false } + } + } + } + .presentationDetents([.medium]) + .presentationDragIndicator(.visible) + } + } + + private var webSocketSectionHeader: some View { + HStack(spacing: 4) { + Text("Real-time Updates") + Button { + showWebSocketInfo = true + } label: { + Image(systemName: "info.circle") + .foregroundStyle(Color.accentColor) + } + .buttonStyle(.plain) + } + } + private var importSection: some View { Section(header: Text("Import Settings")) { NavigationLink(destination: ImportExportSettingsView()) { diff --git a/LoopFollow/Nightscout/NightscoutSettingsViewModel.swift b/LoopFollow/Nightscout/NightscoutSettingsViewModel.swift index 299a73a7d..07cdccf7f 100644 --- a/LoopFollow/Nightscout/NightscoutSettingsViewModel.swift +++ b/LoopFollow/Nightscout/NightscoutSettingsViewModel.swift @@ -3,6 +3,7 @@ import Combine import Foundation +import SwiftUI protocol NightscoutSettingsViewModelDelegate: AnyObject { func nightscoutSettingsDidFinish() @@ -34,6 +35,29 @@ class NightscoutSettingsViewModel: ObservableObject { @Published var nightscoutStatus: String = "Checking..." + @Published var webSocketEnabled: Bool = Storage.shared.webSocketEnabled.value { + didSet { + Storage.shared.webSocketEnabled.value = webSocketEnabled + if webSocketEnabled { + NightscoutSocketManager.shared.connectIfNeeded() + } else { + NightscoutSocketManager.shared.disconnect() + triggerRefresh() + } + } + } + + @Published var webSocketStatus: String = "Disconnected" + + var webSocketStatusColor: Color { + switch NightscoutSocketManager.shared.connectionState { + case .authenticated: return .green + case .connecting, .connected: return .orange + case .disconnected: return .secondary + case .error: return .red + } + } + private var cancellables = Set() private var checkStatusSubject = PassthroughSubject() private var checkStatusWorkItem: DispatchWorkItem? @@ -44,6 +68,7 @@ class NightscoutSettingsViewModel: ObservableObject { setupDebounce() checkNightscoutStatus() + observeWebSocketState() } private func setupDebounce() { @@ -123,6 +148,7 @@ class NightscoutSettingsViewModel: ObservableObject { case .emptyAddress: nightscoutStatus = "Address Empty" } + NightscoutSocketManager.shared.disconnect() } else { let authStatus: String if Storage.shared.nsAdminAuth.value { @@ -142,4 +168,28 @@ class NightscoutSettingsViewModel: ObservableObject { func dismiss() { delegate?.nightscoutSettingsDidFinish() } + + private func triggerRefresh() { + NotificationCenter.default.post(name: NSNotification.Name("refresh"), object: nil) + } + + private func observeWebSocketState() { + updateWebSocketStatus() + NotificationCenter.default.publisher(for: .nightscoutSocketStateChanged) + .receive(on: DispatchQueue.main) + .sink { [weak self] _ in + self?.updateWebSocketStatus() + } + .store(in: &cancellables) + } + + private func updateWebSocketStatus() { + switch NightscoutSocketManager.shared.connectionState { + case .disconnected: webSocketStatus = "Disconnected" + case .connecting: webSocketStatus = "Connecting..." + case .connected: webSocketStatus = "Connected" + case .authenticated: webSocketStatus = "Connected" + case .error: webSocketStatus = "Error" + } + } } diff --git a/LoopFollow/Storage/Storage.swift b/LoopFollow/Storage/Storage.swift index 11997da35..d393217f5 100644 --- a/LoopFollow/Storage/Storage.swift +++ b/LoopFollow/Storage/Storage.swift @@ -192,6 +192,7 @@ class Storage { var device = StorageValue(key: "device", defaultValue: "") var nsWriteAuth = StorageValue(key: "nsWriteAuth", defaultValue: false) var nsAdminAuth = StorageValue(key: "nsAdminAuth", defaultValue: false) + var webSocketEnabled = StorageValue(key: "webSocketEnabled", defaultValue: true) var migrationStep = StorageValue(key: "migrationStep", defaultValue: 0) diff --git a/LoopFollow/Task/ProfileTask.swift b/LoopFollow/Task/ProfileTask.swift index 72de336fb..6b4358ca7 100644 --- a/LoopFollow/Task/ProfileTask.swift +++ b/LoopFollow/Task/ProfileTask.swift @@ -21,6 +21,10 @@ extension MainViewController { webLoadNSProfile() - TaskScheduler.shared.rescheduleTask(id: .profile, to: Date().addingTimeInterval(10 * 60)) + var interval: TimeInterval = 10 * 60 + if NightscoutSocketManager.shared.connectionState == .authenticated { + interval = 30 * 60 + } + TaskScheduler.shared.rescheduleTask(id: .profile, to: Date().addingTimeInterval(interval)) } } diff --git a/LoopFollow/Task/TreatmentsTask.swift b/LoopFollow/Task/TreatmentsTask.swift index d3a0c620e..4e7aa0bc9 100644 --- a/LoopFollow/Task/TreatmentsTask.swift +++ b/LoopFollow/Task/TreatmentsTask.swift @@ -21,7 +21,11 @@ extension MainViewController { WebLoadNSTreatments() - TaskScheduler.shared.rescheduleTask(id: .treatments, to: Date().addingTimeInterval(2 * 60)) + var interval: TimeInterval = 2 * 60 + if NightscoutSocketManager.shared.connectionState == .authenticated { + interval = 10 * 60 + } + TaskScheduler.shared.rescheduleTask(id: .treatments, to: Date().addingTimeInterval(interval)) TaskScheduler.shared.rescheduleTask(id: .alarmCheck, to: Date().addingTimeInterval(3)) } } diff --git a/LoopFollow/ViewControllers/MainViewController.swift b/LoopFollow/ViewControllers/MainViewController.swift index c82ecb590..583aced86 100644 --- a/LoopFollow/ViewControllers/MainViewController.swift +++ b/LoopFollow/ViewControllers/MainViewController.swift @@ -209,6 +209,7 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele showHideNSDetails() scheduleAllTasks() + setupNightscoutSocket() // Set up refreshScrollView for BGText refreshScrollView = UIScrollView() @@ -885,6 +886,7 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele MinAgoText.text = "Refreshing" Observable.shared.minAgoText.value = "Refreshing" scheduleAllTasks() + NightscoutSocketManager.shared.connectIfNeeded() currentCage = nil currentSage = nil @@ -983,6 +985,8 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele if Storage.shared.backgroundRefreshType.value != .none { BackgroundAlertManager.shared.startBackgroundAlert() } + + NightscoutSocketManager.shared.disconnect() } // Migrations must only run when UserDefaults is accessible (i.e. after first unlock). @@ -1084,6 +1088,7 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele } TaskScheduler.shared.checkTasksNow() + NightscoutSocketManager.shared.connectIfNeeded() checkAndNotifyVersionStatus() checkAppExpirationStatus()