1 contributor
753 lines | 26.909kb
//
//  DataStore.swift
//  USB Meter
//
//  Created by Bogdan Timofte on 03/03/2020.
//  Copyright © 2020 Bogdan Timofte. All rights reserved.
//

import SwiftUI
import Combine
import CoreBluetooth
import CoreData
import UserNotifications

final class AppData : ObservableObject {
    struct MeterSummary: Identifiable {
        let macAddress: String
        let displayName: String
        let modelSummary: String
        let advertisedName: String?
        let lastSeen: Date?
        let lastConnected: Date?
        let meter: Meter?

        var id: String {
            macAddress
        }
    }

    private var bluetoothManagerNotification: AnyCancellable?
    private var meterStoreObserver: AnyCancellable?
    private var meterStoreCloudObserver: AnyCancellable?
    private var chargeInsightsStoreObserver: AnyCancellable?
    private var chargeInsightsRemoteObserver: AnyCancellable?
    private let meterStore = MeterNameStore.shared
    private var chargeInsightsStore: ChargeInsightsStore?
    private let chargeNotificationCoordinator = ChargeNotificationCoordinator()

    init() {
        bluetoothManagerNotification = bluetoothManager.objectWillChange.sink { [weak self] _ in
            self?.scheduleObjectWillChange()
        }
        meterStoreObserver = NotificationCenter.default.publisher(for: .meterNameStoreDidChange)
            .receive(on: DispatchQueue.main)
            .sink { [weak self] _ in
                self?.refreshMeterMetadata()
            }
        meterStoreCloudObserver = NotificationCenter.default.publisher(for: .meterNameStoreCloudStatusDidChange)
            .receive(on: DispatchQueue.main)
            .sink { [weak self] _ in
                self?.scheduleObjectWillChange()
            }
    }

    let bluetoothManager = BluetoothManager()

    @Published var enableRecordFeature: Bool = true

    @Published var meters: [UUID:Meter] = [UUID:Meter]()
    @Published private(set) var chargedDevices: [ChargedDeviceSummary] = []

    var deviceSummaries: [ChargedDeviceSummary] {
        chargedDevices.filter { !$0.isCharger }
    }

    var chargerSummaries: [ChargedDeviceSummary] {
        chargedDevices.filter { $0.isCharger }
    }

    var cloudAvailability: MeterNameStore.CloudAvailability {
        meterStore.currentCloudAvailability
    }

    func activateChargeInsights(context: NSManagedObjectContext) {
        guard chargeInsightsStore == nil else {
            return
        }

        context.automaticallyMergesChangesFromParent = true
        context.mergePolicy = NSMergeByPropertyStoreTrumpMergePolicy
        chargeInsightsStore = ChargeInsightsStore(context: context)

        chargeInsightsStoreObserver = NotificationCenter.default.publisher(
            for: .NSManagedObjectContextObjectsDidChange,
            object: context
        )
        .receive(on: DispatchQueue.main)
        .sink { [weak self] _ in
            self?.reloadChargedDevices()
        }

        chargeInsightsRemoteObserver = NotificationCenter.default.publisher(
            for: .NSPersistentStoreRemoteChange,
            object: nil
        )
        .receive(on: DispatchQueue.main)
        .sink { [weak self] _ in
            self?.reloadChargedDevices()
        }

        chargeNotificationCoordinator.ensureAuthorizationIfNeeded()
        reloadChargedDevices()
    }

    func meterName(for macAddress: String) -> String? {
        meterStore.name(for: macAddress)
    }

    func setMeterName(_ name: String, for macAddress: String) {
        meterStore.upsert(macAddress: macAddress, name: name, temperatureUnitRawValue: nil)
    }

    func temperatureUnitPreference(for macAddress: String) -> TemperatureUnitPreference {
        let rawValue = meterStore.temperatureUnitRawValue(for: macAddress) ?? TemperatureUnitPreference.celsius.rawValue
        return TemperatureUnitPreference(rawValue: rawValue) ?? .celsius
    }

    func setTemperatureUnitPreference(_ preference: TemperatureUnitPreference, for macAddress: String) {
        meterStore.upsert(macAddress: macAddress, name: nil, temperatureUnitRawValue: preference.rawValue)
    }

