import Foundation import PromiseKit public class StorageSensor: SensorProvider { public enum StorageError: Error, Equatable { case noData case invalidData case missingData(URLResourceKey) } public let request: SensorProviderRequest public required init(request: SensorProviderRequest) { self.request = request } #if os(watchOS) public func sensors() -> Promise<[WebhookSensor]> { .init(error: StorageError.noData) } #else public func sensors() -> Promise<[WebhookSensor]> { firstly { Promise.value(()) }.map(on: .global(qos: .userInitiated)) { if let volumes = Current.device.volumes(), volumes.isEmpty == false { return volumes } else { throw StorageError.noData } }.map { (volumes: [URLResourceKey: Int64]) -> [WebhookSensor] in try [Self.sensor(for: volumes)] } } private static func sensor(for volumes: [URLResourceKey: Int64]) throws -> WebhookSensor { let sensor = WebhookSensor( name: "Storage", uniqueID: WebhookSensorId.storage.rawValue, icon: .databaseIcon, state: "Unknown" ) let values = try Values(volumes: volumes) sensor.State = values.availablePercent() sensor.UnitOfMeasurement = "% available" sensor.Attributes = [ "Total": values.byteString(for: \.total), "Available": values.byteString(for: \.availableOverall), "Available (Important)": values.byteString(for: \.availableImportant), "Available (Opportunistic)": values.byteString(for: \.availableOpportunistic), ] return sensor } struct Values { let availableOverall: Int64 let availableImportant: Int64 let availableOpportunistic: Int64 let total: Int64 private let formatter = with(ByteCountFormatter()) { $0.allowedUnits = [.useGB, .useMB] $0.countStyle = .file $0.allowsNonnumericFormatting = false $0.formattingContext = .standalone $0.zeroPadsFractionDigits = true } // Custom formatter for consistent locale-independent formatting private let numberFormatter = with(NumberFormatter()) { $0.numberStyle = .decimal $0.minimumFractionDigits = 2 $0.maximumFractionDigits = 2 $0.locale = Locale(identifier: "en_US_POSIX") } init(volumes: [URLResourceKey: Int64]) throws { func value(of key: URLResourceKey) throws -> Int64 { if let value = volumes[key] { return value } else { throw StorageError.missingData(key) } } self.availableOverall = try value(of: .volumeAvailableCapacityKey) self.availableImportant = try value(of: .volumeAvailableCapacityForImportantUsageKey) self.availableOpportunistic = try value(of: .volumeAvailableCapacityForOpportunisticUsageKey) self.total = try value(of: .volumeTotalCapacityKey) guard total > 0 else { throw StorageError.invalidData } } func availablePercent() -> String { precondition(total > 0, "init should prevent this") let percent = Decimal(availableOpportunistic) / Decimal(total) * Decimal(100.0) return String(format: "%.02lf", Double(truncating: percent as NSNumber)) } func byteString(for keyPath: KeyPath) -> String { let bytes = self[keyPath: keyPath] let gb = Double(bytes) / 1_000_000_000.0 // Using decimal GB (1000^3) // Use custom number formatter for consistent locale-independent formatting let formattedNumber = numberFormatter.string(from: NSNumber(value: gb)) ?? String(format: "%.2f", gb) return "\(formattedNumber) GB" } } #endif }