feat(scene): refactor scene automation into modular trigger-action system

- Support manual one-tap commands and automatic condition-based rules
- Add modular triggers (time, temperature, humidity, device state)
- Add modular conditions (environment thresholds, socket state, time range)
- Upgrade actions to support virtual remotes, sockets, delays, and notifications
- Support sequential/parallel concurrency mode and error execution strategies
- Refactor SceneListScreen with filter tabs, quick execution, and enable switches
- Refactor SceneEditorScreen with modular builders and preset pickers
- Enhance SceneExecutionScreen with step timing, skips, and rerun capability
- Add comprehensive domain and UI ViewModel unit tests
This commit is contained in:
2026-08-25 16:15:31 +08:00
parent f70f034119
commit 9ac1616046
18 changed files with 2345 additions and 410 deletions
@@ -181,12 +181,6 @@ class FakeSceneRepository : SceneRepository {
/**
* Fake [SceneExecutor] that drives the existing [SocketGateway] and [InfraredGateway] fakes.
*
* Behaviour matches the project_plan.md §4.7 contract:
* - actions run sequentially in [Scene.actions] order;
* - a single action failure does not abort subsequent actions;
* - the returned [SceneExecutionResult] contains one entry per action;
* - re-entering execute() while a scene is running returns [DomainError.OperationFailed].
*/
class FakeSceneExecutor(
private val socketGateway: SocketGateway,
@@ -203,9 +197,23 @@ class FakeSceneExecutor(
),
)
}
val startTime = System.currentTimeMillis()
val results = mutableListOf<SceneActionExecutionResult>()
return try {
val results = scene.actions.map { action -> executeAction(scene.profileId, action) }
DomainResult.Success(SceneExecutionResult(scene = scene, results = results))
for (action in scene.actions) {
val actionStart = System.currentTimeMillis()
val execResult = executeAction(scene.profileId, action)
val duration = System.currentTimeMillis() - actionStart
results.add(execResult.copy(executionDurationMillis = duration))
}
val totalDuration = System.currentTimeMillis() - startTime
DomainResult.Success(
SceneExecutionResult(
scene = scene,
results = results,
totalDurationMillis = totalDuration,
),
)
} finally {
running.remove(scene.id)
}
@@ -227,6 +235,10 @@ class FakeSceneExecutor(
deviceName = action.deviceName,
key = action.key,
)
is SceneAction.VirtualRemoteButton,
is SceneAction.Delay,
is SceneAction.Notification -> DomainResult.Success(Unit)
}
return when (result) {
is DomainResult.Success -> SceneActionExecutionResult(