    func registerMeter(macAddress: String, modelName: String?, advertisedName: String?) {
        meterStore.registerMeter(macAddress: macAddress, modelName: modelName, advertisedName: advertisedName)
    }

    func noteMeterSeen(at date: Date, macAddress: String) {
        meterStore.noteLastSeen(date, for: macAddress)
    }

    func noteMeterConnected(at date: Date, macAddress: String) {
        meterStore.noteLastConnected(date, for: macAddress)
    }

    func lastSeen(for macAddress: String) -> Date? {
        meterStore.lastSeen(for: macAddress)
    }

    func lastConnected(for macAddress: String) -> Date? {
        meterStore.lastConnected(for: macAddress)
    }

    func chargedDeviceSummary(id: UUID) -> ChargedDeviceSummary? {
        chargedDevices.first(where: { $0.id == id })
    }

    func chargedDevices(for meterMACAddress: String) -> [ChargedDeviceSummary] {
        let normalizedMAC = meterMACAddress.trimmingCharacters(in: .whitespacesAndNewlines).uppercased()
        return chargedDevices.filter { chargedDevice in
            guard chargedDevice.isCharger == false else {
                return false
            }
            return chargedDevice.lastAssociatedMeterMAC == normalizedMAC
                || chargedDevice.sessions.contains(where: { $0.meterMACAddress == normalizedMAC })
        }
    }

    func chargers(for meterMACAddress: String) -> [ChargedDeviceSummary] {
        let normalizedMAC = meterMACAddress.trimmingCharacters(in: .whitespacesAndNewlines).uppercased()
        return chargedDevices.filter { chargedDevice in
            guard chargedDevice.isCharger else {
                return false
            }
            return chargedDevice.lastAssociatedMeterMAC == normalizedMAC
                || chargedDevice.sessions.contains(where: { $0.meterMACAddress == normalizedMAC })
        }
    }

    func currentChargedDeviceSummary(for meterMACAddress: String) -> ChargedDeviceSummary? {
        let normalizedMAC = meterMACAddress.trimmingCharacters(in: .whitespacesAndNewlines).uppercased()

        if let activeSession = activeChargeSessionSummary(for: normalizedMAC),
           let liveDevice = chargedDevices.first(where: {
               $0.id == activeSession.chargedDeviceID && $0.isCharger == false
           }) {
            return liveDevice
        }

        return chargedDevices.first(where: {
            $0.isCharger == false && $0.lastAssociatedMeterMAC == normalizedMAC
        })
    }

    func currentChargerSummary(for meterMACAddress: String) -> ChargedDeviceSummary? {
        let normalizedMAC = meterMACAddress.trimmingCharacters(in: .whitespacesAndNewlines).uppercased()

        if let activeSession = activeChargeSessionSummary(for: normalizedMAC),
           let chargerID = activeSession.chargerID,
           let liveCharger = chargedDevices.first(where: {
               $0.id == chargerID && $0.isCharger
           }) {
            return liveCharger
        }

        return chargedDevices.first(where: {
            $0.isCharger && $0.lastAssociatedMeterMAC == normalizedMAC
        })
    }

    func activeChargeSessionSummary(for meterMACAddress: String) -> ChargeSessionSummary? {
        chargeInsightsStore?.activeChargeSessionSummary(forMeterMACAddress: meterMACAddress)
    }

    @discardableResult
    func createChargedDevice(
        name: String,
        deviceClass: ChargedDeviceClass,
        supportsChargingWhileOff: Bool,
        supportsWiredCharging: Bool,
        supportsWirelessCharging: Bool,
        preferredChargingTransportMode: ChargingTransportMode,
        wirelessChargingProfile: WirelessChargingProfile,
        wiredChargeCompletionCurrentAmps: Double?,
        wirelessChargeCompletionCurrentAmps: Double?,
        notes: String?,
        meterMACAddress: String?
    ) -> Bool {
        let didSave = chargeInsightsStore?.createChargedDevice(
            name: name,
            deviceClass: deviceClass,
            supportsChargingWhileOff: supportsChargingWhileOff,
            supportsWiredCharging: supportsWiredCharging,
            supportsWirelessCharging: supportsWirelessCharging,
            preferredChargingTransportMode: preferredChargingTransportMode,
            wirelessChargingProfile: wirelessChargingProfile,
            wiredChargeCompletionCurrentAmps: wiredChargeCompletionCurrentAmps,
            wirelessChargeCompletionCurrentAmps: wirelessChargeCompletionCurrentAmps,
            notes: notes,
            assignTo: meterMACAddress
        ) ?? false

        if didSave {
            reloadChargedDevices()
        }

        return didSave
    }

