Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
218 changes: 135 additions & 83 deletions LoopFollow.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

13 changes: 12 additions & 1 deletion LoopFollow/Alarm/Alarm.swift
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,15 @@ struct Alarm: Identifiable, Codable, Equatable {
snoozeDuration = 0
aboveBG = 180
belowBG = 70
case .dbSize:
/// Nightscout's own dbsize plugin warns at 60% and calls 75% urgent
soundFile = .wrongAnswer
threshold = 75
snoozeDuration = 1
/// The database fills over weeks, so there is never a reason to wake someone for it
activeOption = .day
playSoundOption = .day
repeatSoundOption = .day
}
}
}
Expand All @@ -383,7 +392,7 @@ extension AlarmType {
case .iob, .cob, .missedBolus, .futureCarbs, .recBolus:
return .insulin
case .battery, .batteryDrop, .pump, .pumpBattery, .pumpChange,
.sensorChange, .notLooping, .buildExpire:
.sensorChange, .notLooping, .buildExpire, .dbSize:
return .device
case .overrideStart, .overrideEnd, .tempTargetStart, .tempTargetEnd:
return .other
Expand All @@ -410,6 +419,7 @@ extension AlarmType {
case .sensorChange: return "sensor.tag.radiowaves.forward"
case .notLooping: return "circle.slash"
case .buildExpire: return "calendar.badge.exclamationmark"
case .dbSize: return "externaldrive.badge.exclamationmark"
case .overrideStart: return "play.circle"
case .overrideEnd: return "stop.circle"
case .tempTargetStart: return "flag"
Expand Down Expand Up @@ -438,6 +448,7 @@ extension AlarmType {
case .sensorChange: return "Sensor change due."
case .notLooping: return "Loop hasn’t completed."
case .buildExpire: return "Looping-app build expiring."
case .dbSize: return "Nightscout database filling up."
case .overrideStart: return "Override just started."
case .overrideEnd: return "Override ended."
case .tempTargetStart: return "Temp target started."
Expand Down
18 changes: 18 additions & 0 deletions LoopFollow/Alarm/AlarmCondition/DBSizeCondition.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// LoopFollow
// DBSizeCondition.swift

import Foundation

/// Fires when the Nightscout database has filled **≥ threshold percent**
/// of the size limit configured on the site (`DBSIZE_MAX`).
struct DBSizeCondition: AlarmCondition {
static let type: AlarmType = .dbSize
init() {}

func evaluate(alarm: Alarm, data: AlarmData, now _: Date) -> Bool {
guard let limitPercentage = alarm.threshold, limitPercentage > 0 else { return false }
guard let usedPercentage = data.dbSizePercentage else { return false }

return usedPercentage >= limitPercentage
}
}
1 change: 1 addition & 0 deletions LoopFollow/Alarm/AlarmData.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,5 @@ struct AlarmData: Codable {
let latestPumpBattery: Double?
let batteryHistory: [DataStructs.batteryStruct]
let recentCarbs: [CarbSample]
let dbSizePercentage: Double?
}
1 change: 1 addition & 0 deletions LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ struct AlarmEditor: View {
case .batteryDrop: BatteryDropAlarmEditor(alarm: $alarm)
case .missedBolus: MissedBolusAlarmEditor(alarm: $alarm)
case .futureCarbs: FutureCarbsAlarmEditor(alarm: $alarm)
case .dbSize: DBSizeAlarmEditor(alarm: $alarm)
}
}
}
33 changes: 33 additions & 0 deletions LoopFollow/Alarm/AlarmEditing/Editors/DBSizeAlarmEditor.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// LoopFollow
// DBSizeAlarmEditor.swift

import SwiftUI

struct DBSizeAlarmEditor: View {
@Binding var alarm: Alarm

var body: some View {
Group {
InfoBanner(
text: "This warns you when the Nightscout database has filled the percentage you choose. Nightscout measures the used space against its own DBSIZE_MAX setting, which is 496 MiB unless the site owner changed it, so the percentage is only meaningful when that setting matches the real hosting limit.",
alarmType: alarm.type
)

AlarmGeneralSection(alarm: $alarm)

AlarmStepperSection(
header: "Database Size",
footer: "This alerts you when the database reaches or exceeds this percentage of the limit configured on the Nightscout site.",
title: "At or Above",
range: 0 ... 100,
step: 5,
unitLabel: "%",
value: $alarm.threshold
)

AlarmActiveSection(alarm: $alarm)
AlarmAudioSection(alarm: $alarm)
AlarmSnoozeSection(alarm: $alarm)
}
}
}
1 change: 1 addition & 0 deletions LoopFollow/Alarm/AlarmManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ class AlarmManager {
BatteryCondition.self,
BatteryDropCondition.self,
FutureCarbsCondition.self,
DBSizeCondition.self,
]
) {
var dict = [AlarmType: AlarmCondition]()
Expand Down
2 changes: 1 addition & 1 deletion LoopFollow/Alarm/AlarmType/AlarmType+Snooze.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ extension AlarmType {
/// What “unit” we use for snoozeDuration for this alarmType.
var snoozeTimeUnit: TimeUnit {
switch self {
case .buildExpire:
case .buildExpire, .dbSize:
return .day
case .low, .high, .fastDrop, .fastRise,
.missedReading, .notLooping, .missedBolus,
Expand Down
2 changes: 1 addition & 1 deletion LoopFollow/Alarm/AlarmType/AlarmType+SortDirection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ extension AlarmType {
return (direction: .descending,
key: { $0.threshold })

case .sensorChange:
case .sensorChange, .dbSize:
return (direction: .ascending,
key: { $0.threshold })

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ extension AlarmType {
return true
// These are alarms without memory, if they only are acknowledged - they would alarm again immediately
case
.batteryDrop, .missedReading, .notLooping, .battery, .pumpBattery, .buildExpire, .iob, .sensorChange, .pumpChange, .pump:
.batteryDrop, .missedReading, .notLooping, .battery, .pumpBattery, .buildExpire, .iob, .sensorChange, .pumpChange, .pump, .dbSize:
return false
}
}
Expand Down
1 change: 1 addition & 0 deletions LoopFollow/Alarm/AlarmType/AlarmType.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ enum AlarmType: String, CaseIterable, Codable {
case tempTargetStart = "Temp Target Started"
case tempTargetEnd = "Temp Target Ended"
case buildExpire = "Looping app expiration"
case dbSize = "Nightscout Database Size"
}

extension AlarmType {
Expand Down
62 changes: 62 additions & 0 deletions LoopFollow/Controllers/Nightscout/DBSize.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// LoopFollow
// DBSize.swift

import Foundation

/// Response shape of `/api/v2/properties/dbsize`.
///
/// Nightscout's `dbsize` plugin reports the Mongo `dataSize + indexSize` sum
/// against `DBSIZE_MAX` (defaults to 496 MiB when the site admin has not set it).
/// The key is absent when the plugin has not produced a property yet.
struct DBSizeProperties: Codable {
let dbsize: DBSizeData?
}

struct DBSizeData: Codable {
/// Used size, in MiB.
let totalDataSize: Double?
/// Used size as a percentage of `details.maxSize`, floored by the server.
let dataPercentage: Int?
let details: Details?

struct Details: Codable {
/// The configured limit in MiB (`DBSIZE_MAX`).
let maxSize: Double?
let dataSize: Double?
}
}

extension MainViewController {
// NS Database Size Web Call
func webLoadNSDBSize() {
NightscoutUtils.executeRequest(eventType: .dbSize, parameters: [:]) { (result: Result<DBSizeProperties, Error>) in
switch result {
case let .success(properties):
self.updateDBSize(data: properties.dbsize)
case let .failure(error):
LogManager.shared.log(category: .nightscout, message: "webLoadNSDBSize, error: \(error.localizedDescription)")
}
}
}

// NS Database Size Response Processor
func updateDBSize(data: DBSizeData?) {
// Nightscout still reports the property when `db.stats()` failed, but with a zero
// size. Its own pill hides on that, so treat it as "no reading" rather than 0%.
guard let data = data, let usedMiB = data.totalDataSize, usedMiB > 0 else {
infoManager.clearInfoData(type: .dbSize)
Observable.shared.dbSizePercentage.set(nil)
return
}

Observable.shared.dbSizePercentage.set(data.dataPercentage.map(Double.init))

let used = Localizer.formatToLocalizedString(usedMiB, maxFractionDigits: 0, minFractionDigits: 0)

if let percentage = data.dataPercentage {
infoManager.updateInfoData(type: .dbSize, value: "\(used) MiB (\(percentage)%)")
} else {
infoManager.updateInfoData(type: .dbSize, value: "\(used) MiB")
}
}
}
3 changes: 3 additions & 0 deletions LoopFollow/Helpers/NightscoutUtils.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ class NightscoutUtils {
case iage = "Insulin Change"
case temporaryOverride = "Temporary Override"
case temporaryOverrideCancel = "Temporary Override Cancel"
case dbSize

var endpoint: String {
switch self {
Expand All @@ -57,6 +58,8 @@ class NightscoutUtils {
return "/api/v1/devicestatus.json"
case .temporaryOverride, .temporaryOverrideCancel:
return "/api/v2/notifications/loop"
case .dbSize:
return "/api/v2/properties/dbsize"
}
}
}
Expand Down
3 changes: 2 additions & 1 deletion LoopFollow/InfoTable/InfoType.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import Foundation

enum InfoType: Int, CaseIterable {
case iob, cob, basal, override, battery, pump, pumpBattery, sage, cage, recBolus, minMax, carbsToday, autosens, profile, target, isf, carbRatio, updated, tdd, iage
case iob, cob, basal, override, battery, pump, pumpBattery, sage, cage, recBolus, minMax, carbsToday, autosens, profile, target, isf, carbRatio, updated, tdd, iage, dbSize

var name: String {
switch self {
Expand All @@ -28,6 +28,7 @@ enum InfoType: Int, CaseIterable {
case .updated: return "Updated"
case .tdd: return "TDD"
case .iage: return "IAGE"
case .dbSize: return "DB Size"
}
}

Expand Down
2 changes: 2 additions & 0 deletions LoopFollow/Settings/ImportExport/AlarmSelectionView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,8 @@ struct AlarmSelectionRow: View {
return "Temp Target Ended"
case .buildExpire:
return "Looping app expiration"
case .dbSize:
return "Nightscout Database Size"
}
}
}
Expand Down
1 change: 1 addition & 0 deletions LoopFollow/Storage/Observable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ class Observable {
var deviceRecBolus = ObservableValue<Double?>(default: nil)
var deviceBatteryLevel = ObservableValue<Double?>(default: nil)
var pumpBatteryLevel = ObservableValue<Double?>(default: nil)
var dbSizePercentage = ObservableValue<Double?>(default: nil)
var enactedOrSuggested = ObservableValue<TimeInterval?>(default: nil)

var lastSentTOTP = ObservableValue<String?>(default: nil)
Expand Down
3 changes: 2 additions & 1 deletion LoopFollow/Task/AlarmTask.swift
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@ extension MainViewController {
latestBattery: latestBattery,
latestPumpBattery: latestPumpBattery,
batteryHistory: self.deviceBatteryData,
recentCarbs: recentCarbs
recentCarbs: recentCarbs,
dbSizePercentage: Observable.shared.dbSizePercentage.value
)

let finalAlarmData: AlarmData
Expand Down
30 changes: 30 additions & 0 deletions LoopFollow/Task/DBSizeTask.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// LoopFollow
// DBSizeTask.swift

import Foundation

extension MainViewController {
/// Nightscout recomputes the database size on every data load, but the value only moves
/// by a few MiB per day, so a slow poll is enough.
private static let dbSizeInterval: TimeInterval = 6 * 60 * 60

func scheduleDBSizeTask(initialDelay: TimeInterval = 5) {
let firstRun = Date().addingTimeInterval(initialDelay)

TaskScheduler.shared.scheduleTask(id: .dbSize, nextRun: firstRun) { [weak self] in
guard let self = self else { return }
self.dbSizeTaskAction()
}
}

func dbSizeTaskAction() {
guard IsNightscoutEnabled() else {
TaskScheduler.shared.rescheduleTask(id: .dbSize, to: Date().addingTimeInterval(60))
return
}

webLoadNSDBSize()

TaskScheduler.shared.rescheduleTask(id: .dbSize, to: Date().addingTimeInterval(Self.dbSizeInterval))
}
}
1 change: 1 addition & 0 deletions LoopFollow/Task/Task.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,6 @@ extension MainViewController {
scheduleMinAgoTask()
scheduleCalendarTask()
scheduleAlarmTask()
scheduleDBSizeTask()
}
}
1 change: 1 addition & 0 deletions LoopFollow/Task/TaskScheduler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ enum TaskID: CaseIterable {
case calendarWrite
case alarmCheck
case telemetry
case dbSize
}

struct ScheduledTask {
Expand Down
Loading