fix(scene): make automation transitions atomic
This commit is contained in:
+14
-10
@@ -1,6 +1,7 @@
|
||||
package com.flagship.abox.manager.automation.data
|
||||
|
||||
import android.content.Context
|
||||
import androidx.room.withTransaction
|
||||
import com.flagship.abox.manager.domain.AutomationType
|
||||
import com.flagship.abox.manager.domain.ComparisonOperator
|
||||
import com.flagship.abox.manager.domain.DeviceName
|
||||
@@ -27,8 +28,9 @@ import kotlinx.coroutines.flow.map
|
||||
|
||||
class RoomSceneRepository(
|
||||
context: Context,
|
||||
private val dao: AutomationDao,
|
||||
private val database: AutomationDatabase,
|
||||
) : SceneRepository {
|
||||
private val dao = database.automationDao()
|
||||
private val seedPreferences = context.getSharedPreferences("scene_seed_state", Context.MODE_PRIVATE)
|
||||
|
||||
override fun observeScenes(profileId: ProviderProfileId): Flow<DomainResult<List<Scene>>> = flow {
|
||||
@@ -57,15 +59,17 @@ class RoomSceneRepository(
|
||||
return DomainResult.Failure(DomainError.InvalidRequest(detail = "Scene profile mismatch"))
|
||||
}
|
||||
return runCatching {
|
||||
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())
|
||||
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)
|
||||
}
|
||||
database.withTransaction {
|
||||
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())
|
||||
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)) })
|
||||
}
|
||||
|
||||
+1
-1
@@ -63,7 +63,7 @@ object AutomationModule {
|
||||
fun provideSceneRepository(
|
||||
@ApplicationContext context: Context,
|
||||
database: AutomationDatabase,
|
||||
): SceneRepository = RoomSceneRepository(context, database.automationDao())
|
||||
): SceneRepository = RoomSceneRepository(context, database)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
|
||||
+12
-6
@@ -38,18 +38,24 @@ class AutomationBootstrapper(
|
||||
previousProfileId?.takeIf { it != active?.id }?.let { scheduler.cancel(it) }
|
||||
previousProfileId = active?.id
|
||||
if (active != null && session?.profileId == active.id) {
|
||||
var scheduleReady = false
|
||||
while (true) {
|
||||
isForeground.first { it }
|
||||
when (val validity = sessionCoordinator.validateSession(active.id)) {
|
||||
is DomainResult.Success -> if (validity.value == SessionValidity.VALID) {
|
||||
scheduler.reconcile(active.id)
|
||||
try {
|
||||
engine.evaluate(active.id)
|
||||
} catch (cancelled: CancellationException) {
|
||||
throw cancelled
|
||||
} catch (_: Exception) {}
|
||||
if (!scheduleReady) {
|
||||
scheduleReady = scheduler.reconcile(active.id) is DomainResult.Success
|
||||
}
|
||||
if (scheduleReady) {
|
||||
try {
|
||||
engine.evaluate(active.id)
|
||||
} catch (cancelled: CancellationException) {
|
||||
throw cancelled
|
||||
} catch (_: Exception) {}
|
||||
}
|
||||
} else {
|
||||
scheduler.cancel(active.id)
|
||||
scheduleReady = false
|
||||
}
|
||||
is DomainResult.Failure -> Unit
|
||||
}
|
||||
|
||||
+12
-2
@@ -118,8 +118,18 @@ class DefaultAutomationEngine(
|
||||
}
|
||||
}
|
||||
if (!newlyMatched) continue
|
||||
if (conditionEvaluator.evaluate(scene.conditions, snapshot, now) !is ConditionsEvaluation.Satisfied) {
|
||||
continue
|
||||
when (conditionEvaluator.evaluate(scene.conditions, snapshot, now)) {
|
||||
is ConditionsEvaluation.Unavailable -> continue
|
||||
is ConditionsEvaluation.NotSatisfied -> {
|
||||
for ((trigger, occurrenceKey) in matchedTimeTriggers) {
|
||||
when (val claim = runtimeRepository.claimOccurrence(profileId, scene.id, trigger.id, occurrenceKey)) {
|
||||
is DomainResult.Failure -> return claim
|
||||
is DomainResult.Success -> Unit
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
ConditionsEvaluation.Satisfied -> Unit
|
||||
}
|
||||
var executionCount = if (newlyMatchedStateTriggers.isEmpty()) 0 else 1
|
||||
if (matchedTimeTriggers.isNotEmpty()) {
|
||||
|
||||
Reference in New Issue
Block a user