    @discardableResult
    func updateChargedDevice(
        id: UUID,
        name: String,
        deviceClass: ChargedDeviceClass,
        supportsChargingWhileOff: Bool,
        supportsWiredCharging: Bool,
        supportsWirelessCharging: Bool,
        preferredChargingTransportMode: ChargingTransportMode,
        wirelessChargingProfile: WirelessChargingProfile,
        wiredChargeCompletionCurrentAmps: Double?,
        wirelessChargeCompletionCurrentAmps: Double?,
        notes: String?
    ) -> Bool {
        let didSave = chargeInsightsStore?.updateChargedDevice(
            id: id,
            name: name,
            deviceClass: deviceClass,
            supportsChargingWhileOff: supportsChargingWhileOff,
            supportsWiredCharging: supportsWiredCharging,
            supportsWirelessCharging: supportsWirelessCharging,
            preferredChargingTransportMode: preferredChargingTransportMode,
            wirelessChargingProfile: wirelessChargingProfile,
            wiredChargeCompletionCurrentAmps: wiredChargeCompletionCurrentAmps,
            wirelessChargeCompletionCurrentAmps: wirelessChargeCompletionCurrentAmps,
            notes: notes
        ) ?? false

        if didSave {
            reloadChargedDevices()
        }

        return didSave
    }

    @discardableResult
    func setChargingTransportMode(_ chargingTransportMode: ChargingTransportMode, for meter: Meter) -> Bool {
        let didSave = chargeInsightsStore?.setChargingTransportMode(
            chargingTransportMode,
            for: meter.btSerial.macAddress.description
        ) ?? false

        if didSave {
            reloadChargedDevices()
        }

        return didSave
    }

    @discardableResult
    func assignChargedDevice(_ chargedDeviceID: UUID, to meterMACAddress: String) -> Bool {
        let didSave = chargeInsightsStore?.assignChargedDevice(id: chargedDeviceID, to: meterMACAddress) ?? false
        if didSave {
            reloadChargedDevices()
        }
        return didSave
    }

    @discardableResult
    func assignCharger(_ chargerID: UUID, to meterMACAddress: String) -> Bool {
        let didSave = chargeInsightsStore?.assignCharger(id: chargerID, to: meterMACAddress) ?? false
        if didSave {
            reloadChargedDevices()
        }
        return didSave
    }

    @discardableResult
    func ensureChargeSession(for meter: Meter) -> Bool {
        guard let snapshot = meter.chargingMonitorSnapshot else {
            return false
        }

        let didSave = chargeInsightsStore?.ensureSession(for: snapshot, forceStart: true) ?? false
        if didSave {
            reloadChargedDevices()
        }
        return didSave
    }

    func observeChargeSnapshot(from meter: Meter, observedAt: Date = Date()) {
        guard let snapshot = meter.chargingMonitorSnapshot(at: observedAt) else {
            return
        }

        if chargeInsightsStore?.observe(snapshot: snapshot) == true {
            reloadChargedDevices()
        }
    }

    @discardableResult
    func addBatteryCheckpoint(percent: Double, label: String?, for meter: Meter) -> Bool {
        let didSave = chargeInsightsStore?.addBatteryCheckpoint(
            percent: percent,
            label: label,
            for: meter.btSerial.macAddress.description
        ) ?? false

        if didSave {
            reloadChargedDevices()
        }

        return didSave
    }

    @discardableResult
    func addBatteryCheckpoint(percent: Double, label: String?, for sessionID: UUID) -> Bool {
        let didSave = chargeInsightsStore?.addBatteryCheckpoint(
            percent: percent,
            label: label,
            for: sessionID
        ) ?? false

        if didSave {
            reloadChargedDevices()
        }

        return didSave
    }

    @discardableResult
    func flushChargeInsights() -> Bool {
        let didSave = chargeInsightsStore?.flushPendingChanges() ?? false
        reloadChargedDevices()
        return didSave
    }

