fix(scene): expire stale scheduled occurrences

This commit is contained in:
2026-08-28 02:52:37 +08:00
parent fe285322ef
commit 1c8a97e446
4 changed files with 33 additions and 9 deletions
@@ -6,6 +6,8 @@ import androidx.lifecycle.ProcessLifecycleOwner
import com.flagship.abox.manager.domain.ProviderProfileId 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 com.flagship.abox.manager.domain.SessionValidity
import com.flagship.abox.manager.domain.DomainResult
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
@@ -36,14 +38,21 @@ class AutomationBootstrapper(
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)
while (true) { while (true) {
isForeground.first { it } isForeground.first { it }
when (val validity = sessionCoordinator.validateSession(active.id)) {
is DomainResult.Success -> if (validity.value == SessionValidity.VALID) {
scheduler.reconcile(active.id)
try { try {
engine.evaluate(active.id) engine.evaluate(active.id)
} catch (cancelled: CancellationException) { } catch (cancelled: CancellationException) {
throw cancelled throw cancelled
} catch (_: Exception) {} } catch (_: Exception) {}
} else {
scheduler.cancel(active.id)
}
is DomainResult.Failure -> Unit
}
if (isForeground.value) delay(60_000L) if (isForeground.value) delay(60_000L)
} }
} else if (active != null) { } else if (active != null) {
@@ -38,6 +38,7 @@ data class ScheduledAutomationOccurrence(
val sceneId: String, val sceneId: String,
val triggerId: String, val triggerId: String,
val occurrenceKey: String, val occurrenceKey: String,
val scheduledAtEpochMillis: Long,
) )
class DefaultAutomationEngine( class DefaultAutomationEngine(
@@ -88,7 +89,8 @@ class DefaultAutomationEngine(
val evaluation = if ( val evaluation = if (
trigger is SceneTrigger.Time && trigger is SceneTrigger.Time &&
scheduledOccurrence?.sceneId == scene.id.value && scheduledOccurrence?.sceneId == scene.id.value &&
scheduledOccurrence.triggerId == trigger.id.value scheduledOccurrence.triggerId == trigger.id.value &&
snapshot.observedAtEpochMillis in scheduledOccurrence.scheduledAtEpochMillis..scheduledOccurrence.expiresAtEpochMillis
) { ) {
TriggerEvaluation.Matched(scheduledOccurrence.occurrenceKey) TriggerEvaluation.Matched(scheduledOccurrence.occurrenceKey)
} else { } else {
@@ -167,4 +169,11 @@ class DefaultAutomationEngine(
socketStatuses = socketDevices.associateWith { socketGateway.getStatus(profileId, it) }, socketStatuses = socketDevices.associateWith { socketGateway.getStatus(profileId, it) },
) )
} }
private val ScheduledAutomationOccurrence.expiresAtEpochMillis: Long
get() = scheduledAtEpochMillis + SCHEDULED_OCCURRENCE_GRACE_MILLIS
private companion object {
const val SCHEDULED_OCCURRENCE_GRACE_MILLIS = 20L * 60L * 1000L
}
} }
@@ -31,12 +31,16 @@ class AutomationPollWorker @AssistedInject constructor(
return when (val validity = sessionCoordinator.validateSession(profileId)) { return when (val validity = sessionCoordinator.validateSession(profileId)) {
is DomainResult.Failure -> Result.retry() is DomainResult.Failure -> Result.retry()
is DomainResult.Success -> if (validity.value != SessionValidity.VALID) { is DomainResult.Success -> if (validity.value != SessionValidity.VALID) {
Result.retry() automationScheduler.cancel(profileId)
Result.success()
} else { } else {
val scheduledOccurrence = inputData.getString(WorkManagerAutomationScheduleStore.KEY_OCCURRENCE_KEY)?.let { key -> val scheduledOccurrence = inputData.getString(WorkManagerAutomationScheduleStore.KEY_OCCURRENCE_KEY)?.let { key ->
val sceneId = inputData.getString(WorkManagerAutomationScheduleStore.KEY_SCENE_ID) val sceneId = inputData.getString(WorkManagerAutomationScheduleStore.KEY_SCENE_ID)
val triggerId = inputData.getString(WorkManagerAutomationScheduleStore.KEY_TRIGGER_ID) val triggerId = inputData.getString(WorkManagerAutomationScheduleStore.KEY_TRIGGER_ID)
if (sceneId != null && triggerId != null) ScheduledAutomationOccurrence(sceneId, triggerId, key) else null val scheduledAt = inputData.getLong(WorkManagerAutomationScheduleStore.KEY_SCHEDULED_AT_EPOCH_MILLIS, -1L)
if (sceneId != null && triggerId != null && scheduledAt >= 0L) {
ScheduledAutomationOccurrence(sceneId, triggerId, key, scheduledAt)
} else null
} }
when (automationEngine.evaluate(profileId, scheduledOccurrence)) { when (automationEngine.evaluate(profileId, scheduledOccurrence)) {
is DomainResult.Success -> { is DomainResult.Success -> {
@@ -56,6 +56,7 @@ class WorkManagerAutomationScheduleStore(context: Context) : AutomationScheduleS
.putString(KEY_SCENE_ID, next.scene.id.value) .putString(KEY_SCENE_ID, next.scene.id.value)
.putString(KEY_TRIGGER_ID, next.trigger.id.value) .putString(KEY_TRIGGER_ID, next.trigger.id.value)
.putString(KEY_OCCURRENCE_KEY, "${next.scheduledAt.toLocalDate()}:${next.trigger.timeOfDay}:${next.trigger.id.value}") .putString(KEY_OCCURRENCE_KEY, "${next.scheduledAt.toLocalDate()}:${next.trigger.timeOfDay}:${next.trigger.id.value}")
.putLong(KEY_SCHEDULED_AT_EPOCH_MILLIS, next.scheduledAt.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli())
.build(), .build(),
) )
.build() .build()
@@ -84,5 +85,6 @@ class WorkManagerAutomationScheduleStore(context: Context) : AutomationScheduleS
const val KEY_SCENE_ID = "scene_id" const val KEY_SCENE_ID = "scene_id"
const val KEY_TRIGGER_ID = "trigger_id" const val KEY_TRIGGER_ID = "trigger_id"
const val KEY_OCCURRENCE_KEY = "occurrence_key" const val KEY_OCCURRENCE_KEY = "occurrence_key"
const val KEY_SCHEDULED_AT_EPOCH_MILLIS = "scheduled_at_epoch_millis"
} }
} }