fix(scene): address bot review on fake executor delays, time input validation, and localization
This commit is contained in:
+121
-56
@@ -223,6 +223,14 @@ fun SceneEditorScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (state.triggers.isEmpty()) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.scene_empty_triggers_hint),
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
state.triggers.forEach { trigger ->
|
state.triggers.forEach { trigger ->
|
||||||
TriggerItemRow(trigger = trigger, onRemove = { viewModel.removeTrigger(trigger.id) })
|
TriggerItemRow(trigger = trigger, onRemove = { viewModel.removeTrigger(trigger.id) })
|
||||||
}
|
}
|
||||||
@@ -273,6 +281,14 @@ fun SceneEditorScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (state.conditions.isEmpty()) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.scene_empty_conditions_hint),
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
state.conditions.forEach { condition ->
|
state.conditions.forEach { condition ->
|
||||||
ConditionItemRow(condition = condition, onRemove = { viewModel.removeCondition(condition.id) })
|
ConditionItemRow(condition = condition, onRemove = { viewModel.removeCondition(condition.id) })
|
||||||
}
|
}
|
||||||
@@ -327,7 +343,7 @@ fun SceneEditorScreen(
|
|||||||
|
|
||||||
if (state.actions.isEmpty()) {
|
if (state.actions.isEmpty()) {
|
||||||
Text(
|
Text(
|
||||||
text = "暂未添加动作,请点击下方按钮添加",
|
text = stringResource(R.string.scene_empty_actions_hint),
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
)
|
)
|
||||||
@@ -514,14 +530,17 @@ fun SceneEditorScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val defaultSensorName = stringResource(R.string.device_temperature_humidity_title)
|
||||||
|
val defaultSocketName = stringResource(R.string.scene_default_socket_name)
|
||||||
|
|
||||||
if (showTriggerDialog) {
|
if (showTriggerDialog) {
|
||||||
AddTriggerDialog(
|
AddTriggerDialog(
|
||||||
onDismiss = { showTriggerDialog = false },
|
onDismiss = { showTriggerDialog = false },
|
||||||
onAdd = { triggerType, time, temp, hum, op ->
|
onAdd = { triggerType, time, temp, hum, op ->
|
||||||
when (triggerType) {
|
when (triggerType) {
|
||||||
0 -> viewModel.addTimeTrigger(time)
|
0 -> viewModel.addTimeTrigger(time)
|
||||||
1 -> viewModel.addTemperatureTrigger("温湿度传感器", op, temp)
|
1 -> viewModel.addTemperatureTrigger(defaultSensorName, op, temp)
|
||||||
2 -> viewModel.addHumidityTrigger("温湿度传感器", op, hum)
|
2 -> viewModel.addHumidityTrigger(defaultSensorName, op, hum)
|
||||||
}
|
}
|
||||||
showTriggerDialog = false
|
showTriggerDialog = false
|
||||||
},
|
},
|
||||||
@@ -532,15 +551,15 @@ fun SceneEditorScreen(
|
|||||||
AddConditionDialog(
|
AddConditionDialog(
|
||||||
onDismiss = { showConditionDialog = false },
|
onDismiss = { showConditionDialog = false },
|
||||||
onAddTemp = { op, temp ->
|
onAddTemp = { op, temp ->
|
||||||
viewModel.addTemperatureCondition("温湿度传感器", op, temp)
|
viewModel.addTemperatureCondition(defaultSensorName, op, temp)
|
||||||
showConditionDialog = false
|
showConditionDialog = false
|
||||||
},
|
},
|
||||||
onAddHumidity = { op, hum ->
|
onAddHumidity = { op, hum ->
|
||||||
viewModel.addHumidityCondition("温湿度传感器", op, hum)
|
viewModel.addHumidityCondition(defaultSensorName, op, hum)
|
||||||
showConditionDialog = false
|
showConditionDialog = false
|
||||||
},
|
},
|
||||||
onAddSocket = { powerState ->
|
onAddSocket = { powerState ->
|
||||||
viewModel.addSocketPowerCondition("Zigbee插座控制器", powerState)
|
viewModel.addSocketPowerCondition(defaultSocketName, powerState)
|
||||||
showConditionDialog = false
|
showConditionDialog = false
|
||||||
},
|
},
|
||||||
onAddTimeRange = { start, end ->
|
onAddTimeRange = { start, end ->
|
||||||
@@ -588,10 +607,10 @@ private fun TriggerItemRow(trigger: SceneTrigger, onRemove: () -> Unit) {
|
|||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
) {
|
) {
|
||||||
val (icon, desc) = when (trigger) {
|
val (icon, desc) = when (trigger) {
|
||||||
is SceneTrigger.Time -> Pair("⏰", "定时: ${trigger.timeOfDay}")
|
is SceneTrigger.Time -> Pair("⏰", stringResource(R.string.scene_trigger_type_time) + ": ${trigger.timeOfDay}")
|
||||||
is SceneTrigger.Temperature -> Pair("🌡️", "温度 ${trigger.operator.symbol} ${trigger.thresholdCelsius}°C")
|
is SceneTrigger.Temperature -> Pair("🌡️", stringResource(R.string.scene_trigger_desc_temp, trigger.operator.symbol, trigger.thresholdCelsius.toInt()))
|
||||||
is SceneTrigger.Humidity -> Pair("💧", "湿度 ${trigger.operator.symbol} ${trigger.thresholdPercent}%")
|
is SceneTrigger.Humidity -> Pair("💧", stringResource(R.string.scene_trigger_desc_humidity, trigger.operator.symbol, trigger.thresholdPercent.toInt()))
|
||||||
is SceneTrigger.DeviceState -> Pair("🔌", "${trigger.deviceName.value} 状态变为 ${trigger.expectedState}")
|
is SceneTrigger.DeviceState -> Pair("🔌", stringResource(R.string.scene_trigger_desc_device, trigger.deviceName.value, trigger.expectedState))
|
||||||
}
|
}
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
Text(icon, style = MaterialTheme.typography.titleMedium)
|
Text(icon, style = MaterialTheme.typography.titleMedium)
|
||||||
@@ -618,10 +637,10 @@ private fun ConditionItemRow(condition: SceneCondition, onRemove: () -> Unit) {
|
|||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
) {
|
) {
|
||||||
val (icon, desc) = when (condition) {
|
val (icon, desc) = when (condition) {
|
||||||
is SceneCondition.Temperature -> Pair("🌡️", "温度需 ${condition.operator.symbol} ${condition.thresholdCelsius}°C")
|
is SceneCondition.Temperature -> Pair("🌡️", stringResource(R.string.scene_condition_desc_temp, condition.operator.symbol, condition.thresholdCelsius.toInt()))
|
||||||
is SceneCondition.Humidity -> Pair("💧", "湿度需 ${condition.operator.symbol} ${condition.thresholdPercent}%")
|
is SceneCondition.Humidity -> Pair("💧", stringResource(R.string.scene_condition_desc_humidity, condition.operator.symbol, condition.thresholdPercent.toInt()))
|
||||||
is SceneCondition.SocketPower -> Pair("🔌", "${condition.deviceName.value} 处于 ${condition.expectedState.name}")
|
is SceneCondition.SocketPower -> Pair("🔌", stringResource(R.string.scene_condition_desc_socket, condition.deviceName.value, condition.expectedState.name))
|
||||||
is SceneCondition.TimeRange -> Pair("🕒", "时间在 ${condition.startTime} ~ ${condition.endTime} 之间")
|
is SceneCondition.TimeRange -> Pair("🕒", stringResource(R.string.scene_condition_desc_time, condition.startTime, condition.endTime))
|
||||||
}
|
}
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
Text(icon, style = MaterialTheme.typography.titleMedium)
|
Text(icon, style = MaterialTheme.typography.titleMedium)
|
||||||
@@ -748,15 +767,18 @@ private fun AddTriggerDialog(
|
|||||||
var humThreshold by remember { mutableDoubleStateOf(40.0) }
|
var humThreshold by remember { mutableDoubleStateOf(40.0) }
|
||||||
var operator by remember { mutableStateOf(ComparisonOperator.GREATER_THAN) }
|
var operator by remember { mutableStateOf(ComparisonOperator.GREATER_THAN) }
|
||||||
|
|
||||||
|
val timeRegex = remember { Regex("^([01]\\d|2[0-3]):[0-5]\\d$") }
|
||||||
|
val isTimeValid = timeRegex.matches(timeInput.trim())
|
||||||
|
|
||||||
AlertDialog(
|
AlertDialog(
|
||||||
onDismissRequest = onDismiss,
|
onDismissRequest = onDismiss,
|
||||||
title = { Text(stringResource(R.string.scene_dialog_add_trigger_title)) },
|
title = { Text(stringResource(R.string.scene_dialog_add_trigger_title)) },
|
||||||
text = {
|
text = {
|
||||||
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
|
Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||||
FilterChip(selected = selectedType == 0, onClick = { selectedType = 0 }, label = { Text("⏰ 定时") })
|
FilterChip(selected = selectedType == 0, onClick = { selectedType = 0 }, label = { Text(stringResource(R.string.scene_chip_time)) })
|
||||||
FilterChip(selected = selectedType == 1, onClick = { selectedType = 1 }, label = { Text("🌡️ 温度") })
|
FilterChip(selected = selectedType == 1, onClick = { selectedType = 1 }, label = { Text(stringResource(R.string.scene_chip_temp)) })
|
||||||
FilterChip(selected = selectedType == 2, onClick = { selectedType = 2 }, label = { Text("💧 湿度") })
|
FilterChip(selected = selectedType == 2, onClick = { selectedType = 2 }, label = { Text(stringResource(R.string.scene_chip_humidity)) })
|
||||||
}
|
}
|
||||||
when (selectedType) {
|
when (selectedType) {
|
||||||
0 -> {
|
0 -> {
|
||||||
@@ -765,19 +787,23 @@ private fun AddTriggerDialog(
|
|||||||
onValueChange = { timeInput = it },
|
onValueChange = { timeInput = it },
|
||||||
label = { Text(stringResource(R.string.scene_trigger_time_label)) },
|
label = { Text(stringResource(R.string.scene_trigger_time_label)) },
|
||||||
singleLine = true,
|
singleLine = true,
|
||||||
|
isError = !isTimeValid,
|
||||||
|
supportingText = if (!isTimeValid) {
|
||||||
|
{ Text(stringResource(R.string.scene_trigger_time_error)) }
|
||||||
|
} else null,
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
1, 2 -> {
|
1, 2 -> {
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
|
Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||||
FilterChip(selected = operator == ComparisonOperator.GREATER_THAN, onClick = { operator = ComparisonOperator.GREATER_THAN }, label = { Text("> 高于") })
|
FilterChip(selected = operator == ComparisonOperator.GREATER_THAN, onClick = { operator = ComparisonOperator.GREATER_THAN }, label = { Text(stringResource(R.string.scene_chip_above)) })
|
||||||
FilterChip(selected = operator == ComparisonOperator.LESS_THAN, onClick = { operator = ComparisonOperator.LESS_THAN }, label = { Text("< 低于") })
|
FilterChip(selected = operator == ComparisonOperator.LESS_THAN, onClick = { operator = ComparisonOperator.LESS_THAN }, label = { Text(stringResource(R.string.scene_chip_below)) })
|
||||||
}
|
}
|
||||||
if (selectedType == 1) {
|
if (selectedType == 1) {
|
||||||
Text("设定阈值: ${tempThreshold.toInt()} °C")
|
Text(stringResource(R.string.scene_threshold_temp, tempThreshold.toInt()))
|
||||||
Slider(value = tempThreshold.toFloat(), onValueChange = { tempThreshold = it.toDouble() }, valueRange = 10f..40f)
|
Slider(value = tempThreshold.toFloat(), onValueChange = { tempThreshold = it.toDouble() }, valueRange = 10f..40f)
|
||||||
} else {
|
} else {
|
||||||
Text("设定阈值: ${humThreshold.toInt()} %")
|
Text(stringResource(R.string.scene_threshold_hum, humThreshold.toInt()))
|
||||||
Slider(value = humThreshold.toFloat(), onValueChange = { humThreshold = it.toDouble() }, valueRange = 10f..90f)
|
Slider(value = humThreshold.toFloat(), onValueChange = { humThreshold = it.toDouble() }, valueRange = 10f..90f)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -785,7 +811,11 @@ private fun AddTriggerDialog(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
confirmButton = {
|
confirmButton = {
|
||||||
Button(onClick = { onAdd(selectedType, timeInput, tempThreshold, humThreshold, operator) }) {
|
val canConfirm = selectedType != 0 || isTimeValid
|
||||||
|
Button(
|
||||||
|
onClick = { onAdd(selectedType, timeInput.trim(), tempThreshold, humThreshold, operator) },
|
||||||
|
enabled = canConfirm,
|
||||||
|
) {
|
||||||
Text(stringResource(R.string.scene_add_action))
|
Text(stringResource(R.string.scene_add_action))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -811,57 +841,86 @@ private fun AddConditionDialog(
|
|||||||
var startTime by remember { mutableStateOf("22:00") }
|
var startTime by remember { mutableStateOf("22:00") }
|
||||||
var endTime by remember { mutableStateOf("07:00") }
|
var endTime by remember { mutableStateOf("07:00") }
|
||||||
|
|
||||||
|
val timeRegex = remember { Regex("^([01]\\d|2[0-3]):[0-5]\\d$") }
|
||||||
|
val isStartTimeValid = timeRegex.matches(startTime.trim())
|
||||||
|
val isEndTimeValid = timeRegex.matches(endTime.trim())
|
||||||
|
val isTimeRangeValid = isStartTimeValid && isEndTimeValid
|
||||||
|
|
||||||
AlertDialog(
|
AlertDialog(
|
||||||
onDismissRequest = onDismiss,
|
onDismissRequest = onDismiss,
|
||||||
title = { Text(stringResource(R.string.scene_dialog_add_condition_title)) },
|
title = { Text(stringResource(R.string.scene_dialog_add_condition_title)) },
|
||||||
text = {
|
text = {
|
||||||
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
|
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
|
Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||||
FilterChip(selected = selectedType == 0, onClick = { selectedType = 0 }, label = { Text("🌡️ 温度") })
|
FilterChip(selected = selectedType == 0, onClick = { selectedType = 0 }, label = { Text(stringResource(R.string.scene_chip_temp)) })
|
||||||
FilterChip(selected = selectedType == 1, onClick = { selectedType = 1 }, label = { Text("💧 湿度") })
|
FilterChip(selected = selectedType == 1, onClick = { selectedType = 1 }, label = { Text(stringResource(R.string.scene_chip_humidity)) })
|
||||||
FilterChip(selected = selectedType == 2, onClick = { selectedType = 2 }, label = { Text("🔌 插座") })
|
FilterChip(selected = selectedType == 2, onClick = { selectedType = 2 }, label = { Text(stringResource(R.string.scene_chip_socket)) })
|
||||||
FilterChip(selected = selectedType == 3, onClick = { selectedType = 3 }, label = { Text("🕒 时间") })
|
FilterChip(selected = selectedType == 3, onClick = { selectedType = 3 }, label = { Text(stringResource(R.string.scene_chip_time)) })
|
||||||
}
|
}
|
||||||
when (selectedType) {
|
when (selectedType) {
|
||||||
0 -> {
|
0 -> {
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
|
Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||||
FilterChip(selected = operator == ComparisonOperator.GREATER_THAN, onClick = { operator = ComparisonOperator.GREATER_THAN }, label = { Text("> 高于") })
|
FilterChip(selected = operator == ComparisonOperator.GREATER_THAN, onClick = { operator = ComparisonOperator.GREATER_THAN }, label = { Text(stringResource(R.string.scene_chip_above)) })
|
||||||
FilterChip(selected = operator == ComparisonOperator.LESS_THAN, onClick = { operator = ComparisonOperator.LESS_THAN }, label = { Text("< 低于") })
|
FilterChip(selected = operator == ComparisonOperator.LESS_THAN, onClick = { operator = ComparisonOperator.LESS_THAN }, label = { Text(stringResource(R.string.scene_chip_below)) })
|
||||||
}
|
}
|
||||||
Text("设定温度: ${tempVal.toInt()} °C")
|
Text(stringResource(R.string.scene_threshold_temp, tempVal.toInt()))
|
||||||
Slider(value = tempVal.toFloat(), onValueChange = { tempVal = it.toDouble() }, valueRange = 10f..40f)
|
Slider(value = tempVal.toFloat(), onValueChange = { tempVal = it.toDouble() }, valueRange = 10f..40f)
|
||||||
}
|
}
|
||||||
1 -> {
|
1 -> {
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
|
Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||||
FilterChip(selected = operator == ComparisonOperator.GREATER_THAN, onClick = { operator = ComparisonOperator.GREATER_THAN }, label = { Text("> 高于") })
|
FilterChip(selected = operator == ComparisonOperator.GREATER_THAN, onClick = { operator = ComparisonOperator.GREATER_THAN }, label = { Text(stringResource(R.string.scene_chip_above)) })
|
||||||
FilterChip(selected = operator == ComparisonOperator.LESS_THAN, onClick = { operator = ComparisonOperator.LESS_THAN }, label = { Text("< 低于") })
|
FilterChip(selected = operator == ComparisonOperator.LESS_THAN, onClick = { operator = ComparisonOperator.LESS_THAN }, label = { Text(stringResource(R.string.scene_chip_below)) })
|
||||||
}
|
}
|
||||||
Text("设定湿度: ${humVal.toInt()} %")
|
Text(stringResource(R.string.scene_threshold_hum, humVal.toInt()))
|
||||||
Slider(value = humVal.toFloat(), onValueChange = { humVal = it.toDouble() }, valueRange = 10f..90f)
|
Slider(value = humVal.toFloat(), onValueChange = { humVal = it.toDouble() }, valueRange = 10f..90f)
|
||||||
}
|
}
|
||||||
2 -> {
|
2 -> {
|
||||||
Text("要求插座当前状态为:")
|
Text(stringResource(R.string.scene_target_socket_state))
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
FilterChip(selected = socketPower == SocketPowerState.ON, onClick = { socketPower = SocketPowerState.ON }, label = { Text("开启") })
|
FilterChip(selected = socketPower == SocketPowerState.ON, onClick = { socketPower = SocketPowerState.ON }, label = { Text(stringResource(R.string.scene_chip_power_on)) })
|
||||||
FilterChip(selected = socketPower == SocketPowerState.OFF, onClick = { socketPower = SocketPowerState.OFF }, label = { Text("关闭") })
|
FilterChip(selected = socketPower == SocketPowerState.OFF, onClick = { socketPower = SocketPowerState.OFF }, label = { Text(stringResource(R.string.scene_chip_power_off)) })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
3 -> {
|
3 -> {
|
||||||
OutlinedTextField(value = startTime, onValueChange = { startTime = it }, label = { Text(stringResource(R.string.scene_condition_start_time)) }, singleLine = true)
|
OutlinedTextField(
|
||||||
OutlinedTextField(value = endTime, onValueChange = { endTime = it }, label = { Text(stringResource(R.string.scene_condition_end_time)) }, singleLine = true)
|
value = startTime,
|
||||||
|
onValueChange = { startTime = it },
|
||||||
|
label = { Text(stringResource(R.string.scene_condition_start_time)) },
|
||||||
|
singleLine = true,
|
||||||
|
isError = !isStartTimeValid,
|
||||||
|
supportingText = if (!isStartTimeValid) {
|
||||||
|
{ Text(stringResource(R.string.scene_condition_time_error)) }
|
||||||
|
} else null,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
OutlinedTextField(
|
||||||
|
value = endTime,
|
||||||
|
onValueChange = { endTime = it },
|
||||||
|
label = { Text(stringResource(R.string.scene_condition_end_time)) },
|
||||||
|
singleLine = true,
|
||||||
|
isError = !isEndTimeValid,
|
||||||
|
supportingText = if (!isEndTimeValid) {
|
||||||
|
{ Text(stringResource(R.string.scene_condition_time_error)) }
|
||||||
|
} else null,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
confirmButton = {
|
confirmButton = {
|
||||||
Button(onClick = {
|
val canConfirm = selectedType != 3 || isTimeRangeValid
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
when (selectedType) {
|
when (selectedType) {
|
||||||
0 -> onAddTemp(operator, tempVal)
|
0 -> onAddTemp(operator, tempVal)
|
||||||
1 -> onAddHumidity(operator, humVal)
|
1 -> onAddHumidity(operator, humVal)
|
||||||
2 -> onAddSocket(socketPower)
|
2 -> onAddSocket(socketPower)
|
||||||
3 -> onAddTimeRange(startTime, endTime)
|
3 -> onAddTimeRange(startTime.trim(), endTime.trim())
|
||||||
}
|
}
|
||||||
}) {
|
},
|
||||||
|
enabled = canConfirm,
|
||||||
|
) {
|
||||||
Text(stringResource(R.string.scene_add_action))
|
Text(stringResource(R.string.scene_add_action))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -882,17 +941,23 @@ private fun AddActionDialog(
|
|||||||
onAddNotify: (title: String, msg: String) -> Unit,
|
onAddNotify: (title: String, msg: String) -> Unit,
|
||||||
) {
|
) {
|
||||||
var actionType by remember { mutableIntStateOf(0) }
|
var actionType by remember { mutableIntStateOf(0) }
|
||||||
var socketName by remember { mutableStateOf("Zigbee插座控制器") }
|
val defaultSocketName = stringResource(R.string.scene_default_socket_name)
|
||||||
|
val defaultIrName = stringResource(R.string.scene_default_ir_name)
|
||||||
|
val defaultPowerBtn = stringResource(R.string.scene_default_button_power)
|
||||||
|
val defaultNotifyTitle = stringResource(R.string.scene_default_notify_title)
|
||||||
|
val defaultNotifyMsg = stringResource(R.string.scene_default_notify_msg)
|
||||||
|
|
||||||
|
var socketName by remember { mutableStateOf(defaultSocketName) }
|
||||||
var socketPower by remember { mutableStateOf(SocketPowerState.ON) }
|
var socketPower by remember { mutableStateOf(SocketPowerState.ON) }
|
||||||
|
|
||||||
var selectedRemoteIndex by remember { mutableIntStateOf(0) }
|
var selectedRemoteIndex by remember { mutableIntStateOf(0) }
|
||||||
var keyBindings by remember { mutableStateOf<List<RemoteKeyBinding>>(emptyList()) }
|
var keyBindings by remember { mutableStateOf<List<RemoteKeyBinding>>(emptyList()) }
|
||||||
var selectedButtonId by remember { mutableStateOf("power") }
|
var selectedButtonId by remember { mutableStateOf("power") }
|
||||||
var selectedButtonName by remember { mutableStateOf("电源") }
|
var selectedButtonName by remember { mutableStateOf(defaultPowerBtn) }
|
||||||
|
|
||||||
var delaySeconds by remember { mutableIntStateOf(3) }
|
var delaySeconds by remember { mutableIntStateOf(3) }
|
||||||
var notifyTitle by remember { mutableStateOf("情景完成") }
|
var notifyTitle by remember { mutableStateOf(defaultNotifyTitle) }
|
||||||
var notifyMsg by remember { mutableStateOf("自动化任务执行完成") }
|
var notifyMsg by remember { mutableStateOf(defaultNotifyMsg) }
|
||||||
|
|
||||||
LaunchedEffect(availableRemotes, selectedRemoteIndex) {
|
LaunchedEffect(availableRemotes, selectedRemoteIndex) {
|
||||||
if (availableRemotes.isNotEmpty() && selectedRemoteIndex in availableRemotes.indices) {
|
if (availableRemotes.isNotEmpty() && selectedRemoteIndex in availableRemotes.indices) {
|
||||||
@@ -905,7 +970,7 @@ private fun AddActionDialog(
|
|||||||
selectedButtonName = bindings[0].displayName.ifBlank { bindings[0].buttonId }
|
selectedButtonName = bindings[0].displayName.ifBlank { bindings[0].buttonId }
|
||||||
} else {
|
} else {
|
||||||
selectedButtonId = "power"
|
selectedButtonId = "power"
|
||||||
selectedButtonName = "电源"
|
selectedButtonName = defaultPowerBtn
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
keyBindings = emptyList()
|
keyBindings = emptyList()
|
||||||
@@ -918,10 +983,10 @@ private fun AddActionDialog(
|
|||||||
text = {
|
text = {
|
||||||
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
|
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
|
Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||||
FilterChip(selected = actionType == 0, onClick = { actionType = 0 }, label = { Text("🔌 插座") })
|
FilterChip(selected = actionType == 0, onClick = { actionType = 0 }, label = { Text(stringResource(R.string.scene_chip_socket)) })
|
||||||
FilterChip(selected = actionType == 1, onClick = { actionType = 1 }, label = { Text("📺 遥控") })
|
FilterChip(selected = actionType == 1, onClick = { actionType = 1 }, label = { Text(stringResource(R.string.scene_chip_remote)) })
|
||||||
FilterChip(selected = actionType == 2, onClick = { actionType = 2 }, label = { Text("⏱️ 延时") })
|
FilterChip(selected = actionType == 2, onClick = { actionType = 2 }, label = { Text(stringResource(R.string.scene_chip_delay)) })
|
||||||
FilterChip(selected = actionType == 3, onClick = { actionType = 3 }, label = { Text("🔔 提示") })
|
FilterChip(selected = actionType == 3, onClick = { actionType = 3 }, label = { Text(stringResource(R.string.scene_chip_notify)) })
|
||||||
}
|
}
|
||||||
when (actionType) {
|
when (actionType) {
|
||||||
0 -> {
|
0 -> {
|
||||||
@@ -934,14 +999,14 @@ private fun AddActionDialog(
|
|||||||
)
|
)
|
||||||
Text(stringResource(R.string.scene_socket_power_state))
|
Text(stringResource(R.string.scene_socket_power_state))
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
FilterChip(selected = socketPower == SocketPowerState.ON, onClick = { socketPower = SocketPowerState.ON }, label = { Text("开启") })
|
FilterChip(selected = socketPower == SocketPowerState.ON, onClick = { socketPower = SocketPowerState.ON }, label = { Text(stringResource(R.string.scene_chip_power_on)) })
|
||||||
FilterChip(selected = socketPower == SocketPowerState.OFF, onClick = { socketPower = SocketPowerState.OFF }, label = { Text("关闭") })
|
FilterChip(selected = socketPower == SocketPowerState.OFF, onClick = { socketPower = SocketPowerState.OFF }, label = { Text(stringResource(R.string.scene_chip_power_off)) })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
1 -> {
|
1 -> {
|
||||||
if (availableRemotes.isEmpty()) {
|
if (availableRemotes.isEmpty()) {
|
||||||
Text("未找到虚拟遥控器,将默认使用红外控制器")
|
Text(stringResource(R.string.scene_no_virtual_remotes))
|
||||||
OutlinedTextField(value = selectedButtonId, onValueChange = { selectedButtonId = it }, label = { Text("按键名称/槽位") }, singleLine = true)
|
OutlinedTextField(value = selectedButtonId, onValueChange = { selectedButtonId = it }, label = { Text(stringResource(R.string.scene_remote_button_slot_label)) }, singleLine = true)
|
||||||
} else {
|
} else {
|
||||||
Text(stringResource(R.string.scene_select_virtual_remote))
|
Text(stringResource(R.string.scene_select_virtual_remote))
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
|
Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||||
@@ -969,12 +1034,12 @@ private fun AddActionDialog(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
OutlinedTextField(value = selectedButtonName, onValueChange = { selectedButtonName = it; selectedButtonId = it }, label = { Text("按键名称") }, singleLine = true)
|
OutlinedTextField(value = selectedButtonName, onValueChange = { selectedButtonName = it; selectedButtonId = it }, label = { Text(stringResource(R.string.scene_remote_button_name_label)) }, singleLine = true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
2 -> {
|
2 -> {
|
||||||
Text(stringResource(R.string.scene_delay_duration_label) + ": $delaySeconds 秒")
|
Text(stringResource(R.string.scene_delay_duration_label) + ": " + stringResource(R.string.scene_unit_seconds, delaySeconds))
|
||||||
Slider(
|
Slider(
|
||||||
value = delaySeconds.toFloat(),
|
value = delaySeconds.toFloat(),
|
||||||
onValueChange = { delaySeconds = it.toInt() },
|
onValueChange = { delaySeconds = it.toInt() },
|
||||||
@@ -998,7 +1063,7 @@ private fun AddActionDialog(
|
|||||||
if (remote != null) {
|
if (remote != null) {
|
||||||
onAddRemote(remote.id.value, remote.displayName, selectedButtonId, selectedButtonName)
|
onAddRemote(remote.id.value, remote.displayName, selectedButtonId, selectedButtonName)
|
||||||
} else {
|
} else {
|
||||||
onAddRemote("default-ir", "红外控制器", selectedButtonId, selectedButtonName)
|
onAddRemote("default-ir", defaultIrName, selectedButtonId, selectedButtonName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
2 -> onAddDelay(delaySeconds)
|
2 -> onAddDelay(delaySeconds)
|
||||||
|
|||||||
+11
-10
@@ -116,7 +116,7 @@ private fun CenteredMessage(paddingValues: PaddingValues, message: String, onRet
|
|||||||
Button(onClick = onRetry) {
|
Button(onClick = onRetry) {
|
||||||
Icon(Icons.Filled.Refresh, contentDescription = null)
|
Icon(Icons.Filled.Refresh, contentDescription = null)
|
||||||
Spacer(Modifier.width(6.dp))
|
Spacer(Modifier.width(6.dp))
|
||||||
Text("重试")
|
Text(stringResource(R.string.action_retry))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -137,7 +137,7 @@ private fun ReadyContent(
|
|||||||
) {
|
) {
|
||||||
Text(sceneName, style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold)
|
Text(sceneName, style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold)
|
||||||
Spacer(Modifier.height(8.dp))
|
Spacer(Modifier.height(8.dp))
|
||||||
Text("点击下方按钮立即开始执行", style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
Text(stringResource(R.string.scene_exec_ready_hint), style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||||
Spacer(Modifier.height(24.dp))
|
Spacer(Modifier.height(24.dp))
|
||||||
Button(
|
Button(
|
||||||
onClick = onExecute,
|
onClick = onExecute,
|
||||||
@@ -247,14 +247,14 @@ private fun CompletedContent(
|
|||||||
) {
|
) {
|
||||||
Icon(Icons.Filled.Refresh, contentDescription = null, modifier = Modifier.size(18.dp))
|
Icon(Icons.Filled.Refresh, contentDescription = null, modifier = Modifier.size(18.dp))
|
||||||
Spacer(Modifier.width(6.dp))
|
Spacer(Modifier.width(6.dp))
|
||||||
Text("重新执行")
|
Text(stringResource(R.string.scene_exec_rerun))
|
||||||
}
|
}
|
||||||
Button(
|
Button(
|
||||||
onClick = onBack,
|
onClick = onBack,
|
||||||
modifier = Modifier.weight(1f).height(48.dp),
|
modifier = Modifier.weight(1f).height(48.dp),
|
||||||
shape = RoundedCornerShape(12.dp),
|
shape = RoundedCornerShape(12.dp),
|
||||||
) {
|
) {
|
||||||
Text("返回", fontWeight = FontWeight.SemiBold)
|
Text(stringResource(R.string.action_back), fontWeight = FontWeight.SemiBold)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -310,7 +310,7 @@ private fun ActionResultRow(stepNumber: Int, entry: SceneActionExecutionResult)
|
|||||||
Spacer(Modifier.width(12.dp))
|
Spacer(Modifier.width(12.dp))
|
||||||
Column(modifier = Modifier.weight(1f)) {
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
Text(
|
Text(
|
||||||
text = "步骤 $stepNumber: ${entry.action.describe()}",
|
text = stringResource(R.string.scene_step_format, stepNumber, entry.action.describe()),
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
fontWeight = FontWeight.Medium,
|
fontWeight = FontWeight.Medium,
|
||||||
)
|
)
|
||||||
@@ -334,11 +334,12 @@ private fun ActionResultRow(stepNumber: Int, entry: SceneActionExecutionResult)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
private fun SceneAction.describe(): String = when (this) {
|
private fun SceneAction.describe(): String = when (this) {
|
||||||
is SceneAction.SocketPower -> "插座【${deviceName.value}】-> ${powerState.name}"
|
is SceneAction.SocketPower -> stringResource(R.string.scene_action_socket, deviceName.value, powerState.name)
|
||||||
is SceneAction.VirtualRemoteButton -> "遥控【$remoteDisplayName】-> $buttonDisplayName"
|
is SceneAction.VirtualRemoteButton -> stringResource(R.string.scene_action_remote, remoteDisplayName, buttonDisplayName)
|
||||||
is SceneAction.InfraredKey -> "红外【${deviceName.value}】-> 槽位 $key"
|
is SceneAction.InfraredKey -> stringResource(R.string.scene_action_infrared, deviceName.value, key)
|
||||||
is SceneAction.Delay -> "⏱️ 等待 ${durationSeconds} 秒"
|
is SceneAction.Delay -> stringResource(R.string.scene_action_delay, durationSeconds)
|
||||||
is SceneAction.Notification -> "🔔 提示: $title"
|
is SceneAction.Notification -> stringResource(R.string.scene_action_notification, title)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -305,13 +305,17 @@ private fun SceneCard(
|
|||||||
) {
|
) {
|
||||||
if (scene.type == AutomationType.AUTOMATIC) {
|
if (scene.type == AutomationType.AUTOMATIC) {
|
||||||
Text(
|
Text(
|
||||||
text = if (scene.triggers.isNotEmpty()) "⚡ ${scene.triggers.size} 个触发器 • ${scene.actions.size} 个动作" else "${scene.actions.size} 个动作",
|
text = if (scene.triggers.isNotEmpty()) {
|
||||||
|
stringResource(R.string.scene_summary_triggers_and_actions, scene.triggers.size, scene.actions.size)
|
||||||
|
} else {
|
||||||
|
stringResource(R.string.scene_summary_actions_only, scene.actions.size)
|
||||||
|
},
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
Text(
|
Text(
|
||||||
text = "${scene.actions.size} 个执行动作",
|
text = stringResource(R.string.scene_summary_actions_only, scene.actions.size),
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -161,6 +161,47 @@
|
|||||||
<string name="scene_step_failed">执行失败</string>
|
<string name="scene_step_failed">执行失败</string>
|
||||||
<string name="scene_step_skipped">已跳过</string>
|
<string name="scene_step_skipped">已跳过</string>
|
||||||
|
|
||||||
|
<!-- 校验与弹窗选项/占位文案 -->
|
||||||
|
<string name="scene_trigger_time_error">时间格式无效(请按 HH:mm 格式输入,如 07:30)</string>
|
||||||
|
<string name="scene_condition_time_error">时间格式无效(请按 HH:mm 格式输入,如 22:00)</string>
|
||||||
|
<string name="scene_chip_time">⏰ 定时</string>
|
||||||
|
<string name="scene_chip_temp">🌡️ 温度</string>
|
||||||
|
<string name="scene_chip_humidity">💧 湿度</string>
|
||||||
|
<string name="scene_chip_socket">🔌 插座</string>
|
||||||
|
<string name="scene_chip_remote">📺 遥控</string>
|
||||||
|
<string name="scene_chip_delay">⏱️ 延时</string>
|
||||||
|
<string name="scene_chip_notify">🔔 提示</string>
|
||||||
|
<string name="scene_chip_above">> 高于</string>
|
||||||
|
<string name="scene_chip_below">< 低于</string>
|
||||||
|
<string name="scene_chip_power_on">开启</string>
|
||||||
|
<string name="scene_chip_power_off">关闭</string>
|
||||||
|
<string name="scene_threshold_temp">设定阈值:%1$d °C</string>
|
||||||
|
<string name="scene_threshold_hum">设定阈值:%1$d %%</string>
|
||||||
|
<string name="scene_target_socket_state">要求插座当前状态为:</string>
|
||||||
|
<string name="scene_no_virtual_remotes">未找到虚拟遥控器,将默认使用红外控制器</string>
|
||||||
|
<string name="scene_remote_button_slot_label">按键名称/槽位</string>
|
||||||
|
<string name="scene_remote_button_name_label">按键名称</string>
|
||||||
|
<string name="scene_empty_triggers_hint">尚未添加触发器,请点击下方按钮添加</string>
|
||||||
|
<string name="scene_empty_conditions_hint">尚未添加限制条件,请点击下方按钮添加</string>
|
||||||
|
<string name="scene_empty_actions_hint">尚未添加执行动作,请点击下方按钮添加</string>
|
||||||
|
<string name="scene_summary_triggers_and_actions">⚡ %1$d 个触发器 • %2$d 个动作</string>
|
||||||
|
<string name="scene_summary_actions_only">%1$d 个执行动作</string>
|
||||||
|
<string name="scene_exec_ready_hint">点击下方按钮立即开始执行</string>
|
||||||
|
<string name="scene_exec_rerun">重新执行</string>
|
||||||
|
<string name="scene_step_format">步骤 %1$d:%2$s</string>
|
||||||
|
<string name="scene_trigger_desc_temp">温度 %1$s %2$s°C</string>
|
||||||
|
<string name="scene_trigger_desc_humidity">湿度 %1$s %2$s%%</string>
|
||||||
|
<string name="scene_trigger_desc_device">%1$s 状态变为 %2$s</string>
|
||||||
|
<string name="scene_condition_desc_temp">温度需 %1$s %2$s°C</string>
|
||||||
|
<string name="scene_condition_desc_humidity">湿度需 %1$s %2$s%%</string>
|
||||||
|
<string name="scene_condition_desc_socket">%1$s 处于 %2$s</string>
|
||||||
|
<string name="scene_condition_desc_time">时间在 %1$s ~ %2$s 之间</string>
|
||||||
|
<string name="scene_default_socket_name">Zigbee插座控制器</string>
|
||||||
|
<string name="scene_default_ir_name">红外控制器</string>
|
||||||
|
<string name="scene_default_button_power">电源</string>
|
||||||
|
<string name="scene_default_notify_title">情景完成</string>
|
||||||
|
<string name="scene_default_notify_msg">自动化任务执行完成</string>
|
||||||
|
|
||||||
<!-- 设备列表 -->
|
<!-- 设备列表 -->
|
||||||
<string name="device_list_title">设备列表</string>
|
<string name="device_list_title">设备列表</string>
|
||||||
<string name="device_list_description">选择需要控制或查看状态的 ABox 终端设备。</string>
|
<string name="device_list_description">选择需要控制或查看状态的 ABox 终端设备。</string>
|
||||||
|
|||||||
@@ -161,6 +161,47 @@
|
|||||||
<string name="scene_step_failed">Failed</string>
|
<string name="scene_step_failed">Failed</string>
|
||||||
<string name="scene_step_skipped">Skipped</string>
|
<string name="scene_step_skipped">Skipped</string>
|
||||||
|
|
||||||
|
<!-- Validation & dialog chips / placeholders -->
|
||||||
|
<string name="scene_trigger_time_error">Invalid time format (use HH:mm, e.g. 07:30)</string>
|
||||||
|
<string name="scene_condition_time_error">Invalid time format (use HH:mm, e.g. 22:00)</string>
|
||||||
|
<string name="scene_chip_time">⏰ Schedule</string>
|
||||||
|
<string name="scene_chip_temp">🌡️ Temperature</string>
|
||||||
|
<string name="scene_chip_humidity">💧 Humidity</string>
|
||||||
|
<string name="scene_chip_socket">🔌 Socket</string>
|
||||||
|
<string name="scene_chip_remote">📺 Remote</string>
|
||||||
|
<string name="scene_chip_delay">⏱️ Delay</string>
|
||||||
|
<string name="scene_chip_notify">🔔 Notify</string>
|
||||||
|
<string name="scene_chip_above">> Above</string>
|
||||||
|
<string name="scene_chip_below">< Below</string>
|
||||||
|
<string name="scene_chip_power_on">Turn ON</string>
|
||||||
|
<string name="scene_chip_power_off">Turn OFF</string>
|
||||||
|
<string name="scene_threshold_temp">Target threshold: %1$d °C</string>
|
||||||
|
<string name="scene_threshold_hum">Target threshold: %1$d %%</string>
|
||||||
|
<string name="scene_target_socket_state">Require socket state to be:</string>
|
||||||
|
<string name="scene_no_virtual_remotes">No virtual remotes found; using infrared controller by default.</string>
|
||||||
|
<string name="scene_remote_button_slot_label">Button Name / Slot</string>
|
||||||
|
<string name="scene_remote_button_name_label">Button Name</string>
|
||||||
|
<string name="scene_empty_triggers_hint">No triggers configured. Tap + below to add.</string>
|
||||||
|
<string name="scene_empty_conditions_hint">No conditions configured. Tap + below to add.</string>
|
||||||
|
<string name="scene_empty_actions_hint">No actions configured. Tap + below to add.</string>
|
||||||
|
<string name="scene_summary_triggers_and_actions">⚡ %1$d triggers • %2$d actions</string>
|
||||||
|
<string name="scene_summary_actions_only">%1$d actions</string>
|
||||||
|
<string name="scene_exec_ready_hint">Tap the button below to run immediately</string>
|
||||||
|
<string name="scene_exec_rerun">Re-run</string>
|
||||||
|
<string name="scene_step_format">Step %1$d: %2$s</string>
|
||||||
|
<string name="scene_trigger_desc_temp">Temp %1$s %2$s°C</string>
|
||||||
|
<string name="scene_trigger_desc_humidity">Humidity %1$s %2$s%%</string>
|
||||||
|
<string name="scene_trigger_desc_device">%1$s state changed to %2$s</string>
|
||||||
|
<string name="scene_condition_desc_temp">Temp must be %1$s %2$s°C</string>
|
||||||
|
<string name="scene_condition_desc_humidity">Humidity must be %1$s %2$s%%</string>
|
||||||
|
<string name="scene_condition_desc_socket">%1$s is %2$s</string>
|
||||||
|
<string name="scene_condition_desc_time">Time is between %1$s ~ %2$s</string>
|
||||||
|
<string name="scene_default_socket_name">Zigbee Smart Socket</string>
|
||||||
|
<string name="scene_default_ir_name">Infrared Controller</string>
|
||||||
|
<string name="scene_default_button_power">Power</string>
|
||||||
|
<string name="scene_default_notify_title">Scene Completed</string>
|
||||||
|
<string name="scene_default_notify_msg">Automation task executed successfully</string>
|
||||||
|
|
||||||
<!-- Device list -->
|
<!-- Device list -->
|
||||||
<string name="device_list_title">Device List</string>
|
<string name="device_list_title">Device List</string>
|
||||||
<string name="device_list_description">Select an ABox terminal device to control or check status.</string>
|
<string name="device_list_description">Select an ABox terminal device to control or check status.</string>
|
||||||
|
|||||||
+55
-18
@@ -25,6 +25,7 @@ import kotlinx.coroutines.CancellationException
|
|||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
import kotlinx.coroutines.awaitAll
|
import kotlinx.coroutines.awaitAll
|
||||||
import kotlinx.coroutines.coroutineScope
|
import kotlinx.coroutines.coroutineScope
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
@@ -201,13 +202,17 @@ class FakeSceneRepository : SceneRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fake [SceneExecutor] that drives the existing [SocketGateway] and [InfraredGateway] fakes.
|
* Fake [SceneExecutor] that drives the existing [SocketGateway] and [InfraredGateway] fakes,
|
||||||
|
* respecting delays and recording virtual remote / notification actions.
|
||||||
*/
|
*/
|
||||||
class FakeSceneExecutor(
|
class FakeSceneExecutor(
|
||||||
private val socketGateway: SocketGateway,
|
private val socketGateway: SocketGateway,
|
||||||
private val infraredGateway: InfraredGateway,
|
private val infraredGateway: InfraredGateway,
|
||||||
|
private val delayDurationProvider: suspend (Long) -> Unit = { delay(it) },
|
||||||
) : SceneExecutor {
|
) : SceneExecutor {
|
||||||
private val running = ConcurrentHashMap<SceneId, Boolean>()
|
private val running = ConcurrentHashMap<SceneId, Boolean>()
|
||||||
|
val executedRemoteButtons = mutableListOf<SceneAction.VirtualRemoteButton>()
|
||||||
|
val sentNotifications = mutableListOf<SceneAction.Notification>()
|
||||||
|
|
||||||
override suspend fun execute(scene: Scene): DomainResult<SceneExecutionResult> {
|
override suspend fun execute(scene: Scene): DomainResult<SceneExecutionResult> {
|
||||||
val alreadyRunning = running.putIfAbsent(scene.id, true) != null
|
val alreadyRunning = running.putIfAbsent(scene.id, true) != null
|
||||||
@@ -277,24 +282,65 @@ class FakeSceneExecutor(
|
|||||||
action: SceneAction,
|
action: SceneAction,
|
||||||
): SceneActionExecutionResult {
|
): SceneActionExecutionResult {
|
||||||
return try {
|
return try {
|
||||||
val result = when (action) {
|
when (action) {
|
||||||
is SceneAction.SocketPower -> socketGateway.setPower(
|
is SceneAction.SocketPower -> {
|
||||||
|
val result = socketGateway.setPower(
|
||||||
profileId = profileId,
|
profileId = profileId,
|
||||||
deviceName = action.deviceName,
|
deviceName = action.deviceName,
|
||||||
powerState = action.powerState,
|
powerState = action.powerState,
|
||||||
)
|
)
|
||||||
|
toExecutionResult(action, result)
|
||||||
|
}
|
||||||
|
|
||||||
is SceneAction.InfraredKey -> infraredGateway.sendKey(
|
is SceneAction.InfraredKey -> {
|
||||||
|
val result = infraredGateway.sendKey(
|
||||||
profileId = profileId,
|
profileId = profileId,
|
||||||
deviceName = action.deviceName,
|
deviceName = action.deviceName,
|
||||||
key = action.key,
|
key = action.key,
|
||||||
)
|
)
|
||||||
|
toExecutionResult(action, result)
|
||||||
is SceneAction.VirtualRemoteButton,
|
|
||||||
is SceneAction.Delay,
|
|
||||||
is SceneAction.Notification -> DomainResult.Success(Unit)
|
|
||||||
}
|
}
|
||||||
when (result) {
|
|
||||||
|
is SceneAction.VirtualRemoteButton -> {
|
||||||
|
executedRemoteButtons.add(action)
|
||||||
|
SceneActionExecutionResult(
|
||||||
|
action = action,
|
||||||
|
outcome = SceneActionOutcome.SUCCESS,
|
||||||
|
error = null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
is SceneAction.Delay -> {
|
||||||
|
delayDurationProvider(action.durationSeconds * 1000L)
|
||||||
|
SceneActionExecutionResult(
|
||||||
|
action = action,
|
||||||
|
outcome = SceneActionOutcome.SUCCESS,
|
||||||
|
error = null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
is SceneAction.Notification -> {
|
||||||
|
sentNotifications.add(action)
|
||||||
|
SceneActionExecutionResult(
|
||||||
|
action = action,
|
||||||
|
outcome = SceneActionOutcome.SUCCESS,
|
||||||
|
error = null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: CancellationException) {
|
||||||
|
throw e
|
||||||
|
} catch (e: Exception) {
|
||||||
|
SceneActionExecutionResult(
|
||||||
|
action = action,
|
||||||
|
outcome = SceneActionOutcome.FAILURE,
|
||||||
|
error = DomainError.OperationFailed(detail = e.message),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun toExecutionResult(action: SceneAction, result: DomainResult<*>): SceneActionExecutionResult {
|
||||||
|
return when (result) {
|
||||||
is DomainResult.Success -> SceneActionExecutionResult(
|
is DomainResult.Success -> SceneActionExecutionResult(
|
||||||
action = action,
|
action = action,
|
||||||
outcome = SceneActionOutcome.SUCCESS,
|
outcome = SceneActionOutcome.SUCCESS,
|
||||||
@@ -307,14 +353,5 @@ class FakeSceneExecutor(
|
|||||||
error = result.error,
|
error = result.error,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} catch (e: CancellationException) {
|
|
||||||
throw e
|
|
||||||
} catch (e: Exception) {
|
|
||||||
SceneActionExecutionResult(
|
|
||||||
action = action,
|
|
||||||
outcome = SceneActionOutcome.FAILURE,
|
|
||||||
error = DomainError.OperationFailed(detail = e.message),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+40
@@ -116,6 +116,46 @@ class FakeSceneExecutorTest {
|
|||||||
assertFalse(executionResult.allSucceeded)
|
assertFalse(executionResult.allSucceeded)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun executesDelayVirtualRemoteAndNotification() = runTest {
|
||||||
|
val delays = mutableListOf<Long>()
|
||||||
|
val executor = FakeSceneExecutor(
|
||||||
|
socketGateway = FakeSocketGateway(),
|
||||||
|
infraredGateway = FakeInfraredGateway(),
|
||||||
|
delayDurationProvider = { delays.add(it) },
|
||||||
|
)
|
||||||
|
|
||||||
|
val remoteAction = SceneAction.VirtualRemoteButton(
|
||||||
|
id = SceneActionId("abox:btn-1"),
|
||||||
|
profileId = profileId,
|
||||||
|
remoteId = "remote-1",
|
||||||
|
remoteDisplayName = "TV Remote",
|
||||||
|
buttonId = "power",
|
||||||
|
buttonDisplayName = "Power",
|
||||||
|
)
|
||||||
|
val delayAction = SceneAction.Delay(
|
||||||
|
id = SceneActionId("abox:delay-1"),
|
||||||
|
profileId = profileId,
|
||||||
|
durationSeconds = 5,
|
||||||
|
)
|
||||||
|
val notificationAction = SceneAction.Notification(
|
||||||
|
id = SceneActionId("abox:notif-1"),
|
||||||
|
profileId = profileId,
|
||||||
|
title = "Test Done",
|
||||||
|
message = "Success",
|
||||||
|
)
|
||||||
|
|
||||||
|
val scene = scene(listOf(remoteAction, delayAction, notificationAction))
|
||||||
|
val result = executor.execute(scene)
|
||||||
|
|
||||||
|
assertTrue(result is DomainResult.Success)
|
||||||
|
val execResult = (result as DomainResult.Success).value
|
||||||
|
assertTrue(execResult.allSucceeded)
|
||||||
|
assertEquals(listOf(5000L), delays)
|
||||||
|
assertEquals(listOf(remoteAction), executor.executedRemoteButtons)
|
||||||
|
assertEquals(listOf(notificationAction), executor.sentNotifications)
|
||||||
|
}
|
||||||
|
|
||||||
private fun scene(actions: List<SceneAction>): Scene = Scene(
|
private fun scene(actions: List<SceneAction>): Scene = Scene(
|
||||||
id = SceneId("${profileId.value}:scene-test"),
|
id = SceneId("${profileId.value}:scene-test"),
|
||||||
profileId = profileId,
|
profileId = profileId,
|
||||||
|
|||||||
Reference in New Issue
Block a user