    @discardableResult
    func setTargetBatteryPercent(_ percent: Double?, for meter: Meter) -> Bool {
        guard let activeSession = activeChargeSessionSummary(for: meter.btSerial.macAddress.description) else {
            return false
        }
        return setTargetBatteryPercent(percent, for: activeSession.id)
    }

    @discardableResult
    func setTargetBatteryPercent(_ percent: Double?, for sessionID: UUID) -> Bool {
        if percent != nil {
            chargeNotificationCoordinator.ensureAuthorizationIfNeeded()
        }

        let didSave = chargeInsightsStore?.setTargetBatteryPercent(percent, for: sessionID) ?? false
        if didSave {
            reloadChargedDevices()
        }
        return didSave
    }

    @discardableResult
    func confirmChargeSessionCompletion(sessionID: UUID) -> Bool {
        let didSave = chargeInsightsStore?.confirmCompletion(for: sessionID) ?? false
        if didSave {
            reloadChargedDevices()
        }
        return didSave
    }

    @discardableResult
    func continueChargeSessionMonitoring(sessionID: UUID) -> Bool {
        let didSave = chargeInsightsStore?.continueMonitoringDespiteCompletionContradiction(for: sessionID) ?? false
        if didSave {
            reloadChargedDevices()
        }
        return didSave
    }

    @discardableResult
    func deleteChargeSession(sessionID: UUID) -> Bool {
        let deletedSession = chargedDevices
            .flatMap(\.sessions)
            .first(where: { $0.id == sessionID })

        let didDelete = chargeInsightsStore?.deleteChargeSession(id: sessionID) ?? false
        guard didDelete else {
            return false
        }

        if deletedSession?.status == .active,
           let meterMACAddress = deletedSession?.meterMACAddress,
           let liveMeter = meter(for: meterMACAddress) {
            liveMeter.resetChargeRecord()
        }

        reloadChargedDevices()
        return true
    }

    @discardableResult
    func deleteChargedDevice(id: UUID) -> Bool {
        let deletedDevice = chargedDeviceSummary(id: id)
        let didDelete = chargeInsightsStore?.deleteChargedDevice(id: id) ?? false
        guard didDelete else {
            return false
        }

        if deletedDevice?.isCharger == false,
           deletedDevice?.activeSession?.status == .active,
           let meterMACAddress = deletedDevice?.activeSession?.meterMACAddress,
           let liveMeter = meter(for: meterMACAddress) {
            liveMeter.resetChargeRecord()
        }

        reloadChargedDevices()
        return true
    }

    @discardableResult
    func createKnownMeter(
        macAddress: String,
        customName: String?,
        modelName: String,
        advertisedName: String?
    ) -> Bool {
        let normalizedMAC = Self.normalizedMACAddress(macAddress)
        guard Self.isValidMACAddress(normalizedMAC) else {
            return false
        }

        registerMeter(macAddress: normalizedMAC, modelName: modelName, advertisedName: advertisedName)
        if let customName = customName?.trimmingCharacters(in: .whitespacesAndNewlines), !customName.isEmpty {
            setMeterName(customName, for: normalizedMAC)
        }
        noteMeterSeen(at: Date(), macAddress: normalizedMAC)
        return true
    }

    @discardableResult
    func deleteMeter(macAddress: String) -> Bool {
        let normalizedMAC = Self.normalizedMACAddress(macAddress)
        guard Self.isValidMACAddress(normalizedMAC) else {
            return false
        }

        for meter in meters.values where meter.btSerial.macAddress.description == normalizedMAC {
            meter.disconnect()
        }
        meters = meters.filter { element in
            element.value.btSerial.macAddress.description != normalizedMAC
        }

        let didDelete = meterStore.remove(macAddress: normalizedMAC)
        if didDelete {
            scheduleObjectWillChange()
        }
        return didDelete
    }

    func restoreChargeMonitoringStateIfNeeded(for meter: Meter) {
        guard let activeSession = activeChargeSessionSummary(for: meter.btSerial.macAddress.description) else {
            return
        }
        meter.restoreChargeMonitoringIfNeeded(from: activeSession)
    }

