Skip to content

Commit

Permalink
Import Profile Settings from Nightcsout (nightscout#238)
Browse files Browse the repository at this point in the history
Import Basal settings, carb ratios, sensitivities and glucose targets from Nightscout manually when tapping "Import Settings from Nightcsout"in the iAPS Nightscout settings. 

co-author @dnzxy
  • Loading branch information
Jon-b-m authored Oct 8, 2023
1 parent 440be77 commit ee787b2
Show file tree
Hide file tree
Showing 14 changed files with 350 additions and 8 deletions.
4 changes: 4 additions & 0 deletions Core_Data.xcdatamodeld/Core_Data.xcdatamodel/contents
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@
<attribute name="hba1c_7" optional="YES" attributeType="Decimal" defaultValueString="0.0"/>
<attribute name="hba1c_30" optional="YES" attributeType="Decimal" defaultValueString="0.0"/>
</entity>
<entity name="ImportError" representedClassName="ImportError" syncable="YES" codeGenerationType="class">
<attribute name="date" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="error" optional="YES" attributeType="String"/>
</entity>
<entity name="InsulinDistribution" representedClassName="InsulinDistribution" syncable="YES" codeGenerationType="class">
<attribute name="bolus" optional="YES" attributeType="Decimal" defaultValueString="0.0"/>
<attribute name="date" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
Expand Down
4 changes: 4 additions & 0 deletions FreeAPS.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,7 @@
BF1667ADE69E4B5B111CECAE /* ManualTempBasalProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 680C4420C9A345D46D90D06C /* ManualTempBasalProvider.swift */; };
C967DACD3B1E638F8B43BE06 /* ManualTempBasalStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CFCFE0781F9074C2917890E8 /* ManualTempBasalStateModel.swift */; };
CA370FC152BC98B3D1832968 /* BasalProfileEditorRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF8BCB0C37DEB5EC377B9612 /* BasalProfileEditorRootView.swift */; };
CC6C406E2ACDD69E009B8058 /* RawFetchedProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC6C406D2ACDD69E009B8058 /* RawFetchedProfile.swift */; };
CD78BB94E43B249D60CC1A1B /* NotificationsConfigRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22963BD06A9C83959D4914E4 /* NotificationsConfigRootView.swift */; };
CE2FAD38297D69E1001A872C /* ShareClient.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = CE398D1A297D69A900DF218F /* ShareClient.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
CE2FAD3A297D93F0001A872C /* BloodGlucoseExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE2FAD39297D93F0001A872C /* BloodGlucoseExtensions.swift */; };
Expand Down Expand Up @@ -816,6 +817,7 @@
C19984D62EFC0035A9E9644D /* BolusProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = BolusProvider.swift; sourceTree = "<group>"; };
C377490C77661D75E8C50649 /* ManualTempBasalRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ManualTempBasalRootView.swift; sourceTree = "<group>"; };
C8D1A7CA8C10C4403D4BBFA7 /* BolusDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = BolusDataFlow.swift; sourceTree = "<group>"; };
CC6C406D2ACDD69E009B8058 /* RawFetchedProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RawFetchedProfile.swift; sourceTree = "<group>"; };
CE2FAD39297D93F0001A872C /* BloodGlucoseExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BloodGlucoseExtensions.swift; sourceTree = "<group>"; };
CE398D012977349800DF218F /* CryptoKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CryptoKit.framework; path = System/Library/Frameworks/CryptoKit.framework; sourceTree = SDKROOT; };
CE398D15297C9D1D00DF218F /* dexcomSourceG7.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = dexcomSourceG7.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1603,6 +1605,7 @@
19D4E4EA29FC6A9F00351451 /* TIRforChart.swift */,
19A910352A24D6D700C8951B /* DateFilter.swift */,
193F6CDC2A512C8F001240FD /* Loops.swift */,
CC6C406D2ACDD69E009B8058 /* RawFetchedProfile.swift */,
);
path = Models;
sourceTree = "<group>";
Expand Down Expand Up @@ -2689,6 +2692,7 @@
63E890B4D951EAA91C071D5C /* BasalProfileEditorStateModel.swift in Sources */,
CE398D16297C9D1D00DF218F /* dexcomSourceG7.swift in Sources */,
38FEF3FA2737E42000574A46 /* BaseStateModel.swift in Sources */,
CC6C406E2ACDD69E009B8058 /* RawFetchedProfile.swift in Sources */,
385CEA8225F23DFD002D6D5B /* NightscoutStatus.swift in Sources */,
F90692AA274B7AAE0037068D /* HealthKitManager.swift in Sources */,
38887CCE25F5725200944304 /* IOBEntry.swift in Sources */,
Expand Down
2 changes: 1 addition & 1 deletion FreeAPS/Resources/javascript/bundle/determine-basal.js

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion FreeAPS/Sources/APS/OpenAPS/Constants.swift
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@ extension OpenAPS {
static let iob = "monitor/iob.json"
static let cgmState = "monitor/cgm-state.json"
static let podAge = "monitor/pod-age.json"
// static let tdd = "monitor/tdd.json"
static let oref2_variables = "monitor/oref2_variables.json"
static let alertHistory = "monitor/alerthistory.json"
static let statistics = "monitor/statistics.json"
Expand Down Expand Up @@ -86,6 +85,8 @@ extension OpenAPS {
static let uploadedCGMState = "upload/uploaded-cgm-state.json"
static let uploadedPodAge = "upload/uploaded-pod-age.json"
static let uploadedProfile = "upload/uploaded-profile.json"
static let profile = "fetched/profile.json"
static let test = "upload/test.json"
}

enum FreeAPS {
Expand Down
30 changes: 30 additions & 0 deletions FreeAPS/Sources/Localizations/Main/en.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,36 @@ Enact a temp Basal or a temp target */
/* Allow remote control from NS */
"Remote control" = "Remote control";

/* Imported Profiles Alert */
"\nNow please verify all of your new settings thoroughly:\n\n* Basal Settings\n * Carb Ratios\n * Glucose Targets\n * Insulin Sensitivities\n\n in iAPS Settings > Configuration.\n\nBad or invalid profile settings could have disatrous effects." = "\nNow please verify all of your new settings thoroughly:\n\n* Basal Settings\n * Carb Ratios\n * Glucose Targets\n * Insulin Sensitivities\n\n in iAPS Settings > Configuration.\n\nBad or invalid profile settings could have disatrous effects.";

/* Failed Profile Import Alert */
"\nImport failed:\n\n*" = "\nImport failed:\n\n*";

/* Profile Import Alert */
"This will replace some or all of your current pump settings. Are you sure you want to import profile settings from Nightscout?" = "This will replace some or all of your current pump settings. Are you sure you want to import profile settings from Nightscout?";

/* */
"Yes, Import" = "Yes, Import";

/* */
"Import settings from Nightscout" = "Import settings from Nightscout";

/* */
"Import settings?" = "Import settings?";

/* */
"Import from Nightscout" = "Import from Nightscout";

/* */
"Settings imported" = "Settings imported";

/* Import Error */
"Mismatching glucose units in Nightscout and Pump Settings. Import settings aborted." = "Mismatching glucose units in Nightscout and Pump Settings. Import settings aborted.";

/* Import Error */
"Can't find the default Nightscout Profile." = "Can't find the default Nightscout Profile.";

/* Add Medtronic pump */
"Add Medtronic" = "Add Medtronic";

Expand Down
30 changes: 30 additions & 0 deletions FreeAPS/Sources/Localizations/Main/sv.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,36 @@ Enact a temp Basal or a temp target */
/* Allow remote control from NS */
"Remote control" = "Fjärrstyrning";

/* Imported Profiles Alert */
"\nNow please verify all of your new settings thoroughly:\n\n* Basal Settings\n * Carb Ratios\n * Glucose Targets\n * Insulin Sensitivities\n\n in iAPS Settings > Configuration.\n\nBad or invalid profile settings could have disatrous effects." = "\nKontrollera nu alla dina nya pumpinställningar noga:\n\n* Basalinställningar\n * Insulinkvoter\n * Målvärden\n * Insulinkänslighet\n\n i Inställningar > Konfiguration.\n\nDåliga eller ogiltiga pumpinställningar kan gå katastrofala följder.";

/* Failed Profile Import Alert */
"\nImport failed:\n\n*" = "\nImport misslyckades:\n\n*";

/* Profile Import Alert */
"This will replace some or all of your current pump settings. Are you sure you want to import profile settings from Nightscout?" = "Detta kommer att ersätta alla eller vissa av dina pumpinställningar. Är du säker att du vill fortsätta med import av inställningar?";

/* */
"Yes, Import" = "Ja, importera";

/* */
"Import settings from Nightscout" = "Importera inställningar från Nightscout";

/* */
"Import settings?" = "Importera inställningar?";

/* */
"Import from Nightscout" = "Import från Nightscout";

/* */
"Settings imported" = "Inställningar importerade";

/* Import Error */
"Mismatching glucose units in Nightscout and Pump Settings. Import settings aborted." = "Du har olika inställningar för blodsockernehet i Nightscout och iAPS. Import därför avbruten.";

/* Import Error */
"Can't find the default Nightscout Profile." = "Kan inte hitta en profil i Nightcsout";

/* Add Medtronic pump */
"Add Medtronic" = "Lägg till Medtronic";

Expand Down
24 changes: 24 additions & 0 deletions FreeAPS/Sources/Models/FetchedProfile.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import Foundation

struct FetchedNightscoutProfileStore: JSON {
let _id: String
let defaultProfile: String
let startDate: String
let mills: Decimal
let enteredBy: String
let store: [String: ScheduledNightscoutProfile]
let created_at: String
}

struct FetchedNightscoutProfile: JSON {
let dia: Decimal
let carbs_hr: Int
let delay: Decimal
let timezone: String
let target_low: [NightscoutTimevalue]
let target_high: [NightscoutTimevalue]
let sens: [NightscoutTimevalue]
let basal: [NightscoutTimevalue]
let carbratio: [NightscoutTimevalue]
let units: String
}
2 changes: 1 addition & 1 deletion FreeAPS/Sources/Models/NightscoutStatus.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ struct Uploader: JSON {
struct NightscoutTimevalue: JSON {
let time: String
let value: Decimal
let timeAsSeconds: Int
let timeAsSeconds: Int?
}

struct ScheduledNightscoutProfile: JSON {
Expand Down
24 changes: 24 additions & 0 deletions FreeAPS/Sources/Models/RawFetchedProfile.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import Foundation

struct FetchedNightscoutProfileStore: JSON {
let _id: String
let defaultProfile: String
let startDate: String
let mills: Decimal
let enteredBy: String
let store: [String: ScheduledNightscoutProfile]
let created_at: String
}

struct FetchedNightscoutProfile: JSON {
let dia: Decimal
let carbs_hr: Int
let delay: Decimal
let timezone: String
let target_low: [NightscoutTimevalue]
let target_high: [NightscoutTimevalue]
let sens: [NightscoutTimevalue]
let basal: [NightscoutTimevalue]
let carbratio: [NightscoutTimevalue]
let units: String
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import CGMBLEKit
import Combine
import CoreData
import G7SensorKit
import SwiftDate
import SwiftUI
Expand All @@ -11,6 +12,9 @@ extension NightscoutConfig {
@Injected() private var glucoseStorage: GlucoseStorage!
@Injected() private var healthKitManager: HealthKitManager!
@Injected() private var cgmManager: FetchGlucoseManager!
@Injected() private var storage: FileStorage!

let coredataContext = CoreDataStack.shared.persistentContainer.viewContext

@Published var url = ""
@Published var secret = ""
Expand All @@ -22,10 +26,12 @@ extension NightscoutConfig {
@Published var uploadGlucose = true // Upload Glucose
@Published var useLocalSource = false
@Published var localPort: Decimal = 0
@Published var units: GlucoseUnits = .mmolL

override func subscribe() {
url = keychain.getValue(String.self, forKey: Config.urlKey) ?? ""
secret = keychain.getValue(String.self, forKey: Config.secretKey) ?? ""
units = settingsManager.settings.units

subscribeSetting(\.isUploadEnabled, on: $isUploadEnabled) { isUploadEnabled = $0 }
subscribeSetting(\.useLocalGlucoseSource, on: $useLocalSource) { useLocalSource = $0 }
Expand Down Expand Up @@ -68,6 +74,161 @@ extension NightscoutConfig {
.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)
guard let fetchedProfile: ScheduledNightscoutProfile = fetchedProfileStore.first?.store["default"]
else {
error = "Can'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 = "Mismatching glucose units in Nightscout and Pump Settings. Import settings aborted."
group.leave()
return
}

let carbratios = fetchedProfile.carbratio
.map { carbratio -> CarbRatioEntry in
CarbRatioEntry(
start: carbratio.time,
offset: (carbratio.timeAsSeconds ?? self.offset(carbratio.time)) / 60,
ratio: carbratio.value
) }
let carbratiosProfile = CarbRatios(units: CarbUnit.grams, schedule: carbratios)

let basals = fetchedProfile.basal
.map { basal -> BasalProfileEntry in
BasalProfileEntry(
start: basal.time,
minutes: (basal.timeAsSeconds ?? self.offset(basal.time)) / 60,
rate: basal.value
) }

let sensitivities = fetchedProfile.sens.map { sensitivity -> InsulinSensitivityEntry in
InsulinSensitivityEntry(
sensitivity: self.units == .mmolL ? sensitivity.value : sensitivity.value.asMgdL,
offset: (sensitivity.timeAsSeconds ?? self.offset(sensitivity.time)) / 60,
start: sensitivity.time
) }
let sensitivitiesProfile = InsulinSensitivities(
units: self.units,
userPrefferedUnits: self.units,
sensitivities: sensitivities
)

let targets = fetchedProfile.target_low
.map { target -> BGTargetEntry in
BGTargetEntry(
low: self.units == .mmolL ? target.value : target.value.asMgdL,
high: self.units == .mmolL ? target.value : target.value.asMgdL,
start: target.time,
offset: (target.timeAsSeconds ?? self.offset(target.time)) / 60
) }
let targetsProfile = BGTargets(
units: self.units,
userPrefferedUnits: self.units,
targets: targets
)

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)

group.leave()

} catch let parsingError {
print(parsingError)
}
}
}
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))
Expand Down
Loading

0 comments on commit ee787b2

Please sign in to comment.