feat: add fail closed automation evaluation

This commit is contained in:
2026-08-26 20:26:59 +08:00
parent cce0441476
commit fd67820c63
5 changed files with 114 additions and 2 deletions
@@ -191,11 +191,13 @@ class SceneEditorViewModel @Inject constructor(
}
fun addDeviceStateTrigger(deviceName: String, expectedState: String) {
val parsedState = runCatching { SocketPowerState.valueOf(expectedState.trim().uppercase()) }.getOrNull()
?: return
val trigger = SceneTrigger.DeviceState(
id = SceneTriggerId(UUID.randomUUID().toString()),
deviceName = DeviceName(deviceName),
expectedState = expectedState,
label = "$deviceName 状态变为 $expectedState",
expectedState = parsedState,
label = "$deviceName 状态变为 ${parsedState.name}",
)
_state.update { it.copy(triggers = it.triggers + trigger) }
}
@@ -0,0 +1,16 @@
package com.flagship.abox.manager.automation.runtime
import com.flagship.abox.manager.domain.ProviderProfileStore
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
class AutomationBootstrapper(
private val profileStore: ProviderProfileStore,
private val scheduler: AutomationScheduler,
private val scope: CoroutineScope,
) {
fun start() {
scope.launch { profileStore.observeActiveProfile().collectLatest { scheduler.reconcileActiveProfile() } }
}
}
@@ -0,0 +1,10 @@
package com.flagship.abox.manager.automation.runtime
import android.content.Context
import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters
class AutomationPollWorker(appContext: Context, workerParams: WorkerParameters) : CoroutineWorker(appContext, workerParams) {
override suspend fun doWork(): Result =
if (inputData.getString(WorkManagerAutomationScheduleStore.KEY_PROFILE_ID).isNullOrBlank()) Result.failure() else Result.success()
}
@@ -0,0 +1,53 @@
package com.flagship.abox.manager.automation.runtime
import com.flagship.abox.manager.domain.AutomationType
import com.flagship.abox.manager.domain.DomainError
import com.flagship.abox.manager.domain.DomainResult
import com.flagship.abox.manager.domain.ProviderProfileId
import com.flagship.abox.manager.domain.ProviderProfileStore
import com.flagship.abox.manager.domain.Scene
import com.flagship.abox.manager.domain.SceneRepository
import com.flagship.abox.manager.domain.SessionGateway
import com.flagship.abox.manager.domain.SessionValidity
import kotlinx.coroutines.flow.first
interface AutomationScheduler {
suspend fun reconcileActiveProfile(): DomainResult<Unit>
suspend fun reconcile(profileId: ProviderProfileId): DomainResult<Unit>
suspend fun cancel(profileId: ProviderProfileId): DomainResult<Unit>
}
interface AutomationScheduleStore {
suspend fun replace(profileId: ProviderProfileId, scenes: List<Scene>): DomainResult<Unit>
suspend fun remove(profileId: ProviderProfileId): DomainResult<Unit>
}
class ProfileAutomationScheduler(
private val profileStore: ProviderProfileStore,
private val sessionGateway: SessionGateway,
private val sceneRepository: SceneRepository,
private val scheduleStore: AutomationScheduleStore,
) : AutomationScheduler {
override suspend fun reconcileActiveProfile(): DomainResult<Unit> {
val active = profileStore.activeProfile() ?: return DomainResult.Success(Unit)
return reconcile(active.id)
}
override suspend fun reconcile(profileId: ProviderProfileId): DomainResult<Unit> {
val active = profileStore.activeProfile()
if (active?.id != profileId) return DomainResult.Failure(DomainError.InvalidRequest(detail = "Provider profile is not active"))
return when (val validity = sessionGateway.validateSession(profileId)) {
is DomainResult.Success -> if (validity.value == SessionValidity.VALID) {
when (val result = sceneRepository.observeScenes(profileId).first()) {
is DomainResult.Failure -> result
is DomainResult.Success -> scheduleStore.replace(profileId, result.value.filter { it.enabled && it.type == AutomationType.AUTOMATIC })
}
} else {
scheduleStore.remove(profileId)
}
is DomainResult.Failure -> scheduleStore.remove(profileId)
}
}
override suspend fun cancel(profileId: ProviderProfileId): DomainResult<Unit> = scheduleStore.remove(profileId)
}
@@ -0,0 +1,31 @@
package com.flagship.abox.manager.automation.runtime
import android.content.Context
import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkManager
import com.flagship.abox.manager.domain.DomainResult
import com.flagship.abox.manager.domain.ProviderProfileId
import com.flagship.abox.manager.domain.Scene
import java.util.concurrent.TimeUnit
class WorkManagerAutomationScheduleStore(context: Context) : AutomationScheduleStore {
private val workManager = WorkManager.getInstance(context)
override suspend fun replace(profileId: ProviderProfileId, scenes: List<Scene>): DomainResult<Unit> {
val request = PeriodicWorkRequestBuilder<AutomationPollWorker>(15, TimeUnit.MINUTES)
.setInputData(androidx.work.Data.Builder().putString(KEY_PROFILE_ID, profileId.value).build())
.build()
workManager.enqueueUniquePeriodicWork(workName(profileId), ExistingPeriodicWorkPolicy.UPDATE, request)
return DomainResult.Success(Unit)
}
override suspend fun remove(profileId: ProviderProfileId): DomainResult<Unit> {
workManager.cancelUniqueWork(workName(profileId))
return DomainResult.Success(Unit)
}
private fun workName(profileId: ProviderProfileId) = "automation:${profileId.value}"
companion object { const val KEY_PROFILE_ID = "profile_id" }
}