    var meterSummaries: [MeterSummary] {
        let liveMetersByMAC = Dictionary(uniqueKeysWithValues: meters.values.map { ($0.btSerial.macAddress.description, $0) })
        let recordsByMAC = Dictionary(uniqueKeysWithValues: meterStore.allRecords().map { ($0.macAddress, $0) })
        let macAddresses = Set(recordsByMAC.keys).union(liveMetersByMAC.keys)

        return macAddresses.map { macAddress in
            let liveMeter = liveMetersByMAC[macAddress]
            let record = recordsByMAC[macAddress]

            return MeterSummary(
                macAddress: macAddress,
                displayName: Self.friendlyDisplayName(liveMeter: liveMeter, record: record),
                modelSummary: liveMeter?.deviceModelSummary ?? record?.modelName ?? "Meter",
                advertisedName: liveMeter?.modelString ?? record?.advertisedName,
                lastSeen: liveMeter?.lastSeen ?? record?.lastSeen,
                lastConnected: liveMeter?.lastConnectedAt ?? record?.lastConnected,
                meter: liveMeter
            )
        }
        .sorted { lhs, rhs in
            if lhs.meter != nil && rhs.meter == nil {
                return true
            }
            if lhs.meter == nil && rhs.meter != nil {
                return false
            }
            let byName = lhs.displayName.localizedCaseInsensitiveCompare(rhs.displayName)
            if byName != .orderedSame {
                return byName == .orderedAscending
            }
            return lhs.macAddress < rhs.macAddress
        }
    }

    private func scheduleObjectWillChange() {
        DispatchQueue.main.async { [weak self] in
            self?.objectWillChange.send()
        }
    }

    private func reloadChargedDevices() {
        chargedDevices = chargeInsightsStore?.fetchChargedDeviceSummaries() ?? []
        chargeNotificationCoordinator.process(chargedDevices: deviceSummaries)
        for meter in meters.values {
            restoreChargeMonitoringStateIfNeeded(for: meter)
        }
    }

    private func meter(for meterMACAddress: String) -> Meter? {
        meters.values.first { meter in
            meter.btSerial.macAddress.description == meterMACAddress
        }
    }

    private func refreshMeterMetadata() {
        DispatchQueue.main.async { [weak self] in
            guard let self else { return }
            var didUpdateAnyMeter = false
            for meter in self.meters.values {
                let mac = meter.btSerial.macAddress.description
                let displayName = self.meterName(for: mac) ?? mac
                if meter.name != displayName {
                    meter.updateNameFromStore(displayName)
                    didUpdateAnyMeter = true
                }

                let previousTemperaturePreference = meter.tc66TemperatureUnitPreference
                meter.reloadTemperatureUnitPreference()
                if meter.tc66TemperatureUnitPreference != previousTemperaturePreference {
                    didUpdateAnyMeter = true
                }
            }

            if didUpdateAnyMeter {
                self.scheduleObjectWillChange()
            }
        }
    }
}

extension AppData.MeterSummary {
    var tint: Color {
        switch modelSummary {
        case "UM25C":
            return .blue
        case "UM34C":
            return .yellow
        case "TC66C":
            return Model.TC66C.color
        default:
            return .secondary
        }
    }
}

extension AppData {
    static func friendlyDisplayName(liveMeter: Meter?, record: MeterNameStore.Record?) -> String {
        if let liveName = liveMeter?.name.trimmingCharacters(in: .whitespacesAndNewlines), !liveName.isEmpty {
            return liveName
        }
        if let customName = record?.customName {
            return customName
        }
        if let advertisedName = record?.advertisedName {
            return advertisedName
        }
        if let recordModel = record?.modelName {
            return recordModel
        }
        if let liveModel = liveMeter?.deviceModelSummary {
            return liveModel
        }
        return "Meter"
    }

    static func normalizedMACAddress(_ macAddress: String) -> String {
        macAddress
            .trimmingCharacters(in: .whitespacesAndNewlines)
            .uppercased()
    }

    static func isValidMACAddress(_ macAddress: String) -> Bool {
        macAddress.range(
            of: #"^[0-9A-F]{2}(:[0-9A-F]{2}){5}$"#,
            options: .regularExpression
        ) != nil
    }
}

private final class ChargeNotificationCoordinator {
    private struct Payload {
        let id: String
        let title: String
        let body: String
        let threadIdentifier: String
    }

    private let notificationCenter = UNUserNotificationCenter.current()
    private let deliveredIDsDefaultsKey = "ChargeNotificationCoordinator.deliveredIDs"
    private let eventRecencyWindow: TimeInterval = 24 * 60 * 60
    private var inFlightEventIDs: Set<String> = []

