Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Tracks primarily percent available of storage space, includes attributes: - Total space - Available space - Available (opportunistic) space - Available (important) space
- Loading branch information
Showing
5 changed files
with
195 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,103 @@ | ||
import Foundation | ||
import PromiseKit | ||
import DeviceKit | ||
|
||
extension WebhookSensor { | ||
public enum StorageError: Error, Equatable { | ||
case noData | ||
case invalidData | ||
case missingData(URLResourceKey) | ||
} | ||
|
||
#if os(watchOS) | ||
public static func storage() -> Promise<[WebhookSensor]> { | ||
return .init(error: StorageError.noData) | ||
} | ||
#else | ||
public static func storage() -> Promise<[WebhookSensor]> { | ||
firstly { | ||
Promise<Void>.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 | ||
if #available(iOS 11, *) { | ||
return [ try sensor(for: volumes) ] | ||
} else { | ||
return [] | ||
} | ||
} | ||
} | ||
|
||
@available(iOS 11, *) | ||
private static func sensor(for volumes: [URLResourceKey: Int64]) throws -> WebhookSensor { | ||
let sensor = WebhookSensor( | ||
name: "Storage", | ||
uniqueID: "storage", | ||
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 | ||
} | ||
|
||
@available(iOS 11, *) | ||
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 | ||
} | ||
|
||
init(volumes: [URLResourceKey: Int64]) throws { | ||
func value(of key: URLResourceKey) throws -> Int64 { | ||
if let value = volumes[key] { | ||
return value | ||
} else { | ||
throw StorageError.missingData(key) | ||
} | ||
} | ||
|
||
availableOverall = try value(of: .volumeAvailableCapacityKey) | ||
availableImportant = try value(of: .volumeAvailableCapacityForImportantUsageKey) | ||
availableOpportunistic = try value(of: .volumeAvailableCapacityForOpportunisticUsageKey) | ||
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(availableOverall) / Decimal(total) * Decimal(100.0) | ||
return String(format: "%.02lf", Double(truncating: percent as NSNumber)) | ||
} | ||
|
||
func byteString(for keyPath: KeyPath<Self, Int64>) -> String { | ||
formatter.string(fromByteCount: self[keyPath: keyPath]) | ||
} | ||
} | ||
#endif | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
70 changes: 70 additions & 0 deletions
70
SharedTests/WebhookSensor/WebhookSensor+Storage.test.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
import Foundation | ||
import PromiseKit | ||
import XCTest | ||
@testable import Shared | ||
|
||
@available(iOS 11, *) | ||
class WebhookSensorStorageTests: XCTestCase { | ||
func testNilDataReturnsError() { | ||
Current.device.volumes = { nil } | ||
let promise = WebhookSensor.storage() | ||
XCTAssertThrowsError(try hang(promise)) { error in | ||
XCTAssertEqual(error as? WebhookSensor.StorageError, .noData) | ||
} | ||
} | ||
|
||
func testEmptyDataReturnsError() { | ||
Current.device.volumes = { [:] } | ||
let promise = WebhookSensor.storage() | ||
XCTAssertThrowsError(try hang(promise)) { error in | ||
XCTAssertEqual(error as? WebhookSensor.StorageError, .noData) | ||
} | ||
} | ||
|
||
func testMissingKeyReturnsError() { | ||
Current.device.volumes = { [ | ||
.volumeTotalCapacityKey: 0, | ||
.volumeAvailableCapacityForImportantUsageKey: 100, | ||
.volumeAvailableCapacityForOpportunisticUsageKey: 100 | ||
] } | ||
let promise = WebhookSensor.storage() | ||
XCTAssertThrowsError(try hang(promise)) { error in | ||
XCTAssertEqual(error as? WebhookSensor.StorageError, .missingData(.volumeAvailableCapacityKey)) | ||
} | ||
} | ||
|
||
func testZeroDenominator() { | ||
Current.device.volumes = { [ | ||
.volumeTotalCapacityKey: 0, | ||
.volumeAvailableCapacityKey: 100, | ||
.volumeAvailableCapacityForImportantUsageKey: 100, | ||
.volumeAvailableCapacityForOpportunisticUsageKey: 100 | ||
] } | ||
let promise = WebhookSensor.storage() | ||
XCTAssertThrowsError(try hang(promise)) { error in | ||
XCTAssertEqual(error as? WebhookSensor.StorageError, .invalidData) | ||
} | ||
} | ||
|
||
func testBasicSensor() throws { | ||
Current.device.volumes = { [ | ||
.volumeTotalCapacityKey: 100_000_000_000, | ||
.volumeAvailableCapacityKey: 20_000_000_000, | ||
.volumeAvailableCapacityForImportantUsageKey: 21_000_000_000, | ||
.volumeAvailableCapacityForOpportunisticUsageKey: 22_000_000_000 | ||
] } | ||
let promise = WebhookSensor.storage() | ||
let sensors = try hang(promise) | ||
XCTAssertEqual(sensors.count, 1) | ||
|
||
XCTAssertEqual(sensors[0].Name, "Storage") | ||
XCTAssertEqual(sensors[0].UniqueID, "storage") | ||
XCTAssertEqual(sensors[0].Icon, "mdi:database") | ||
XCTAssertEqual(sensors[0].State as? String, "20.00") | ||
XCTAssertEqual(sensors[0].UnitOfMeasurement, "% available") | ||
XCTAssertEqual(sensors[0].Attributes?["Total"] as? String, "100.00 GB") | ||
XCTAssertEqual(sensors[0].Attributes?["Available"] as? String, "20.00 GB") | ||
XCTAssertEqual(sensors[0].Attributes?["Available (Important)"] as? String, "21.00 GB") | ||
XCTAssertEqual(sensors[0].Attributes?["Available (Opportunistic)"] as? String, "22.00 GB") | ||
} | ||
} |