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 64b1875..1a0c980 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 @@ -38,6 +38,7 @@ data class ScheduledAutomationOccurrence( val sceneId: String, val triggerId: String, val occurrenceKey: String, + val triggerDefinition: String, val scheduledAtEpochMillis: Long, val expiresAtEpochMillis: Long, ) @@ -57,23 +58,37 @@ class DefaultAutomationEngine( override suspend fun evaluate( profileId: ProviderProfileId, scheduledOccurrence: ScheduledAutomationOccurrence?, - ): DomainResult = evaluationMutex.withLock { - evaluateLocked(profileId, scheduledOccurrence) + ): DomainResult { + val scenesToExecute = when (val prepared = evaluationMutex.withLock { prepareExecutions(profileId, scheduledOccurrence) }) { + is DomainResult.Failure -> return prepared + is DomainResult.Success -> prepared.value + } + var executed = 0 + for (scene in scenesToExecute) { + sceneExecutor.execute( + scene = scene, + executionId = SceneExecutionId("auto-${UUID.randomUUID()}"), + source = SceneExecutionSource.AUTOMATION, + ).collect { update -> + if (update is com.flagship.abox.manager.domain.SceneExecutionUpdate.Completed) executed++ + } + } + return DomainResult.Success(executed) } - private suspend fun evaluateLocked( + private suspend fun prepareExecutions( profileId: ProviderProfileId, scheduledOccurrence: ScheduledAutomationOccurrence?, - ): DomainResult { + ): DomainResult> { val scenes = when (val result = sceneRepository.observeScenes(profileId).first()) { is DomainResult.Failure -> return result is DomainResult.Success -> result.value.filter { it.enabled && it.type == AutomationType.AUTOMATIC } } - if (scenes.isEmpty()) return DomainResult.Success(0) + if (scenes.isEmpty()) return DomainResult.Success(emptyList()) val snapshot = collectSnapshot(profileId, scenes) val now = LocalDateTime.ofInstant(Instant.ofEpochMilli(snapshot.observedAtEpochMillis), ZoneId.systemDefault()) - var executed = 0 + val executions = mutableListOf() for (scene in scenes) { var newlyMatched = false val newlyMatchedStateTriggers = mutableListOf() @@ -91,6 +106,7 @@ class DefaultAutomationEngine( trigger is SceneTrigger.Time && scheduledOccurrence?.sceneId == scene.id.value && scheduledOccurrence.triggerId == trigger.id.value && + scheduledOccurrence.triggerDefinition == trigger.automationDefinition() && snapshot.observedAtEpochMillis in scheduledOccurrence.scheduledAtEpochMillis..scheduledOccurrence.expiresAtEpochMillis ) { TriggerEvaluation.Matched(scheduledOccurrence.occurrenceKey) @@ -143,16 +159,10 @@ class DefaultAutomationEngine( if (executionCount == 0) continue repeat(executionCount) { - sceneExecutor.execute( - scene = scene, - executionId = SceneExecutionId("auto-${UUID.randomUUID()}"), - source = SceneExecutionSource.AUTOMATION, - ).collect { update -> - if (update is com.flagship.abox.manager.domain.SceneExecutionUpdate.Completed) executed++ - } + executions += scene } } - return DomainResult.Success(executed) + return DomainResult.Success(executions) } private suspend fun collectSnapshot(profileId: ProviderProfileId, scenes: List): AutomationSnapshot { @@ -184,3 +194,6 @@ class DefaultAutomationEngine( } } + +internal fun SceneTrigger.Time.automationDefinition(): String = + listOf(timeOfDay, daysOfWeek.sorted().joinToString(",")).joinToString("|") diff --git a/android/feature/automation/src/main/kotlin/com/flagship/abox/manager/automation/runtime/AutomationPollWorker.kt b/android/feature/automation/src/main/kotlin/com/flagship/abox/manager/automation/runtime/AutomationPollWorker.kt index 5d10e42..9348b2b 100644 --- a/android/feature/automation/src/main/kotlin/com/flagship/abox/manager/automation/runtime/AutomationPollWorker.kt +++ b/android/feature/automation/src/main/kotlin/com/flagship/abox/manager/automation/runtime/AutomationPollWorker.kt @@ -37,10 +37,11 @@ class AutomationPollWorker @AssistedInject constructor( val scheduledOccurrence = inputData.getString(WorkManagerAutomationScheduleStore.KEY_OCCURRENCE_KEY)?.let { key -> val sceneId = inputData.getString(WorkManagerAutomationScheduleStore.KEY_SCENE_ID) val triggerId = inputData.getString(WorkManagerAutomationScheduleStore.KEY_TRIGGER_ID) + val triggerDefinition = inputData.getString(WorkManagerAutomationScheduleStore.KEY_TRIGGER_DEFINITION) val scheduledAt = inputData.getLong(WorkManagerAutomationScheduleStore.KEY_SCHEDULED_AT_EPOCH_MILLIS, -1L) val expiresAt = inputData.getLong(WorkManagerAutomationScheduleStore.KEY_EXPIRES_AT_EPOCH_MILLIS, -1L) - if (sceneId != null && triggerId != null && scheduledAt >= 0L && expiresAt >= scheduledAt) { - ScheduledAutomationOccurrence(sceneId, triggerId, key, scheduledAt, expiresAt) + if (sceneId != null && triggerId != null && triggerDefinition != null && scheduledAt >= 0L && expiresAt >= scheduledAt) { + ScheduledAutomationOccurrence(sceneId, triggerId, key, triggerDefinition, scheduledAt, expiresAt) } else null } when (automationEngine.evaluate(profileId, scheduledOccurrence)) { diff --git a/android/feature/automation/src/main/kotlin/com/flagship/abox/manager/automation/runtime/WorkManagerAutomationScheduleStore.kt b/android/feature/automation/src/main/kotlin/com/flagship/abox/manager/automation/runtime/WorkManagerAutomationScheduleStore.kt index 798be10..1318c3c 100644 --- a/android/feature/automation/src/main/kotlin/com/flagship/abox/manager/automation/runtime/WorkManagerAutomationScheduleStore.kt +++ b/android/feature/automation/src/main/kotlin/com/flagship/abox/manager/automation/runtime/WorkManagerAutomationScheduleStore.kt @@ -24,7 +24,6 @@ class WorkManagerAutomationScheduleStore(context: Context) : AutomationScheduleS .setInputData(androidx.work.Data.Builder().putString(KEY_PROFILE_ID, profileId.value).build()) .build() workManager.enqueueUniquePeriodicWork(workName(profileId), ExistingPeriodicWorkPolicy.UPDATE, request) - workManager.cancelAllWorkByTag(timeWorkTag(profileId)) scenes.forEach { scene -> scene.triggers.filterIsInstance().forEach { trigger -> scheduleNextTimeTrigger(profileId, scene, trigger) @@ -40,8 +39,8 @@ class WorkManagerAutomationScheduleStore(context: Context) : AutomationScheduleS } private fun workName(profileId: ProviderProfileId) = "automation:${profileId.value}" - private fun timeWorkName(profileId: ProviderProfileId, scene: Scene, trigger: SceneTrigger.Time) = - "automation-time:${profileId.value}:${scene.id.value}:${trigger.id.value}" + private fun timeWorkName(profileId: ProviderProfileId, scene: Scene, trigger: SceneTrigger.Time, occurrenceKey: String) = + "automation-time:${profileId.value}:${scene.id.value}:${trigger.id.value}:$occurrenceKey" private fun timeWorkTag(profileId: ProviderProfileId) = "automation-time:${profileId.value}" private fun scheduleNextTimeTrigger(profileId: ProviderProfileId, scene: Scene, trigger: SceneTrigger.Time) { @@ -55,6 +54,7 @@ class WorkManagerAutomationScheduleStore(context: Context) : AutomationScheduleS ?.toInstant() ?.minusMillis(1) val expiresAt = listOfNotNull(maximumExpiry, followingOccurrence).minOrNull() ?: maximumExpiry + val occurrenceKey = "${scheduledAt.toLocalDate()}:${trigger.timeOfDay}:${trigger.id.value}" val request = OneTimeWorkRequestBuilder() .setInitialDelay(delay.toMillis().coerceAtLeast(0), TimeUnit.MILLISECONDS) .addTag(timeWorkTag(profileId)) @@ -63,15 +63,16 @@ class WorkManagerAutomationScheduleStore(context: Context) : AutomationScheduleS .putString(KEY_PROFILE_ID, profileId.value) .putString(KEY_SCENE_ID, scene.id.value) .putString(KEY_TRIGGER_ID, trigger.id.value) - .putString(KEY_OCCURRENCE_KEY, "${scheduledAt.toLocalDate()}:${trigger.timeOfDay}:${trigger.id.value}") + .putString(KEY_OCCURRENCE_KEY, occurrenceKey) + .putString(KEY_TRIGGER_DEFINITION, trigger.automationDefinition()) .putLong(KEY_SCHEDULED_AT_EPOCH_MILLIS, scheduledAtInstant.toEpochMilli()) .putLong(KEY_EXPIRES_AT_EPOCH_MILLIS, expiresAt.toEpochMilli()) .build(), ) .build() workManager.enqueueUniqueWork( - timeWorkName(profileId, scene, trigger), - androidx.work.ExistingWorkPolicy.REPLACE, + timeWorkName(profileId, scene, trigger, occurrenceKey), + androidx.work.ExistingWorkPolicy.KEEP, request, ) } @@ -92,6 +93,7 @@ class WorkManagerAutomationScheduleStore(context: Context) : AutomationScheduleS const val KEY_SCENE_ID = "scene_id" const val KEY_TRIGGER_ID = "trigger_id" const val KEY_OCCURRENCE_KEY = "occurrence_key" + const val KEY_TRIGGER_DEFINITION = "trigger_definition" const val KEY_SCHEDULED_AT_EPOCH_MILLIS = "scheduled_at_epoch_millis" const val KEY_EXPIRES_AT_EPOCH_MILLIS = "expires_at_epoch_millis"