    func ensureAuthorizationIfNeeded() {
        notificationCenter.getNotificationSettings { [weak self] settings in
            guard settings.authorizationStatus == .notDetermined else {
                return
            }

            self?.notificationCenter.requestAuthorization(options: [.alert, .badge, .sound]) { _, error in
                if let error {
                    track("Notification authorization request failed: \(error.localizedDescription)")
                }
            }
        }
    }

    func process(chargedDevices: [ChargedDeviceSummary]) {
        let now = Date()
        let pendingPayloads = chargedDevices.flatMap { chargedDevice in
            payloads(for: chargedDevice, now: now)
        }

        for payload in pendingPayloads {
            scheduleIfNeeded(payload)
        }
    }

    private func payloads(for chargedDevice: ChargedDeviceSummary, now: Date) -> [Payload] {
        chargedDevice.sessions.compactMap { session in
            if let triggeredAt = session.targetBatteryAlertTriggeredAt,
               now.timeIntervalSince(triggeredAt) <= eventRecencyWindow,
               let targetBatteryPercent = session.targetBatteryPercent {
                let estimatedPercent = chargedDevice.batteryLevelPrediction(for: session)?.predictedPercent
                    ?? session.endBatteryPercent
                    ?? targetBatteryPercent

                return Payload(
                    id: "target-\(session.id.uuidString)-\(Int(triggeredAt.timeIntervalSince1970))",
                    title: "Battery target reached",
                    body: "\(chargedDevice.name) reached about \(estimatedPercent.format(decimalDigits: 0))% on \(session.meterName ?? "USB Meter") (target \(targetBatteryPercent.format(decimalDigits: 0))%).",
                    threadIdentifier: session.id.uuidString
                )
            }

            if session.requiresCompletionConfirmation,
               let requestedAt = session.completionConfirmationRequestedAt,
               now.timeIntervalSince(requestedAt) <= eventRecencyWindow {
                let estimatedPercent = session.completionContradictionPercent
                    ?? chargedDevice.batteryLevelPrediction(for: session)?.predictedPercent
                let bodyPrefix = "\(chargedDevice.name) may have stopped too early on \(session.meterName ?? "USB Meter")."
                let detail = estimatedPercent.map {
                    " Estimated battery is only \($0.format(decimalDigits: 0))%."
                } ?? ""

                return Payload(
                    id: "completion-\(session.id.uuidString)-\(Int(requestedAt.timeIntervalSince1970))",
                    title: "Confirm charge completion",
                    body: bodyPrefix + detail,
                    threadIdentifier: session.id.uuidString
                )
            }

            return nil
        }
    }

    private func scheduleIfNeeded(_ payload: Payload) {
        guard deliveredEventIDs().contains(payload.id) == false else {
            return
        }

        guard inFlightEventIDs.contains(payload.id) == false else {
            return
        }

        inFlightEventIDs.insert(payload.id)

        notificationCenter.getNotificationSettings { [weak self] settings in
            guard let self else { return }
            guard settings.authorizationStatus == .authorized || settings.authorizationStatus == .provisional else {
                DispatchQueue.main.async {
                    self.inFlightEventIDs.remove(payload.id)
                }
                return
            }

            let content = UNMutableNotificationContent()
            content.title = payload.title
            content.body = payload.body
            content.sound = .default
            content.threadIdentifier = payload.threadIdentifier

            let request = UNNotificationRequest(
                identifier: payload.id,
                content: content,
                trigger: nil
            )

            self.notificationCenter.add(request) { error in
                DispatchQueue.main.async {
                    self.inFlightEventIDs.remove(payload.id)
                    if let error {
                        track("Failed scheduling local notification: \(error.localizedDescription)")
                        return
                    }
                    self.storeDeliveredEventID(payload.id)
                }
            }
        }
    }

    private func deliveredEventIDs() -> Set<String> {
        let values = UserDefaults.standard.stringArray(forKey: deliveredIDsDefaultsKey) ?? []
        return Set(values)
    }

    private func storeDeliveredEventID(_ id: String) {
        var values = deliveredEventIDs()
        values.insert(id)
        UserDefaults.standard.set(Array(values), forKey: deliveredIDsDefaultsKey)
    }
}