Skip to content

Commit

Permalink
Periodic update app entities instead of subscribing to state updates (#…
Browse files Browse the repository at this point in the history
…3231)

<!-- Thank you for submitting a Pull Request and helping to improve Home
Assistant. Please complete the following sections to help the processing
and review of your changes. Please do not delete anything from this
template. -->

## Summary
<!-- Provide a brief summary of the changes you have made and most
importantly what they aim to achieve -->
This PR will change the approach of how the app keeps it's data updated
by not subscribing to all state changes and only periodic updating it's
app entities instead.
This will avoid several issues where users experience their app to
freeze due to 9k entities updating every second and notifying the app.

## Screenshots
<!-- If this is a user-facing change not in the frontend, please include
screenshots in light and dark mode. -->

## Link to pull request in Documentation repository
<!-- Pull requests that add, change or remove functionality must have a
corresponding pull request in the Companion App Documentation repository
(https://github.com/home-assistant/companion.home-assistant). Please add
the number of this pull request after the "#" -->
Documentation: home-assistant/companion.home-assistant#

## Any other notes
<!-- If there is any other information of note, like if this Pull
Request is part of a bigger change, please include it here. -->
  • Loading branch information
bgoncal authored Dec 5, 2024
1 parent 36d4dac commit f2c3526
Show file tree
Hide file tree
Showing 11 changed files with 106 additions and 56 deletions.
18 changes: 12 additions & 6 deletions HomeAssistant.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -427,8 +427,8 @@
11ED43A027279AFA00B5FD45 /* OnboardingAuthLoginImpl.test.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11ED439F27279AFA00B5FD45 /* OnboardingAuthLoginImpl.test.swift */; };
11EE9B4624C4E01500404AF8 /* SharedPlist.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11EE9B4524C4E01500404AF8 /* SharedPlist.swift */; };
11EE9B4724C4E01500404AF8 /* SharedPlist.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11EE9B4524C4E01500404AF8 /* SharedPlist.swift */; };
11EE9B4924C5116F00404AF8 /* ModelManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11EE9B4824C5116F00404AF8 /* ModelManager.swift */; };
11EE9B4A24C5116F00404AF8 /* ModelManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11EE9B4824C5116F00404AF8 /* ModelManager.swift */; };
11EE9B4924C5116F00404AF8 /* LegacyModelManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11EE9B4824C5116F00404AF8 /* LegacyModelManager.swift */; };
11EE9B4A24C5116F00404AF8 /* LegacyModelManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11EE9B4824C5116F00404AF8 /* LegacyModelManager.swift */; };
11EE9B4C24C5181A00404AF8 /* ModelManager.test.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11EE9B4B24C5181A00404AF8 /* ModelManager.test.swift */; };
11EE9B4E24C6089800404AF8 /* RealmPersistable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11EE9B4D24C6089800404AF8 /* RealmPersistable.swift */; };
11EE9B4F24C6089800404AF8 /* RealmPersistable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11EE9B4D24C6089800404AF8 /* RealmPersistable.swift */; };
Expand Down Expand Up @@ -570,6 +570,8 @@
421B1C182BD6524E001ED18C /* WidgetsSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 421B1C172BD6524E001ED18C /* WidgetsSettingsViewModel.swift */; };
421B1C1A2BD65255001ED18C /* WidgetsSettingsView+build.swift in Sources */ = {isa = PBXBuildFile; fileRef = 421B1C192BD65255001ED18C /* WidgetsSettingsView+build.swift */; };
421B1C1D2BD65C04001ED18C /* View+ConditionalModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 421B1C1B2BD65BFA001ED18C /* View+ConditionalModifier.swift */; };
4221ED352D009EF700BAE3EB /* PeriodicAppEntitiesModelUpdater.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4221ED332D009BD000BAE3EB /* PeriodicAppEntitiesModelUpdater.swift */; };
4221ED362D009EF700BAE3EB /* PeriodicAppEntitiesModelUpdater.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4221ED332D009BD000BAE3EB /* PeriodicAppEntitiesModelUpdater.swift */; };
42266B112B740E4C00E94A71 /* BarcodeScannerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42266B102B740E4C00E94A71 /* BarcodeScannerView.swift */; };
42266B252B7A4BA900E94A71 /* BarcodeScannerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42266B242B7A4BA900E94A71 /* BarcodeScannerViewModel.swift */; };
422E25ED2C7FF28900256D87 /* ControlScriptsValueProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 422E25EC2C7FF28900256D87 /* ControlScriptsValueProvider.swift */; };
Expand Down Expand Up @@ -1742,7 +1744,7 @@
11ED439B2726600000B5FD45 /* OnboardingAuthStepSensors.test.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingAuthStepSensors.test.swift; sourceTree = "<group>"; };
11ED439F27279AFA00B5FD45 /* OnboardingAuthLoginImpl.test.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingAuthLoginImpl.test.swift; sourceTree = "<group>"; };
11EE9B4524C4E01500404AF8 /* SharedPlist.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SharedPlist.swift; sourceTree = "<group>"; };
11EE9B4824C5116F00404AF8 /* ModelManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModelManager.swift; sourceTree = "<group>"; };
11EE9B4824C5116F00404AF8 /* LegacyModelManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyModelManager.swift; sourceTree = "<group>"; };
11EE9B4B24C5181A00404AF8 /* ModelManager.test.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModelManager.test.swift; sourceTree = "<group>"; };
11EE9B4D24C6089800404AF8 /* RealmPersistable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RealmPersistable.swift; sourceTree = "<group>"; };
11EE9B5324C62EB300404AF8 /* RealmScene.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RealmScene.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1845,6 +1847,7 @@
421B1C172BD6524E001ED18C /* WidgetsSettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetsSettingsViewModel.swift; sourceTree = "<group>"; };
421B1C192BD65255001ED18C /* WidgetsSettingsView+build.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WidgetsSettingsView+build.swift"; sourceTree = "<group>"; };
421B1C1B2BD65BFA001ED18C /* View+ConditionalModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+ConditionalModifier.swift"; sourceTree = "<group>"; };
4221ED332D009BD000BAE3EB /* PeriodicAppEntitiesModelUpdater.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PeriodicAppEntitiesModelUpdater.swift; sourceTree = "<group>"; };
42266B102B740E4C00E94A71 /* BarcodeScannerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BarcodeScannerView.swift; sourceTree = "<group>"; };
42266B242B7A4BA900E94A71 /* BarcodeScannerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BarcodeScannerViewModel.swift; sourceTree = "<group>"; };
422E25EC2C7FF28900256D87 /* ControlScriptsValueProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ControlScriptsValueProvider.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -4852,6 +4855,7 @@
isa = PBXGroup;
children = (
D00302BD20D4BEDB004C2CA9 /* Environment.swift */,
4221ED332D009BD000BAE3EB /* PeriodicAppEntitiesModelUpdater.swift */,
D03D893A20E0B2E300D4F28D /* AppConstants.swift */,
1101568624D7712F009424C9 /* TagManagerProtocol.swift */,
11C8E8AB24F36535003E7F89 /* DeviceWrapper.swift */,
Expand Down Expand Up @@ -5029,7 +5033,7 @@
B62CD2A4225B099C008DF3C5 /* WebhookSensor.swift */,
B6EE36A120CF593E001494E3 /* RealmZone.swift */,
4297ADA42C89C43F00790812 /* AppEntitiesModel.swift */,
11EE9B4824C5116F00404AF8 /* ModelManager.swift */,
11EE9B4824C5116F00404AF8 /* LegacyModelManager.swift */,
11EE9B4D24C6089800404AF8 /* RealmPersistable.swift */,
11EE9B5324C62EB300404AF8 /* RealmScene.swift */,
B6B6B14E215B6866003DE2DD /* WatchComplication.swift */,
Expand Down Expand Up @@ -7099,7 +7103,7 @@
11AF4D1D249C8AA0006C74C0 /* BatterySensor.swift in Sources */,
B67CE8A922200F220034C1D0 /* SettingsStore.swift in Sources */,
11AF4D26249D1931006C74C0 /* LastUpdateSensor.swift in Sources */,
11EE9B4A24C5116F00404AF8 /* ModelManager.swift in Sources */,
11EE9B4A24C5116F00404AF8 /* LegacyModelManager.swift in Sources */,
119A7E0E2529769A00D7000D /* UIImageView+UIActivityIndicator.swift in Sources */,
42DEDA9B2C5B926400E9D29D /* AppVersionSensor.swift in Sources */,
B67CE8B622200F220034C1D0 /* UIColor+HA.swift in Sources */,
Expand All @@ -7116,6 +7120,7 @@
424151FD2CD8F27100D7A6F9 /* CarPlayConfig.swift in Sources */,
113A8D4A283C7B1700B9DA32 /* PeriodicUpdateManager.swift in Sources */,
4264906C2C0F1B60002155CC /* AssistChatItem.swift in Sources */,
4221ED362D009EF700BAE3EB /* PeriodicAppEntitiesModelUpdater.swift in Sources */,
424151FA2CD8EF2200D7A6F9 /* MagicItem+Migration.swift in Sources */,
42070EED2BAC523F0031E96F /* AssistService.swift in Sources */,
B6872E642226841400C475D1 /* MobileAppRegistrationRequest.swift in Sources */,
Expand Down Expand Up @@ -7351,7 +7356,7 @@
B6B74CBD228399AB00D58A68 /* Action.swift in Sources */,
11CB98CA249E62E700B05222 /* Version+HA.swift in Sources */,
420F53EA2C4E9D54003C8415 /* WidgetsKind.swift in Sources */,
11EE9B4924C5116F00404AF8 /* ModelManager.swift in Sources */,
11EE9B4924C5116F00404AF8 /* LegacyModelManager.swift in Sources */,
42CE8FB62B46D14C00C707F9 /* FrontendStrings+Values.swift in Sources */,
D0C3DC142134CD4E000C9EE1 /* CMMotion+StringExtensions.swift in Sources */,
B6872E662226842100C475D1 /* MobileAppRegistrationResponse.swift in Sources */,
Expand Down Expand Up @@ -7404,6 +7409,7 @@
113E73102518457C004006D8 /* LocalizedManager.swift in Sources */,
111D295624F30E2400C8A7D1 /* Updater.swift in Sources */,
11B38EE5275C54A200205C7B /* SendLocationIntentHandler.swift in Sources */,
4221ED352D009EF700BAE3EB /* PeriodicAppEntitiesModelUpdater.swift in Sources */,
1120C57F274638330046C38B /* PerServerContainer.swift in Sources */,
42FCCFD62B9B195D0057783F /* Image+SharedAssets.swift in Sources */,
426D9C742C9C60B000F278AF /* ControlEntityProvider.swift in Sources */,
Expand Down
3 changes: 3 additions & 0 deletions Sources/App/Scenes/WebViewSceneDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ final class WebViewSceneDelegate: NSObject, UIWindowSceneDelegate {
func sceneDidEnterBackground(_ scene: UIScene) {
DataWidgetsUpdater.update()
Current.modelManager.unsubscribe()
Current.periodicAppEntitiesUpdater().stop()
}

func sceneWillEnterForeground(_ scene: UIScene) {
Expand All @@ -118,6 +119,8 @@ final class WebViewSceneDelegate: NSObject, UIWindowSceneDelegate {
Current.modelManager.subscribe(isAppInForeground: {
UIApplication.shared.applicationState == .active
})
Current.periodicAppEntitiesUpdater().setup()
Current.periodicAppEntitiesUpdater().updateAppEntities()
}

func windowScene(
Expand Down
6 changes: 6 additions & 0 deletions Sources/App/Settings/SettingsViewController.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Communicator
import Eureka
import HAKit
import PromiseKit
import Shared

Expand Down Expand Up @@ -156,6 +157,11 @@ class SettingsViewController: HAFormViewController {
<<< SettingsRootDataSource.Row.whatsNew.row
}

override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
Current.periodicAppEntitiesUpdater().updateAppEntities()
}

@objc func openAbout(_ sender: UIButton) {
let aboutView = AboutViewController()

Expand Down
2 changes: 1 addition & 1 deletion Sources/Shared/API/Models/AppEntitiesModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ public protocol AppEntitiesModelProtocol {
func updateModel(_ entities: Set<HAEntity>, server: Server)
}

public final class AppEntitiesModel: AppEntitiesModelProtocol {
final class AppEntitiesModel: AppEntitiesModelProtocol {
static var shared = AppEntitiesModel()
/// ServerId: Date
private var lastDatabaseUpdate: [String: Date] = [:]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,44 +3,15 @@ import HAKit
import PromiseKit
import RealmSwift

public class ModelManager: ServerObserver {
// Legacy manager which was previously used to handle all model updates and cleanup.
// Now it is used just for zones and legacy iOS Actions
public class LegacyModelManager: ServerObserver {
private var notificationTokens = [NotificationToken]()
private var hakitTokens = [HACancellable]()
private var subscribedSubscriptions = [SubscribeDefinition]()
private var cleanupDefinitions = [CleanupDefinition]()

private static var includedDomains: [Domain] = {
// Mac does not need all domains given it does not have all features as iOS (CarPlay, Watch)
#if targetEnvironment(macCatalyst)
[
.cover,
.light,
.scene,
.script,
.switch,
.sensor,
.binarySensor,
.zone,
.person,
]
#else
[
.button,
.cover,
.inputBoolean,
.inputButton,
.light,
.lock,
.scene,
.script,
.switch,
.sensor,
.binarySensor,
.zone,
.person,
]
#endif
}()
private static var includedDomains: [Domain] = [.zone, .scene, .person]

public var workQueue: DispatchQueue = .global(qos: .userInitiated)
static var isAppInForeground: () -> Bool = { false }
Expand Down Expand Up @@ -226,7 +197,7 @@ public class ModelManager: ServerObserver {
_ connection: HAConnection,
_ server: Server,
_ queue: DispatchQueue,
_ modelManager: ModelManager
_ modelManager: LegacyModelManager
) -> [HACancellable]

static func states<
Expand All @@ -245,7 +216,7 @@ public class ModelManager: ServerObserver {
if server.info.version > .canSubscribeEntitiesChangesWithFilter {
filter = [
"include": [
"domains": ModelManager.includedDomains.map(\.rawValue),
"domains": LegacyModelManager.includedDomains.map(\.rawValue),
],
]
}
Expand All @@ -258,9 +229,7 @@ public class ModelManager: ServerObserver {
return
}
DispatchQueue.main.async {
guard ModelManager.isAppInForeground() else { return }
Current.appEntitiesModel().updateModel(value.all, server: server)

guard LegacyModelManager.isAppInForeground() else { return }
if let lastUpdate {
// Prevent sequential updates in short time
guard Date().timeIntervalSince(lastUpdate) > 15 else { return }
Expand Down Expand Up @@ -289,7 +258,7 @@ public class ModelManager: ServerObserver {
definitions: [SubscribeDefinition] = SubscribeDefinition.defaults,
isAppInForeground: @escaping () -> Bool
) {
ModelManager.isAppInForeground = isAppInForeground
LegacyModelManager.isAppInForeground = isAppInForeground
Current.servers.add(observer: self)

subscribedSubscriptions.removeAll()
Expand All @@ -312,7 +281,7 @@ public class ModelManager: ServerObserver {
public var update: (
_ api: HomeAssistantAPI,
_ queue: DispatchQueue,
_ modelManager: ModelManager
_ modelManager: LegacyModelManager
) -> Promise<Void>

public static let defaults: [Self] = [
Expand Down Expand Up @@ -421,7 +390,7 @@ public class ModelManager: ServerObserver {
}

public func serversDidChange(_ serverManager: ServerManager) {
subscribe(definitions: subscribedSubscriptions, isAppInForeground: ModelManager.isAppInForeground)
subscribe(definitions: subscribedSubscriptions, isAppInForeground: LegacyModelManager.isAppInForeground)
cleanup(definitions: cleanupDefinitions).cauterize()
}
}
8 changes: 6 additions & 2 deletions Sources/Shared/Environment/Environment.swift
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,10 @@ public class AppEnvironment {
AppEntitiesModel.shared
}

public var periodicAppEntitiesUpdater: () -> PeriodicAppEntitiesModelUpdaterProtocol = {
PeriodicAppEntitiesModelUpdater.shared
}

#if os(iOS)
public var realmFatalPresentation: ((UIViewController) -> Void)?
#endif
Expand All @@ -132,7 +136,7 @@ public class AppEnvironment {

private var lastActiveURLForServer = [Identifier<Server>: URL?]()
public func api(for server: Server) -> HomeAssistantAPI? {
guard let activeURL = server.info.connection.activeURL() else {
guard server.info.connection.activeURL() != nil else {
return nil
}

Expand All @@ -147,7 +151,7 @@ public class AppEnvironment {

private var underlyingAPI: Promise<HomeAssistantAPI>?

public var modelManager = ModelManager()
public var modelManager = LegacyModelManager()

public var settingsStore = SettingsStore()

Expand Down
56 changes: 56 additions & 0 deletions Sources/Shared/Environment/PeriodicAppEntitiesModelUpdater.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import Foundation
import HAKit

public protocol PeriodicAppEntitiesModelUpdaterProtocol {
func setup()
func stop()
func updateAppEntities()
}

final class PeriodicAppEntitiesModelUpdater: PeriodicAppEntitiesModelUpdaterProtocol {
static var shared = PeriodicAppEntitiesModelUpdater()

private var requestTokens: [HACancellable?] = []
private var timer: Timer?

func setup() {
startUpdateTimer()
}

func stop() {
cancelOnGoingRequests()
timer?.invalidate()
}

func updateAppEntities() {
cancelOnGoingRequests()
Current.servers.all.forEach { server in
guard server.info.connection.activeURL() != nil else { return }
let requestToken = Current.api(for: server)?.connection.send(
HATypedRequest<[HAEntity]>.fetchStates(),
completion: { result in
switch result {
case let .success(entities):
Current.appEntitiesModel().updateModel(Set(entities), server: server)
case let .failure(error):
Current.Log.error("Failed to fetch states: \(error)")
}
}
)
requestTokens.append(requestToken)
}
}

private func cancelOnGoingRequests() {
requestTokens.forEach { $0?.cancel() }
requestTokens = []
}

// Start timer that updates app entities every 5 minutes
private func startUpdateTimer() {
timer?.invalidate()
timer = Timer.scheduledTimer(withTimeInterval: 5 * 60, repeats: true) { [weak self] _ in
self?.updateAppEntities()
}
}
}
6 changes: 6 additions & 0 deletions Sources/Shared/HATypedRequest+App.swift
Original file line number Diff line number Diff line change
Expand Up @@ -108,4 +108,10 @@ public extension HATypedRequest {
type: "config/device_registry/list"
))
}

static func fetchStates() -> HATypedRequest<[HAEntity]> {
HATypedRequest<[HAEntity]>(request: .init(
type: .rest(.get, "states")
))
}
}
2 changes: 1 addition & 1 deletion Tests/App/Auth/OnboardingAuthStepModels.test.swift
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ private enum TestError: Error {
case any
}

private class FakeModelManager: ModelManager {
private class FakeModelManager: LegacyModelManager {
var fetchResult: Promise<Void> = .value(())
var expectedApis: [HomeAssistantAPI] = []

Expand Down
2 changes: 1 addition & 1 deletion Tests/App/Auth/OnboardingAuthStepNotify.test.swift
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ private class FakeOnboardingStateObserver: OnboardingStateObserver {
}
}

private class FakeModelManager: ModelManager {
private class FakeModelManager: LegacyModelManager {
var fetchResult: Promise<Void> = .value(())

override func fetch(
Expand Down
8 changes: 4 additions & 4 deletions Tests/Shared/ModelManager.test.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import XCTest
class ModelManagerTests: XCTestCase {
private var realm: Realm!
private var testQueue: DispatchQueue!
private var manager: ModelManager!
private var manager: LegacyModelManager!
private var servers: FakeServerManager!
private var api1: FakeHomeAssistantAPI!
private var api2: FakeHomeAssistantAPI!
Expand All @@ -19,7 +19,7 @@ class ModelManagerTests: XCTestCase {
try super.setUpWithError()

testQueue = DispatchQueue(label: #file)
manager = ModelManager()
manager = LegacyModelManager()
manager.workQueue = testQueue

servers = FakeServerManager(initial: 0)
Expand Down Expand Up @@ -374,7 +374,7 @@ class ModelManagerTests: XCTestCase {
var handlers1APIs = [(HAConnection, Server)]()
var handlers2APIs = [(HAConnection, Server)]()

let definitions: [ModelManager.SubscribeDefinition] = [
let definitions: [LegacyModelManager.SubscribeDefinition] = [
.init(subscribe: { connection, server, queue, manager -> [HACancellable] in
XCTAssertEqual(queue, self.testQueue)
XCTAssertTrue(manager === self.manager)
Expand Down Expand Up @@ -455,7 +455,7 @@ class ModelManagerTests: XCTestCase {
}

XCTAssertThrowsError(try doStore()) { error in
XCTAssertEqual(error as? ModelManager.StoreError, .missingPrimaryKey)
XCTAssertEqual(error as? LegacyModelManager.StoreError, .missingPrimaryKey)
}
}

Expand Down

0 comments on commit f2c3526

Please sign in to comment.