// // ChargingWindowDetector.swift // USB Meter // import Foundation enum ChargingWindowDetector { struct DetectedWindow { let start: Date let end: Date // How much shorter the window is vs total session (0..1). Higher = more trimming needed. let trimRatio: Double } // Power above this threshold counts as "charging activity" (Watts) private static let activityThreshold = 0.05 // A charging segment must last at least this long to be considered real private static let minimumSegmentDuration: TimeInterval = 3 * 60 // Gaps shorter than this between active segments are bridged (e.g. brief wireless drop) private static let mergeGapDuration: TimeInterval = 120 // Padding added before first and after last sample of the detected window private static let padding: TimeInterval = 30 // Only surface detection when active window is shorter than this fraction of total session // e.g. 0.30 means: show banner if active charging < 70% of total session time static let significantTrimThreshold = 0.30 static func detect( samples: [ChargeSessionSampleSummary], sessionStart: Date, sessionEnd: Date ) -> DetectedWindow? { guard !samples.isEmpty else { return nil } let sorted = samples.sorted { $0.timestamp < $1.timestamp } // Build contiguous active segments struct Segment { var start: Date; var end: Date } var segments: [Segment] = [] var segStart: Date? var segEnd: Date? for sample in sorted { if sample.averagePowerWatts >= activityThreshold { if segStart == nil { segStart = sample.timestamp } segEnd = sample.timestamp } else { if let s = segStart, let e = segEnd { segments.append(Segment(start: s, end: e)) } segStart = nil segEnd = nil } } if let s = segStart, let e = segEnd { segments.append(Segment(start: s, end: e)) } guard !segments.isEmpty else { return nil } // Merge segments separated by short gaps var merged: [Segment] = [segments[0]] for seg in segments.dropFirst() { let gap = seg.start.timeIntervalSince(merged[merged.count - 1].end) if gap <= mergeGapDuration { merged[merged.count - 1].end = seg.end } else { merged.append(seg) } } // Filter out short segments let significant = merged.filter { $0.end.timeIntervalSince($0.start) >= minimumSegmentDuration } guard !significant.isEmpty else { return nil } // Pick primary segment: the longest one let primary = significant.max { a, b in a.end.timeIntervalSince(a.start) < b.end.timeIntervalSince(b.start) }! let windowStart = max(sessionStart, primary.start.addingTimeInterval(-padding)) let windowEnd = min(sessionEnd, primary.end.addingTimeInterval(padding)) let sessionDuration = sessionEnd.timeIntervalSince(sessionStart) let windowDuration = windowEnd.timeIntervalSince(windowStart) guard sessionDuration > 0 else { return nil } let trimRatio = 1.0 - (windowDuration / sessionDuration) return DetectedWindow(start: windowStart, end: windowEnd, trimRatio: trimRatio) } }