Skip to content

Commit

Permalink
New Sensor: Device Storage (#681)
Browse files Browse the repository at this point in the history
Tracks primarily percent available of storage space, includes attributes:

- Total space
- Available space
- Available (opportunistic) space
- Available (important) space
  • Loading branch information
zacwest committed Jun 20, 2020
1 parent 5a72a0f commit a90315b
Show file tree
Hide file tree
Showing 5 changed files with 195 additions and 0 deletions.
10 changes: 10 additions & 0 deletions HomeAssistant.xcodeproj/project.pbxproj
Expand Up @@ -19,6 +19,9 @@
113D29E124946EE50014067C /* CLLocationManager+OneShotLocationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 113D29E024946EE50014067C /* CLLocationManager+OneShotLocationTests.swift */; };
113D29E324946F930014067C /* OneShotLocationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 113D29E224946F930014067C /* OneShotLocationManager.swift */; };
113D29E424946F930014067C /* OneShotLocationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 113D29E224946F930014067C /* OneShotLocationManager.swift */; };
119385A4249E8E360097F497 /* WebhookSensor+Storage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 119385A3249E8E360097F497 /* WebhookSensor+Storage.swift */; };
119385A5249E8E360097F497 /* WebhookSensor+Storage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 119385A3249E8E360097F497 /* WebhookSensor+Storage.swift */; };
119385A7249E9F930097F497 /* WebhookSensor+Storage.test.swift in Sources */ = {isa = PBXBuildFile; fileRef = 119385A6249E9F930097F497 /* WebhookSensor+Storage.test.swift */; };
119D765F2492F8FA00183C5F /* UIApplication+BackgroundTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 119D765E2492F8FA00183C5F /* UIApplication+BackgroundTask.swift */; };
11AF4D13249C7E08006C74C0 /* WebhookSensor+Activity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11AF4D10249C7DFD006C74C0 /* WebhookSensor+Activity.swift */; };
11AF4D14249C7E09006C74C0 /* WebhookSensor+Activity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11AF4D10249C7DFD006C74C0 /* WebhookSensor+Activity.swift */; };
Expand Down Expand Up @@ -752,6 +755,8 @@
113D29E024946EE50014067C /* CLLocationManager+OneShotLocationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CLLocationManager+OneShotLocationTests.swift"; sourceTree = "<group>"; };
113D29E224946F930014067C /* OneShotLocationManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OneShotLocationManager.swift; sourceTree = "<group>"; };
1166363289B5E05B7BAB7204 /* Pods_Shared_iOS.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Shared_iOS.framework; sourceTree = BUILT_PRODUCTS_DIR; };
119385A3249E8E360097F497 /* WebhookSensor+Storage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WebhookSensor+Storage.swift"; sourceTree = "<group>"; };
119385A6249E9F930097F497 /* WebhookSensor+Storage.test.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WebhookSensor+Storage.test.swift"; sourceTree = "<group>"; };
119D765E2492F8FA00183C5F /* UIApplication+BackgroundTask.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIApplication+BackgroundTask.swift"; sourceTree = "<group>"; };
11AF4D10249C7DFD006C74C0 /* WebhookSensor+Activity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WebhookSensor+Activity.swift"; sourceTree = "<group>"; };
11AF4D15249C8082006C74C0 /* With.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = With.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1489,6 +1494,7 @@
11AF4D21249C924B006C74C0 /* WebhookSensor+Geocoder.swift */,
11AF4D24249D1931006C74C0 /* WebhookSensor+LastUpdateTrigger.swift */,
11AF4D18249C8253006C74C0 /* WebhookSensor+Pedometer.swift */,
119385A3249E8E360097F497 /* WebhookSensor+Storage.swift */,
);
path = Sensors;
sourceTree = "<group>";
Expand All @@ -1502,6 +1508,7 @@
11AF4D2D249DA5AF006C74C0 /* WebhookSensor+Geocoder.test.swift */,
11CB98C5249DE15B00B05222 /* WebhookSensor+LastUpdateTrigger.test.swift */,
11CB98C7249DE24000B05222 /* WebhookSensor+Pedometer.test.swift */,
119385A6249E9F930097F497 /* WebhookSensor+Storage.test.swift */,
);
path = WebhookSensor;
sourceTree = "<group>";
Expand Down Expand Up @@ -4232,6 +4239,7 @@
B67CE87B22200F220034C1D0 /* RealmDeviceTracker.swift in Sources */,
113D29DF24946EDA0014067C /* CLLocationManager+OneShotLocation.swift in Sources */,
11AF4D17249C8083006C74C0 /* With.swift in Sources */,
119385A5249E8E360097F497 /* WebhookSensor+Storage.swift in Sources */,
B67CE8AD22200F220034C1D0 /* AuthenticationAPI.swift in Sources */,
B67CE88122200F220034C1D0 /* BinarySensor.swift in Sources */,
B67CE8B722200F220034C1D0 /* UIImage+Icons.swift in Sources */,
Expand Down Expand Up @@ -4388,6 +4396,7 @@
B6C091452151F93D00A326DC /* Zone.swift in Sources */,
B6A258482232539900ADD202 /* WebhookUpdateLocation.swift in Sources */,
B6B74CBA2283983800D58A68 /* CLKComplication+Strings.swift in Sources */,
119385A4249E8E360097F497 /* WebhookSensor+Storage.swift in Sources */,
B6624E4F22584F8F00354CDF /* CrashlyticsLogDestination.swift in Sources */,
B6C0913D2151F93D00A326DC /* Automation.swift in Sources */,
B6C0913E2151F93D00A326DC /* BinarySensor.swift in Sources */,
Expand All @@ -4409,6 +4418,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
119385A7249E9F930097F497 /* WebhookSensor+Storage.test.swift in Sources */,
11AF4D2C249D965C006C74C0 /* WebhookSensor+Battery.test.swift in Sources */,
11CB98CD249E637300B05222 /* Version+HA.test.swift in Sources */,
11AF4D2E249DA5AF006C74C0 /* WebhookSensor+Geocoder.test.swift in Sources */,
Expand Down
103 changes: 103 additions & 0 deletions Shared/API/Webhook/Sensors/WebhookSensor+Storage.swift
@@ -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
}
1 change: 1 addition & 0 deletions Shared/API/Webhook/Sensors/WebhookSensor+_All.swift
Expand Up @@ -12,6 +12,7 @@ extension WebhookSensor {
activity(),
pedometer(),
battery(),
storage(),
connectivity(),
geocoder(location: location),
lastUpdate(trigger: trigger)
Expand Down
11 changes: 11 additions & 0 deletions Shared/Common/Structs/Environment.swift
Expand Up @@ -203,6 +203,17 @@ public class Environment {
public lazy var batteryLevel: () -> Int = { Device.current.batteryLevel ?? 0 }
public lazy var batteryState: () -> Device.BatteryState = { Device.current.batteryState ?? .full }
public lazy var isLowPowerMode: () -> Bool = { Device.current.batteryState?.lowPowerMode ?? false }
public lazy var volumes: () -> [URLResourceKey: Int64]? = {
#if os(iOS)
if #available(iOS 11, *) {
return Device.volumes
} else {
return nil
}
#else
return nil
#endif
}
}
public var device = DeviceWrapper()

Expand Down
70 changes: 70 additions & 0 deletions SharedTests/WebhookSensor/WebhookSensor+Storage.test.swift
@@ -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")
}
}

0 comments on commit a90315b

Please sign in to comment.