From 076a1daf5d0a9ebab8d9987bfcd6fde387288f29 Mon Sep 17 00:00:00 2001 From: KremeCN <3265494849@qq.com> Date: Fri, 28 Aug 2026 02:17:01 +0800 Subject: [PATCH] fix(scene): preserve automation trigger semantics --- .../manager/automation/data/AutomationDao.kt | 3 +++ .../automation/data/RoomSceneRepository.kt | 23 ++++++++++++++++--- .../automation/runtime/AutomationEngine.kt | 9 -------- .../abox/manager/domain/Automation.kt | 6 ++--- 4 files changed, 26 insertions(+), 15 deletions(-) diff --git a/android/feature/automation/src/main/kotlin/com/flagship/abox/manager/automation/data/AutomationDao.kt b/android/feature/automation/src/main/kotlin/com/flagship/abox/manager/automation/data/AutomationDao.kt index b162fea..0a19a61 100644 --- a/android/feature/automation/src/main/kotlin/com/flagship/abox/manager/automation/data/AutomationDao.kt +++ b/android/feature/automation/src/main/kotlin/com/flagship/abox/manager/automation/data/AutomationDao.kt @@ -55,4 +55,7 @@ interface AutomationDao { @Query("DELETE FROM automation_trigger_state WHERE profileId = :profileId AND sceneId = :sceneId") suspend fun deleteTriggerStates(profileId: String, sceneId: String): Int + + @Query("DELETE FROM automation_trigger_state WHERE profileId = :profileId AND sceneId = :sceneId AND triggerId = :triggerId") + suspend fun deleteTriggerState(profileId: String, sceneId: String, triggerId: String): Int } diff --git a/android/feature/automation/src/main/kotlin/com/flagship/abox/manager/automation/data/RoomSceneRepository.kt b/android/feature/automation/src/main/kotlin/com/flagship/abox/manager/automation/data/RoomSceneRepository.kt index 4c2abcc..f242468 100644 --- a/android/feature/automation/src/main/kotlin/com/flagship/abox/manager/automation/data/RoomSceneRepository.kt +++ b/android/feature/automation/src/main/kotlin/com/flagship/abox/manager/automation/data/RoomSceneRepository.kt @@ -57,14 +57,31 @@ class RoomSceneRepository( return DomainResult.Failure(DomainError.InvalidRequest(detail = "Scene profile mismatch")) } return runCatching { - if (requireExisting && dao.getScene(profileId.value, scene.id.value) == null) throw IllegalArgumentException("Scene not found") - if (!requireExisting && dao.getScene(profileId.value, scene.id.value) != null) throw IllegalArgumentException("Scene already exists") + val existing = dao.getScene(profileId.value, scene.id.value) + if (requireExisting && existing == null) throw IllegalArgumentException("Scene not found") + if (!requireExisting && existing != null) throw IllegalArgumentException("Scene already exists") dao.upsertScene(scene.toEntity()) - dao.deleteTriggerStates(profileId.value, scene.id.value) + existing?.let(SceneDocumentCodec::decode)?.triggers.orEmpty() + .filter { previous -> scene.triggers.none { current -> current.hasSameDefinitionAs(previous) } } + .forEach { changed -> + dao.deleteTriggerState(profileId.value, scene.id.value, changed.id.value) + } scene }.fold({ DomainResult.Success(it) }, { DomainResult.Failure(DomainError.InvalidRequest(detail = it.message)) }) } + private fun SceneTrigger.hasSameDefinitionAs(other: SceneTrigger): Boolean = when { + this is SceneTrigger.Time && other is SceneTrigger.Time -> + id == other.id && timeOfDay == other.timeOfDay && daysOfWeek == other.daysOfWeek + this is SceneTrigger.Temperature && other is SceneTrigger.Temperature -> + id == other.id && deviceName == other.deviceName && operator == other.operator && thresholdCelsius == other.thresholdCelsius + this is SceneTrigger.Humidity && other is SceneTrigger.Humidity -> + id == other.id && deviceName == other.deviceName && operator == other.operator && thresholdPercent == other.thresholdPercent + this is SceneTrigger.DeviceState && other is SceneTrigger.DeviceState -> + id == other.id && deviceName == other.deviceName && expectedState == other.expectedState + else -> false + } + private suspend fun ensureSeeded(profileId: ProviderProfileId) { val marker = "seed_v1_${profileId.value}" if (seedPreferences.getBoolean(marker, false)) return diff --git a/android/feature/automation/src/main/kotlin/com/flagship/abox/manager/automation/runtime/AutomationEngine.kt b/android/feature/automation/src/main/kotlin/com/flagship/abox/manager/automation/runtime/AutomationEngine.kt index b8df156..7b58a32 100644 --- a/android/feature/automation/src/main/kotlin/com/flagship/abox/manager/automation/runtime/AutomationEngine.kt +++ b/android/feature/automation/src/main/kotlin/com/flagship/abox/manager/automation/runtime/AutomationEngine.kt @@ -93,15 +93,6 @@ class DefaultAutomationEngine( } if (!newlyMatched) continue if (conditionEvaluator.evaluate(scene.conditions, snapshot, now) !is ConditionsEvaluation.Satisfied) { - newlyMatchedStateTriggers.forEach { trigger -> - runtimeRepository.transitionMatch( - profileId, - scene.id, - trigger.id, - false, - snapshot.observedAtEpochMillis, - ) - } continue } if (matchedTimeTriggers.isNotEmpty()) { diff --git a/domain/src/main/kotlin/com/flagship/abox/manager/domain/Automation.kt b/domain/src/main/kotlin/com/flagship/abox/manager/domain/Automation.kt index 8cfb318..4fe9a1f 100644 --- a/domain/src/main/kotlin/com/flagship/abox/manager/domain/Automation.kt +++ b/domain/src/main/kotlin/com/flagship/abox/manager/domain/Automation.kt @@ -69,8 +69,8 @@ class SceneTriggerEvaluator { .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 + val latenessSeconds = Duration.between(candidate, now).seconds + dayMatches && latenessSeconds in 0..TIME_TRIGGER_GRACE_SECONDS } scheduled?.let { occurrence -> TriggerEvaluation.Matched("${occurrence.toLocalDate()}:${trigger.timeOfDay}:${trigger.id.value}") @@ -138,7 +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 + const val TIME_TRIGGER_GRACE_SECONDS = 20L * 60L } }