feat: add fail closed automation evaluation
This commit is contained in:
+4
-2
@@ -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) }
|
||||
}
|
||||
|
||||
+16
@@ -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() } }
|
||||
}
|
||||
}
|
||||
+10
@@ -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()
|
||||
}
|
||||
+53
@@ -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)
|
||||
}
|
||||
+31
@@ -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" }
|
||||
}
|
||||
Reference in New Issue
Block a user