| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365 |
- import CoreData
- import Foundation
- import Swinject
- class TrioRemoteControl: Injectable {
- static let shared = TrioRemoteControl()
- @Injected() private var tempTargetsStorage: TempTargetsStorage!
- @Injected() private var carbsStorage: CarbsStorage!
- @Injected() private var nightscoutManager: NightscoutManager!
- @Injected() private var pumpHistoryStorage: PumpHistoryStorage!
- @Injected() private var overrideStorage: OverrideStorage!
- private let timeWindow: TimeInterval = 600 // Defines how old messages that are accepted, 10 minutes
- private init() {
- injectServices(FreeAPSApp.resolver)
- }
- private func logError(_ errorMessage: String, pushMessage: PushMessage? = nil) async {
- var note = errorMessage
- if let pushMessage = pushMessage {
- note += " Details: \(pushMessage.humanReadableDescription())"
- }
- debug(.remoteControl, note)
- await nightscoutManager.uploadNoteTreatment(note: note)
- }
- func handleRemoteNotification(userInfo: [AnyHashable: Any]) async {
- let enabled = UserDefaults.standard.bool(forKey: "TRCenabled")
- guard enabled else {
- await logError("Remote command received, but remote control is disabled in settings. Ignoring the command.")
- return
- }
- do {
- let jsonData = try JSONSerialization.data(withJSONObject: userInfo)
- let pushMessage = try JSONDecoder().decode(PushMessage.self, from: jsonData)
- let currentTime = Date().timeIntervalSince1970
- let timeDifference = currentTime - pushMessage.timestamp
- if timeDifference > timeWindow {
- await logError(
- "Command rejected: the message is too old (sent \(Int(timeDifference)) seconds ago, which exceeds the allowed limit).",
- pushMessage: pushMessage
- )
- return
- } else if timeDifference < -timeWindow {
- await logError(
- "Command rejected: the message has an invalid future timestamp (timestamp is \(Int(-timeDifference)) seconds ahead of the current time).",
- pushMessage: pushMessage
- )
- return
- }
- debug(.remoteControl, "Command received with acceptable time difference: \(Int(timeDifference)) seconds.")
- let storedSecret = UserDefaults.standard.string(forKey: "TRCsharedSecret") ?? ""
- guard !storedSecret.isEmpty else {
- await logError(
- "Command rejected: shared secret is missing in settings. Cannot authenticate the command.",
- pushMessage: pushMessage
- )
- return
- }
- guard pushMessage.sharedSecret == storedSecret else {
- await logError(
- "Command rejected: shared secret does not match. Cannot authenticate the command.",
- pushMessage: pushMessage
- )
- return
- }
- switch pushMessage.commandType {
- case .bolus:
- await handleBolusCommand(pushMessage)
- case .tempTarget:
- await handleTempTargetCommand(pushMessage)
- case .cancelTempTarget:
- await cancelTempTarget()
- case .meal:
- await handleMealCommand(pushMessage)
- case .startOverride:
- await handleStartOverrideCommand(pushMessage)
- case .cancelOverride:
- await handleCancelOverrideCommand(pushMessage)
- default:
- await logError(
- "Command rejected: unsupported command type '\(pushMessage.commandType)'.",
- pushMessage: pushMessage
- )
- }
- } catch {
- await logError("Error: unable to process the command due to decoding failure (\(error.localizedDescription)).")
- }
- }
- private func handleMealCommand(_ pushMessage: PushMessage) async {
- guard
- let carbs = pushMessage.carbs,
- let fat = pushMessage.fat,
- let protein = pushMessage.protein
- else {
- await logError("Command rejected: meal data is incomplete or invalid.", pushMessage: pushMessage)
- return
- }
- let settings = await FreeAPSApp.resolver.resolve(SettingsManager.self)?.settings
- let maxCarbs = settings?.maxCarbs ?? Decimal(0)
- let maxFat = settings?.maxFat ?? Decimal(0)
- let maxProtein = settings?.maxProtein ?? Decimal(0)
- if Decimal(carbs) > maxCarbs {
- await logError(
- "Command rejected: carbs amount (\(carbs)g) exceeds the maximum allowed (\(maxCarbs)g).",
- pushMessage: pushMessage
- )
- return
- }
- if Decimal(fat) > maxFat {
- await logError(
- "Command rejected: fat amount (\(fat)g) exceeds the maximum allowed (\(maxFat)g).",
- pushMessage: pushMessage
- )
- return
- }
- if Decimal(protein) > maxProtein {
- await logError(
- "Command rejected: protein amount (\(protein)g) exceeds the maximum allowed (\(maxProtein)g).",
- pushMessage: pushMessage
- )
- return
- }
- let pushMessageDate = Date(timeIntervalSince1970: pushMessage.timestamp)
- let recentCarbEntries = carbsStorage.recent()
- let carbsAfterPushMessage = recentCarbEntries.filter { $0.createdAt > pushMessageDate }
- if !carbsAfterPushMessage.isEmpty {
- await logError(
- "Command rejected: newer carb entries have been logged since the command was sent.",
- pushMessage: pushMessage
- )
- return
- }
- let mealEntry = CarbsEntry(
- id: UUID().uuidString,
- createdAt: Date(),
- actualDate: nil,
- carbs: Decimal(carbs),
- fat: Decimal(fat),
- protein: Decimal(protein),
- note: "Remote meal command",
- enteredBy: CarbsEntry.manual,
- isFPU: false,
- fpuID: nil
- )
- await carbsStorage.storeCarbs([mealEntry], areFetchedFromRemote: false)
- debug(.remoteControl, "Meal command processed successfully with carbs: \(carbs)g, fat: \(fat)g, protein: \(protein)g.")
- }
- private func handleBolusCommand(_ pushMessage: PushMessage) async {
- guard let bolusAmount = pushMessage.bolusAmount else {
- await logError("Command rejected: bolus amount is missing or invalid.", pushMessage: pushMessage)
- return
- }
- let maxBolus = await FreeAPSApp.resolver.resolve(SettingsManager.self)?.pumpSettings.maxBolus ?? Decimal(0)
- if bolusAmount > maxBolus {
- await logError(
- "Command rejected: bolus amount (\(bolusAmount) units) exceeds the maximum allowed (\(maxBolus) units).",
- pushMessage: pushMessage
- )
- return
- }
- let recentPumpEvents = pumpHistoryStorage.recent()
- let recentBoluses = recentPumpEvents.filter { event in
- event.type == .bolus && event.timestamp > Date(timeIntervalSince1970: pushMessage.timestamp)
- }
- let totalRecentBolusAmount = recentBoluses.reduce(Decimal(0)) { $0 + ($1.amount ?? 0) }
- if totalRecentBolusAmount >= bolusAmount * 0.2 {
- await logError(
- "Command rejected: boluses totaling more than 20% of the requested amount have been delivered since the command was sent.",
- pushMessage: pushMessage
- )
- return
- }
- debug(.remoteControl, "Enacting bolus command with amount: \(bolusAmount) units.")
- guard let apsManager = await FreeAPSApp.resolver.resolve(APSManager.self) else {
- await logError(
- "Error: unable to process bolus command because the APS Manager is not available.",
- pushMessage: pushMessage
- )
- return
- }
- await apsManager.enactBolus(amount: Double(truncating: bolusAmount as NSNumber), isSMB: false)
- }
- private func handleTempTargetCommand(_ pushMessage: PushMessage) async {
- guard let targetValue = pushMessage.target,
- let durationValue = pushMessage.duration
- else {
- await logError("Command rejected: temp target data is incomplete or invalid.", pushMessage: pushMessage)
- return
- }
- let durationInMinutes = Int(durationValue)
- let pushMessageDate = Date(timeIntervalSince1970: pushMessage.timestamp)
- let tempTarget = TempTarget(
- name: TempTarget.custom,
- createdAt: pushMessageDate,
- targetTop: Decimal(targetValue),
- targetBottom: Decimal(targetValue),
- duration: Decimal(durationInMinutes),
- enteredBy: TempTarget.manual,
- reason: TempTarget.custom
- )
- tempTargetsStorage.storeTempTargets([tempTarget])
- debug(.remoteControl, "Temp target set with target: \(targetValue), duration: \(durationInMinutes) minutes.")
- }
- func cancelTempTarget() async {
- debug(.remoteControl, "Cancelling temp target.")
- guard tempTargetsStorage.current() != nil else {
- await logError("Command rejected: no active temp target to cancel.")
- return
- }
- let cancelEntry = TempTarget.cancel(at: Date())
- tempTargetsStorage.storeTempTargets([cancelEntry])
- debug(.remoteControl, "Temp target cancelled successfully.")
- }
- @MainActor private func handleCancelOverrideCommand(_: PushMessage) async {
- await disableAllActiveOverrides()
- debug(.remoteControl, "Active override cancelled successfully.")
- }
- @MainActor private func handleStartOverrideCommand(_ pushMessage: PushMessage) async {
- guard let overrideName = pushMessage.overrideName, !overrideName.isEmpty else {
- await logError("Command rejected: override name is missing.", pushMessage: pushMessage)
- return
- }
- let viewContext = CoreDataStack.shared.persistentContainer.viewContext
- let presetIDs = await overrideStorage.fetchForOverridePresets()
- let presets = presetIDs.compactMap { id in
- try? viewContext.existingObject(with: id) as? OverrideStored
- }
- if let preset = presets.first(where: { $0.name == overrideName }) {
- await enactOverridePreset(preset: preset)
- debug(.remoteControl, "Override '\(overrideName)' started successfully.")
- } else {
- await logError("Command rejected: override preset '\(overrideName)' not found.", pushMessage: pushMessage)
- }
- }
- @MainActor private func enactOverridePreset(preset: OverrideStored) async {
- await disableAllActiveOverrides()
- let viewContext = CoreDataStack.shared.persistentContainer.viewContext
- preset.enabled = true
- preset.date = Date()
- preset.isUploadedToNS = false
- do {
- if viewContext.hasChanges {
- try viewContext.save()
- }
- } catch {
- debug(.remoteControl, "Failed to enact override preset: \(error.localizedDescription)")
- }
- }
- @MainActor func disableAllActiveOverrides() async {
- let viewContext = CoreDataStack.shared.persistentContainer.viewContext
- let ids = await overrideStorage.loadLatestOverrideConfigurations(fetchLimit: 0) // 0 = no fetch limit
- await viewContext.perform {
- do {
- let results = try ids.compactMap { id in
- try viewContext.existingObject(with: id) as? OverrideStored
- }
- guard !results.isEmpty else { return }
- for canceledOverride in results where canceledOverride.enabled {
- let newOverrideRunStored = OverrideRunStored(context: viewContext)
- newOverrideRunStored.id = UUID()
- newOverrideRunStored.name = canceledOverride.name
- newOverrideRunStored.startDate = canceledOverride.date ?? .distantPast
- newOverrideRunStored.endDate = Date()
- newOverrideRunStored
- .target = NSDecimalNumber(decimal: self.overrideStorage.calculateTarget(override: canceledOverride))
- newOverrideRunStored.override = canceledOverride
- newOverrideRunStored.isUploadedToNS = false
- canceledOverride.enabled = false
- }
- if viewContext.hasChanges {
- try viewContext.save()
- }
- } catch {
- debugPrint(
- "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to disable active Overrides with error: \(error.localizedDescription)"
- )
- }
- }
- }
- func handleAPNSChanges(deviceToken: String?) async {
- let previousDeviceToken = UserDefaults.standard.string(forKey: "deviceToken")
- let previousIsAPNSProduction = UserDefaults.standard.bool(forKey: "isAPNSProduction")
- let isAPNSProduction = isRunningInAPNSProductionEnvironment()
- var shouldUploadProfiles = false
- if let token = deviceToken, token != previousDeviceToken {
- UserDefaults.standard.set(token, forKey: "deviceToken")
- debug(.remoteControl, "Device token updated: \(token)")
- shouldUploadProfiles = true
- }
- if previousIsAPNSProduction != isAPNSProduction {
- UserDefaults.standard.set(isAPNSProduction, forKey: "isAPNSProduction")
- debug(.remoteControl, "APNS environment changed to: \(isAPNSProduction ? "Production" : "Sandbox")")
- shouldUploadProfiles = true
- }
- if shouldUploadProfiles {
- await nightscoutManager.uploadProfiles()
- } else {
- debug(.remoteControl, "No changes detected in device token or APNS environment.")
- }
- }
- private func isRunningInAPNSProductionEnvironment() -> Bool {
- if let appStoreReceiptURL = Bundle.main.appStoreReceiptURL {
- return appStoreReceiptURL.lastPathComponent != "sandboxReceipt"
- }
- return false
- }
- }
|