iOS/Sources/Shared/API/Webhook/Sensors/SensorContainer.swift

227 lines
7.5 KiB
Swift

import CoreLocation
import Foundation
import HAKit
import PromiseKit
import Version
public struct SensorObserverUpdate {
public let sensors: Guarantee<[WebhookSensor]>
public let on: Date
init(sensors: Guarantee<[WebhookSensor]>) {
self.sensors = sensors
self.on = Current.date()
}
}
public enum SensorContainerUpdateReason {
case settingsChange
case signal
}
public protocol SensorObserver: AnyObject {
func sensorContainer(
_ container: SensorContainer,
didUpdate update: SensorObserverUpdate
)
func sensorContainer(
_ container: SensorContainer,
didSignalForUpdateBecause reason: SensorContainerUpdateReason
)
}
public struct SensorResponse {
/// The sensors that require update
public let sensors: [WebhookSensor]
fileprivate init(sensors: [WebhookSensor]) {
self.sensors = sensors
}
}
public class SensorContainer {
private var providers = [SensorProvider.Type]()
private var observers = NSHashTable<AnyObject>(options: .weakMemory)
private var providerDependencies: SensorProviderDependencies
init() {
self.providerDependencies = SensorProviderDependencies()
providerDependencies.updateSignalHandler = { [weak self] type in
self?.updateSignaled(from: type)
}
}
public func register(provider: SensorProvider.Type) {
providers.append(provider)
}
public func register(observer: SensorObserver) {
observers.add(observer)
if let lastUpdate {
observer.sensorContainer(self, didUpdate: lastUpdate)
}
}
public func unregister(observer: SensorObserver) {
observers.remove(observer)
}
private var disabledSensorIDs: Set<String> {
get {
Set(Current.settingsStore.prefs.object(forKey: "disabledSensors") as? [String] ?? [])
}
set {
Current.settingsStore.prefs.set(Array(newValue), forKey: "disabledSensors")
notifySignal(reason: .settingsChange)
}
}
public func isEnabled(sensor: WebhookSensor) -> Bool {
guard let id = sensor.UniqueID else { return false }
return !disabledSensorIDs.contains(id)
}
public func isAllowedToSend(sensor: WebhookSensor, for server: Server) -> Bool {
guard isEnabled(sensor: sensor) else { return false }
switch server.info.setting(for: .sensorPrivacy) {
case .all: return true
case .none: return false
}
}
public func setEnabled(_ value: Bool, for sensor: WebhookSensor) {
guard let id = sensor.UniqueID else { return }
if value {
disabledSensorIDs.remove(id)
} else {
disabledSensorIDs.insert(id)
}
}
private var lastUpdate: SensorObserverUpdate? {
didSet {
guard let lastUpdate else { return }
observers
.allObjects
.compactMap { $0 as? SensorObserver }
.forEach { $0.sensorContainer(self, didUpdate: lastUpdate) }
}
}
private struct LastSentSensors {
private var value = [String: WebhookSensor]()
var sensors: AnyCollection<WebhookSensor> {
AnyCollection(value.values)
}
private func combined(
with sensors: [WebhookSensor],
ignoringKeys: Set<String>
) -> [String: WebhookSensor] {
sensors.reduce(into: value) { result, sensor in
if let uniqueID = sensor.UniqueID, !ignoringKeys.contains(uniqueID) {
result[uniqueID] = sensor
}
}
}
mutating func combine(with sensors: [WebhookSensor], ignoringExisting: Bool) {
let keys = ignoringExisting ? Set(value.keys) : Set()
value = combined(with: sensors, ignoringKeys: keys)
}
}
private var lastSentSensors: HAProtected<LastSentSensors> = .init(value: .init())
func sensors(
reason: SensorProviderRequest.Reason,
limitedTo: [SensorProvider.Type]? = nil,
location: CLLocation? = nil,
server: Server
) -> Guarantee<SensorResponse> {
let request = SensorProviderRequest(
reason: reason,
dependencies: providerDependencies,
location: location,
serverVersion: server.info.version
)
let generatedSensors = firstly {
let promises = providers
.filter { providerType in
if let limitedTo {
return limitedTo.contains(where: { ObjectIdentifier($0) == ObjectIdentifier(providerType) })
} else {
return true
}
}
.map { providerType in providerType.init(request: request) }
.map { provider in provider.sensors().map { ($0, provider) } }
return when(resolved: promises)
}.map { (sensors: [Result<([WebhookSensor], SensorProvider)>]) -> [WebhookSensor] in
// now that we are done, we don't need to keep a strong reference to the provider instance anymore
sensors.compactMap { (result: Result<([WebhookSensor], SensorProvider)>) -> [WebhookSensor]? in
if case let .fulfilled(value) = result {
return value.0
} else {
return nil
}
}.flatMap { $0 }
}
lastUpdate = .init(sensors: generatedSensors.map { [lastSentSensors] new in
// doesn't store the sent values, that happens when the network request ends
// this is just what's presented to the user, so we always have the latest version
let ignoringExisting: Bool
switch request.reason {
case .registration:
// we may want to show sensor settings, so allow even registration-focused data to populate
// however, we don't allow any registration values to override existing ones
ignoringExisting = true
case .trigger:
ignoringExisting = false
}
return lastSentSensors.mutate { lastSentSensors -> AnyCollection<WebhookSensor> in
lastSentSensors.combine(with: new, ignoringExisting: ignoringExisting)
return lastSentSensors.sensors
}.sorted(by: { [weak self] lhs, rhs in
guard let self else { return true }
switch (isEnabled(sensor: lhs), isEnabled(sensor: rhs)) {
case (true, true): return lhs < rhs
case (false, false): return lhs < rhs
case (true, false): return true
case (false, true): return false
}
})
})
return generatedSensors.mapValues { [weak self] sensor -> WebhookSensor in
guard let self else { return sensor }
if isAllowedToSend(sensor: sensor, for: server) {
return sensor
} else {
return WebhookSensor(redacting: sensor)
}
}.map(SensorResponse.init(sensors:))
}
private func notifySignal(reason: SensorContainerUpdateReason) {
observers
.allObjects
.compactMap { $0 as? SensorObserver }
.forEach { $0.sensorContainer(self, didSignalForUpdateBecause: reason) }
}
private func updateSignaled(from type: SensorProvider.Type) {
Current.Log.info("live update triggering from \(type)")
notifySignal(reason: .signal)
}
}