OmnipodSettingsViewModel.swift 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659
  1. //
  2. // OmnipodSettingsViewModel.swift
  3. // OmniKit
  4. //
  5. // Created by Pete Schwamb on 3/8/20.
  6. // Copyright © 2021 LoopKit Authors. All rights reserved.
  7. //
  8. import SwiftUI
  9. import LoopKit
  10. import LoopKitUI
  11. import HealthKit
  12. import OmniKit
  13. import Combine
  14. enum OmnipodSettingsViewAlert {
  15. case suspendError(Error)
  16. case resumeError(Error)
  17. case cancelManualBasalError(Error)
  18. case syncTimeError(OmnipodPumpManagerError)
  19. }
  20. struct OmnipodSettingsNotice {
  21. let title: String
  22. let description: String
  23. }
  24. class OmnipodSettingsViewModel: ObservableObject {
  25. @Published var lifeState: PodLifeState
  26. @Published var activatedAt: Date?
  27. @Published var expiresAt: Date?
  28. @Published var beepPreference: BeepPreference
  29. @Published var silencePodPreference: SilencePodPreference
  30. @Published var rileylinkConnected: Bool
  31. var activatedAtString: String {
  32. if let activatedAt = activatedAt {
  33. return dateFormatter.string(from: activatedAt)
  34. } else {
  35. return "—"
  36. }
  37. }
  38. var expiresAtString: String {
  39. if let expiresAt = expiresAt {
  40. return dateFormatter.string(from: expiresAt)
  41. } else {
  42. return "—"
  43. }
  44. }
  45. var serviceTimeRemainingString: String? {
  46. if let serviceTimeRemaining = pumpManager.podServiceTimeRemaining, let serviceTimeRemainingString = timeRemainingFormatter.string(from: serviceTimeRemaining) {
  47. return serviceTimeRemainingString
  48. } else {
  49. return nil
  50. }
  51. }
  52. // Expiration reminder date for current pod
  53. @Published var expirationReminderDate: Date?
  54. var allowedScheduledReminderDates: [Date]? {
  55. return pumpManager.allowedExpirationReminderDates
  56. }
  57. // Hours before expiration
  58. @Published var expirationReminderDefault: Int {
  59. didSet {
  60. self.pumpManager.defaultExpirationReminderOffset = .hours(Double(expirationReminderDefault))
  61. }
  62. }
  63. // Units to alert at
  64. @Published var lowReservoirAlertValue: Int
  65. @Published var basalDeliveryState: PumpManagerStatus.BasalDeliveryState?
  66. @Published var basalDeliveryRate: Double?
  67. @Published var activeAlert: OmnipodSettingsViewAlert? = nil {
  68. didSet {
  69. if activeAlert != nil {
  70. alertIsPresented = true
  71. }
  72. }
  73. }
  74. @Published var alertIsPresented: Bool = false {
  75. didSet {
  76. if !alertIsPresented {
  77. activeAlert = nil
  78. }
  79. }
  80. }
  81. @Published var reservoirLevel: ReservoirLevel?
  82. @Published var reservoirLevelHighlightState: ReservoirLevelHighlightState?
  83. @Published var synchronizingTime: Bool = false
  84. @Published var podCommState: PodCommState
  85. @Published var insulinType: InsulinType?
  86. @Published var podDetails: PodDetails?
  87. @Published var previousPodDetails: PodDetails?
  88. var timeZone: TimeZone {
  89. return pumpManager.status.timeZone
  90. }
  91. var viewTitle: String {
  92. return pumpManager.localizedTitle
  93. }
  94. var isClockOffset: Bool {
  95. return pumpManager.isClockOffset
  96. }
  97. var isPodDataStale: Bool {
  98. return Date().timeIntervalSince(pumpManager.lastSync ?? .distantPast) > .minutes(12)
  99. }
  100. var recoveryText: String? {
  101. if case .fault = podCommState {
  102. return LocalizedString("⚠️ Insulin delivery stopped. Change Pod now.", comment: "The action string on pod status page when pod faulted")
  103. } else if podOk && isPodDataStale {
  104. return LocalizedString("Make sure your phone and pod are close to each other. If communication issues persist, move to a new area.", comment: "The action string on pod status page when pod data is stale")
  105. } else if let serviceTimeRemaining = pumpManager.podServiceTimeRemaining, serviceTimeRemaining <= Pod.serviceDuration - Pod.nominalPodLife {
  106. if let serviceTimeRemainingString = serviceTimeRemainingString {
  107. return String(format: LocalizedString("Change Pod now. Insulin delivery will stop in %1$@ or when no more insulin remains.", comment: "Format string for the action string on pod status page when pod expired. (1: service time remaining)"), serviceTimeRemainingString)
  108. } else {
  109. return LocalizedString("Change Pod now. Insulin delivery will stop 8 hours after the Pod has expired or when no more insulin remains.", comment: "The action string on pod status page when pod expired")
  110. }
  111. } else {
  112. return nil
  113. }
  114. }
  115. var notice: OmnipodSettingsNotice? {
  116. if pumpManager.isClockOffset {
  117. return OmnipodSettingsNotice(
  118. title: LocalizedString("Time Change Detected", comment: "title for time change detected notice"),
  119. description: LocalizedString("The time on your pump is different from the current time. Your pump’s time controls your scheduled therapy settings. Scroll down to Pump Time row to review the time difference and configure your pump.", comment: "description for time change detected notice"))
  120. } else {
  121. return nil
  122. }
  123. }
  124. var isScheduledBasal: Bool {
  125. switch basalDeliveryState {
  126. case .active(_), .initiatingTempBasal:
  127. return true
  128. case .tempBasal(_), .cancelingTempBasal, .suspending, .suspended(_), .resuming, .none:
  129. return false
  130. }
  131. }
  132. let dateFormatter: DateFormatter = {
  133. let dateFormatter = DateFormatter()
  134. dateFormatter.timeStyle = .short
  135. dateFormatter.dateStyle = .medium
  136. dateFormatter.doesRelativeDateFormatting = true
  137. return dateFormatter
  138. }()
  139. let timeFormatter: DateFormatter = {
  140. let dateFormatter = DateFormatter()
  141. dateFormatter.timeStyle = .short
  142. dateFormatter.dateStyle = .none
  143. return dateFormatter
  144. }()
  145. let timeRemainingFormatter: DateComponentsFormatter = {
  146. let dateComponentsFormatter = DateComponentsFormatter()
  147. dateComponentsFormatter.allowedUnits = [.hour, .minute]
  148. dateComponentsFormatter.unitsStyle = .full
  149. dateComponentsFormatter.zeroFormattingBehavior = .dropAll
  150. return dateComponentsFormatter
  151. }()
  152. let basalRateFormatter: NumberFormatter = {
  153. let numberFormatter = NumberFormatter()
  154. numberFormatter.numberStyle = .decimal
  155. numberFormatter.minimumFractionDigits = 1
  156. numberFormatter.minimumIntegerDigits = 1
  157. return numberFormatter
  158. }()
  159. var manualBasalTimeRemaining: TimeInterval? {
  160. if case .tempBasal(let dose) = basalDeliveryState, !(dose.automatic ?? true) {
  161. let remaining = dose.endDate.timeIntervalSinceNow
  162. if remaining > 0 {
  163. return remaining
  164. }
  165. }
  166. return nil
  167. }
  168. let reservoirVolumeFormatter = QuantityFormatter(for: .internationalUnit())
  169. var didFinish: (() -> Void)?
  170. var navigateTo: ((OmnipodUIScreen) -> Void)?
  171. private let pumpManager: OmnipodPumpManager
  172. private lazy var cancellables = Set<AnyCancellable>()
  173. init(pumpManager: OmnipodPumpManager) {
  174. self.pumpManager = pumpManager
  175. lifeState = pumpManager.lifeState
  176. activatedAt = pumpManager.podActivatedAt
  177. expiresAt = pumpManager.expiresAt
  178. basalDeliveryState = pumpManager.status.basalDeliveryState
  179. basalDeliveryRate = self.pumpManager.basalDeliveryRate
  180. reservoirLevel = self.pumpManager.reservoirLevel
  181. reservoirLevelHighlightState = self.pumpManager.reservoirLevelHighlightState
  182. expirationReminderDate = self.pumpManager.scheduledExpirationReminder
  183. expirationReminderDefault = Int(self.pumpManager.defaultExpirationReminderOffset.hours)
  184. lowReservoirAlertValue = Int(self.pumpManager.state.lowReservoirReminderValue)
  185. podCommState = self.pumpManager.podCommState
  186. beepPreference = self.pumpManager.beepPreference
  187. silencePodPreference = self.pumpManager.silencePod ? .enabled : .disabled
  188. insulinType = self.pumpManager.insulinType
  189. podDetails = self.pumpManager.podDetails
  190. previousPodDetails = self.pumpManager.previousPodDetails
  191. // TODO:
  192. rileylinkConnected = false
  193. pumpManager.addPodStateObserver(self, queue: DispatchQueue.main)
  194. pumpManager.addStatusObserver(self, queue: DispatchQueue.main)
  195. // Register for device notifications
  196. NotificationCenter.default.publisher(for: .DeviceConnectionStateDidChange)
  197. .sink { [weak self] _ in
  198. self?.updateConnectionStatus()
  199. }
  200. .store(in: &cancellables)
  201. // Trigger refresh
  202. pumpManager.getPodStatus() { _ in }
  203. updateConnectionStatus()
  204. }
  205. func updateConnectionStatus() {
  206. pumpManager.rileyLinkDeviceProvider.getDevices { (devices) in
  207. DispatchQueue.main.async { [weak self] in
  208. self?.rileylinkConnected = devices.firstConnected != nil
  209. }
  210. }
  211. }
  212. func changeTimeZoneTapped() {
  213. synchronizingTime = true
  214. pumpManager.setTime { (error) in
  215. DispatchQueue.main.async {
  216. self.synchronizingTime = false
  217. self.lifeState = self.pumpManager.lifeState
  218. if let error = error {
  219. self.activeAlert = .syncTimeError(error)
  220. }
  221. }
  222. }
  223. }
  224. func doneTapped() {
  225. self.didFinish?()
  226. }
  227. func stopUsingOmnipodTapped() {
  228. pumpManager.notifyDelegateOfDeactivation {
  229. DispatchQueue.main.async {
  230. self.didFinish?()
  231. }
  232. }
  233. }
  234. func suspendDelivery(duration: TimeInterval) {
  235. pumpManager.suspendDelivery(withSuspendReminders: duration) { (error) in
  236. DispatchQueue.main.async {
  237. if let error = error {
  238. self.activeAlert = .suspendError(error)
  239. }
  240. }
  241. }
  242. }
  243. func resumeDelivery() {
  244. pumpManager.resumeDelivery { (error) in
  245. DispatchQueue.main.async {
  246. if let error = error {
  247. self.activeAlert = .resumeError(error)
  248. }
  249. }
  250. }
  251. }
  252. func runTemporaryBasalProgram(unitsPerHour: Double, for duration: TimeInterval, completion: @escaping (PumpManagerError?) -> Void) {
  253. pumpManager.runTemporaryBasalProgram(unitsPerHour: unitsPerHour, for: duration, automatic: false, completion: completion)
  254. }
  255. func saveScheduledExpirationReminder(_ selectedDate: Date?, _ completion: @escaping (Error?) -> Void) {
  256. if let podExpiresAt = pumpManager.podExpiresAt {
  257. var intervalBeforeExpiration : TimeInterval?
  258. if let selectedDate = selectedDate {
  259. intervalBeforeExpiration = .hours(round(podExpiresAt.timeIntervalSince(selectedDate).hours))
  260. }
  261. pumpManager.updateExpirationReminder(intervalBeforeExpiration) { (error) in
  262. DispatchQueue.main.async {
  263. if error == nil {
  264. self.expirationReminderDate = selectedDate
  265. }
  266. completion(error)
  267. }
  268. }
  269. }
  270. }
  271. func saveLowReservoirReminder(_ selectedValue: Int, _ completion: @escaping (Error?) -> Void) {
  272. pumpManager.updateLowReservoirReminder(selectedValue) { (error) in
  273. DispatchQueue.main.async {
  274. if error == nil {
  275. self.lowReservoirAlertValue = selectedValue
  276. }
  277. completion(error)
  278. }
  279. }
  280. }
  281. func readPodStatus(_ completion: @escaping (_ result: PumpManagerResult<DetailedStatus>) -> Void) {
  282. pumpManager.getDetailedStatus() { (result) in
  283. DispatchQueue.main.async {
  284. completion(result)
  285. }
  286. }
  287. }
  288. func playTestBeeps(_ completion: @escaping (Error?) -> Void) {
  289. pumpManager.playTestBeeps(completion: completion)
  290. }
  291. func readPulseLog(_ completion: @escaping (_ result: Result<String, Error>) -> Void) {
  292. pumpManager.readPulseLog() { (result) in
  293. DispatchQueue.main.async {
  294. completion(result)
  295. }
  296. }
  297. }
  298. func readPulseLogPlus(_ completion: @escaping (_ result: Result<String, Error>) -> Void) {
  299. pumpManager.readPulseLogPlus() { (result) in
  300. DispatchQueue.main.async {
  301. completion(result)
  302. }
  303. }
  304. }
  305. func readActivationTime(_ completion: @escaping (_ result: Result<String, Error>) -> Void) {
  306. pumpManager.readActivationTime() { (result) in
  307. DispatchQueue.main.async {
  308. completion(result)
  309. }
  310. }
  311. }
  312. func readTriggeredAlerts(_ completion: @escaping (_ result: Result<String, Error>) -> Void) {
  313. pumpManager.readTriggeredAlerts() { (result) in
  314. DispatchQueue.main.async {
  315. completion(result)
  316. }
  317. }
  318. }
  319. func pumpManagerDetails(_ completion: @escaping (_ result: String) -> Void) {
  320. completion(pumpManager.debugDescription)
  321. }
  322. func setConfirmationBeeps(_ preference: BeepPreference, _ completion: @escaping (_ error: LocalizedError?) -> Void) {
  323. pumpManager.setConfirmationBeeps(newPreference: preference) { error in
  324. DispatchQueue.main.async {
  325. if error == nil {
  326. self.beepPreference = preference
  327. }
  328. completion(error)
  329. }
  330. }
  331. }
  332. func setSilencePod(_ silencePodPreference: SilencePodPreference, _ completion: @escaping (_ error: LocalizedError?) -> Void) {
  333. pumpManager.setSilencePod(silencePod: silencePodPreference == .enabled) { error in
  334. DispatchQueue.main.async {
  335. if error == nil {
  336. self.silencePodPreference = silencePodPreference
  337. }
  338. completion(error)
  339. }
  340. }
  341. }
  342. func didChangeInsulinType(_ newType: InsulinType?) {
  343. self.pumpManager.insulinType = newType
  344. }
  345. var podOk: Bool {
  346. guard basalDeliveryState != nil else { return false }
  347. switch podCommState {
  348. case .noPod, .activating, .deactivating, .fault:
  349. return false
  350. default:
  351. return true
  352. }
  353. }
  354. var noPod: Bool {
  355. return podCommState == .noPod
  356. }
  357. var podError: String? {
  358. switch podCommState {
  359. case .fault(let status):
  360. switch status.faultEventCode.faultType {
  361. case .reservoirEmpty:
  362. return LocalizedString("No Insulin", comment: "Error message for reservoir view when reservoir empty")
  363. case .exceededMaximumPodLife80Hrs:
  364. return LocalizedString("Pod Expired", comment: "Error message for reservoir view when pod expired")
  365. case .occluded, .occlusionCheckStartup1, .occlusionCheckStartup2, .occlusionCheckTimeouts1, .occlusionCheckTimeouts2, .occlusionCheckTimeouts3, .occlusionCheckPulseIssue, .occlusionCheckBolusProblem, .occlusionCheckAboveThreshold, .occlusionCheckValueTooHigh:
  366. return LocalizedString("Pod Occlusion", comment: "Error message for reservoir view when pod occlusion checks failed")
  367. default:
  368. return String(format: LocalizedString("Pod Fault %1$03d", comment: "Error message for reservoir view during general pod fault: (1: fault code value)"), status.faultEventCode.rawValue)
  369. }
  370. case .active:
  371. if isPodDataStale {
  372. return LocalizedString("Signal Loss", comment: "Error message for reservoir view during general pod fault")
  373. } else {
  374. return nil
  375. }
  376. default:
  377. return nil
  378. }
  379. }
  380. func reservoirText(for level: ReservoirLevel) -> String {
  381. switch level {
  382. case .aboveThreshold:
  383. let quantity = HKQuantity(unit: .internationalUnit(), doubleValue: Pod.maximumReservoirReading)
  384. let thresholdString = reservoirVolumeFormatter.string(from: quantity, for: .internationalUnit(), includeUnit: false) ?? ""
  385. let unitString = reservoirVolumeFormatter.string(from: .internationalUnit(), forValue: Pod.maximumReservoirReading, avoidLineBreaking: true)
  386. return String(format: LocalizedString("%1$@+ %2$@", comment: "Format string for reservoir level above max measurable threshold. (1: measurable reservoir threshold) (2: units)"),
  387. thresholdString, unitString)
  388. case .valid(let value):
  389. let quantity = HKQuantity(unit: .internationalUnit(), doubleValue: value)
  390. return reservoirVolumeFormatter.string(from: quantity, for: .internationalUnit()) ?? ""
  391. }
  392. }
  393. var suspendResumeActionText: String {
  394. let defaultText = LocalizedString("Suspend Insulin Delivery", comment: "Text for suspend resume button when insulin delivery active")
  395. guard podOk else {
  396. return defaultText
  397. }
  398. switch basalDeliveryState {
  399. case .suspending:
  400. return LocalizedString("Suspending insulin delivery...", comment: "Text for suspend resume button when insulin delivery is suspending")
  401. case .suspended:
  402. return LocalizedString("Resume Insulin Delivery", comment: "Text for suspend resume button when insulin delivery is suspended")
  403. case .resuming:
  404. return LocalizedString("Resuming insulin delivery...", comment: "Text for suspend resume button when insulin delivery is resuming")
  405. default:
  406. return defaultText
  407. }
  408. }
  409. var basalTransitioning: Bool {
  410. switch basalDeliveryState {
  411. case .suspending, .resuming:
  412. return true
  413. default:
  414. return false
  415. }
  416. }
  417. func suspendResumeButtonColor(guidanceColors: GuidanceColors) -> Color {
  418. guard podOk else {
  419. return Color.secondary
  420. }
  421. switch basalDeliveryState {
  422. case .suspending, .resuming:
  423. return Color.secondary
  424. case .suspended:
  425. return guidanceColors.warning
  426. default:
  427. return .accentColor
  428. }
  429. }
  430. func suspendResumeActionColor() -> Color {
  431. guard podOk else {
  432. return Color.secondary
  433. }
  434. switch basalDeliveryState {
  435. case .suspending, .resuming:
  436. return Color.secondary
  437. default:
  438. return Color.accentColor
  439. }
  440. }
  441. var isSuspendedOrResuming: Bool {
  442. switch basalDeliveryState {
  443. case .suspended, .resuming:
  444. return true
  445. default:
  446. return false
  447. }
  448. }
  449. public var allowedTempBasalRates: [Double] {
  450. return Pod.supportedTempBasalRates.filter { $0 <= pumpManager.state.maximumTempBasalRate }
  451. }
  452. }
  453. extension OmnipodSettingsViewModel: PodStateObserver {
  454. func podStateDidUpdate(_ state: PodState?) {
  455. lifeState = self.pumpManager.lifeState
  456. basalDeliveryRate = self.pumpManager.basalDeliveryRate
  457. reservoirLevel = self.pumpManager.reservoirLevel
  458. activatedAt = state?.activatedAt
  459. expiresAt = state?.expiresAt
  460. reservoirLevelHighlightState = self.pumpManager.reservoirLevelHighlightState
  461. expirationReminderDate = self.pumpManager.scheduledExpirationReminder
  462. podCommState = self.pumpManager.podCommState
  463. beepPreference = self.pumpManager.beepPreference
  464. insulinType = self.pumpManager.insulinType
  465. podDetails = self.pumpManager.podDetails
  466. previousPodDetails = self.pumpManager.previousPodDetails
  467. }
  468. func podConnectionStateDidChange(isConnected: Bool) {
  469. self.rileylinkConnected = isConnected
  470. }
  471. }
  472. extension OmnipodSettingsViewModel: PumpManagerStatusObserver {
  473. func pumpManager(_ pumpManager: PumpManager, didUpdate status: PumpManagerStatus, oldStatus: PumpManagerStatus) {
  474. basalDeliveryState = self.pumpManager.status.basalDeliveryState
  475. }
  476. }
  477. extension OmnipodPumpManager {
  478. var lifeState: PodLifeState {
  479. switch podCommState {
  480. case .fault(let status):
  481. switch status.faultEventCode.faultType {
  482. case .exceededMaximumPodLife80Hrs:
  483. return .expired
  484. default:
  485. let remaining = Pod.nominalPodLife - (status.faultEventTimeSinceActivation ?? Pod.nominalPodLife)
  486. let podTimeUntilReminder = remaining - (state.scheduledExpirationReminderOffset ?? 0)
  487. if remaining > 0 {
  488. return .timeRemaining(timeUntilExpiration: remaining, timeUntilExpirationReminder: podTimeUntilReminder)
  489. } else {
  490. return .expired
  491. }
  492. }
  493. case .noPod:
  494. return .noPod
  495. case .activating:
  496. return .podActivating
  497. case .deactivating:
  498. return .podDeactivating
  499. case .active:
  500. if let podTimeRemaining = podTimeRemaining {
  501. if podTimeRemaining > 0 {
  502. let podTimeUntilReminder = podTimeRemaining - (state.scheduledExpirationReminderOffset ?? 0)
  503. return .timeRemaining(timeUntilExpiration: podTimeRemaining, timeUntilExpirationReminder: podTimeUntilReminder)
  504. } else {
  505. return .expired
  506. }
  507. } else {
  508. return .podDeactivating
  509. }
  510. }
  511. }
  512. var basalDeliveryRate: Double? {
  513. if let tempBasal = state.podState?.unfinalizedTempBasal, !tempBasal.isFinished() {
  514. return tempBasal.rate
  515. } else {
  516. switch state.podState?.suspendState {
  517. case .resumed:
  518. var calendar = Calendar(identifier: .gregorian)
  519. calendar.timeZone = state.timeZone
  520. return state.basalSchedule.currentRate(using: calendar, at: dateGenerator())
  521. case .suspended, .none:
  522. return nil
  523. }
  524. }
  525. }
  526. fileprivate var podServiceTimeRemaining : TimeInterval? {
  527. guard let podTimeRemaining = podTimeRemaining else {
  528. return nil;
  529. }
  530. return max(0, Pod.serviceDuration - Pod.nominalPodLife + podTimeRemaining);
  531. }
  532. private func podDetails(fromPodState podState: PodState) -> PodDetails {
  533. return PodDetails(
  534. lotNumber: podState.lot,
  535. sequenceNumber: podState.tid,
  536. piVersion: podState.piVersion,
  537. pmVersion: podState.pmVersion,
  538. totalDelivery: podState.lastInsulinMeasurements?.delivered,
  539. lastStatus: podState.lastInsulinMeasurements?.validTime,
  540. fault: podState.fault?.faultEventCode,
  541. activatedAt: podState.activatedAt,
  542. activeTime: podState.activeTime,
  543. pdmRef: podState.fault?.pdmRef
  544. )
  545. }
  546. public var podDetails: PodDetails? {
  547. guard let podState = state.podState else {
  548. return nil
  549. }
  550. return podDetails(fromPodState: podState)
  551. }
  552. public var previousPodDetails: PodDetails? {
  553. guard let podState = state.previousPodState else {
  554. return nil
  555. }
  556. return podDetails(fromPodState: podState)
  557. }
  558. }