From 3b5c422c41ce8543ef7cccc4223fbe9e55d0c4d0 Mon Sep 17 00:00:00 2001 From: Timothy <6560631+TimoPtr@users.noreply.github.com> Date: Wed, 17 Jun 2026 16:44:54 +0200 Subject: [PATCH 1/3] Implement SensorRepository on top of SensorDao to handle default --- .../android/common/sensors/SensorModule.kt | 13 ++ .../common/sensors/SensorRepository.kt | 96 +++++++++++- .../common/sensors/SensorRepositoryImpl.kt | 146 ++++++++++++++++++ .../android/database/sensor/SensorDao.kt | 93 ++--------- 4 files changed, 257 insertions(+), 91 deletions(-) create mode 100644 common/src/main/kotlin/io/homeassistant/companion/android/common/sensors/SensorModule.kt create mode 100644 common/src/main/kotlin/io/homeassistant/companion/android/common/sensors/SensorRepositoryImpl.kt diff --git a/common/src/main/kotlin/io/homeassistant/companion/android/common/sensors/SensorModule.kt b/common/src/main/kotlin/io/homeassistant/companion/android/common/sensors/SensorModule.kt new file mode 100644 index 00000000000..ac2e8f06311 --- /dev/null +++ b/common/src/main/kotlin/io/homeassistant/companion/android/common/sensors/SensorModule.kt @@ -0,0 +1,13 @@ +package io.homeassistant.companion.android.common.sensors + +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent + +@Module +@InstallIn(SingletonComponent::class) +abstract class SensorModule { + @Binds + internal abstract fun bindSensorRepository(impl: SensorRepositoryImpl): SensorRepository +} diff --git a/common/src/main/kotlin/io/homeassistant/companion/android/common/sensors/SensorRepository.kt b/common/src/main/kotlin/io/homeassistant/companion/android/common/sensors/SensorRepository.kt index 78d2a9eb8c3..0b125a65286 100644 --- a/common/src/main/kotlin/io/homeassistant/companion/android/common/sensors/SensorRepository.kt +++ b/common/src/main/kotlin/io/homeassistant/companion/android/common/sensors/SensorRepository.kt @@ -1,14 +1,94 @@ package io.homeassistant.companion.android.common.sensors -import javax.inject.Inject -import javax.inject.Singleton +import io.homeassistant.companion.android.database.sensor.Attribute +import io.homeassistant.companion.android.database.sensor.Sensor +import io.homeassistant.companion.android.database.sensor.SensorSetting +import kotlinx.coroutines.flow.Flow /** - * Single access point for sensor state and the sensor catalog. + * Single access point to sensors their state, settings and attributes. * - * Stub: for now it only receives the generated [SensorManager.BasicSensor] catalog (built by the - * sensor-catalog KSP processor from `@CatalogSensor` annotations) and exposes it. The catalog/DB - * merge, effective-state reads, and writes will be added in a later stage. + * A known sensor always has a state: reads fall back to its default when nothing has been set yet, + * so they never come back empty for a real sensor. There is no separate create step — [update] sets + * a sensor's state on demand. */ -@Singleton -class SensorRepository @Inject constructor(val basicSensors: Set<@JvmSuppressWildcards SensorManager.BasicSensor>) +interface SensorRepository { + + /** State of sensor [id] on every server the state set for it. */ + suspend fun get(id: String): List + + /** + * State of sensor [id] on [serverId]: the state set for it. `null` only when [id] isn't a + * real sensor. + */ + suspend fun get(id: String, serverId: Int): Sensor? + + /** + * Reactive state of every known sensor on every server the state set for it. + * Re-emits when any sensor's state or the set of servers changes. + */ + fun getAllFlow(): Flow> + + /** [get] together with each sensor's attributes. */ + suspend fun getFull(id: String): Map> + + /** [get] together with attributes for one server. */ + suspend fun getFull(id: String, serverId: Int): Map> + + /** Reactive form of [getFull], re-emitting when the sensor's state or the set of servers changes. */ + fun getFullFlow(id: String): Flow>> + + /** Number of enabled (sensor, server) pairs, counting defaults. */ + suspend fun getEnabledCount(): Int + + /** Update [sensor]'s state. */ + suspend fun update(sensor: Sensor) + + /** Enables or disables sensor [sensorId] on each of [serverIds]. */ + suspend fun setSensorEnabled(sensorId: String, serverIds: List, enabled: Boolean) + + /** Enables or disables each of [sensorIds] on [serverId]. */ + suspend fun setSensorsEnabled(sensorIds: List, serverId: Int, enabled: Boolean) + + /** Records the last state and icon sent to [serverId] for sensor [sensorId]. */ + suspend fun updateLastSentStateAndIcon(sensorId: String, serverId: Int, state: String?, icon: String?) + + /** Records the last sent state and icon for sensor [sensorId] across all servers. */ + suspend fun updateLastSentStatesAndIcons(sensorId: String, state: String?, icon: String?) + + /** Forgets all data for the sensors of [serverId]. */ + suspend fun removeServer(serverId: Int) + + /** Remove the persisted state of sensors for any server not in [serverIds] */ + suspend fun removeSensorsExceptServers(serverIds: List) + + /** Adds a single sensor [attribute], replacing any existing one with the same name. */ + suspend fun add(attribute: Attribute) + + /** Adds the given sensor [attributes], replacing any existing ones with the same name. */ + suspend fun add(attributes: List) + + /** Replaces all attributes of sensor [sensorId] with [attributes]. */ + suspend fun replaceAllAttributes(sensorId: String, attributes: List) + + /** Settings of sensor [id]. */ + suspend fun getSettings(id: String): List + + /** Reactive settings of sensor [id]. */ + fun getSettingsFlow(id: String): Flow> + + /** Adds a sensor [sensorSetting], replacing any existing one with the same name. */ + suspend fun add(sensorSetting: SensorSetting) + + /** Enables or disables setting [settingName] of sensor [sensorId]. */ + suspend fun updateSettingEnabled(sensorId: String, settingName: String, enabled: Boolean) + + /** Sets the value of setting [settingName] of sensor [sensorId]. */ + suspend fun updateSettingValue(sensorId: String, settingName: String, value: String) + + /** Removes setting [settingName] from sensor [sensorId]. */ + suspend fun removeSetting(sensorId: String, settingName: String) + + /** Removes settings [settingNames] from sensor [sensorId]. */ + suspend fun removeSettings(sensorId: String, settingNames: List) +} diff --git a/common/src/main/kotlin/io/homeassistant/companion/android/common/sensors/SensorRepositoryImpl.kt b/common/src/main/kotlin/io/homeassistant/companion/android/common/sensors/SensorRepositoryImpl.kt new file mode 100644 index 00000000000..2f7a4704c58 --- /dev/null +++ b/common/src/main/kotlin/io/homeassistant/companion/android/common/sensors/SensorRepositoryImpl.kt @@ -0,0 +1,146 @@ +package io.homeassistant.companion.android.common.sensors + +import io.homeassistant.companion.android.common.util.FailFast +import io.homeassistant.companion.android.database.sensor.Attribute +import io.homeassistant.companion.android.database.sensor.Sensor +import io.homeassistant.companion.android.database.sensor.SensorDao +import io.homeassistant.companion.android.database.sensor.SensorSetting +import io.homeassistant.companion.android.database.server.ServerDao +import javax.inject.Inject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import timber.log.Timber + +/** + * [SensorRepository] backed by [SensorDao], with [ServerDao] providing the configured servers and + * [basicSensors] the catalog used to fill in defaults. See [SensorRepository] for the behavior. + */ +internal class SensorRepositoryImpl @Inject constructor( + private val dao: SensorDao, + private val serverDao: ServerDao, + basicSensors: Set<@JvmSuppressWildcards SensorManager.BasicSensor>, +) : SensorRepository { + + // Catalog default (enabled-by-default) per sensor id, used to synthesize a Sensor when no row exists. + private val enabledByDefaultById: Map = basicSensors.associate { it.id to it.enabledByDefault } + + // This could in theory return orphan sensors for server that have been removed but the DB was not cleared properly + override suspend fun get(id: String): List = sensorsByServer(id, dao.get(id), configuredServerIds()) + + override suspend fun get(id: String, serverId: Int): Sensor? = dao.get(id, serverId) ?: defaultSensor(id, serverId) + + override fun getAllFlow(): Flow> = combine(dao.getAllFlow(), serverDao.getAllFlow()) { rows, servers -> + withDefaults(rows, servers.map { it.id }) + } + + override suspend fun getFull(id: String): Map> = + fullByServer(id, dao.getFull(id), configuredServerIds()) + + override suspend fun getFull(id: String, serverId: Int): Map> { + val existing = dao.getFull(id, serverId) + if (existing.isNotEmpty()) return existing + val default = defaultSensor(id, serverId) ?: return emptyMap() + return mapOf(default to emptyList()) + } + + override fun getFullFlow(id: String): Flow>> = + combine(dao.getFullFlow(id), serverDao.getAllFlow()) { existing, servers -> + fullByServer(id, existing, servers.map { it.id }) + } + + // Catalog-aware: a default-enabled sensor counts even before it has a stored row, so this is the + // count of effective-enabled (sensor, server) pairs, not just persisted enabled rows. + override suspend fun getEnabledCount(): Int = + configuredServerIds().sumOf { serverId -> getAllServer(serverId).count { it.enabled } } + + // Upsert: a single statement that inserts the row when absent and overwrites it when present, + // matching the catalog model where a sensor always "exists" and has no separate create step. + override suspend fun update(sensor: Sensor) = dao.upsert(sensor) + + override suspend fun setSensorEnabled(sensorId: String, serverIds: List, enabled: Boolean) = + dao.setSensorEnabled(sensorId, serverIds, enabled) + + override suspend fun setSensorsEnabled(sensorIds: List, serverId: Int, enabled: Boolean) = + dao.setSensorsEnabled(sensorIds, serverId, enabled) + + override suspend fun updateLastSentStateAndIcon(sensorId: String, serverId: Int, state: String?, icon: String?) = + dao.updateLastSentStateAndIcon(sensorId, serverId, state, icon) + + override suspend fun updateLastSentStatesAndIcons(sensorId: String, state: String?, icon: String?) = + dao.updateLastSentStatesAndIcons(sensorId, state, icon) + + override suspend fun removeServer(serverId: Int) = dao.removeServer(serverId) + + override suspend fun removeSensorsExceptServers(serverIds: List) { + val toRemove = dao.getAllExceptServer(serverIds) + if (toRemove.isEmpty()) return + Timber.i("Cleaning up ${toRemove.size} sensor entries") + toRemove.forEach { dao.removeSensor(it.id, it.serverId) } + } + + override suspend fun add(attribute: Attribute) = dao.add(attribute) + override suspend fun add(attributes: List) = dao.add(attributes) + override suspend fun replaceAllAttributes(sensorId: String, attributes: List) = + dao.replaceAllAttributes(sensorId, attributes) + + override suspend fun getSettings(id: String) = dao.getSettings(id) + override fun getSettingsFlow(id: String) = dao.getSettingsFlow(id) + override suspend fun add(sensorSetting: SensorSetting) = dao.add(sensorSetting) + override suspend fun updateSettingEnabled(sensorId: String, settingName: String, enabled: Boolean) = + dao.updateSettingEnabled(sensorId, settingName, enabled) + override suspend fun updateSettingValue(sensorId: String, settingName: String, value: String) = + dao.updateSettingValue(sensorId, settingName, value) + override suspend fun removeSetting(sensorId: String, settingName: String) = dao.removeSetting(sensorId, settingName) + override suspend fun removeSettings(sensorId: String, settingNames: List) = + dao.removeSettings(sensorId, settingNames) + + private suspend fun configuredServerIds(): List = serverDao.getAll().map { it.id } + + /** + * In-memory default state for ([id], [serverId]), or `null` when [id] has no backing + * `BasicSensor` in the catalog — meaning the sensor does not exist. That should never happen for + * a sensor the app actually queries, so it is also surfaced via [FailFast]. + */ + private fun defaultSensor(id: String, serverId: Int): Sensor? { + val enabledByDefault = enabledByDefaultById[id] + if (enabledByDefault == null) { + FailFast.fail { "No BasicSensor in the catalog for sensor id=$id" } + return null + } + return Sensor(id = id, serverId = serverId, enabled = enabledByDefault, state = "") + } + + /** + * Row-or-default state for [id], one entry per server in [serverIds] plus any server that + * already has a stored row, using [rows] where present and a catalog default otherwise. Empty + * when [id] isn't a known sensor (no rows and no catalog entry). + */ + private fun sensorsByServer(id: String, rows: List, serverIds: Collection): List { + val byServer = rows.associateBy { it.serverId } + return (serverIds + byServer.keys).toSet() + .mapNotNull { serverId -> byServer[serverId] ?: defaultSensor(id, serverId) } + } + + /** [sensorsByServer] paired with each sensor's attributes (empty for a defaulted sensor). */ + private fun fullByServer( + id: String, + existing: Map>, + serverIds: Collection, + ): Map> = sensorsByServer(id, existing.keys.toList(), serverIds) + .associateWith { sensor -> existing[sensor] ?: emptyList() } + + /** + * Every catalog sensor across [serverIds], as its stored row in [rows] when present or a catalog + * default otherwise. Stored rows for sensors absent from the catalog are **not** surfaced — only + * catalog-backed sensors exist. + */ + private fun withDefaults(rows: List, serverIds: Collection): List { + val byKey = rows.associateBy { it.id to it.serverId } + return serverIds.flatMap { serverId -> + enabledByDefaultById.keys.mapNotNull { id -> byKey[id to serverId] ?: defaultSensor(id, serverId) } + } + } + + private suspend fun getAllServer(serverId: Int): List = + withDefaults(dao.getAllServer(serverId), listOf(serverId)) +} diff --git a/common/src/main/kotlin/io/homeassistant/companion/android/database/sensor/SensorDao.kt b/common/src/main/kotlin/io/homeassistant/companion/android/database/sensor/SensorDao.kt index 51ac0653804..b994b543e31 100644 --- a/common/src/main/kotlin/io/homeassistant/companion/android/database/sensor/SensorDao.kt +++ b/common/src/main/kotlin/io/homeassistant/companion/android/database/sensor/SensorDao.kt @@ -5,14 +5,14 @@ import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.Query import androidx.room.Transaction -import androidx.room.Update +import androidx.room.Upsert import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.Flow @Dao -interface SensorDao { +internal interface SensorDao { @Query("SELECT * FROM Sensors WHERE id = :id") suspend fun get(id: String): List @@ -55,9 +55,6 @@ interface SensorDao { @Query("SELECT * FROM sensor_settings WHERE sensor_id = :id ORDER BY sensor_id") fun getSettingsFlow(id: String): Flow> - @Insert(onConflict = OnConflictStrategy.IGNORE) - suspend fun add(sensor: Sensor) - @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun add(attribute: Attribute) @@ -90,8 +87,8 @@ interface SensorDao { @Query("DELETE FROM sensor_settings WHERE sensor_id = :sensorId AND name IN (:settingNames)") suspend fun removeSettings(sensorId: String, settingNames: List) - @Update - suspend fun update(sensor: Sensor) + @Upsert + suspend fun upsert(sensor: Sensor) @Query("DELETE FROM sensor_attributes WHERE sensor_id = :sensorId") suspend fun clearAttributes(sensorId: String) @@ -116,9 +113,6 @@ interface SensorDao { @Query("UPDATE sensors SET last_sent_state = :state, last_sent_icon = :icon WHERE id = :sensorId") suspend fun updateLastSentStatesAndIcons(sensorId: String, state: String?, icon: String?) - @Query("SELECT COUNT(id) FROM sensors WHERE enabled = 1") - suspend fun getEnabledCount(): Int? - @Transaction suspend fun setSensorEnabled(sensorId: String, serverIds: List, enabled: Boolean) { serverIds.forEach { @@ -131,81 +125,14 @@ interface SensorDao { coroutineScope { sensorIds.map { sensorId -> async { - val sensorEntity = get(sensorId, serverId) - if (sensorEntity != null) { - update(sensorEntity.copy(enabled = enabled, lastSentState = null, lastSentIcon = null)) - } else { - add(Sensor(sensorId, serverId, enabled, state = "")) - } + // Keep an existing row's other fields, otherwise start from a fresh entity; upsert + // creates it when absent and overwrites it when present. + val sensor = get(sensorId, serverId) + ?.copy(enabled = enabled, lastSentState = null, lastSentIcon = null) + ?: Sensor(sensorId, serverId, enabled, state = "") + upsert(sensor) } }.awaitAll() } } - - @Transaction - suspend fun getOrDefault(sensorId: String, serverId: Int, permission: Boolean, enabledByDefault: Boolean): Sensor? { - val sensor = get(sensorId, serverId) - - if (sensor?.enabled == true && !permission) { - // If we don't have permission but we are still enabled then we aren't really enabled. - sensor.enabled = false - update(sensor) - } - - return sensor - } - - @Transaction - suspend fun getAnyIsEnabled( - sensorId: String, - servers: List, - permission: Boolean, - enabledByDefault: Boolean, - ): Boolean { - // Create and update entries for all - var sensorList = get(sensorId) - var changedList = false - if (sensorList.isEmpty()) { - // If we haven't created the entity yet do so and default to enabled if required - servers.forEach { - add(Sensor(sensorId, it, enabled = permission && enabledByDefault, state = "")) - } - changedList = true - } else { - if (!permission) { - // If we don't have permission but we are still enabled then we aren't really enabled. - sensorList.filter { it.enabled }.forEach { - update(it.apply { enabled = false }) - changedList = true - } - } - val newServers = servers.filter { it !in sensorList.map { sensor -> sensor.serverId } } - if (newServers.isNotEmpty()) { - // If we have any new servers but don't have entries create one for updates. - val singleSensor = sensorList.maxBy { it.enabled } // Prefer enabled - newServers.forEach { - add( - singleSensor.copy( - serverId = it, - registered = null, - state = "", - stateType = "", - lastSentState = null, - lastSentIcon = null, - coreRegistration = null, - ), - ) - } - changedList = true - } - } - if (changedList) sensorList = get(sensorId) - - // Return if any are enabled - return if (sensorList.isEmpty()) { - false // No servers - } else { - sensorList.any { it.enabled && permission } - } - } } From 788442b2a56f477792f3a466d98ebce39559b399 Mon Sep 17 00:00:00 2001 From: Timothy <6560631+TimoPtr@users.noreply.github.com> Date: Wed, 17 Jun 2026 17:04:45 +0200 Subject: [PATCH 2/3] Replace all usages of SensorDao with SensorRepository --- .../android/sensors/LocationSensorManager.kt | 42 ++-- .../android/HomeAssistantApplication.kt | 6 +- .../android/notifications/MessagingManager.kt | 6 +- .../LocationSharingViewModel.kt | 10 +- .../android/sensors/GeocodeSensorManager.kt | 6 +- .../settings/sensor/SensorDetailViewModel.kt | 12 +- .../sensor/SensorSettingsViewModel.kt | 6 +- .../LocationSharingViewModelTest.kt | 16 +- .../android/sensors/SensorRepositoryTest.kt | 15 +- .../ServerConnectionStateProviderImpl.kt | 2 +- .../common/data/servers/ServerManagerImpl.kt | 6 +- .../common/notifications/DeviceCommands.kt | 12 +- .../common/sensors/AudioSensorManager.kt | 2 +- .../common/sensors/BluetoothSensorManager.kt | 14 +- .../common/sensors/LastRebootSensorManager.kt | 8 +- .../common/sensors/LastUpdateManager.kt | 20 +- .../common/sensors/NetworkSensorManager.kt | 14 +- .../common/sensors/NextAlarmManager.kt | 6 +- .../android/common/sensors/SensorManager.kt | 64 ++--- .../common/sensors/SensorReceiverBase.kt | 47 ++-- .../common/sensors/SensorWorkerBase.kt | 17 +- .../android/database/DatabaseEntryPoint.kt | 9 +- .../data/servers/ServerManagerImplTest.kt | 16 +- .../common/sensors/AudioSensorManagerTest.kt | 80 +++--- .../sensors/SensorRepositoryImplTest.kt | 227 ++++++++++++++++++ .../android/sensors/SensorManagerTest.kt | 12 +- .../companion/android/home/MainViewModel.kt | 16 +- .../android/notifications/MessagingManager.kt | 6 +- .../sensors/HealthServicesSensorManager.kt | 6 +- 29 files changed, 473 insertions(+), 230 deletions(-) create mode 100644 common/src/test/kotlin/io/homeassistant/companion/android/common/sensors/SensorRepositoryImplTest.kt diff --git a/app/src/full/kotlin/io/homeassistant/companion/android/sensors/LocationSensorManager.kt b/app/src/full/kotlin/io/homeassistant/companion/android/sensors/LocationSensorManager.kt index 3ef5072413d..ae685d7054c 100644 --- a/app/src/full/kotlin/io/homeassistant/companion/android/sensors/LocationSensorManager.kt +++ b/app/src/full/kotlin/io/homeassistant/companion/android/sensors/LocationSensorManager.kt @@ -204,7 +204,7 @@ class LocationSensorManager : } suspend fun setHighAccuracyModeSetting(context: Context, enabled: Boolean) { - DatabaseEntryPoint.resolve(context).sensorDao().add( + DatabaseEntryPoint.resolve(context).sensorRepository().add( SensorSetting( backgroundLocation.id, SETTING_HIGH_ACCURACY_MODE, @@ -215,7 +215,7 @@ class LocationSensorManager : } suspend fun getHighAccuracyModeIntervalSetting(context: Context): Int { - val sensorSettings = DatabaseEntryPoint.resolve(context).sensorDao() + val sensorSettings = DatabaseEntryPoint.resolve(context).sensorRepository() .getSettings(backgroundLocation.id) return sensorSettings.firstOrNull { it.name == SETTING_HIGH_ACCURACY_MODE_UPDATE_INTERVAL @@ -224,7 +224,7 @@ class LocationSensorManager : } suspend fun setHighAccuracyModeIntervalSetting(context: Context, updateInterval: Int) { - DatabaseEntryPoint.resolve(context).sensorDao().add( + DatabaseEntryPoint.resolve(context).sensorRepository().add( SensorSetting( backgroundLocation.id, SETTING_HIGH_ACCURACY_MODE_UPDATE_INTERVAL, @@ -584,7 +584,7 @@ class LocationSensorManager : } } if (updatedBtDeviceNames) { - sensorDao(latestContext).add( + sensorRepository(latestContext).add( SensorSetting( backgroundLocation.id, SETTING_HIGH_ACCURACY_MODE_BLUETOOTH_DEVICES, @@ -773,12 +773,12 @@ class LocationSensorManager : lastLocationReceived[it] = System.currentTimeMillis() } LocationResult.extractResult(intent)?.lastLocation?.let { location -> - val sensorDao = sensorDao(latestContext) - val sensorSettings = sensorDao.getSettings(backgroundLocation.id) + val sensorRepository = sensorRepository(latestContext) + val sensorSettings = sensorRepository.getSettings(backgroundLocation.id) val minAccuracy = sensorSettings .firstOrNull { it.name == SETTING_ACCURACY }?.value?.toIntOrNull() ?: DEFAULT_MINIMUM_ACCURACY - sensorDao.add( + sensorRepository.add( SensorSetting( backgroundLocation.id, SETTING_ACCURACY, @@ -893,12 +893,12 @@ class LocationSensorManager : } } - val sensorDao = sensorDao(latestContext) - val sensorSettings = sensorDao.getSettings(zoneLocation.id) + val sensorRepository = sensorRepository(latestContext) + val sensorSettings = sensorRepository.getSettings(zoneLocation.id) val minAccuracy = sensorSettings .firstOrNull { it.name == SETTING_ACCURACY }?.value?.toIntOrNull() ?: DEFAULT_MINIMUM_ACCURACY - sensorDao.add( + sensorRepository.add( SensorSetting(zoneLocation.id, SETTING_ACCURACY, minAccuracy.toString(), SensorSettingType.NUMBER), ) @@ -1216,7 +1216,7 @@ class LocationSensorManager : if (highAccuracyTriggerRangeInt < 0) { highAccuracyTriggerRangeInt = DEFAULT_TRIGGER_RANGE_METERS - sensorDao(latestContext).add( + sensorRepository(latestContext).add( SensorSetting( backgroundLocation.id, SETTING_HIGH_ACCURACY_MODE_TRIGGER_RANGE_ZONE, @@ -1261,17 +1261,17 @@ class LocationSensorManager : } val now = System.currentTimeMillis() - val sensorDao = sensorDao(latestContext) - val fullSensor = sensorDao.getFull(singleAccurateLocation.id).toSensorWithAttributes() + val sensorRepository = sensorRepository(latestContext) + val fullSensor = sensorRepository.getFull(singleAccurateLocation.id).toSensorWithAttributes() val latestAccurateLocation = fullSensor?.attributes?.firstOrNull { it.name == "lastAccurateLocationRequest" }?.value?.toLongOrNull() ?: 0L - val sensorSettings = sensorDao.getSettings(singleAccurateLocation.id) + val sensorSettings = sensorRepository.getSettings(singleAccurateLocation.id) val minAccuracy = sensorSettings .firstOrNull { it.name == SETTING_ACCURACY }?.value?.toIntOrNull() ?: DEFAULT_MINIMUM_ACCURACY - sensorDao.add( + sensorRepository.add( SensorSetting( singleAccurateLocation.id, SETTING_ACCURACY, @@ -1282,7 +1282,7 @@ class LocationSensorManager : val minTimeBetweenUpdates = sensorSettings .firstOrNull { it.name == SETTING_ACCURATE_UPDATE_TIME }?.value?.toIntOrNull() ?: 60000 - sensorDao.add( + sensorRepository.add( SensorSetting( singleAccurateLocation.id, SETTING_ACCURATE_UPDATE_TIME, @@ -1296,7 +1296,9 @@ class LocationSensorManager : Timber.d("Not requesting accurate location, last accurate location was too recent") return } - sensorDao.add(Attribute(singleAccurateLocation.id, "lastAccurateLocationRequest", now.toString(), "string")) + sensorRepository.add( + Attribute(singleAccurateLocation.id, "lastAccurateLocationRequest", now.toString(), "string"), + ) val maxRetries = 5 val request = LocationRequest.Builder(10000).apply { @@ -1433,8 +1435,8 @@ class LocationSensorManager : setupLocationTracking() } cleanupLocationHistory(context) - val sensorDao = sensorDao(latestContext) - val sensorSetting = sensorDao.getSettings(singleAccurateLocation.id) + val sensorRepository = sensorRepository(latestContext) + val sensorSetting = sensorRepository.getSettings(singleAccurateLocation.id) val includeSensorUpdate = sensorSetting.firstOrNull { it.name == SETTING_INCLUDE_SENSOR_UPDATE }?.value ?: "false" if (includeSensorUpdate == "true") { @@ -1442,7 +1444,7 @@ class LocationSensorManager : context.sendBroadcast(createRequestAccurateLocationUpdateIntent(context)) } } else { - sensorDao.add( + sensorRepository.add( SensorSetting( singleAccurateLocation.id, SETTING_INCLUDE_SENSOR_UPDATE, diff --git a/app/src/main/kotlin/io/homeassistant/companion/android/HomeAssistantApplication.kt b/app/src/main/kotlin/io/homeassistant/companion/android/HomeAssistantApplication.kt index 9c33feca3bb..03260821346 100644 --- a/app/src/main/kotlin/io/homeassistant/companion/android/HomeAssistantApplication.kt +++ b/app/src/main/kotlin/io/homeassistant/companion/android/HomeAssistantApplication.kt @@ -24,11 +24,11 @@ import io.homeassistant.companion.android.common.data.keychain.NamedKeyChain import io.homeassistant.companion.android.common.data.prefs.PrefsRepository import io.homeassistant.companion.android.common.sensors.AudioSensorManager import io.homeassistant.companion.android.common.sensors.LastUpdateManager +import io.homeassistant.companion.android.common.sensors.SensorRepository import io.homeassistant.companion.android.common.util.HAStrictMode import io.homeassistant.companion.android.common.util.SdkVersion import io.homeassistant.companion.android.common.util.configureComposeDiagnosticStackTrace import io.homeassistant.companion.android.common.util.isAutomotive -import io.homeassistant.companion.android.database.sensor.SensorDao import io.homeassistant.companion.android.database.settings.SensorUpdateFrequencySetting import io.homeassistant.companion.android.database.settings.SettingsDao import io.homeassistant.companion.android.sensors.SensorReceiver @@ -78,7 +78,7 @@ open class HomeAssistantApplication : lateinit var nightModeManager: NightModeManager @Inject - lateinit var sensorDao: SensorDao + lateinit var sensorRepository: SensorRepository @Inject lateinit var settingsDao: SettingsDao @@ -281,7 +281,7 @@ open class HomeAssistantApplication : // Register for all saved user intents ioScope.launch { - val allSettings = sensorDao.getSettings(LastUpdateManager.lastUpdate.id) + val allSettings = sensorRepository.getSettings(LastUpdateManager.lastUpdate.id) for (setting in allSettings) { if (setting.value != "" && setting.value != "SensorWorker") { val settingSplit = setting.value.split(',') diff --git a/app/src/main/kotlin/io/homeassistant/companion/android/notifications/MessagingManager.kt b/app/src/main/kotlin/io/homeassistant/companion/android/notifications/MessagingManager.kt index a2d336958f1..f747882dc91 100644 --- a/app/src/main/kotlin/io/homeassistant/companion/android/notifications/MessagingManager.kt +++ b/app/src/main/kotlin/io/homeassistant/companion/android/notifications/MessagingManager.kt @@ -66,6 +66,7 @@ import io.homeassistant.companion.android.common.notifications.handleText import io.homeassistant.companion.android.common.notifications.parseColor import io.homeassistant.companion.android.common.notifications.parseVibrationPattern import io.homeassistant.companion.android.common.notifications.prepareText +import io.homeassistant.companion.android.common.sensors.SensorRepository import io.homeassistant.companion.android.common.util.SdkVersion import io.homeassistant.companion.android.common.util.cancelGroupIfNeeded import io.homeassistant.companion.android.common.util.createSystemAppSettingsIntent @@ -78,7 +79,6 @@ import io.homeassistant.companion.android.common.util.tts.TextToSpeechClient import io.homeassistant.companion.android.common.util.tts.TextToSpeechData import io.homeassistant.companion.android.database.notification.NotificationDao import io.homeassistant.companion.android.database.notification.NotificationItem -import io.homeassistant.companion.android.database.sensor.SensorDao import io.homeassistant.companion.android.database.settings.SettingsDao import io.homeassistant.companion.android.database.settings.WebsocketSetting import io.homeassistant.companion.android.sensors.LocationSensorManager @@ -122,7 +122,7 @@ class MessagingManager @Inject constructor( private val serverManager: ServerManager, private val prefsRepository: PrefsRepository, private val notificationDao: NotificationDao, - private val sensorDao: SensorDao, + private val sensorRepository: SensorRepository, private val settingsDao: SettingsDao, private val textToSpeechClient: TextToSpeechClient, private val flashlightHelper: FlashlightHelper, @@ -448,7 +448,7 @@ class MessagingManager @Inject constructor( } DeviceCommandData.COMMAND_BLE_TRANSMITTER -> { - if (!commandBleTransmitter(context, jsonData, sensorDao)) { + if (!commandBleTransmitter(context, jsonData, sensorRepository)) { sendNotification(jsonData) } } diff --git a/app/src/main/kotlin/io/homeassistant/companion/android/onboarding/locationsharing/LocationSharingViewModel.kt b/app/src/main/kotlin/io/homeassistant/companion/android/onboarding/locationsharing/LocationSharingViewModel.kt index 6747633731b..3974aa4e035 100644 --- a/app/src/main/kotlin/io/homeassistant/companion/android/onboarding/locationsharing/LocationSharingViewModel.kt +++ b/app/src/main/kotlin/io/homeassistant/companion/android/onboarding/locationsharing/LocationSharingViewModel.kt @@ -6,7 +6,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.navigation.toRoute import dagger.hilt.android.lifecycle.HiltViewModel -import io.homeassistant.companion.android.database.sensor.SensorDao +import io.homeassistant.companion.android.common.sensors.SensorRepository import io.homeassistant.companion.android.onboarding.locationsharing.navigation.LocationSharingRoute import io.homeassistant.companion.android.sensors.LocationSensorManager import javax.inject.Inject @@ -16,19 +16,19 @@ import timber.log.Timber @HiltViewModel internal class LocationSharingViewModel @VisibleForTesting constructor( private val serverId: Int, - private val sensorDao: SensorDao, + private val sensorRepository: SensorRepository, ) : ViewModel() { @Inject constructor( savedStateHandle: SavedStateHandle, - sensorDao: SensorDao, - ) : this(serverId = savedStateHandle.toRoute().serverId, sensorDao) + sensorRepository: SensorRepository, + ) : this(serverId = savedStateHandle.toRoute().serverId, sensorRepository) fun setupLocationSensor(enabled: Boolean) { viewModelScope.launch { try { - sensorDao.setSensorsEnabled( + sensorRepository.setSensorsEnabled( sensorIds = listOf( LocationSensorManager.backgroundLocation.id, LocationSensorManager.zoneLocation.id, diff --git a/app/src/main/kotlin/io/homeassistant/companion/android/sensors/GeocodeSensorManager.kt b/app/src/main/kotlin/io/homeassistant/companion/android/sensors/GeocodeSensorManager.kt index 3f8b7f0cbbb..31e5bdc6dbd 100644 --- a/app/src/main/kotlin/io/homeassistant/companion/android/sensors/GeocodeSensorManager.kt +++ b/app/src/main/kotlin/io/homeassistant/companion/android/sensors/GeocodeSensorManager.kt @@ -102,12 +102,12 @@ class GeocodeSensorManager : SensorManager { } var address: Address? = null - val sensorDao = sensorDao(context) - val sensorSettings = sensorDao.getSettings(geocodedLocation.id) + val sensorRepository = sensorRepository(context) + val sensorSettings = sensorRepository.getSettings(geocodedLocation.id) val minAccuracy = sensorSettings .firstOrNull { it.name == SETTING_ACCURACY }?.value?.toIntOrNull() ?: DEFAULT_MINIMUM_ACCURACY - sensorDao.add( + sensorRepository.add( SensorSetting(geocodedLocation.id, SETTING_ACCURACY, minAccuracy.toString(), SensorSettingType.NUMBER), ) diff --git a/app/src/main/kotlin/io/homeassistant/companion/android/settings/sensor/SensorDetailViewModel.kt b/app/src/main/kotlin/io/homeassistant/companion/android/settings/sensor/SensorDetailViewModel.kt index 0ebba7fa25a..2a89904f1b9 100644 --- a/app/src/main/kotlin/io/homeassistant/companion/android/settings/sensor/SensorDetailViewModel.kt +++ b/app/src/main/kotlin/io/homeassistant/companion/android/settings/sensor/SensorDetailViewModel.kt @@ -23,9 +23,9 @@ import io.homeassistant.companion.android.common.data.servers.ServerManager import io.homeassistant.companion.android.common.sensors.BluetoothSensorManager import io.homeassistant.companion.android.common.sensors.NetworkSensorManager import io.homeassistant.companion.android.common.sensors.SensorManager +import io.homeassistant.companion.android.common.sensors.SensorRepository import io.homeassistant.companion.android.common.util.DisabledLocationHandler import io.homeassistant.companion.android.common.util.SdkVersion -import io.homeassistant.companion.android.database.sensor.SensorDao import io.homeassistant.companion.android.database.sensor.SensorSetting import io.homeassistant.companion.android.database.sensor.SensorSettingType import io.homeassistant.companion.android.database.sensor.SensorWithAttributes @@ -53,7 +53,7 @@ import timber.log.Timber class SensorDetailViewModel @Inject constructor( state: SavedStateHandle, private val serverManager: ServerManager, - private val sensorDao: SensorDao, + private val sensorRepository: SensorRepository, private val settingsDao: SettingsDao, private val prefsRepository: PrefsRepository, application: Application, @@ -116,7 +116,7 @@ class SensorDetailViewModel @Inject constructor( var sensor by mutableStateOf(null) private set private var sensorCheckedEnabled = false - val sensorSettings = sensorDao.getSettingsFlow(sensorId).collectAsState() + val sensorSettings = sensorRepository.getSettingsFlow(sensorId).collectAsState() var sensorSettingsDialog by mutableStateOf(null) private set @@ -160,7 +160,7 @@ class SensorDetailViewModel @Inject constructor( } init { - val sensorFlow = sensorDao.getFullFlow(sensorId) + val sensorFlow = sensorRepository.getFullFlow(sensorId) viewModelScope.launch { serverNames = serverManager.servers().associate { it.id to it.friendlyName } @@ -325,7 +325,7 @@ class SensorDetailViewModel @Inject constructor( fun setSetting(setting: SensorSetting) { viewModelScope.launch { - sensorDao.add(setting) + sensorRepository.add(setting) try { sensorManager?.requestSensorUpdate(getApplication()) } catch (e: Exception) { @@ -342,7 +342,7 @@ class SensorDetailViewModel @Inject constructor( } else { listOf(serverId) } - sensorDao.setSensorEnabled(sensorId, serverIds, isEnabled) + sensorRepository.setSensorEnabled(sensorId, serverIds, isEnabled) refreshSensorData() } diff --git a/app/src/main/kotlin/io/homeassistant/companion/android/settings/sensor/SensorSettingsViewModel.kt b/app/src/main/kotlin/io/homeassistant/companion/android/settings/sensor/SensorSettingsViewModel.kt index 32691e0daad..6b832781077 100644 --- a/app/src/main/kotlin/io/homeassistant/companion/android/settings/sensor/SensorSettingsViewModel.kt +++ b/app/src/main/kotlin/io/homeassistant/companion/android/settings/sensor/SensorSettingsViewModel.kt @@ -10,8 +10,8 @@ import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import io.homeassistant.companion.android.R import io.homeassistant.companion.android.common.sensors.SensorManager +import io.homeassistant.companion.android.common.sensors.SensorRepository import io.homeassistant.companion.android.database.sensor.Sensor -import io.homeassistant.companion.android.database.sensor.SensorDao import io.homeassistant.companion.android.sensors.SensorReceiver import javax.inject.Inject import kotlinx.coroutines.Dispatchers @@ -19,7 +19,7 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @HiltViewModel -class SensorSettingsViewModel @Inject constructor(sensorDao: SensorDao, application: Application) : +class SensorSettingsViewModel @Inject constructor(sensorRepository: SensorRepository, application: Application) : AndroidViewModel(application) { enum class SensorFilter(@IdRes val menuItemId: Int) { @@ -46,7 +46,7 @@ class SensorSettingsViewModel @Inject constructor(sensorDao: SensorDao, applicat init { viewModelScope.launch { - sensorDao.getAllFlow().collect { + sensorRepository.getAllFlow().collect { withContext(Dispatchers.IO) { // Compare contents, because the worker typically pushes a DB update on // sensor updates even when contents don't change diff --git a/app/src/test/kotlin/io/homeassistant/companion/android/onboarding/locationsharing/LocationSharingViewModelTest.kt b/app/src/test/kotlin/io/homeassistant/companion/android/onboarding/locationsharing/LocationSharingViewModelTest.kt index 59d539632c2..5eef74a59b3 100644 --- a/app/src/test/kotlin/io/homeassistant/companion/android/onboarding/locationsharing/LocationSharingViewModelTest.kt +++ b/app/src/test/kotlin/io/homeassistant/companion/android/onboarding/locationsharing/LocationSharingViewModelTest.kt @@ -1,6 +1,6 @@ package io.homeassistant.companion.android.onboarding.locationsharing -import io.homeassistant.companion.android.database.sensor.SensorDao +import io.homeassistant.companion.android.common.sensors.SensorRepository import io.homeassistant.companion.android.testing.unit.MainDispatcherJUnit5Extension import io.mockk.coEvery import io.mockk.coVerify @@ -18,7 +18,7 @@ import org.junit.jupiter.params.provider.ValueSource @ExtendWith(MainDispatcherJUnit5Extension::class) class LocationSharingViewModelTest { private val serverId = 42 - private val sensorDao: SensorDao = mockk(relaxUnitFun = true) + private val sensorRepository: SensorRepository = mockk(relaxUnitFun = true) private lateinit var viewModel: LocationSharingViewModel @@ -32,7 +32,7 @@ class LocationSharingViewModelTest { fun setup() { viewModel = LocationSharingViewModel( serverId = serverId, - sensorDao = sensorDao, + sensorRepository = sensorRepository, ) } @@ -46,7 +46,7 @@ class LocationSharingViewModelTest { runCurrent() coVerify { - sensorDao.setSensorsEnabled( + sensorRepository.setSensorsEnabled( sensorIds = locationSensorIds, serverId = serverId, enabled = enabled, @@ -55,16 +55,16 @@ class LocationSharingViewModelTest { } @Test - fun `Given sensorDao throws exception When setupLocationSensor is called Then exception is caught`() = runTest { + fun `Given repository throws exception When setupLocationSensor is called Then exception is caught`() = runTest { val enabled = true - val exception = RuntimeException("Test exception from sensorDao") - coEvery { sensorDao.setSensorsEnabled(any(), any(), any()) } throws exception + val exception = RuntimeException("Test exception from repository") + coEvery { sensorRepository.setSensorsEnabled(any(), any(), any()) } throws exception viewModel.setupLocationSensor(enabled) runCurrent() coVerify { - sensorDao.setSensorsEnabled( + sensorRepository.setSensorsEnabled( sensorIds = locationSensorIds, serverId = serverId, enabled = enabled, diff --git a/app/src/test/kotlin/io/homeassistant/companion/android/sensors/SensorRepositoryTest.kt b/app/src/test/kotlin/io/homeassistant/companion/android/sensors/SensorRepositoryTest.kt index b27fe32a823..ea7d20a7bfa 100644 --- a/app/src/test/kotlin/io/homeassistant/companion/android/sensors/SensorRepositoryTest.kt +++ b/app/src/test/kotlin/io/homeassistant/companion/android/sensors/SensorRepositoryTest.kt @@ -3,8 +3,10 @@ package io.homeassistant.companion.android.sensors import dagger.hilt.android.testing.HiltAndroidRule import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltTestApplication +import io.homeassistant.companion.android.common.sensors.SensorManager import io.homeassistant.companion.android.common.sensors.SensorRepository import javax.inject.Inject +import org.junit.Assert.assertNotNull import org.junit.Assert.assertTrue import org.junit.Rule import org.junit.Test @@ -22,10 +24,17 @@ class SensorRepositoryTest { @Inject lateinit var repository: SensorRepository + // The generated catalog multibinding the repository consumes for its missing-sensor default. + @Inject + lateinit var basicSensors: Set<@JvmSuppressWildcards SensorManager.BasicSensor> + @Test - fun `Given hilt graph then repository is injected with the generated sensor catalog`() { + fun `Given hilt graph then repository is built from the generated sensor catalog`() { hilt.inject() - assertTrue(repository.basicSensors.isNotEmpty()) - assertTrue(repository.basicSensors.any { it.id == "last_update" }) + // The repository resolves, so its injected BasicSensor catalog dependency is satisfied. + assertNotNull(repository) + // That catalog is the generated multibinding, populated with the annotated sensors. + assertTrue(basicSensors.isNotEmpty()) + assertTrue(basicSensors.any { it.id == "last_update" }) } } diff --git a/common/src/main/kotlin/io/homeassistant/companion/android/common/data/servers/ServerConnectionStateProviderImpl.kt b/common/src/main/kotlin/io/homeassistant/companion/android/common/data/servers/ServerConnectionStateProviderImpl.kt index f25c47a6d2e..c45e1ddc9a4 100644 --- a/common/src/main/kotlin/io/homeassistant/companion/android/common/data/servers/ServerConnectionStateProviderImpl.kt +++ b/common/src/main/kotlin/io/homeassistant/companion/android/common/data/servers/ServerConnectionStateProviderImpl.kt @@ -29,7 +29,7 @@ import kotlinx.coroutines.flow.merge import okhttp3.HttpUrl import timber.log.Timber -class ServerConnectionStateProviderImpl @AssistedInject constructor( +internal class ServerConnectionStateProviderImpl @AssistedInject constructor( @param:ApplicationContext private val context: Context, private val serverManager: ServerManager, private val serverDao: ServerDao, diff --git a/common/src/main/kotlin/io/homeassistant/companion/android/common/data/servers/ServerManagerImpl.kt b/common/src/main/kotlin/io/homeassistant/companion/android/common/data/servers/ServerManagerImpl.kt index 325f2c5a9dd..28cf5593d69 100644 --- a/common/src/main/kotlin/io/homeassistant/companion/android/common/data/servers/ServerManagerImpl.kt +++ b/common/src/main/kotlin/io/homeassistant/companion/android/common/data/servers/ServerManagerImpl.kt @@ -10,8 +10,8 @@ import io.homeassistant.companion.android.common.data.prefs.PrefsRepository import io.homeassistant.companion.android.common.data.servers.ServerManager.Companion.SERVER_ID_ACTIVE import io.homeassistant.companion.android.common.data.websocket.WebSocketRepository import io.homeassistant.companion.android.common.data.websocket.WebSocketRepositoryFactory +import io.homeassistant.companion.android.common.sensors.SensorRepository import io.homeassistant.companion.android.common.util.FailFast -import io.homeassistant.companion.android.database.sensor.SensorDao import io.homeassistant.companion.android.database.server.Server import io.homeassistant.companion.android.database.server.ServerDao import io.homeassistant.companion.android.database.server.TemporaryServer @@ -73,7 +73,7 @@ internal class ServerManagerImpl @Inject constructor( private val serverConnectionStateProviderFactory: ServerConnectionStateProviderFactory, private val prefsRepository: PrefsRepository, private val serverDao: ServerDao, - private val sensorDao: SensorDao, + private val sensorRepository: SensorRepository, private val settingsDao: SettingsDao, @NamedSessionStorage private val localStorage: LocalStorage, ) : ServerManager { @@ -142,7 +142,7 @@ internal class ServerManagerImpl @Inject constructor( if (localStorage.getInt(PREF_ACTIVE_SERVER) == id) localStorage.remove(PREF_ACTIVE_SERVER) settingsDao.delete(id) - sensorDao.removeServer(id) + sensorRepository.removeServer(id) serverDao.delete(id) } diff --git a/common/src/main/kotlin/io/homeassistant/companion/android/common/notifications/DeviceCommands.kt b/common/src/main/kotlin/io/homeassistant/companion/android/common/notifications/DeviceCommands.kt index 6facee3ef93..d8c8215a3f4 100755 --- a/common/src/main/kotlin/io/homeassistant/companion/android/common/notifications/DeviceCommands.kt +++ b/common/src/main/kotlin/io/homeassistant/companion/android/common/notifications/DeviceCommands.kt @@ -2,8 +2,8 @@ package io.homeassistant.companion.android.common.notifications import android.content.Context import io.homeassistant.companion.android.common.sensors.BluetoothSensorManager +import io.homeassistant.companion.android.common.sensors.SensorRepository import io.homeassistant.companion.android.common.sensors.SensorUpdateReceiver -import io.homeassistant.companion.android.database.sensor.SensorDao import java.util.UUID import timber.log.Timber @@ -123,7 +123,11 @@ suspend fun commandBeaconMonitor(context: Context, data: Map): B return true } -suspend fun commandBleTransmitter(context: Context, data: Map, sensorDao: SensorDao): Boolean { +suspend fun commandBleTransmitter( + context: Context, + data: Map, + sensorRepository: SensorRepository, +): Boolean { if (!checkCommandFormat(data)) { Timber.d( "Invalid ble transmitter command received, posting notification to device", @@ -139,7 +143,7 @@ suspend fun commandBleTransmitter(context: Context, data: Map, s BluetoothSensorManager.enableDisableBLETransmitter(context, true) } if (command in DeviceCommandData.BLE_COMMANDS) { - sensorDao.updateSettingValue( + sensorRepository.updateSettingValue( BluetoothSensorManager.bleTransmitter.id, when (command) { DeviceCommandData.BLE_SET_ADVERTISE_MODE -> BluetoothSensorManager.SETTING_BLE_ADVERTISE_MODE @@ -178,7 +182,7 @@ suspend fun commandBleTransmitter(context: Context, data: Map, s ) // Force the transmitter to restart and send updated attributes - sensorDao.updateLastSentStatesAndIcons( + sensorRepository.updateLastSentStatesAndIcons( BluetoothSensorManager.bleTransmitter.id, null, null, diff --git a/common/src/main/kotlin/io/homeassistant/companion/android/common/sensors/AudioSensorManager.kt b/common/src/main/kotlin/io/homeassistant/companion/android/common/sensors/AudioSensorManager.kt index e1fadeffc30..b3e2cac150c 100644 --- a/common/src/main/kotlin/io/homeassistant/companion/android/common/sensors/AudioSensorManager.kt +++ b/common/src/main/kotlin/io/homeassistant/companion/android/common/sensors/AudioSensorManager.kt @@ -471,7 +471,7 @@ class AudioSensorManager : SensorManager { return true } - val sensorsWithAttributes = sensorDao(context) + val sensorsWithAttributes = sensorRepository(context) .getFull(sensor.id) .toSensorsWithAttributes() diff --git a/common/src/main/kotlin/io/homeassistant/companion/android/common/sensors/BluetoothSensorManager.kt b/common/src/main/kotlin/io/homeassistant/companion/android/common/sensors/BluetoothSensorManager.kt index e52f90c10ec..8d011d0a0dc 100644 --- a/common/src/main/kotlin/io/homeassistant/companion/android/common/sensors/BluetoothSensorManager.kt +++ b/common/src/main/kotlin/io/homeassistant/companion/android/common/sensors/BluetoothSensorManager.kt @@ -128,13 +128,13 @@ class BluetoothSensorManager : SensorManager { ) suspend fun enableDisableBLETransmitter(context: Context, transmitEnabled: Boolean) { - val sensorDao = DatabaseEntryPoint.resolve(context).sensorDao() - val sensorEntity = sensorDao.get(bleTransmitter.id) + val sensorRepository = DatabaseEntryPoint.resolve(context).sensorRepository() + val sensorEntity = sensorRepository.get(bleTransmitter.id) if (sensorEntity.none { it.enabled }) { return } - sensorDao.add( + sensorRepository.add( SensorSetting( bleTransmitter.id, SETTING_BLE_TRANSMIT_ENABLED, @@ -145,8 +145,8 @@ class BluetoothSensorManager : SensorManager { } suspend fun enableDisableBeaconMonitor(context: Context, monitorEnabled: Boolean) { - val sensorDao = DatabaseEntryPoint.resolve(context).sensorDao() - val sensorEntity = sensorDao.get(beaconMonitor.id) + val sensorRepository = DatabaseEntryPoint.resolve(context).sensorRepository() + val sensorEntity = sensorRepository.get(beaconMonitor.id) if (sensorEntity.none { it.enabled }) { return } @@ -156,7 +156,7 @@ class BluetoothSensorManager : SensorManager { } else { monitoringManager.stopMonitoring(context, beaconMonitoringDevice) } - sensorDao.add( + sensorRepository.add( SensorSetting( beaconMonitor.id, SETTING_BEACON_MONITOR_ENABLED, @@ -492,7 +492,7 @@ class BluetoothSensorManager : SensorManager { } val lastState = - sensorDao(context).get(bleTransmitter.id).firstOrNull()?.state ?: STATE_UNKNOWN + sensorRepository(context).get(bleTransmitter.id).firstOrNull()?.state ?: STATE_UNKNOWN val state = if (isBtOn(context)) bleTransmitterDevice.state else "Bluetooth is turned off" val icon = if (bleTransmitterDevice.transmitting) "mdi:bluetooth" else "mdi:bluetooth-off" onSensorUpdated( diff --git a/common/src/main/kotlin/io/homeassistant/companion/android/common/sensors/LastRebootSensorManager.kt b/common/src/main/kotlin/io/homeassistant/companion/android/common/sensors/LastRebootSensorManager.kt index 01b218c50b1..0dc54962a00 100644 --- a/common/src/main/kotlin/io/homeassistant/companion/android/common/sensors/LastRebootSensorManager.kt +++ b/common/src/main/kotlin/io/homeassistant/companion/android/common/sensors/LastRebootSensorManager.kt @@ -62,13 +62,13 @@ class LastRebootSensorManager : SensorManager { var local = "" var utc = STATE_UNAVAILABLE - val sensorDao = sensorDao(context) - val fullSensor = sensorDao.getFull(lastRebootSensor.id).toSensorWithAttributes() - val sensorSetting = sensorDao.getSettings(lastRebootSensor.id) + val sensorRepository = sensorRepository(context) + val fullSensor = sensorRepository.getFull(lastRebootSensor.id).toSensorWithAttributes() + val sensorSetting = sensorRepository.getSettings(lastRebootSensor.id) val lastTimeMillis = fullSensor?.attributes?.firstOrNull { it.name == TIME_MILLISECONDS }?.value?.toLongOrNull() ?: 0L val settingDeadband = sensorSetting.firstOrNull { it.name == SETTING_DEADBAND }?.value?.toIntOrNull() ?: 60000 - sensorDao.add( + sensorRepository.add( SensorSetting(lastRebootSensor.id, SETTING_DEADBAND, settingDeadband.toString(), SensorSettingType.NUMBER), ) try { diff --git a/common/src/main/kotlin/io/homeassistant/companion/android/common/sensors/LastUpdateManager.kt b/common/src/main/kotlin/io/homeassistant/companion/android/common/sensors/LastUpdateManager.kt index fc0c7e19b64..939b95a753f 100644 --- a/common/src/main/kotlin/io/homeassistant/companion/android/common/sensors/LastUpdateManager.kt +++ b/common/src/main/kotlin/io/homeassistant/companion/android/common/sensors/LastUpdateManager.kt @@ -60,12 +60,12 @@ class LastUpdateManager : SensorManager { mapOf(), ) - val sensorDao = sensorDao(context) - val (settingsToRemove, allSettings) = sensorDao.getSettings(lastUpdate.id).partition { setting -> + val sensorRepository = sensorRepository(context) + val (settingsToRemove, allSettings) = sensorRepository.getSettings(lastUpdate.id).partition { setting -> setting.value.isEmpty() } if (settingsToRemove.isNotEmpty()) { - sensorDao.removeSettings(lastUpdate.id, settingsToRemove.map { it.name }) + sensorRepository.removeSettings(lastUpdate.id, settingsToRemove.map { it.name }) } val intentSettings = allSettings.filter { it.name.startsWith(INTENT_SETTING_PREFIX) @@ -82,24 +82,28 @@ class LastUpdateManager : SensorManager { it.copy(name = "$INTENT_SETTING_PREFIX${index + 1}:") } // delete old settings from DB: - sensorDao.removeSettings(lastUpdate.id, intentSettings.map { it.name }) + sensorRepository.removeSettings(lastUpdate.id, intentSettings.map { it.name }) // add new settings to DB: newIntentSettings.forEach { - sensorDao.add(it) + sensorRepository.add(it) } } val addNewIntentToggle = allSettings.firstOrNull { it.name == SETTING_ADD_NEW_INTENT } if (addNewIntentToggle == null) { // add the toggle if it was not already added. - sensorDao.add(SensorSetting(lastUpdate.id, SETTING_ADD_NEW_INTENT, "false", SensorSettingType.TOGGLE)) + sensorRepository.add( + SensorSetting(lastUpdate.id, SETTING_ADD_NEW_INTENT, "false", SensorSettingType.TOGGLE), + ) } else if (addNewIntentToggle.value == "true") { val newIntentSettingOrdinal = intentSettings.size + 1 val newIntentSettingName = "$INTENT_SETTING_PREFIX$newIntentSettingOrdinal:" if (allSettings.none { it.name == newIntentSettingName }) { // turn off the toggle: - sensorDao.add(SensorSetting(lastUpdate.id, SETTING_ADD_NEW_INTENT, "false", SensorSettingType.TOGGLE)) + sensorRepository.add( + SensorSetting(lastUpdate.id, SETTING_ADD_NEW_INTENT, "false", SensorSettingType.TOGGLE), + ) // add the new Intent: - sensorDao.add( + sensorRepository.add( SensorSetting(lastUpdate.id, newIntentSettingName, intentAction, SensorSettingType.STRING), ) } diff --git a/common/src/main/kotlin/io/homeassistant/companion/android/common/sensors/NetworkSensorManager.kt b/common/src/main/kotlin/io/homeassistant/companion/android/common/sensors/NetworkSensorManager.kt index 39007484b11..0c6cd2e6328 100644 --- a/common/src/main/kotlin/io/homeassistant/companion/android/common/sensors/NetworkSensorManager.kt +++ b/common/src/main/kotlin/io/homeassistant/companion/android/common/sensors/NetworkSensorManager.kt @@ -316,25 +316,27 @@ class NetworkSensorManager : SensorManager { var bssid = if (conInfo?.bssid == null) "" else conInfo.bssid val settingName = "network_replace_mac_var1:$bssid:" - val sensorDao = sensorDao(context) - val sensorSettings = sensorDao.getSettings(bssidState.id) + val sensorRepository = sensorRepository(context) + val sensorSettings = sensorRepository.getSettings(bssidState.id) val getCurrentBSSID = sensorSettings.firstOrNull { it.name == SETTING_GET_CURRENT_BSSID }?.value ?: "false" val currentSetting = sensorSettings.firstOrNull { it.name == settingName }?.value ?: "" if (getCurrentBSSID == "true") { if (currentSetting == "") { - sensorDao.add( + sensorRepository.add( SensorSetting(bssidState.id, SETTING_GET_CURRENT_BSSID, "false", SensorSettingType.TOGGLE), ) - sensorDao.add(SensorSetting(bssidState.id, settingName, bssid, SensorSettingType.STRING)) + sensorRepository.add(SensorSetting(bssidState.id, settingName, bssid, SensorSettingType.STRING)) } } else { if (currentSetting != "") { bssid = currentSetting } else { - sensorDao.removeSetting(bssidState.id, settingName) + sensorRepository.removeSetting(bssidState.id, settingName) } - sensorDao.add(SensorSetting(bssidState.id, SETTING_GET_CURRENT_BSSID, "false", SensorSettingType.TOGGLE)) + sensorRepository.add( + SensorSetting(bssidState.id, SETTING_GET_CURRENT_BSSID, "false", SensorSettingType.TOGGLE), + ) } val icon = if (bssid != "") "mdi:wifi" else "mdi:wifi-off" diff --git a/common/src/main/kotlin/io/homeassistant/companion/android/common/sensors/NextAlarmManager.kt b/common/src/main/kotlin/io/homeassistant/companion/android/common/sensors/NextAlarmManager.kt index cf6c0e42c1a..d97e38d33bc 100644 --- a/common/src/main/kotlin/io/homeassistant/companion/android/common/sensors/NextAlarmManager.kt +++ b/common/src/main/kotlin/io/homeassistant/companion/android/common/sensors/NextAlarmManager.kt @@ -65,8 +65,8 @@ class NextAlarmManager : SensorManager { var utc = STATE_UNAVAILABLE var pendingIntent = "" - val sensorDao = sensorDao(context) - val sensorSetting = sensorDao.getSettings(nextAlarm.id) + val sensorRepository = sensorRepository(context) + val sensorSetting = sensorRepository.getSettings(nextAlarm.id) val allowPackageList = sensorSetting.firstOrNull { it.name == SETTING_ALLOW_LIST }?.value ?: "" try { @@ -86,7 +86,7 @@ class NextAlarmManager : SensorManager { return } } else { - sensorDao.add( + sensorRepository.add( SensorSetting(nextAlarm.id, SETTING_ALLOW_LIST, allowPackageList, SensorSettingType.LIST_APPS), ) } diff --git a/common/src/main/kotlin/io/homeassistant/companion/android/common/sensors/SensorManager.kt b/common/src/main/kotlin/io/homeassistant/companion/android/common/sensors/SensorManager.kt index 54c770889ff..2bc60bc480a 100644 --- a/common/src/main/kotlin/io/homeassistant/companion/android/common/sensors/SensorManager.kt +++ b/common/src/main/kotlin/io/homeassistant/companion/android/common/sensors/SensorManager.kt @@ -18,7 +18,6 @@ import io.homeassistant.companion.android.common.util.AnySerializer import io.homeassistant.companion.android.common.util.SdkVersion import io.homeassistant.companion.android.common.util.kotlinJsonMapper import io.homeassistant.companion.android.database.sensor.Attribute -import io.homeassistant.companion.android.database.sensor.SensorDao import io.homeassistant.companion.android.database.sensor.SensorSetting import io.homeassistant.companion.android.database.sensor.SensorSettingType import java.util.Locale @@ -107,32 +106,27 @@ interface SensorManager { return mode == AppOpsManager.MODE_ALLOWED } - /** @return `true` if this sensor is enabled on any server */ + /** + * @return `true` if this sensor is enabled on any server. + */ suspend fun isEnabled(context: Context, basicSensor: BasicSensor): Boolean { - val permission = checkPermission(context, basicSensor.id) - return sensorDao(context).getAnyIsEnabled( - basicSensor.id, - serverManager(context).servers().map { it.id }, - permission, - basicSensor.enabledByDefault, - ) + if (!checkPermission(context, basicSensor.id)) return false + return sensorRepository(context).get(basicSensor.id).any { it.enabled } } - /** @return `true` if this sensor is enabled for the specified server */ + /** + * @return `true` if this sensor is enabled for the specified server. + */ suspend fun isEnabled(context: Context, basicSensor: BasicSensor, serverId: Int): Boolean { - val permission = checkPermission(context, basicSensor.id) - return sensorDao(context).getOrDefault( - basicSensor.id, - serverId, - permission, - basicSensor.enabledByDefault, - )?.enabled == true + if (!checkPermission(context, basicSensor.id)) return false + return sensorRepository(context).get(basicSensor.id, serverId)?.enabled == true } /** @return Set of server IDs for which this sensor is enabled */ suspend fun getEnabledServers(context: Context, basicSensor: BasicSensor): Set { val permission = checkPermission(context, basicSensor.id) - return sensorDao(context).get(basicSensor.id).filter { it.enabled && permission }.map { it.serverId }.toSet() + return sensorRepository(context).get(basicSensor.id).filter { it.enabled && permission }.map { it.serverId } + .toSet() } /** @@ -161,7 +155,7 @@ interface SensorManager { } suspend fun isSettingEnabled(context: Context, sensor: BasicSensor, settingName: String): Boolean { - val setting = sensorDao(context) + val setting = sensorRepository(context) .getSettings(sensor.id) .firstOrNull { it.name == settingName } return setting?.enabled ?: false @@ -174,7 +168,7 @@ interface SensorManager { !enabled && settingEnabled ) { - sensorDao(context).updateSettingEnabled(sensor.id, settingName, enabled) + sensorRepository(context).updateSettingEnabled(sensor.id, settingName, enabled) } } @@ -226,13 +220,22 @@ interface SensorManager { enabled: Boolean = true, entries: List = arrayListOf(), ): String { - val sensorDao = sensorDao(context) - val setting = sensorDao + val sensorRepository = sensorRepository(context) + val setting = sensorRepository .getSettings(sensor.id) .firstOrNull { it.name == settingName } ?.value if (setting == null) { - sensorDao.add(SensorSetting(sensor.id, settingName, default, settingType, enabled, entries = entries)) + sensorRepository.add( + SensorSetting( + sensor.id, + settingName, + default, + settingType, + enabled, + entries = entries, + ), + ) } return setting ?: default @@ -246,8 +249,8 @@ interface SensorManager { attributes: Map, forceUpdate: Boolean = false, ) = withContext(Dispatchers.Default) { - val sensorDao = sensorDao(context) - val sensors = sensorDao.get(basicSensor.id) + val sensorRepository = sensorRepository(context) + val sensors = sensorRepository.get(basicSensor.id) if (sensors.isEmpty()) return@withContext sensors.forEach { @@ -270,9 +273,9 @@ interface SensorManager { lastSentState = if (forceUpdate) null else it.lastSentState, lastSentIcon = if (forceUpdate) null else it.lastSentIcon, ) - sensorDao.update(sensor) + sensorRepository.update(sensor) } - sensorDao.replaceAllAttributes( + sensorRepository.replaceAllAttributes( basicSensor.id, attributes = attributes.map { item -> val valueType = when (item.value) { @@ -289,14 +292,17 @@ interface SensorManager { else -> "liststring" } } + else -> "string" // Always default to String for attributes } val value = when { valueType == "liststring" -> kotlinJsonMapper.encodeToString((item.value as List<*>).map { it.toString() }) + valueType.startsWith("list") -> kotlinJsonMapper.encodeToString(AnySerializer, item.value) + else -> item.value.toString() } @@ -315,7 +321,7 @@ interface SensorManager { @InstallIn(SingletonComponent::class) interface SensorManagerEntryPoint { fun serverManager(): ServerManager - fun sensorDao(): SensorDao + fun sensorRepository(): SensorRepository } private fun sensorManagerEntryPoint(context: Context): SensorManagerEntryPoint = @@ -326,7 +332,7 @@ interface SensorManager { fun serverManager(context: Context) = sensorManagerEntryPoint(context).serverManager() - fun sensorDao(context: Context) = sensorManagerEntryPoint(context).sensorDao() + fun sensorRepository(context: Context) = sensorManagerEntryPoint(context).sensorRepository() } fun SensorManager.id(): String { diff --git a/common/src/main/kotlin/io/homeassistant/companion/android/common/sensors/SensorReceiverBase.kt b/common/src/main/kotlin/io/homeassistant/companion/android/common/sensors/SensorReceiverBase.kt index d79d3b71d58..e181cb61094 100644 --- a/common/src/main/kotlin/io/homeassistant/companion/android/common/sensors/SensorReceiverBase.kt +++ b/common/src/main/kotlin/io/homeassistant/companion/android/common/sensors/SensorReceiverBase.kt @@ -21,7 +21,6 @@ import io.homeassistant.companion.android.common.data.websocket.impl.entities.Ge import io.homeassistant.companion.android.common.util.CHANNEL_SENSOR_SYNC import io.homeassistant.companion.android.common.util.SdkVersion import io.homeassistant.companion.android.database.DatabaseEntryPoint -import io.homeassistant.companion.android.database.sensor.SensorDao import io.homeassistant.companion.android.database.sensor.SensorWithAttributes import io.homeassistant.companion.android.database.sensor.toSensorWithAttributes import io.homeassistant.companion.android.database.sensor.toSensorsWithAttributes @@ -32,6 +31,7 @@ import java.net.ConnectException import java.net.SocketTimeoutException import java.util.Locale import javax.inject.Inject +import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -78,7 +78,7 @@ abstract class SensorReceiverBase : BroadcastReceiver() { lateinit var serverManager: ServerManager @Inject - lateinit var sensorDao: SensorDao + lateinit var sensorRepository: SensorRepository private val chargingActions = listOf( Intent.ACTION_BATTERY_LOW, @@ -123,7 +123,7 @@ abstract class SensorReceiverBase : BroadcastReceiver() { @Suppress("DEPRECATION") if (isSensorEnabled(LastUpdateManager.lastUpdate.id)) { LastUpdateManager().sendLastUpdate(context, intent.action) - val allSettings = sensorDao.getSettings(LastUpdateManager.lastUpdate.id) + val allSettings = sensorRepository.getSettings(LastUpdateManager.lastUpdate.id) for (setting in allSettings) { if (setting.value != "" && intent.action == setting.value) { val eventData = intent.extras?.keySet() @@ -131,7 +131,7 @@ abstract class SensorReceiverBase : BroadcastReceiver() { ?.plus("intent" to intent.action.toString()) ?: mapOf("intent" to intent.action.toString()) Timber.d("Event data: $eventData") - sensorDao.get(LastUpdateManager.lastUpdate.id).forEach { sensor -> + sensorRepository.get(LastUpdateManager.lastUpdate.id).forEach { sensor -> if (!sensor.enabled) return@forEach try { serverManager.integrationRepository(sensor.serverId).fireEvent( @@ -157,22 +157,27 @@ abstract class SensorReceiverBase : BroadcastReceiver() { updateSensor(context, sensorId) } } else { - updateSensors(context, serverManager, sensorDao, intent) + updateSensors(context, serverManager, sensorRepository, intent) if (chargingActions.contains(intent.action)) { // Add a 5 second delay to perform another update so charging state updates completely. // This is necessary as the system needs a few seconds to verify the charger. - delay(5000L) - updateSensors(context, serverManager, sensorDao, intent) + delay(5.seconds) + updateSensors(context, serverManager, sensorRepository, intent) } } } } private suspend fun isSensorEnabled(id: String): Boolean { - return sensorDao.get(id).any { it.enabled } + return sensorRepository.get(id).any { it.enabled } } - suspend fun updateSensors(context: Context, serverManager: ServerManager, sensorDao: SensorDao, intent: Intent?) { + suspend fun updateSensors( + context: Context, + serverManager: ServerManager, + sensorRepository: SensorRepository, + intent: Intent?, + ) { if (!serverManager.isRegistered()) { Timber.w("Device not registered, skipping sensor update/registration") return @@ -191,7 +196,7 @@ abstract class SensorReceiverBase : BroadcastReceiver() { try { serverManager.servers().map { server -> - ioScope.async { syncSensorsWithServer(context, serverManager, server, sensorDao) } + ioScope.async { syncSensorsWithServer(context, serverManager, server, sensorRepository) } }.awaitAll() Timber.i("Sensor updates and sync completed") } catch (e: Exception) { @@ -203,7 +208,7 @@ abstract class SensorReceiverBase : BroadcastReceiver() { context: Context, serverManager: ServerManager, server: Server, - sensorDao: SensorDao, + sensorRepository: SensorRepository, ): Boolean { val config: GetConfigResponse val integrationRepository: IntegrationRepository @@ -220,7 +225,7 @@ abstract class SensorReceiverBase : BroadcastReceiver() { val supportsDisabledSensors = integrationRepository.isHomeAssistantVersionAtLeast(2022, 6, 0) val serverIsTrusted = integrationRepository.isTrusted() val coreSensorStatus: Map? = - if (supportsDisabledSensors && (serverIsTrusted || (sensorDao.getEnabledCount() ?: 0) > 0)) { + if (supportsDisabledSensors && (serverIsTrusted || sensorRepository.getEnabledCount() > 0)) { config.entities ?.filter { it.value["disabled"] != null } ?.mapValues { !(it.value["disabled"] as Boolean) } // Map to sensor id -> enabled @@ -237,7 +242,7 @@ abstract class SensorReceiverBase : BroadcastReceiver() { val hasSensor = manager.hasSensor(context) manager.getAvailableSensors(context).forEach sensorForEach@{ basicSensor -> - val fullSensor = sensorDao.getFull(basicSensor.id, server.id).toSensorWithAttributes() + val fullSensor = sensorRepository.getFull(basicSensor.id, server.id).toSensorWithAttributes() val sensor = fullSensor?.sensor ?: return@sensorForEach val sensorCoreEnabled = coreSensorStatus?.get(basicSensor.id) val canBeRegistered = hasSensor && @@ -265,7 +270,7 @@ abstract class SensorReceiverBase : BroadcastReceiver() { sensor.registered = sensor.enabled sensor.coreRegistration = currentHAversion sensor.appRegistration = currentAppVersion - sensorDao.update(sensor) + sensorRepository.update(sensor) } catch (e: Exception) { Timber.e(e, "Issue registering sensor ${basicSensor.id}") } @@ -313,7 +318,7 @@ abstract class SensorReceiverBase : BroadcastReceiver() { sensor.coreRegistration = currentHAversion sensor.appRegistration = currentAppVersion - sensorDao.update(sensor) + sensorRepository.update(sensor) } catch (e: Exception) { Timber.e(e, "Issue enabling/disabling sensor ${basicSensor.id}") } @@ -330,7 +335,7 @@ abstract class SensorReceiverBase : BroadcastReceiver() { sensor.registered = sensor.enabled sensor.coreRegistration = currentHAversion sensor.appRegistration = currentAppVersion - sensorDao.update(sensor) + sensorRepository.update(sensor) } catch (e: Exception) { Timber.e(e, "Issue re-registering sensor ${basicSensor.id}") if (e is IntegrationException && @@ -359,7 +364,7 @@ abstract class SensorReceiverBase : BroadcastReceiver() { success = try { val serverSuccess = integrationRepository.updateSensors(enabledRegistrations) enabledRegistrations.forEach { - sensorDao.updateLastSentStateAndIcon(it.uniqueId, it.serverId, it.state.toString(), it.icon) + sensorRepository.updateLastSentStateAndIcon(it.uniqueId, it.serverId, it.state.toString(), it.icon) } serverSuccess } catch (e: Exception) { @@ -377,12 +382,12 @@ abstract class SensorReceiverBase : BroadcastReceiver() { // We failed to update a sensor, we should re register next time if (!success) { enabledRegistrations.forEach { - val sensor = sensorDao.get(it.uniqueId, it.serverId) + val sensor = sensorRepository.get(it.uniqueId, it.serverId) if (sensor != null) { sensor.registered = null sensor.lastSentState = null sensor.lastSentIcon = null - sensorDao.update(sensor) + sensorRepository.update(sensor) } } } @@ -416,7 +421,7 @@ abstract class SensorReceiverBase : BroadcastReceiver() { Timber.e(e, "Issue requesting updates for ${context.getString(sensorManager.name)}") } val basicSensor = sensorManager.getAvailableSensors(context).firstOrNull { it.id == sensorId } ?: return - val fullSensors = sensorDao.getFull(sensorId).toSensorsWithAttributes() + val fullSensors = sensorRepository.getFull(sensorId).toSensorsWithAttributes() fullSensors.filter { it.sensor.enabled && it.sensor.registered == true && @@ -427,7 +432,7 @@ abstract class SensorReceiverBase : BroadcastReceiver() { serverManager.integrationRepository( fullSensor.sensor.serverId, ).updateSensors(listOf(fullSensor.toSensorRegistration(basicSensor))) - sensorDao.updateLastSentStateAndIcon( + sensorRepository.updateLastSentStateAndIcon( basicSensor.id, fullSensor.sensor.serverId, fullSensor.sensor.state, diff --git a/common/src/main/kotlin/io/homeassistant/companion/android/common/sensors/SensorWorkerBase.kt b/common/src/main/kotlin/io/homeassistant/companion/android/common/sensors/SensorWorkerBase.kt index ef5444c8eb7..a7cc9299bdd 100644 --- a/common/src/main/kotlin/io/homeassistant/companion/android/common/sensors/SensorWorkerBase.kt +++ b/common/src/main/kotlin/io/homeassistant/companion/android/common/sensors/SensorWorkerBase.kt @@ -36,8 +36,9 @@ abstract class SensorWorkerBase(val appContext: Context, workerParams: WorkerPar private val notificationManager = appContext.getSystemService()!! override suspend fun doWork(): Result = withContext(Dispatchers.IO) { - val sensorDao = DatabaseEntryPoint.resolve(applicationContext).sensorDao() - val enabledSensorCount = sensorDao.getEnabledCount() ?: 0 + val databaseEntryPoint = DatabaseEntryPoint.resolve(applicationContext) + val sensorRepository = databaseEntryPoint.sensorRepository() + val enabledSensorCount = sensorRepository.getEnabledCount() if ( enabledSensorCount > 0 || serverManager.servers().any { @@ -74,23 +75,17 @@ abstract class SensorWorkerBase(val appContext: Context, workerParams: WorkerPar Timber.d(e, "Updating all Sensors in background.") } - val lastUpdateSensor = sensorDao.get(LastUpdateManager.lastUpdate.id) + val lastUpdateSensor = sensorRepository.get(LastUpdateManager.lastUpdate.id) if (lastUpdateSensor.any { it.enabled }) { LastUpdateManager().sendLastUpdate(appContext, TAG) } - sensorReceiver.updateSensors(appContext, serverManager, sensorDao, null) + sensorReceiver.updateSensors(appContext, serverManager, sensorRepository, null) } // Cleanup orphaned sensors that may have been created by a slow or long running update // writing data when deleting the server. val currentServerIds = serverManager.servers().map { it.id } - val orphanedSensors = sensorDao.getAllExceptServer(currentServerIds) - if (orphanedSensors.any()) { - Timber.i("Cleaning up ${orphanedSensors.size} orphaned sensor entries") - orphanedSensors.forEach { - sensorDao.removeSensor(it.id, it.serverId) - } - } + sensorRepository.removeSensorsExceptServers(currentServerIds) Result.success() } diff --git a/common/src/main/kotlin/io/homeassistant/companion/android/database/DatabaseEntryPoint.kt b/common/src/main/kotlin/io/homeassistant/companion/android/database/DatabaseEntryPoint.kt index d9ac005a9ce..5991133c8f4 100644 --- a/common/src/main/kotlin/io/homeassistant/companion/android/database/DatabaseEntryPoint.kt +++ b/common/src/main/kotlin/io/homeassistant/companion/android/database/DatabaseEntryPoint.kt @@ -5,12 +5,13 @@ import dagger.hilt.EntryPoint import dagger.hilt.InstallIn import dagger.hilt.android.EntryPointAccessors import dagger.hilt.components.SingletonComponent +import io.homeassistant.companion.android.common.sensors.SensorRepository import io.homeassistant.companion.android.database.notification.NotificationDao -import io.homeassistant.companion.android.database.sensor.SensorDao import io.homeassistant.companion.android.database.settings.SettingsDao /** - * Hilt EntryPoint for accessing database DAOs in classes that cannot use constructor injection. + * Hilt EntryPoint for accessing database DAOs and the sensor repository in classes that cannot use + * constructor injection. * * Use this for BroadcastReceivers, Services, SensorManagers, and other classes where * Hilt's automatic injection is not available. @@ -18,13 +19,13 @@ import io.homeassistant.companion.android.database.settings.SettingsDao * Usage: * ``` * val entryPoint = DatabaseEntryPoint.resolve(context) - * val sensorDao = entryPoint.sensorDao() + * val sensorRepository = entryPoint.sensorRepository() * ``` */ @EntryPoint @InstallIn(SingletonComponent::class) interface DatabaseEntryPoint { - fun sensorDao(): SensorDao + fun sensorRepository(): SensorRepository fun settingsDao(): SettingsDao fun notificationDao(): NotificationDao diff --git a/common/src/test/kotlin/io/homeassistant/companion/android/common/data/servers/ServerManagerImplTest.kt b/common/src/test/kotlin/io/homeassistant/companion/android/common/data/servers/ServerManagerImplTest.kt index 4175af9f339..4f6fa207386 100644 --- a/common/src/test/kotlin/io/homeassistant/companion/android/common/data/servers/ServerManagerImplTest.kt +++ b/common/src/test/kotlin/io/homeassistant/companion/android/common/data/servers/ServerManagerImplTest.kt @@ -10,7 +10,7 @@ import io.homeassistant.companion.android.common.data.prefs.PrefsRepository import io.homeassistant.companion.android.common.data.servers.ServerManager.Companion.SERVER_ID_ACTIVE import io.homeassistant.companion.android.common.data.websocket.WebSocketRepository import io.homeassistant.companion.android.common.data.websocket.WebSocketRepositoryFactory -import io.homeassistant.companion.android.database.sensor.SensorDao +import io.homeassistant.companion.android.common.sensors.SensorRepository import io.homeassistant.companion.android.database.server.Server import io.homeassistant.companion.android.database.server.ServerConnectionInfo import io.homeassistant.companion.android.database.server.ServerDao @@ -51,7 +51,7 @@ class ServerManagerImplTest { private val serverConnectionStateProviderFactory: ServerConnectionStateProviderFactory = mockk() private val prefsRepository: PrefsRepository = mockk() private val serverDao: ServerDao = mockk() - private val sensorDao: SensorDao = mockk() + private val sensorRepository: SensorRepository = mockk() private val settingsDao: SettingsDao = mockk() private val localStorage: LocalStorage = mockk() @@ -79,7 +79,7 @@ class ServerManagerImplTest { serverConnectionStateProviderFactory = serverConnectionStateProviderFactory, prefsRepository = prefsRepository, serverDao = serverDao, - sensorDao = sensorDao, + sensorRepository = sensorRepository, settingsDao = settingsDao, localStorage = localStorage, ) @@ -342,7 +342,7 @@ class ServerManagerImplTest { coEvery { prefsRepository.removeServer(serverId) } just Runs coEvery { localStorage.getInt("active_server") } returns null coEvery { settingsDao.delete(serverId) } just Runs - coEvery { sensorDao.removeServer(serverId) } just Runs + coEvery { sensorRepository.removeServer(serverId) } just Runs coEvery { serverDao.delete(serverId) } just Runs coEvery { webSocketRepo.shutdown() } just Runs @@ -356,7 +356,7 @@ class ServerManagerImplTest { prefsRepository.removeServer(serverId) webSocketRepo.shutdown() settingsDao.delete(serverId) - sensorDao.removeServer(serverId) + sensorRepository.removeServer(serverId) serverDao.delete(serverId) } } @@ -376,7 +376,7 @@ class ServerManagerImplTest { coEvery { localStorage.getInt("active_server") } returns serverId coEvery { localStorage.remove("active_server") } just Runs coEvery { settingsDao.delete(serverId) } just Runs - coEvery { sensorDao.removeServer(serverId) } just Runs + coEvery { sensorRepository.removeServer(serverId) } just Runs coEvery { serverDao.delete(serverId) } just Runs serverManager.removeServer(serverId) @@ -398,7 +398,7 @@ class ServerManagerImplTest { coEvery { prefsRepository.removeServer(serverId) } just Runs coEvery { localStorage.getInt("active_server") } returns 10 coEvery { settingsDao.delete(serverId) } just Runs - coEvery { sensorDao.removeServer(serverId) } just Runs + coEvery { sensorRepository.removeServer(serverId) } just Runs coEvery { serverDao.delete(serverId) } just Runs serverManager.removeServer(serverId) @@ -422,7 +422,7 @@ class ServerManagerImplTest { coEvery { prefsRepository.removeServer(serverId) } just Runs coEvery { localStorage.getInt("active_server") } returns null coEvery { settingsDao.delete(serverId) } just Runs - coEvery { sensorDao.removeServer(serverId) } just Runs + coEvery { sensorRepository.removeServer(serverId) } just Runs coEvery { serverDao.delete(serverId) } just Runs coEvery { webSocketRepo.shutdown() } just Runs diff --git a/common/src/test/kotlin/io/homeassistant/companion/android/common/sensors/AudioSensorManagerTest.kt b/common/src/test/kotlin/io/homeassistant/companion/android/common/sensors/AudioSensorManagerTest.kt index 38042c25d30..cf0cbc38761 100644 --- a/common/src/test/kotlin/io/homeassistant/companion/android/common/sensors/AudioSensorManagerTest.kt +++ b/common/src/test/kotlin/io/homeassistant/companion/android/common/sensors/AudioSensorManagerTest.kt @@ -8,7 +8,6 @@ import dagger.hilt.android.testing.HiltTestApplication import io.homeassistant.companion.android.common.data.servers.ServerManager import io.homeassistant.companion.android.database.sensor.Attribute import io.homeassistant.companion.android.database.sensor.Sensor -import io.homeassistant.companion.android.database.sensor.SensorDao import io.mockk.coEvery import io.mockk.coJustRun import io.mockk.coVerify @@ -34,7 +33,7 @@ class AudioSensorManagerTest { private lateinit var sensorManager: AudioSensorManager private lateinit var context: Context private lateinit var audioManager: AudioManager - private lateinit var sensorDao: SensorDao + private lateinit var sensorRepository: SensorRepository private lateinit var serverManager: ServerManager private val volumeMusicSensor = @@ -54,7 +53,7 @@ class AudioSensorManagerTest { sensorManager = AudioSensorManager() context = mockk() audioManager = mockk(relaxed = true) - sensorDao = mockk() + sensorRepository = mockk() serverManager = mockk() val entryPoint = mockk() @@ -69,27 +68,20 @@ class AudioSensorManagerTest { SensorManager.SensorManagerEntryPoint::class.java, ) } returns entryPoint - every { entryPoint.sensorDao() } returns sensorDao + every { entryPoint.sensorRepository() } returns sensorRepository every { entryPoint.serverManager() } returns serverManager coEvery { serverManager.servers() } returns listOf(mockk(relaxed = true)) - coEvery { - sensorDao.getAnyIsEnabled( - sensorId = any(), - servers = any(), - permission = any(), - enabledByDefault = any(), - ) - } answers { firstArg() == AudioSensorManager.volMusic.id } - coEvery { sensorDao.get(any()) } returns emptyList() - coEvery { sensorDao.get(AudioSensorManager.volMusic.id) } returns listOf(volumeMusicSensor) + coEvery { sensorRepository.get(any()) } returns emptyList() + coEvery { sensorRepository.get(AudioSensorManager.volMusic.id) } returns listOf(volumeMusicSensor) + coEvery { sensorRepository.get(AudioSensorManager.volMusic.id, any()) } returns volumeMusicSensor every { audioManager.getStreamVolume(AudioManager.STREAM_MUSIC) } returns 5 every { audioManager.getStreamMinVolume(AudioManager.STREAM_MUSIC) } returns 2 every { audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC) } returns 15 - coJustRun { sensorDao.update(any()) } - coJustRun { sensorDao.replaceAllAttributes(any(), any()) } + coJustRun { sensorRepository.update(any()) } + coJustRun { sensorRepository.replaceAllAttributes(any(), any()) } } @After @@ -101,9 +93,9 @@ class AudioSensorManagerTest { fun `Given missing min and max attributes when requesting update then force update with min and max attributes`() = runTest { val updatedSensor = slot() val updatedAttributes = slot>() - coEvery { sensorDao.getFull(AudioSensorManager.volMusic.id) } returns mapOf(volumeMusicSensor to emptyList()) - coJustRun { sensorDao.update(capture(updatedSensor)) } - coJustRun { sensorDao.replaceAllAttributes(AudioSensorManager.volMusic.id, capture(updatedAttributes)) } + coEvery { sensorRepository.getFull(AudioSensorManager.volMusic.id) } returns mapOf(volumeMusicSensor to emptyList()) + coJustRun { sensorRepository.update(capture(updatedSensor)) } + coJustRun { sensorRepository.replaceAllAttributes(AudioSensorManager.volMusic.id, capture(updatedAttributes)) } sensorManager.requestSensorUpdate(context) @@ -117,13 +109,13 @@ class AudioSensorManagerTest { @Test fun `Given changed max attribute when requesting update then force update`() = runTest { val updatedSensor = slot() - coEvery { sensorDao.getFull(AudioSensorManager.volMusic.id) } returns mapOf( + coEvery { sensorRepository.getFull(AudioSensorManager.volMusic.id) } returns mapOf( volumeMusicSensor to listOf( Attribute(AudioSensorManager.volMusic.id, "min", "2", "int"), Attribute(AudioSensorManager.volMusic.id, "max", "10", "int"), ), ) - coJustRun { sensorDao.update(capture(updatedSensor)) } + coJustRun { sensorRepository.update(capture(updatedSensor)) } sensorManager.requestSensorUpdate(context) @@ -133,13 +125,13 @@ class AudioSensorManagerTest { @Test fun `Given mismatched max attribute when requesting update twice then force update each time`() = runTest { val updatedSensors = mutableListOf() - coEvery { sensorDao.getFull(AudioSensorManager.volMusic.id) } returns mapOf( + coEvery { sensorRepository.getFull(AudioSensorManager.volMusic.id) } returns mapOf( volumeMusicSensor to listOf( Attribute(AudioSensorManager.volMusic.id, "min", "2", "int"), Attribute(AudioSensorManager.volMusic.id, "max", "10", "int"), ), ) - coJustRun { sensorDao.update(capture(updatedSensors)) } + coJustRun { sensorRepository.update(capture(updatedSensors)) } sensorManager.requestSensorUpdate(context) sensorManager.requestSensorUpdate(context) @@ -152,8 +144,8 @@ class AudioSensorManagerTest { fun `Given multi-server with one server having mismatched attributes then force update`() = runTest { val updatedSensors = mutableListOf() val volumeMusicSensorServer2 = volumeMusicSensor.copy(serverId = 2) - coEvery { sensorDao.get(AudioSensorManager.volMusic.id) } returns listOf(volumeMusicSensor, volumeMusicSensorServer2) - coEvery { sensorDao.getFull(AudioSensorManager.volMusic.id) } returns mapOf( + coEvery { sensorRepository.get(AudioSensorManager.volMusic.id) } returns listOf(volumeMusicSensor, volumeMusicSensorServer2) + coEvery { sensorRepository.getFull(AudioSensorManager.volMusic.id) } returns mapOf( volumeMusicSensor to listOf( Attribute(AudioSensorManager.volMusic.id, "min", "2", "int"), Attribute(AudioSensorManager.volMusic.id, "max", "15", "int"), @@ -163,7 +155,7 @@ class AudioSensorManagerTest { Attribute(AudioSensorManager.volMusic.id, "max", "10", "int"), ), ) - coJustRun { sensorDao.update(capture(updatedSensors)) } + coJustRun { sensorRepository.update(capture(updatedSensors)) } sensorManager.requestSensorUpdate(context) @@ -175,8 +167,8 @@ class AudioSensorManagerTest { fun `Given multi-server with all servers having matching attributes then does not force update`() = runTest { val updatedSensor = slot() val volumeMusicSensorServer2 = volumeMusicSensor.copy(serverId = 2) - coEvery { sensorDao.get(AudioSensorManager.volMusic.id) } returns listOf(volumeMusicSensor, volumeMusicSensorServer2) - coEvery { sensorDao.getFull(AudioSensorManager.volMusic.id) } returns mapOf( + coEvery { sensorRepository.get(AudioSensorManager.volMusic.id) } returns listOf(volumeMusicSensor, volumeMusicSensorServer2) + coEvery { sensorRepository.getFull(AudioSensorManager.volMusic.id) } returns mapOf( volumeMusicSensor to listOf( Attribute(AudioSensorManager.volMusic.id, "min", "2", "int"), Attribute(AudioSensorManager.volMusic.id, "max", "15", "int"), @@ -186,7 +178,7 @@ class AudioSensorManagerTest { Attribute(AudioSensorManager.volMusic.id, "max", "15", "int"), ), ) - coJustRun { sensorDao.update(capture(updatedSensor)) } + coJustRun { sensorRepository.update(capture(updatedSensor)) } sensorManager.requestSensorUpdate(context) @@ -196,13 +188,13 @@ class AudioSensorManagerTest { @Test fun `Given unchanged min and max attributes when requesting update then does not force update`() = runTest { val updatedSensor = slot() - coEvery { sensorDao.getFull(AudioSensorManager.volMusic.id) } returns mapOf( + coEvery { sensorRepository.getFull(AudioSensorManager.volMusic.id) } returns mapOf( volumeMusicSensor to listOf( Attribute(AudioSensorManager.volMusic.id, "min", "2", "int"), Attribute(AudioSensorManager.volMusic.id, "max", "15", "int"), ), ) - coJustRun { sensorDao.update(capture(updatedSensor)) } + coJustRun { sensorRepository.update(capture(updatedSensor)) } sensorManager.requestSensorUpdate(context) @@ -211,7 +203,7 @@ class AudioSensorManagerTest { @Test fun `Given unchanged attributes when requesting update twice then getFull is only called once`() = runTest { - coEvery { sensorDao.getFull(AudioSensorManager.volMusic.id) } returns mapOf( + coEvery { sensorRepository.getFull(AudioSensorManager.volMusic.id) } returns mapOf( volumeMusicSensor to listOf( Attribute(AudioSensorManager.volMusic.id, "min", "2", "int"), Attribute(AudioSensorManager.volMusic.id, "max", "15", "int"), @@ -221,16 +213,16 @@ class AudioSensorManagerTest { sensorManager.requestSensorUpdate(context) sensorManager.requestSensorUpdate(context) - coVerify(exactly = 1) { sensorDao.getFull(AudioSensorManager.volMusic.id) } + coVerify(exactly = 1) { sensorRepository.getFull(AudioSensorManager.volMusic.id) } } @Test @Config(sdk = [26]) // Build.VERSION_CODES.O fun `Given SDK below P when requesting update then min defaults to 0`() = runTest { val updatedAttributes = slot>() - coEvery { sensorDao.getFull(AudioSensorManager.volMusic.id) } returns mapOf(volumeMusicSensor to emptyList()) - coJustRun { sensorDao.update(any()) } - coJustRun { sensorDao.replaceAllAttributes(AudioSensorManager.volMusic.id, capture(updatedAttributes)) } + coEvery { sensorRepository.getFull(AudioSensorManager.volMusic.id) } returns mapOf(volumeMusicSensor to emptyList()) + coJustRun { sensorRepository.update(any()) } + coJustRun { sensorRepository.replaceAllAttributes(AudioSensorManager.volMusic.id, capture(updatedAttributes)) } sensorManager.requestSensorUpdate(context) @@ -243,9 +235,9 @@ class AudioSensorManagerTest { @Test fun `Given SDK at or above P when requesting update then min comes from AudioManager`() = runTest { val updatedAttributes = slot>() - coEvery { sensorDao.getFull(AudioSensorManager.volMusic.id) } returns mapOf(volumeMusicSensor to emptyList()) - coJustRun { sensorDao.update(any()) } - coJustRun { sensorDao.replaceAllAttributes(AudioSensorManager.volMusic.id, capture(updatedAttributes)) } + coEvery { sensorRepository.getFull(AudioSensorManager.volMusic.id) } returns mapOf(volumeMusicSensor to emptyList()) + coJustRun { sensorRepository.update(any()) } + coJustRun { sensorRepository.replaceAllAttributes(AudioSensorManager.volMusic.id, capture(updatedAttributes)) } sensorManager.requestSensorUpdate(context) @@ -260,13 +252,13 @@ class AudioSensorManagerTest { val updatedSensors = mutableListOf() // First two calls: DB still returns old (mismatched) attributes → force each time. - coEvery { sensorDao.getFull(AudioSensorManager.volMusic.id) } returns mapOf( + coEvery { sensorRepository.getFull(AudioSensorManager.volMusic.id) } returns mapOf( volumeMusicSensor to listOf( Attribute(AudioSensorManager.volMusic.id, "min", "2", "int"), Attribute(AudioSensorManager.volMusic.id, "max", "10", "int"), ), ) - coJustRun { sensorDao.update(capture(updatedSensors)) } + coJustRun { sensorRepository.update(capture(updatedSensors)) } sensorManager.requestSensorUpdate(context) sensorManager.requestSensorUpdate(context) @@ -276,7 +268,7 @@ class AudioSensorManagerTest { // Simulate that the force update has now persisted the correct attributes. updatedSensors.clear() - coEvery { sensorDao.getFull(AudioSensorManager.volMusic.id) } returns mapOf( + coEvery { sensorRepository.getFull(AudioSensorManager.volMusic.id) } returns mapOf( volumeMusicSensor to listOf( Attribute(AudioSensorManager.volMusic.id, "min", "2", "int"), Attribute(AudioSensorManager.volMusic.id, "max", "15", "int"), @@ -290,7 +282,7 @@ class AudioSensorManagerTest { updatedSensors.single().assertNoForceUpdate() // Verify that getFull was called 3 times total (twice mismatched + once to confirm match). - coVerify(exactly = 3) { sensorDao.getFull(AudioSensorManager.volMusic.id) } + coVerify(exactly = 3) { sensorRepository.getFull(AudioSensorManager.volMusic.id) } // Fourth call: should use cache, no additional getFull call. updatedSensors.clear() @@ -298,7 +290,7 @@ class AudioSensorManagerTest { assertEquals(1, updatedSensors.size) updatedSensors.single().assertNoForceUpdate() - coVerify(exactly = 3) { sensorDao.getFull(AudioSensorManager.volMusic.id) } + coVerify(exactly = 3) { sensorRepository.getFull(AudioSensorManager.volMusic.id) } } private fun Sensor.assertForceUpdate() { diff --git a/common/src/test/kotlin/io/homeassistant/companion/android/common/sensors/SensorRepositoryImplTest.kt b/common/src/test/kotlin/io/homeassistant/companion/android/common/sensors/SensorRepositoryImplTest.kt new file mode 100644 index 00000000000..1185c234d3e --- /dev/null +++ b/common/src/test/kotlin/io/homeassistant/companion/android/common/sensors/SensorRepositoryImplTest.kt @@ -0,0 +1,227 @@ +package io.homeassistant.companion.android.common.sensors + +import app.cash.turbine.test +import io.homeassistant.companion.android.common.util.FailFast +import io.homeassistant.companion.android.database.sensor.Attribute +import io.homeassistant.companion.android.database.sensor.Sensor +import io.homeassistant.companion.android.database.sensor.SensorDao +import io.homeassistant.companion.android.database.server.Server +import io.homeassistant.companion.android.database.server.ServerDao +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.every +import io.mockk.mockk +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.Assertions.assertNull +import org.junit.jupiter.api.Test + +class SensorRepositoryImplTest { + + private val dao: SensorDao = mockk(relaxed = true) + private val serverDao: ServerDao = mockk(relaxed = true) + + private val catalog = setOf( + SensorManager.BasicSensor(id = "last_update", type = "sensor", enabledByDefault = true), + SensorManager.BasicSensor(id = "app_inactive", type = "sensor", enabledByDefault = false), + ) + private val repository: SensorRepository = SensorRepositoryImpl(dao, serverDao, catalog) + + private fun serverMock(id: Int): Server = mockk(relaxed = true).also { every { it.id } returns id } + + @Test + fun `Given a stored row when get by id and server then returns the row without writing`() = runTest { + val stored = Sensor("last_update", 1, enabled = false, state = "on") + coEvery { dao.get("last_update", 1) } returns stored + + val result = repository.get("last_update", 1) + + // Stored row wins even though the catalog marks last_update enabled-by-default. + assertEquals(stored, result) + coVerify(exactly = 0) { dao.upsert(any()) } + } + + @Test + fun `Given no row when get by id and server then returns catalog default without writing`() = runTest { + coEvery { dao.get("last_update", 1) } returns null + + val result = repository.get("last_update", 1) + + assertEquals(Sensor("last_update", 1, enabled = true, state = ""), result) + coVerify(exactly = 0) { dao.upsert(any()) } + } + + @Test + fun `Given no row for a disabled-by-default sensor when get by id and server then default is disabled`() = runTest { + coEvery { dao.get("app_inactive", 1) } returns null + + val result = repository.get("app_inactive", 1) + + assertEquals(Sensor("app_inactive", 1, enabled = false, state = ""), result) + } + + @Test + fun `Given a sensor absent from the catalog and no row when get then fails fast and returns null`() = runTest { + coEvery { dao.get("unknown", 1) } returns null + var throwableCaptured: Throwable? = null + FailFast.setHandler { throwable, _ -> throwableCaptured = throwable } + + val result = repository.get("unknown", 1) + + // No backing BasicSensor means the sensor doesn't exist: FailFast fires and the read is null. + assertNotNull(throwableCaptured) + assertNull(result) + } + + @Test + fun `Given get by id then configured servers without a row get a catalog default`() = runTest { + coEvery { serverDao.getAll() } returns listOf(serverMock(1), serverMock(2)) + coEvery { dao.get("last_update") } returns listOf(Sensor("last_update", 1, enabled = false, state = "on")) + + val result = repository.get("last_update").toSet() + + assertEquals( + setOf( + Sensor("last_update", 1, enabled = false, state = "on"), + Sensor("last_update", 2, enabled = true, state = ""), + ), + result, + ) + } + + @Test + fun `Given no row when getFull by server then returns catalog default with empty attributes`() = runTest { + coEvery { dao.getFull("last_update", 1) } returns emptyMap() + + val result = repository.getFull("last_update", 1) + + assertEquals(mapOf(Sensor("last_update", 1, enabled = true, state = "") to emptyList()), result) + } + + @Test + fun `Given a stored row when getFull by server then returns it with its attributes`() = runTest { + val stored = Sensor("last_update", 1, enabled = false, state = "on") + val storedAttrs = listOf(Attribute("last_update", "k", "v", "string")) + coEvery { dao.getFull("last_update", 1) } returns mapOf(stored to storedAttrs) + + val result = repository.getFull("last_update", 1) + + assertEquals(mapOf(stored to storedAttrs), result) + } + + @Test + fun `Given getFull by id then configured servers without a row get a catalog default with no attributes`() = runTest { + coEvery { serverDao.getAll() } returns listOf(serverMock(1), serverMock(2)) + val stored = Sensor("last_update", 1, enabled = false, state = "on") + val storedAttrs = listOf(Attribute("last_update", "k", "v", "string")) + coEvery { dao.getFull("last_update") } returns mapOf(stored to storedAttrs) + + val result = repository.getFull("last_update") + + // Server 1 keeps its stored row and attributes; server 2 (no row) is an empty default. + assertEquals( + setOf(stored, Sensor("last_update", 2, enabled = true, state = "")), + result.keys, + ) + val byServer = result.mapKeys { it.key.serverId } + assertEquals(storedAttrs, byServer[1]) + assertEquals(emptyList(), byServer[2]) + } + + @Test + fun `Given getFullFlow then configured servers without a row get a catalog default`() = runTest { + every { serverDao.getAllFlow() } returns flowOf(listOf(serverMock(1), serverMock(2))) + val stored = Sensor("last_update", 1, enabled = false, state = "on") + val storedAttrs = listOf(Attribute("last_update", "k", "v", "string")) + every { dao.getFullFlow("last_update") } returns flowOf(mapOf(stored to storedAttrs)) + + repository.getFullFlow("last_update").test { + val emitted = awaitItem() + // Server 1 keeps its stored row (with attributes); server 2 (no row) is an empty default. + assertEquals( + setOf(stored, Sensor("last_update", 2, enabled = true, state = "")), + emitted.keys, + ) + val byServer = emitted.mapKeys { it.key.serverId } + assertEquals(storedAttrs, byServer[1]) + assertEquals(emptyList(), byServer[2]) + awaitComplete() + } + } + + @Test + fun `Given removeSensorsExceptServers then deletes every orphaned row`() = runTest { + val orphans = listOf( + Sensor("last_update", 9, enabled = true, state = ""), + Sensor("app_inactive", 9, enabled = false, state = ""), + ) + coEvery { dao.getAllExceptServer(listOf(1)) } returns orphans + + repository.removeSensorsExceptServers(listOf(1)) + + coVerify { dao.removeSensor("last_update", 9) } + coVerify { dao.removeSensor("app_inactive", 9) } + } + + @Test + fun `Given getAllFlow then emits every catalog sensor per configured server, stored or defaulted`() = runTest { + every { dao.getAllFlow() } returns flowOf(listOf(Sensor("last_update", 1, enabled = false, state = "on"))) + every { serverDao.getAllFlow() } returns flowOf(listOf(serverMock(1), serverMock(2))) + + repository.getAllFlow().test { + val emitted = awaitItem().associateBy { it.id to it.serverId } + // Stored row preserved... + assertEquals(Sensor("last_update", 1, enabled = false, state = "on"), emitted["last_update" to 1]) + // ...and defaults filled in for the other catalog/server combinations. + assertEquals(Sensor("last_update", 2, enabled = true, state = ""), emitted["last_update" to 2]) + assertEquals(Sensor("app_inactive", 1, enabled = false, state = ""), emitted["app_inactive" to 1]) + assertEquals(Sensor("app_inactive", 2, enabled = false, state = ""), emitted["app_inactive" to 2]) + awaitComplete() + } + } + + @Test + fun `Given setSensorEnabled when called then delegates to dao`() = runTest { + repository.setSensorEnabled("last_update", listOf(1, 2), enabled = true) + + coVerify { dao.setSensorEnabled("last_update", listOf(1, 2), true) } + } + + @Test + fun `Given setSensorsEnabled when called then delegates to dao`() = runTest { + repository.setSensorsEnabled(listOf("last_update", "app_inactive"), serverId = 1, enabled = false) + + coVerify { dao.setSensorsEnabled(listOf("last_update", "app_inactive"), 1, false) } + } + + @Test + fun `Given update when called then upserts the row in a single call`() = runTest { + val sensor = Sensor("last_update", 1, enabled = true, state = "on") + + repository.update(sensor) + + coVerify(exactly = 1) { dao.upsert(sensor) } // single insert-or-update + } + + @Test + fun `Given removeServer when called then delegates to dao`() = runTest { + repository.removeServer(serverId = 2) + + coVerify { dao.removeServer(2) } + } + + @Test + fun `Given getEnabledCount then counts effective-enabled across servers including catalog defaults`() = runTest { + coEvery { serverDao.getAll() } returns listOf(serverMock(1), serverMock(2)) + // Server 1 has no rows; server 2 has app_inactive explicitly enabled. + coEvery { dao.getAllServer(1) } returns emptyList() + coEvery { dao.getAllServer(2) } returns listOf(Sensor("app_inactive", 2, enabled = true, state = "")) + + val result = repository.getEnabledCount() + + // last_update is enabled-by-default on both servers (2), plus app_inactive enabled on server 2 (1). + assertEquals(3, result) + } +} diff --git a/common/src/test/kotlin/io/homeassistant/companion/android/sensors/SensorManagerTest.kt b/common/src/test/kotlin/io/homeassistant/companion/android/sensors/SensorManagerTest.kt index ba2d401cfb6..16489d85685 100644 --- a/common/src/test/kotlin/io/homeassistant/companion/android/sensors/SensorManagerTest.kt +++ b/common/src/test/kotlin/io/homeassistant/companion/android/sensors/SensorManagerTest.kt @@ -3,9 +3,9 @@ package io.homeassistant.companion.android.sensors import android.content.Context import dagger.hilt.android.EntryPointAccessors import io.homeassistant.companion.android.common.sensors.SensorManager +import io.homeassistant.companion.android.common.sensors.SensorRepository import io.homeassistant.companion.android.database.sensor.Attribute import io.homeassistant.companion.android.database.sensor.Sensor -import io.homeassistant.companion.android.database.sensor.SensorDao import io.mockk.coEvery import io.mockk.coJustRun import io.mockk.every @@ -22,7 +22,7 @@ class SensorManagerTest { fun `Given attributes when invoking onSensorUpdated then attributes are replaced in DAO properly formatted in json`() = runTest { val context: Context = mockk() val sensorManager = FakeSensorManager() - val sensorDao = mockk() + val sensorRepository = mockk() val entryPoint = mockk() mockkStatic(EntryPointAccessors::class) @@ -33,8 +33,8 @@ class SensorManagerTest { SensorManager.SensorManagerEntryPoint::class.java, ) } returns entryPoint - every { entryPoint.sensorDao() } returns sensorDao - coEvery { sensorDao.get("test") } returns listOf( + every { entryPoint.sensorRepository() } returns sensorRepository + coEvery { sensorRepository.get("test") } returns listOf( Sensor( id = "test", serverId = 0, @@ -42,9 +42,9 @@ class SensorManagerTest { state = "test", ), ) - coJustRun { sensorDao.update(any()) } + coJustRun { sensorRepository.update(any()) } val slot = slot>() - coJustRun { sensorDao.replaceAllAttributes(any(), capture(slot)) } + coJustRun { sensorRepository.replaceAllAttributes(any(), capture(slot)) } sensorManager.onSensorUpdated( context, diff --git a/wear/src/main/kotlin/io/homeassistant/companion/android/home/MainViewModel.kt b/wear/src/main/kotlin/io/homeassistant/companion/android/home/MainViewModel.kt index 8bf61987ef5..ff54c95052e 100644 --- a/wear/src/main/kotlin/io/homeassistant/companion/android/home/MainViewModel.kt +++ b/wear/src/main/kotlin/io/homeassistant/companion/android/home/MainViewModel.kt @@ -21,8 +21,8 @@ import io.homeassistant.companion.android.common.data.websocket.impl.entities.Ar import io.homeassistant.companion.android.common.data.websocket.impl.entities.DeviceRegistryResponse import io.homeassistant.companion.android.common.data.websocket.impl.entities.EntityRegistryResponse import io.homeassistant.companion.android.common.sensors.SensorManager +import io.homeassistant.companion.android.common.sensors.SensorRepository import io.homeassistant.companion.android.data.SimplifiedEntity -import io.homeassistant.companion.android.database.sensor.SensorDao import io.homeassistant.companion.android.database.wear.CameraTile import io.homeassistant.companion.android.database.wear.CameraTileDao import io.homeassistant.companion.android.database.wear.FavoriteCaches @@ -50,7 +50,7 @@ import timber.log.Timber class MainViewModel @Inject constructor( private val favoritesDao: FavoritesDao, private val favoriteCachesDao: FavoriteCachesDao, - private val sensorsDao: SensorDao, + private val sensorRepository: SensorRepository, private val cameraTileDao: CameraTileDao, private val thermostatTileDao: ThermostatTileDao, application: Application, @@ -193,7 +193,7 @@ class MainViewModel @Inject constructor( fun stringForDomain(domain: String): String? = HomePresenterImpl.domainsWithNames[domain]?.let { getApplication().getString(it) } - val sensors = sensorsDao.getAllFlow().collectAsState() + val sensors = sensorRepository.getAllFlow().collectAsState() var availableSensors = emptyList() @@ -530,7 +530,7 @@ class MainViewModel @Inject constructor( viewModelScope.launch { val basicSensor = sensorManager.getAvailableSensors(getApplication()) .first { basicSensor -> basicSensor.id == sensorId } - updateSensorEntity(sensorsDao, basicSensor, isEnabled) + updateSensorEntity(basicSensor, isEnabled) if (isEnabled) { try { @@ -542,13 +542,9 @@ class MainViewModel @Inject constructor( } } - private suspend fun updateSensorEntity( - sensorDao: SensorDao, - basicSensor: SensorManager.BasicSensor, - isEnabled: Boolean, - ) { + private suspend fun updateSensorEntity(basicSensor: SensorManager.BasicSensor, isEnabled: Boolean) { homePresenter.getServerId()?.let { serverId -> - sensorDao.setSensorsEnabled(listOf(basicSensor.id), serverId, isEnabled) + sensorRepository.setSensorsEnabled(listOf(basicSensor.id), serverId, isEnabled) SensorReceiver.updateAllSensors(getApplication()) } } diff --git a/wear/src/main/kotlin/io/homeassistant/companion/android/notifications/MessagingManager.kt b/wear/src/main/kotlin/io/homeassistant/companion/android/notifications/MessagingManager.kt index a095584a8ec..b74afd17533 100755 --- a/wear/src/main/kotlin/io/homeassistant/companion/android/notifications/MessagingManager.kt +++ b/wear/src/main/kotlin/io/homeassistant/companion/android/notifications/MessagingManager.kt @@ -16,6 +16,7 @@ import io.homeassistant.companion.android.common.notifications.handleChannel import io.homeassistant.companion.android.common.notifications.handleDeleteIntent import io.homeassistant.companion.android.common.notifications.handleSmallIcon import io.homeassistant.companion.android.common.notifications.handleText +import io.homeassistant.companion.android.common.sensors.SensorRepository import io.homeassistant.companion.android.common.util.cancelGroupIfNeeded import io.homeassistant.companion.android.common.util.getActiveNotification import io.homeassistant.companion.android.common.util.toJsonObject @@ -23,7 +24,6 @@ import io.homeassistant.companion.android.common.util.tts.TextToSpeechClient import io.homeassistant.companion.android.common.util.tts.TextToSpeechData import io.homeassistant.companion.android.database.notification.NotificationDao import io.homeassistant.companion.android.database.notification.NotificationItem -import io.homeassistant.companion.android.database.sensor.SensorDao import io.homeassistant.companion.android.sensors.SensorReceiver import io.homeassistant.companion.android.util.sensitive import javax.inject.Inject @@ -36,7 +36,7 @@ import timber.log.Timber class MessagingManager @Inject constructor( @ApplicationContext val context: Context, private val serverManager: ServerManager, - private val sensorDao: SensorDao, + private val sensorRepository: SensorRepository, private val notificationDao: NotificationDao, private val textToSpeechClient: TextToSpeechClient, ) { @@ -94,7 +94,7 @@ class MessagingManager @Inject constructor( } } message == DeviceCommandData.COMMAND_BLE_TRANSMITTER && allowCommands -> { - if (!commandBleTransmitter(context, notificationData, sensorDao)) { + if (!commandBleTransmitter(context, notificationData, sensorRepository)) { sendNotification(notificationData) } } diff --git a/wear/src/main/kotlin/io/homeassistant/companion/android/sensors/HealthServicesSensorManager.kt b/wear/src/main/kotlin/io/homeassistant/companion/android/sensors/HealthServicesSensorManager.kt index a48a6a18c34..ce710e9d926 100755 --- a/wear/src/main/kotlin/io/homeassistant/companion/android/sensors/HealthServicesSensorManager.kt +++ b/wear/src/main/kotlin/io/homeassistant/companion/android/sensors/HealthServicesSensorManager.kt @@ -251,7 +251,7 @@ class HealthServicesSensorManager : SensorManager { ), forceUpdate = forceUpdate, ) - val sensorData = sensorDao(latestContext).get(userActivityState.id) + val sensorData = sensorRepository(latestContext).get(userActivityState.id) if (sensorData.any { it.state != it.lastSentState } || forceUpdate) { SensorReceiver.updateAllSensors(latestContext) @@ -291,10 +291,10 @@ class HealthServicesSensorManager : SensorManager { } override fun onPermissionLost() { - val sensorDao = sensorDao(latestContext) + val sensorRepository = sensorRepository(latestContext) runBlocking { serverManager(latestContext).servers().forEach { - sensorDao.setSensorsEnabled(listOf(userActivityState.id), it.id, false) + sensorRepository.setSensorsEnabled(listOf(userActivityState.id), it.id, false) } } } From 2bc7c02420534018839a17ffc5ed7047ebbf63ed Mon Sep 17 00:00:00 2001 From: Timothy <6560631+TimoPtr@users.noreply.github.com> Date: Wed, 17 Jun 2026 17:26:52 +0200 Subject: [PATCH 3/3] Small optimization in SensorManager --- .../companion/android/common/sensors/SensorManager.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/common/src/main/kotlin/io/homeassistant/companion/android/common/sensors/SensorManager.kt b/common/src/main/kotlin/io/homeassistant/companion/android/common/sensors/SensorManager.kt index 2bc60bc480a..11beed26c13 100644 --- a/common/src/main/kotlin/io/homeassistant/companion/android/common/sensors/SensorManager.kt +++ b/common/src/main/kotlin/io/homeassistant/companion/android/common/sensors/SensorManager.kt @@ -124,8 +124,8 @@ interface SensorManager { /** @return Set of server IDs for which this sensor is enabled */ suspend fun getEnabledServers(context: Context, basicSensor: BasicSensor): Set { - val permission = checkPermission(context, basicSensor.id) - return sensorRepository(context).get(basicSensor.id).filter { it.enabled && permission }.map { it.serverId } + if (!checkPermission(context, basicSensor.id)) return emptySet() + return sensorRepository(context).get(basicSensor.id).filter { it.enabled }.map { it.serverId } .toSet() }