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)
implementation(libs.androidx.work.runtime.ktx)
implementation(libs.androidx.hilt.work)
kapt(libs.androidx.hilt.compiler)
implementation(libs.androidx.lifecycle.process)
implementation(platform(libs.compose.bom))
implementation(libs.compose.ui)
implementation(libs.compose.material3)
@@ -1,14 +1,18 @@
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.SessionCoordinator
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.delay
import com.flagship.abox.manager.domain.ProviderProfileId
import kotlinx.coroutines.CancellationException
class AutomationBootstrapper(
private val profileStore: ProviderProfileStore,
@@ -16,20 +20,24 @@ class AutomationBootstrapper(
private val scheduler: AutomationScheduler,
private val engine: AutomationEngine,
private val scope: CoroutineScope,
) {
) : DefaultLifecycleObserver {
private var previousProfileId: ProviderProfileId? = null
private val isForeground = MutableStateFlow(false)
fun start() {
ProcessLifecycleOwner.get().lifecycle.addObserver(this)
scope.launch {
combine(
profileStore.observeActiveProfile(),
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 = active?.id
if (active != null && session?.profileId == active.id) {
scheduler.reconcile(active.id)
while (true) {
while (foreground) {
try {
engine.evaluate(active.id)
} 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) {
is SceneTrigger.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 = LocalDateTime.of(now.toLocalDate(), time)
val lateness = Duration.between(scheduled, now).toMinutes()
if (dayMatches && lateness in 0..20) {
TriggerEvaluation.Matched("${now.toLocalDate()}:${trigger.timeOfDay}:${trigger.id.value}")
} else null
val scheduled = listOf(now.toLocalDate(), now.toLocalDate().minusDays(1))
.map { date -> LocalDateTime.of(date, time) }
.firstOrNull { candidate ->
val dayMatches = trigger.daysOfWeek.isEmpty() || candidate.dayOfWeek.value in trigger.daysOfWeek
val lateness = Duration.between(candidate, now).toMinutes()
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) {
it.temperature
@@ -134,6 +138,7 @@ class SceneTriggerEvaluator {
private companion object {
const val TEMPERATURE_HYSTERESIS = 0.5
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-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-compiler = { module = "androidx.hilt:hilt-compiler", version.ref = "hiltNavigationCompose" }
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-navigation-suite = { module = "androidx.compose.material3:material3-adaptive-navigation-suite", version.ref = "adaptiveNavigationSuite" }
androidx-window = { module = "androidx.window:window", version.ref = "window" }