fix(scene): address automation review findings

This commit is contained in:
2026-08-28 01:58:16 +08:00
parent 8e824b50da
commit 2624a8e1e5
4 changed files with 37 additions and 12 deletions
@@ -40,6 +40,8 @@ dependencies {
kapt(libs.hilt.compiler) kapt(libs.hilt.compiler)
implementation(libs.androidx.work.runtime.ktx) implementation(libs.androidx.work.runtime.ktx)
implementation(libs.androidx.hilt.work) implementation(libs.androidx.hilt.work)
kapt(libs.androidx.hilt.compiler)
implementation(libs.androidx.lifecycle.process)
implementation(platform(libs.compose.bom)) implementation(platform(libs.compose.bom))
implementation(libs.compose.ui) implementation(libs.compose.ui)
implementation(libs.compose.material3) implementation(libs.compose.material3)
@@ -1,14 +1,18 @@
package com.flagship.abox.manager.automation.runtime package com.flagship.abox.manager.automation.runtime
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ProcessLifecycleOwner
import com.flagship.abox.manager.domain.ProviderProfileId
import com.flagship.abox.manager.domain.ProviderProfileStore import com.flagship.abox.manager.domain.ProviderProfileStore
import com.flagship.abox.manager.domain.SessionCoordinator import com.flagship.abox.manager.domain.SessionCoordinator
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.delay
import com.flagship.abox.manager.domain.ProviderProfileId
import kotlinx.coroutines.CancellationException
class AutomationBootstrapper( class AutomationBootstrapper(
private val profileStore: ProviderProfileStore, private val profileStore: ProviderProfileStore,
@@ -16,20 +20,24 @@ class AutomationBootstrapper(
private val scheduler: AutomationScheduler, private val scheduler: AutomationScheduler,
private val engine: AutomationEngine, private val engine: AutomationEngine,
private val scope: CoroutineScope, private val scope: CoroutineScope,
) { ) : DefaultLifecycleObserver {
private var previousProfileId: ProviderProfileId? = null private var previousProfileId: ProviderProfileId? = null
private val isForeground = MutableStateFlow(false)
fun start() { fun start() {
ProcessLifecycleOwner.get().lifecycle.addObserver(this)
scope.launch { scope.launch {
combine( combine(
profileStore.observeActiveProfile(), profileStore.observeActiveProfile(),
sessionCoordinator.observeActiveSession(), sessionCoordinator.observeActiveSession(),
) { active, session -> active to session }.collectLatest { (active, session) -> isForeground,
) { active, session, foreground -> Triple(active, session, foreground) }
.collectLatest { (active, session, foreground) ->
previousProfileId?.takeIf { it != active?.id }?.let { scheduler.cancel(it) } previousProfileId?.takeIf { it != active?.id }?.let { scheduler.cancel(it) }
previousProfileId = active?.id previousProfileId = active?.id
if (active != null && session?.profileId == active.id) { if (active != null && session?.profileId == active.id) {
scheduler.reconcile(active.id) scheduler.reconcile(active.id)
while (true) { while (foreground) {
try { try {
engine.evaluate(active.id) engine.evaluate(active.id)
} catch (cancelled: CancellationException) { } catch (cancelled: CancellationException) {
@@ -43,4 +51,12 @@ class AutomationBootstrapper(
} }
} }
} }
override fun onStart(owner: LifecycleOwner) {
isForeground.value = true
}
override fun onStop(owner: LifecycleOwner) {
isForeground.value = false
}
} }
@@ -65,12 +65,16 @@ class SceneTriggerEvaluator {
private fun evaluateOne(trigger: SceneTrigger, snapshot: AutomationSnapshot, now: LocalDateTime): TriggerEvaluation? = when (trigger) { private fun evaluateOne(trigger: SceneTrigger, snapshot: AutomationSnapshot, now: LocalDateTime): TriggerEvaluation? = when (trigger) {
is SceneTrigger.Time -> { is SceneTrigger.Time -> {
val time = parseTime(trigger.timeOfDay) ?: return TriggerEvaluation.Unavailable(DomainError.InvalidRequest(detail = "Invalid trigger time")) val time = parseTime(trigger.timeOfDay) ?: return TriggerEvaluation.Unavailable(DomainError.InvalidRequest(detail = "Invalid trigger time"))
val dayMatches = trigger.daysOfWeek.isEmpty() || now.dayOfWeek.value in trigger.daysOfWeek val scheduled = listOf(now.toLocalDate(), now.toLocalDate().minusDays(1))
val scheduled = LocalDateTime.of(now.toLocalDate(), time) .map { date -> LocalDateTime.of(date, time) }
val lateness = Duration.between(scheduled, now).toMinutes() .firstOrNull { candidate ->
if (dayMatches && lateness in 0..20) { val dayMatches = trigger.daysOfWeek.isEmpty() || candidate.dayOfWeek.value in trigger.daysOfWeek
TriggerEvaluation.Matched("${now.toLocalDate()}:${trigger.timeOfDay}:${trigger.id.value}") val lateness = Duration.between(candidate, now).toMinutes()
} else null dayMatches && lateness in 0..TIME_TRIGGER_GRACE_MINUTES
}
scheduled?.let { occurrence ->
TriggerEvaluation.Matched("${occurrence.toLocalDate()}:${trigger.timeOfDay}:${trigger.id.value}")
}
} }
is SceneTrigger.Temperature -> threshold(snapshot.readings[trigger.deviceName], trigger.operator, trigger.thresholdCelsius) { is SceneTrigger.Temperature -> threshold(snapshot.readings[trigger.deviceName], trigger.operator, trigger.thresholdCelsius) {
it.temperature it.temperature
@@ -134,6 +138,7 @@ class SceneTriggerEvaluator {
private companion object { private companion object {
const val TEMPERATURE_HYSTERESIS = 0.5 const val TEMPERATURE_HYSTERESIS = 0.5
const val HUMIDITY_HYSTERESIS = 2.0 const val HUMIDITY_HYSTERESIS = 2.0
const val TIME_TRIGGER_GRACE_MINUTES = 20L
} }
} }
+2
View File
@@ -28,7 +28,9 @@ androidx-lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-
androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigationCompose" } androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigationCompose" }
androidx-hilt-navigation-compose = { module = "androidx.hilt:hilt-navigation-compose", version.ref = "hiltNavigationCompose" } androidx-hilt-navigation-compose = { module = "androidx.hilt:hilt-navigation-compose", version.ref = "hiltNavigationCompose" }
androidx-hilt-work = { module = "androidx.hilt:hilt-work", version.ref = "hiltNavigationCompose" } androidx-hilt-work = { module = "androidx.hilt:hilt-work", version.ref = "hiltNavigationCompose" }
androidx-hilt-compiler = { module = "androidx.hilt:hilt-compiler", version.ref = "hiltNavigationCompose" }
androidx-work-runtime-ktx = { module = "androidx.work:work-runtime-ktx", version.ref = "work" } androidx-work-runtime-ktx = { module = "androidx.work:work-runtime-ktx", version.ref = "work" }
androidx-lifecycle-process = { module = "androidx.lifecycle:lifecycle-process", version.ref = "lifecycle" }
androidx-compose-material3-adaptive = { module = "androidx.compose.material3.adaptive:adaptive", version.ref = "adaptive" } androidx-compose-material3-adaptive = { module = "androidx.compose.material3.adaptive:adaptive", version.ref = "adaptive" }
androidx-compose-material3-adaptive-navigation-suite = { module = "androidx.compose.material3:material3-adaptive-navigation-suite", version.ref = "adaptiveNavigationSuite" } androidx-compose-material3-adaptive-navigation-suite = { module = "androidx.compose.material3:material3-adaptive-navigation-suite", version.ref = "adaptiveNavigationSuite" }
androidx-window = { module = "androidx.window:window", version.ref = "window" } androidx-window = { module = "androidx.window:window", version.ref = "window" }