import Foundation public enum MeasurementSource: String, Codable, CaseIterable, Sendable { case iphoneCameraPPG case appleWatchOptical } public struct WatchHeartRateSampleV1: Codable, Equatable, Sendable, Identifiable { public var id: UUID public var date: Date public var bpm: Double public init(id: UUID = UUID(), date: Date, bpm: Double) { self.id = id self.date = date self.bpm = bpm } } public struct WatchDeviceIdentityV1: Codable, Equatable, Sendable { public var id: String public var name: String public var model: String? public var systemVersion: String? public init(id: String, name: String, model: String?, systemVersion: String?) { self.id = id self.name = name self.model = model self.systemVersion = systemVersion } private enum CodingKeys: String, CodingKey { case id, name, model, systemVersion } public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(id, forKey: .id) try container.encode(name, forKey: .name) if let model { try container.encode(model, forKey: .model) } else { try container.encodeNil(forKey: .model) } if let systemVersion { try container.encode(systemVersion, forKey: .systemVersion) } else { try container.encodeNil(forKey: .systemVersion) } } } public enum WatchMeasurementPosture: String, Codable, CaseIterable, Sendable { case seated case standing case lying case unknown } public struct WatchMeasurementContextV1: Codable, Equatable, Sendable { public var posture: WatchMeasurementPosture public var isAtRest: Bool public var recentCaffeine: Bool public var recentMeal: Bool public var recentExercise: Bool public var note: String? public init( posture: WatchMeasurementPosture = .unknown, isAtRest: Bool = true, recentCaffeine: Bool = false, recentMeal: Bool = false, recentExercise: Bool = false, note: String? = nil ) { self.posture = posture self.isAtRest = isAtRest self.recentCaffeine = recentCaffeine self.recentMeal = recentMeal self.recentExercise = recentExercise self.note = note } public var isBaselineComparable: Bool { isAtRest && !recentCaffeine && !recentMeal && !recentExercise } } public struct WatchMeasurementSummaryV1: Codable, Equatable, Sendable, Identifiable { public static let schemaVersion = 1 public var id: UUID public var updatedAt: Date public var startedAt: Date public var endedAt: Date public var source: MeasurementSource public var device: WatchDeviceIdentityV1 public var currentBPM: Double? public var averageBPM: Double? public var minimumBPM: Double? public var maximumBPM: Double? public var samples: [WatchHeartRateSampleV1] public var coverageSeconds: TimeInterval public var quality: Double public var isValid: Bool public var invalidReasons: [WatchMeasurementInvalidReason] public var context: WatchMeasurementContextV1 public var feeling: Int? public var feelingRecordedAt: Date? public init( id: UUID = UUID(), updatedAt: Date = Date(), startedAt: Date, endedAt: Date, source: MeasurementSource = .appleWatchOptical, device: WatchDeviceIdentityV1, currentBPM: Double?, averageBPM: Double?, minimumBPM: Double?, maximumBPM: Double?, samples: [WatchHeartRateSampleV1], coverageSeconds: TimeInterval, quality: Double, isValid: Bool, invalidReasons: [WatchMeasurementInvalidReason] = [], context: WatchMeasurementContextV1 = .init(), feeling: Int? = nil, feelingRecordedAt: Date? = nil ) { self.id = id self.updatedAt = updatedAt self.startedAt = startedAt self.endedAt = endedAt self.source = source self.device = device self.currentBPM = currentBPM self.averageBPM = averageBPM self.minimumBPM = minimumBPM self.maximumBPM = maximumBPM self.samples = samples self.coverageSeconds = coverageSeconds self.quality = quality self.isValid = isValid self.invalidReasons = invalidReasons self.context = context self.feeling = feeling self.feelingRecordedAt = feelingRecordedAt } private enum CodingKeys: String, CodingKey { case schemaVersion, id, updatedAt, startedAt, endedAt, source, device case currentBpm, averageBpm, minimumBpm, maximumBpm, samples, coverage, quality case valid, invalidReason, context, feeling } private struct WireSample: Codable { let offsetSeconds: TimeInterval let bpm: Double } private struct WireContext: Codable { let atRest: Bool let posture: String? let recentExercise: Bool? let recentCaffeine: Bool? let recentMeal: Bool? } private struct WireFeeling: Codable { let score: Int let recordedAt: Date } public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(Self.schemaVersion, forKey: .schemaVersion) try container.encode(id, forKey: .id) try container.encode(updatedAt, forKey: .updatedAt) try container.encode(startedAt, forKey: .startedAt) try container.encode(endedAt, forKey: .endedAt) try container.encode(source, forKey: .source) try container.encode(device, forKey: .device) if let currentBPM { try container.encode(currentBPM, forKey: .currentBpm) } else { try container.encodeNil(forKey: .currentBpm) } try container.encodeIfPresent(averageBPM, forKey: .averageBpm) try container.encodeIfPresent(minimumBPM, forKey: .minimumBpm) try container.encodeIfPresent(maximumBPM, forKey: .maximumBpm) try container.encode(samples.map { WireSample(offsetSeconds: max(0, $0.date.timeIntervalSince(startedAt)), bpm: $0.bpm) }, forKey: .samples) try container.encode(min(1, max(0, coverageSeconds / 60)), forKey: .coverage) try container.encode(quality, forKey: .quality) try container.encode(isValid, forKey: .valid) if let invalidReason = invalidReasons.first?.rawValue { try container.encode(invalidReason, forKey: .invalidReason) } else { try container.encodeNil(forKey: .invalidReason) } try container.encode(WireContext( atRest: context.isAtRest, posture: context.posture == .unknown ? nil : context.posture.rawValue, recentExercise: context.recentExercise, recentCaffeine: context.recentCaffeine, recentMeal: context.recentMeal ), forKey: .context) if let feeling { try container.encode(WireFeeling(score: feeling, recordedAt: feelingRecordedAt ?? updatedAt), forKey: .feeling) } else { try container.encodeNil(forKey: .feeling) } } public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) guard try container.decode(Int.self, forKey: .schemaVersion) == Self.schemaVersion else { throw DecodingError.dataCorruptedError(forKey: .schemaVersion, in: container, debugDescription: "Unsupported Watch measurement schema") } id = try container.decode(UUID.self, forKey: .id) updatedAt = try container.decode(Date.self, forKey: .updatedAt) startedAt = try container.decode(Date.self, forKey: .startedAt) endedAt = try container.decode(Date.self, forKey: .endedAt) source = try container.decode(MeasurementSource.self, forKey: .source) device = try container.decode(WatchDeviceIdentityV1.self, forKey: .device) currentBPM = try container.decodeIfPresent(Double.self, forKey: .currentBpm) averageBPM = try container.decodeIfPresent(Double.self, forKey: .averageBpm) minimumBPM = try container.decodeIfPresent(Double.self, forKey: .minimumBpm) maximumBPM = try container.decodeIfPresent(Double.self, forKey: .maximumBpm) let wireSamples = try container.decode([WireSample].self, forKey: .samples) let decodedStartedAt = startedAt samples = wireSamples.map { .init(date: decodedStartedAt.addingTimeInterval($0.offsetSeconds), bpm: $0.bpm) } coverageSeconds = try container.decode(Double.self, forKey: .coverage) * 60 quality = try container.decode(Double.self, forKey: .quality) isValid = try container.decode(Bool.self, forKey: .valid) invalidReasons = (try container.decodeIfPresent(String.self, forKey: .invalidReason)) .flatMap(WatchMeasurementInvalidReason.init(rawValue:)).map { [$0] } ?? [] let wireContext = try container.decode(WireContext.self, forKey: .context) context = .init( posture: wireContext.posture.flatMap(WatchMeasurementPosture.init(rawValue:)) ?? .unknown, isAtRest: wireContext.atRest, recentCaffeine: wireContext.recentCaffeine ?? false, recentMeal: wireContext.recentMeal ?? false, recentExercise: wireContext.recentExercise ?? false ) let wireFeeling = try container.decodeIfPresent(WireFeeling.self, forKey: .feeling) feeling = wireFeeling?.score feelingRecordedAt = wireFeeling?.recordedAt } } public enum WatchRegulationSessionState: String, Codable, Sendable { case inProgress case awaitingAfter case completed case cancelled } public struct WatchRegulationSessionV1: Codable, Equatable, Sendable, Identifiable { public static let currentSchemaVersion = 1 public let schemaVersion = 1 public var id: UUID public var protocolID: String public var protocolVersion: Int = 1 public var startedAt: Date public var endedAt: Date public var durationSeconds: TimeInterval public var state: WatchRegulationSessionState public var beforeMeasurementID: UUID? public var afterMeasurementID: UUID? public var feelingBefore: Int? public var feelingAfter: Int? public var healthKitWorkoutID: UUID? public var updatedAt: Date public init( id: UUID = UUID(), protocolID: String, protocolVersion: Int = 1, startedAt: Date, endedAt: Date, durationSeconds: TimeInterval, state: WatchRegulationSessionState, beforeMeasurementID: UUID? = nil, afterMeasurementID: UUID? = nil, feelingBefore: Int? = nil, feelingAfter: Int? = nil, healthKitWorkoutID: UUID? = nil, updatedAt: Date = Date() ) { self.id = id self.protocolID = protocolID self.protocolVersion = protocolVersion self.startedAt = startedAt self.endedAt = endedAt self.durationSeconds = durationSeconds self.state = state self.beforeMeasurementID = beforeMeasurementID self.afterMeasurementID = afterMeasurementID self.feelingBefore = feelingBefore self.feelingAfter = feelingAfter self.healthKitWorkoutID = healthKitWorkoutID self.updatedAt = updatedAt } private enum CodingKeys: String, CodingKey { case schemaVersion, id, protocolID = "protocolId", startedAt, endedAt, durationSeconds, state case beforeMeasurementID = "beforeMeasurementId", afterMeasurementID = "afterMeasurementId" case feelingBefore, feelingAfter, healthKitWorkoutID = "workoutUUID", updatedAt } public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(schemaVersion, forKey: .schemaVersion) try container.encode(id, forKey: .id) try container.encode(protocolID, forKey: .protocolID) try container.encode(startedAt, forKey: .startedAt) try container.encode(endedAt, forKey: .endedAt) try container.encode(durationSeconds, forKey: .durationSeconds) try container.encode(state, forKey: .state) try container.encode(updatedAt, forKey: .updatedAt) if let beforeMeasurementID { try container.encode(beforeMeasurementID, forKey: .beforeMeasurementID) } else { try container.encodeNil(forKey: .beforeMeasurementID) } if let afterMeasurementID { try container.encode(afterMeasurementID, forKey: .afterMeasurementID) } else { try container.encodeNil(forKey: .afterMeasurementID) } if let feelingBefore { try container.encode(feelingBefore, forKey: .feelingBefore) } else { try container.encodeNil(forKey: .feelingBefore) } if let feelingAfter { try container.encode(feelingAfter, forKey: .feelingAfter) } else { try container.encodeNil(forKey: .feelingAfter) } if let healthKitWorkoutID { try container.encode(healthKitWorkoutID, forKey: .healthKitWorkoutID) } else { try container.encodeNil(forKey: .healthKitWorkoutID) } } public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) guard try container.decode(Int.self, forKey: .schemaVersion) == 1 else { throw DecodingError.dataCorruptedError(forKey: .schemaVersion, in: container, debugDescription: "Unsupported Watch session schema") } id = try container.decode(UUID.self, forKey: .id) protocolID = try container.decode(String.self, forKey: .protocolID) protocolVersion = 1 startedAt = try container.decode(Date.self, forKey: .startedAt) endedAt = try container.decode(Date.self, forKey: .endedAt) durationSeconds = try container.decode(TimeInterval.self, forKey: .durationSeconds) state = try container.decode(WatchRegulationSessionState.self, forKey: .state) beforeMeasurementID = try container.decodeIfPresent(UUID.self, forKey: .beforeMeasurementID) afterMeasurementID = try container.decodeIfPresent(UUID.self, forKey: .afterMeasurementID) feelingBefore = try container.decodeIfPresent(Int.self, forKey: .feelingBefore) feelingAfter = try container.decodeIfPresent(Int.self, forKey: .feelingAfter) healthKitWorkoutID = try container.decodeIfPresent(UUID.self, forKey: .healthKitWorkoutID) updatedAt = try container.decode(Date.self, forKey: .updatedAt) } } public struct WatchDeletionTombstoneV1: Codable, Equatable, Sendable, Identifiable { public let schemaVersion = 1 public let id: UUID = UUID() public let entityID: UUID public let entityKind: String public let deletedAt: Date public let updatedAt: Date public init(entityID: UUID, entityKind: String, deletedAt: Date = Date(), updatedAt: Date = Date()) { self.entityID = entityID self.entityKind = entityKind self.deletedAt = deletedAt self.updatedAt = updatedAt } private enum CodingKeys: String, CodingKey { case schemaVersion, entityID = "entityId", entityKind, deletedAt, updatedAt } } public struct WatchPreferencesV1: Codable, Equatable, Sendable { public var schemaVersion = 1 public var saveMindAndBodyWorkouts: Bool public var protocolCatalogVersion: String public var updatedAt: Date public init( saveMindAndBodyWorkouts: Bool = true, protocolCatalogVersion: String = "1.0.0", updatedAt: Date = Date() ) { self.saveMindAndBodyWorkouts = saveMindAndBodyWorkouts self.protocolCatalogVersion = protocolCatalogVersion self.updatedAt = updatedAt } }