fix(scene): decouple automation execution scheduling
This commit is contained in:
+27
-14
@@ -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 triggerDefinition: String,
|
||||||
val scheduledAtEpochMillis: Long,
|
val scheduledAtEpochMillis: Long,
|
||||||
val expiresAtEpochMillis: Long,
|
val expiresAtEpochMillis: Long,
|
||||||
)
|
)
|
||||||
@@ -57,23 +58,37 @@ class DefaultAutomationEngine(
|
|||||||
override suspend fun evaluate(
|
override suspend fun evaluate(
|
||||||
profileId: ProviderProfileId,
|
profileId: ProviderProfileId,
|
||||||
scheduledOccurrence: ScheduledAutomationOccurrence?,
|
scheduledOccurrence: ScheduledAutomationOccurrence?,
|
||||||
): DomainResult<Int> = evaluationMutex.withLock {
|
): DomainResult<Int> {
|
||||||
evaluateLocked(profileId, scheduledOccurrence)
|
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,
|
profileId: ProviderProfileId,
|
||||||
scheduledOccurrence: ScheduledAutomationOccurrence?,
|
scheduledOccurrence: ScheduledAutomationOccurrence?,
|
||||||
): DomainResult<Int> {
|
): DomainResult<List<Scene>> {
|
||||||
val scenes = when (val result = sceneRepository.observeScenes(profileId).first()) {
|
val scenes = when (val result = sceneRepository.observeScenes(profileId).first()) {
|
||||||
is DomainResult.Failure -> return result
|
is DomainResult.Failure -> return result
|
||||||
is DomainResult.Success -> result.value.filter { it.enabled && it.type == AutomationType.AUTOMATIC }
|
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 snapshot = collectSnapshot(profileId, scenes)
|
||||||
val now = LocalDateTime.ofInstant(Instant.ofEpochMilli(snapshot.observedAtEpochMillis), ZoneId.systemDefault())
|
val now = LocalDateTime.ofInstant(Instant.ofEpochMilli(snapshot.observedAtEpochMillis), ZoneId.systemDefault())
|
||||||
var executed = 0
|
val executions = mutableListOf<Scene>()
|
||||||
for (scene in scenes) {
|
for (scene in scenes) {
|
||||||
var newlyMatched = false
|
var newlyMatched = false
|
||||||
val newlyMatchedStateTriggers = mutableListOf<SceneTrigger>()
|
val newlyMatchedStateTriggers = mutableListOf<SceneTrigger>()
|
||||||
@@ -91,6 +106,7 @@ class DefaultAutomationEngine(
|
|||||||
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 &&
|
||||||
|
scheduledOccurrence.triggerDefinition == trigger.automationDefinition() &&
|
||||||
snapshot.observedAtEpochMillis in scheduledOccurrence.scheduledAtEpochMillis..scheduledOccurrence.expiresAtEpochMillis
|
snapshot.observedAtEpochMillis in scheduledOccurrence.scheduledAtEpochMillis..scheduledOccurrence.expiresAtEpochMillis
|
||||||
) {
|
) {
|
||||||
TriggerEvaluation.Matched(scheduledOccurrence.occurrenceKey)
|
TriggerEvaluation.Matched(scheduledOccurrence.occurrenceKey)
|
||||||
@@ -143,16 +159,10 @@ class DefaultAutomationEngine(
|
|||||||
if (executionCount == 0) continue
|
if (executionCount == 0) continue
|
||||||
|
|
||||||
repeat(executionCount) {
|
repeat(executionCount) {
|
||||||
sceneExecutor.execute(
|
executions += scene
|
||||||
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)
|
return DomainResult.Success(executions)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun collectSnapshot(profileId: ProviderProfileId, scenes: List<Scene>): AutomationSnapshot {
|
private suspend fun collectSnapshot(profileId: ProviderProfileId, scenes: List<Scene>): AutomationSnapshot {
|
||||||
@@ -184,3 +194,6 @@ class DefaultAutomationEngine(
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal fun SceneTrigger.Time.automationDefinition(): String =
|
||||||
|
listOf(timeOfDay, daysOfWeek.sorted().joinToString(",")).joinToString("|")
|
||||||
|
|||||||
+3
-2
@@ -37,10 +37,11 @@ class AutomationPollWorker @AssistedInject constructor(
|
|||||||
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)
|
||||||
|
val triggerDefinition = inputData.getString(WorkManagerAutomationScheduleStore.KEY_TRIGGER_DEFINITION)
|
||||||
val scheduledAt = inputData.getLong(WorkManagerAutomationScheduleStore.KEY_SCHEDULED_AT_EPOCH_MILLIS, -1L)
|
val scheduledAt = inputData.getLong(WorkManagerAutomationScheduleStore.KEY_SCHEDULED_AT_EPOCH_MILLIS, -1L)
|
||||||
val expiresAt = inputData.getLong(WorkManagerAutomationScheduleStore.KEY_EXPIRES_AT_EPOCH_MILLIS, -1L)
|
val expiresAt = inputData.getLong(WorkManagerAutomationScheduleStore.KEY_EXPIRES_AT_EPOCH_MILLIS, -1L)
|
||||||
if (sceneId != null && triggerId != null && scheduledAt >= 0L && expiresAt >= scheduledAt) {
|
if (sceneId != null && triggerId != null && triggerDefinition != null && scheduledAt >= 0L && expiresAt >= scheduledAt) {
|
||||||
ScheduledAutomationOccurrence(sceneId, triggerId, key, scheduledAt, expiresAt)
|
ScheduledAutomationOccurrence(sceneId, triggerId, key, triggerDefinition, scheduledAt, expiresAt)
|
||||||
} else null
|
} else null
|
||||||
}
|
}
|
||||||
when (automationEngine.evaluate(profileId, scheduledOccurrence)) {
|
when (automationEngine.evaluate(profileId, scheduledOccurrence)) {
|
||||||
|
|||||||
+8
-6
@@ -24,7 +24,6 @@ class WorkManagerAutomationScheduleStore(context: Context) : AutomationScheduleS
|
|||||||
.setInputData(androidx.work.Data.Builder().putString(KEY_PROFILE_ID, profileId.value).build())
|
.setInputData(androidx.work.Data.Builder().putString(KEY_PROFILE_ID, profileId.value).build())
|
||||||
.build()
|
.build()
|
||||||
workManager.enqueueUniquePeriodicWork(workName(profileId), ExistingPeriodicWorkPolicy.UPDATE, request)
|
workManager.enqueueUniquePeriodicWork(workName(profileId), ExistingPeriodicWorkPolicy.UPDATE, request)
|
||||||
workManager.cancelAllWorkByTag(timeWorkTag(profileId))
|
|
||||||
scenes.forEach { scene ->
|
scenes.forEach { scene ->
|
||||||
scene.triggers.filterIsInstance<SceneTrigger.Time>().forEach { trigger ->
|
scene.triggers.filterIsInstance<SceneTrigger.Time>().forEach { trigger ->
|
||||||
scheduleNextTimeTrigger(profileId, scene, trigger)
|
scheduleNextTimeTrigger(profileId, scene, trigger)
|
||||||
@@ -40,8 +39,8 @@ class WorkManagerAutomationScheduleStore(context: Context) : AutomationScheduleS
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun workName(profileId: ProviderProfileId) = "automation:${profileId.value}"
|
private fun workName(profileId: ProviderProfileId) = "automation:${profileId.value}"
|
||||||
private fun timeWorkName(profileId: ProviderProfileId, scene: Scene, trigger: SceneTrigger.Time) =
|
private fun timeWorkName(profileId: ProviderProfileId, scene: Scene, trigger: SceneTrigger.Time, occurrenceKey: String) =
|
||||||
"automation-time:${profileId.value}:${scene.id.value}:${trigger.id.value}"
|
"automation-time:${profileId.value}:${scene.id.value}:${trigger.id.value}:$occurrenceKey"
|
||||||
private fun timeWorkTag(profileId: ProviderProfileId) = "automation-time:${profileId.value}"
|
private fun timeWorkTag(profileId: ProviderProfileId) = "automation-time:${profileId.value}"
|
||||||
|
|
||||||
private fun scheduleNextTimeTrigger(profileId: ProviderProfileId, scene: Scene, trigger: SceneTrigger.Time) {
|
private fun scheduleNextTimeTrigger(profileId: ProviderProfileId, scene: Scene, trigger: SceneTrigger.Time) {
|
||||||
@@ -55,6 +54,7 @@ class WorkManagerAutomationScheduleStore(context: Context) : AutomationScheduleS
|
|||||||
?.toInstant()
|
?.toInstant()
|
||||||
?.minusMillis(1)
|
?.minusMillis(1)
|
||||||
val expiresAt = listOfNotNull(maximumExpiry, followingOccurrence).minOrNull() ?: maximumExpiry
|
val expiresAt = listOfNotNull(maximumExpiry, followingOccurrence).minOrNull() ?: maximumExpiry
|
||||||
|
val occurrenceKey = "${scheduledAt.toLocalDate()}:${trigger.timeOfDay}:${trigger.id.value}"
|
||||||
val request = OneTimeWorkRequestBuilder<AutomationPollWorker>()
|
val request = OneTimeWorkRequestBuilder<AutomationPollWorker>()
|
||||||
.setInitialDelay(delay.toMillis().coerceAtLeast(0), TimeUnit.MILLISECONDS)
|
.setInitialDelay(delay.toMillis().coerceAtLeast(0), TimeUnit.MILLISECONDS)
|
||||||
.addTag(timeWorkTag(profileId))
|
.addTag(timeWorkTag(profileId))
|
||||||
@@ -63,15 +63,16 @@ class WorkManagerAutomationScheduleStore(context: Context) : AutomationScheduleS
|
|||||||
.putString(KEY_PROFILE_ID, profileId.value)
|
.putString(KEY_PROFILE_ID, profileId.value)
|
||||||
.putString(KEY_SCENE_ID, scene.id.value)
|
.putString(KEY_SCENE_ID, scene.id.value)
|
||||||
.putString(KEY_TRIGGER_ID, trigger.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_SCHEDULED_AT_EPOCH_MILLIS, scheduledAtInstant.toEpochMilli())
|
||||||
.putLong(KEY_EXPIRES_AT_EPOCH_MILLIS, expiresAt.toEpochMilli())
|
.putLong(KEY_EXPIRES_AT_EPOCH_MILLIS, expiresAt.toEpochMilli())
|
||||||
.build(),
|
.build(),
|
||||||
)
|
)
|
||||||
.build()
|
.build()
|
||||||
workManager.enqueueUniqueWork(
|
workManager.enqueueUniqueWork(
|
||||||
timeWorkName(profileId, scene, trigger),
|
timeWorkName(profileId, scene, trigger, occurrenceKey),
|
||||||
androidx.work.ExistingWorkPolicy.REPLACE,
|
androidx.work.ExistingWorkPolicy.KEEP,
|
||||||
request,
|
request,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -92,6 +93,7 @@ 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_TRIGGER_DEFINITION = "trigger_definition"
|
||||||
const val KEY_SCHEDULED_AT_EPOCH_MILLIS = "scheduled_at_epoch_millis"
|
const val KEY_SCHEDULED_AT_EPOCH_MILLIS = "scheduled_at_epoch_millis"
|
||||||
const val KEY_EXPIRES_AT_EPOCH_MILLIS = "expires_at_epoch_millis"
|
const val KEY_EXPIRES_AT_EPOCH_MILLIS = "expires_at_epoch_millis"
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user