| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337 |
- import Combine
- import CoreData
- import LoopKit
- import SwiftDate
- import SwiftUI
- extension NightscoutConfig {
- final class StateModel: BaseStateModel<Provider> {
- @Injected() private var keychain: Keychain!
- @Injected() private var nightscoutManager: NightscoutManager!
- @Injected() private var glucoseStorage: GlucoseStorage!
- @Injected() private var healthKitManager: HealthKitManager!
- @Injected() private var cgmManager: FetchGlucoseManager!
- @Injected() private var storage: FileStorage!
- @Injected() var apsManager: APSManager!
- let coredataContext = CoreDataStack.shared.persistentContainer.viewContext
- @Published var url = ""
- @Published var secret = ""
- @Published var message = ""
- @Published var connecting = false
- @Published var backfilling = false
- @Published var isUploadEnabled = false // Allow uploads
- @Published var isDownloadEnabled = false // Allow downloads
- @Published var uploadGlucose = true // Upload Glucose
- @Published var changeUploadGlucose = true // if plugin, need to be change in CGM configuration
- @Published var useLocalSource = false
- @Published var localPort: Decimal = 0
- @Published var units: GlucoseUnits = .mmolL
- @Published var dia: Decimal = 6
- @Published var maxBasal: Decimal = 2
- @Published var maxBolus: Decimal = 10
- @Published var allowAnnouncements: Bool = false
- override func subscribe() {
- url = keychain.getValue(String.self, forKey: Config.urlKey) ?? ""
- secret = keychain.getValue(String.self, forKey: Config.secretKey) ?? ""
- units = settingsManager.settings.units
- dia = settingsManager.pumpSettings.insulinActionCurve
- maxBasal = settingsManager.pumpSettings.maxBasal
- maxBolus = settingsManager.pumpSettings.maxBolus
- changeUploadGlucose = (cgmManager.cgmGlucoseSourceType != CGMType.plugin)
- subscribeSetting(\.allowAnnouncements, on: $allowAnnouncements) { allowAnnouncements = $0 }
- subscribeSetting(\.isUploadEnabled, on: $isUploadEnabled) { isUploadEnabled = $0 }
- subscribeSetting(\.isDownloadEnabled, on: $isDownloadEnabled) { isDownloadEnabled = $0 }
- subscribeSetting(\.useLocalGlucoseSource, on: $useLocalSource) { useLocalSource = $0 }
- subscribeSetting(\.localGlucosePort, on: $localPort.map(Int.init)) { localPort = Decimal($0) }
- subscribeSetting(\.uploadGlucose, on: $uploadGlucose, initial: { uploadGlucose = $0 })
- }
- func connect() {
- guard let url = URL(string: url) else {
- message = "Invalid URL"
- return
- }
- connecting = true
- message = ""
- provider.checkConnection(url: url, secret: secret.isEmpty ? nil : secret)
- .receive(on: DispatchQueue.main)
- .sink { completion in
- switch completion {
- case .finished: break
- case let .failure(error):
- self.message = "Error: \(error.localizedDescription)"
- }
- self.connecting = false
- } receiveValue: {
- self.message = "Connected!"
- self.keychain.setValue(self.url, forKey: Config.urlKey)
- self.keychain.setValue(self.secret, forKey: Config.secretKey)
- }
- .store(in: &lifetime)
- }
- private var nightscoutAPI: NightscoutAPI? {
- guard let urlString = keychain.getValue(String.self, forKey: NightscoutConfig.Config.urlKey),
- let url = URL(string: urlString),
- let secret = keychain.getValue(String.self, forKey: NightscoutConfig.Config.secretKey)
- else {
- return nil
- }
- return NightscoutAPI(url: url, secret: secret)
- }
- func importSettings() {
- guard let nightscout = nightscoutAPI else {
- saveError("Can't access nightscoutAPI")
- return
- }
- let group = DispatchGroup()
- group.enter()
- var error = ""
- let path = "/api/v1/profile.json"
- let timeout: TimeInterval = 60
- var components = URLComponents()
- components.scheme = nightscout.url.scheme
- components.host = nightscout.url.host
- components.port = nightscout.url.port
- components.path = path
- components.queryItems = [
- URLQueryItem(name: "count", value: "1")
- ]
- var url = URLRequest(url: components.url!)
- url.allowsConstrainedNetworkAccess = false
- url.timeoutInterval = timeout
- if let secret = nightscout.secret {
- url.addValue(secret.sha1(), forHTTPHeaderField: "api-secret")
- }
- let task = URLSession.shared.dataTask(with: url) { data, response, error_ in
- if let error_ = error_ {
- print("Error occured: " + error_.localizedDescription)
- // handle error
- self.saveError("Error occured: " + error_.localizedDescription)
- error = error_.localizedDescription
- return
- }
- guard let httpResponse = response as? HTTPURLResponse,
- (200 ... 299).contains(httpResponse.statusCode)
- else {
- print("Error occured! " + error_.debugDescription)
- // handle error
- self.saveError(error_.debugDescription)
- return
- }
- let jsonDecoder = JSONCoding.decoder
- if let mimeType = httpResponse.mimeType, mimeType == "application/json",
- let data = data
- {
- do {
- let fetchedProfileStore = try jsonDecoder.decode([FetchedNightscoutProfileStore].self, from: data)
- let loop = fetchedProfileStore.first?.enteredBy.contains("Loop")
- guard let fetchedProfile: FetchedNightscoutProfile = fetchedProfileStore.first?
- .store[loop! ? "Default" : "default"]
- else {
- error = "\nCan't find the default Nightscout Profile."
- group.leave()
- return
- }
- guard fetchedProfile.units.contains(self.units.rawValue.prefix(4)) else {
- debug(
- .nightscout,
- "Mismatching glucose units in Nightscout and Pump Settings. Import settings aborted."
- )
- error = "\nMismatching glucose units in Nightscout and Pump Settings. Import settings aborted."
- group.leave()
- return
- }
- var areCRsOK = true
- let carbratios = fetchedProfile.carbratio
- .map { carbratio -> CarbRatioEntry in
- if carbratio.value <= 0 {
- error =
- "\nInvalid Carb Ratio settings in Nightscout.\n\nImport aborted. Please check your Nightscout Profile Carb Ratios Settings!"
- areCRsOK = false
- }
- return CarbRatioEntry(
- start: carbratio.time,
- offset: self.offset(carbratio.time) / 60,
- ratio: carbratio.value
- ) }
- let carbratiosProfile = CarbRatios(units: CarbUnit.grams, schedule: carbratios)
- guard areCRsOK else {
- group.leave()
- return
- }
- var areBasalsOK = true
- let pumpName = self.apsManager.pumpName.value
- let basals = fetchedProfile.basal
- .map { basal -> BasalProfileEntry in
- if pumpName != "Omnipod DASH", basal.value <= 0
- {
- error =
- "\nInvalid Nightcsout Basal Settings. Some or all of your basal settings are 0 U/h.\n\nImport aborted. Please check your Nightscout Profile Basal Settings before trying to import again. Import has been aborted.)"
- areBasalsOK = false
- }
- return BasalProfileEntry(
- start: basal.time,
- minutes: self.offset(basal.time) / 60,
- rate: basal.value
- ) }
- // DASH pumps can have 0U/h basal rates but don't import if total basals (24 hours) amount to 0 U.
- if pumpName == "Omnipod DASH", basals.map({ each in each.rate }).reduce(0, +) <= 0
- {
- error =
- "\nYour total Basal insulin amount to 0 U or lower in Nightscout Profile settings.\n\n Please check your Nightscout Profile Basal Settings before trying to import again. Import has been aborted.)"
- areBasalsOK = false
- }
- guard areBasalsOK else {
- group.leave()
- return
- }
- let sensitivities = fetchedProfile.sens.map { sensitivity -> InsulinSensitivityEntry in
- InsulinSensitivityEntry(
- sensitivity: sensitivity.value,
- offset: self.offset(sensitivity.time) / 60,
- start: sensitivity.time
- )
- }
- if sensitivities.filter({ $0.sensitivity <= 0 }).isNotEmpty {
- error =
- "\nInvalid Nightcsout Sensitivities Settings. \n\nImport aborted. Please check your Nightscout Profile Sensitivities Settings!"
- group.leave()
- return
- }
- let sensitivitiesProfile = InsulinSensitivities(
- units: self.units,
- userPrefferedUnits: self.units,
- sensitivities: sensitivities
- )
- let targets = fetchedProfile.target_low
- .map { target -> BGTargetEntry in
- BGTargetEntry(
- low: target.value,
- high: target.value,
- start: target.time,
- offset: self.offset(target.time) / 60
- ) }
- let targetsProfile = BGTargets(
- units: self.units,
- userPrefferedUnits: self.units,
- targets: targets
- )
- // IS THERE A PUMP?
- guard let pump = self.apsManager.pumpManager else {
- self.storage.save(carbratiosProfile, as: OpenAPS.Settings.carbRatios)
- self.storage.save(basals, as: OpenAPS.Settings.basalProfile)
- self.storage.save(sensitivitiesProfile, as: OpenAPS.Settings.insulinSensitivities)
- self.storage.save(targetsProfile, as: OpenAPS.Settings.bgTargets)
- debug(
- .service,
- "Settings were imported but the Basals couldn't be saved to pump (No pump). Check your basal settings and tap ´Save on Pump´ to sync the new basal settings"
- )
- error =
- "\nSettings were imported but the Basals couldn't be saved to pump (No pump). Check your basal settings and tap ´Save on Pump´ to sync the new basal settings"
- group.leave()
- return
- }
- let syncValues = basals.map {
- RepeatingScheduleValue(startTime: TimeInterval($0.minutes * 60), value: Double($0.rate))
- }
- // SSAVE TO STORAGE. SAVE TO PUMP (LoopKit)
- pump.syncBasalRateSchedule(items: syncValues) { result in
- switch result {
- case .success:
- self.storage.save(basals, as: OpenAPS.Settings.basalProfile)
- self.storage.save(carbratiosProfile, as: OpenAPS.Settings.carbRatios)
- self.storage.save(sensitivitiesProfile, as: OpenAPS.Settings.insulinSensitivities)
- self.storage.save(targetsProfile, as: OpenAPS.Settings.bgTargets)
- debug(.service, "Settings have been imported and the Basals saved to pump!")
- // DIA. Save if changed.
- let dia = fetchedProfile.dia
- print("dia: " + dia.description)
- print("pump dia: " + self.dia.description)
- if dia != self.dia, dia >= 0 {
- let file = PumpSettings(
- insulinActionCurve: dia,
- maxBolus: self.maxBolus,
- maxBasal: self.maxBasal
- )
- self.storage.save(file, as: OpenAPS.Settings.settings)
- debug(.nightscout, "DIA setting updated to " + dia.description + " after a NS import.")
- }
- group.leave()
- case .failure:
- error =
- "\nSettings were imported but the Basals couldn't be saved to pump (communication error). Check your basal settings and tap ´Save on Pump´ to sync the new basal settings"
- debug(.service, "Basals couldn't be save to pump")
- group.leave()
- }
- }
- } catch let parsingError {
- print(parsingError)
- error = parsingError.localizedDescription
- group.leave()
- }
- }
- }
- task.resume()
- group.wait(wallTimeout: .now() + 5)
- group.notify(queue: .global(qos: .background)) {
- self.saveError(error)
- }
- }
- func offset(_ string: String) -> Int {
- let hours = Int(string.prefix(2)) ?? 0
- let minutes = Int(string.suffix(2)) ?? 0
- return ((hours * 60) + minutes) * 60
- }
- func saveError(_ string: String) {
- coredataContext.performAndWait {
- let saveToCoreData = ImportError(context: self.coredataContext)
- saveToCoreData.date = Date()
- saveToCoreData.error = string
- if coredataContext.hasChanges {
- try? coredataContext.save()
- }
- }
- }
- func backfillGlucose() {
- backfilling = true
- nightscoutManager.fetchGlucose(since: Date().addingTimeInterval(-1.days.timeInterval))
- .sink { [weak self] glucose in
- guard let self = self else { return }
- DispatchQueue.main.async {
- self.backfilling = false
- }
- guard glucose.isNotEmpty else { return }
- self.healthKitManager.saveIfNeeded(bloodGlucose: glucose)
- self.glucoseStorage.storeGlucose(glucose)
- }
- .store(in: &lifetime)
- }
- func delete() {
- keychain.removeObject(forKey: Config.urlKey)
- keychain.removeObject(forKey: Config.secretKey)
- url = ""
- secret = ""
- }
- }
- }
|