feat(app): wire socket and temperature control into device list and app shell

- connect DeviceListScreen callbacks to smart socket and temperature/humidity detail routes
- set default smart socket device name to 'Zigbee插座控制器'
- add in-screen dialog to dynamically edit and switch gateway device names
- encode and decode route parameters for non-ASCII device names
- add unit test coverage for dynamic device name switching
This commit is contained in:
2026-08-25 15:35:47 +08:00
parent d3eb4860ec
commit dc45e6affc
13 changed files with 225 additions and 6 deletions
@@ -19,6 +19,7 @@ import androidx.navigation.navArgument
import com.flagship.abox.manager.designsystem.ABoxLoadingIndicator import com.flagship.abox.manager.designsystem.ABoxLoadingIndicator
import com.flagship.abox.manager.designsystem.ABoxSurface import com.flagship.abox.manager.designsystem.ABoxSurface
import com.flagship.abox.manager.designsystem.ABoxTheme import com.flagship.abox.manager.designsystem.ABoxTheme
import com.flagship.abox.manager.domain.DeviceName
import com.flagship.abox.manager.domain.ProviderProfile import com.flagship.abox.manager.domain.ProviderProfile
import com.flagship.abox.manager.domain.Username import com.flagship.abox.manager.domain.Username
import com.flagship.abox.manager.infrared.hub.InfraredHubRoute import com.flagship.abox.manager.infrared.hub.InfraredHubRoute
@@ -27,6 +28,9 @@ import com.flagship.abox.manager.infrared.ui.InfraredControlRoute
import com.flagship.abox.manager.ui.devicecontrollist.DeviceListScreen import com.flagship.abox.manager.ui.devicecontrollist.DeviceListScreen
import com.flagship.abox.manager.ui.login.LoginRoute import com.flagship.abox.manager.ui.login.LoginRoute
import com.flagship.abox.manager.ui.login.LoginViewModel import com.flagship.abox.manager.ui.login.LoginViewModel
import com.flagship.abox.manager.ui.navigation.deviceDetailGraph
import com.flagship.abox.manager.ui.navigation.navigateToSocket
import com.flagship.abox.manager.ui.navigation.navigateToTemperatureHumidity
import com.flagship.abox.manager.ui.provider.ProviderSelectionRoute import com.flagship.abox.manager.ui.provider.ProviderSelectionRoute
import com.flagship.abox.manager.ui.session.AppSessionUiState import com.flagship.abox.manager.ui.session.AppSessionUiState
import com.flagship.abox.manager.ui.session.AppSessionViewModel import com.flagship.abox.manager.ui.session.AppSessionViewModel
@@ -103,6 +107,18 @@ private fun AuthenticatedNavHost(
devicesSlot = { devicesSlot = {
val encodedProfileId = android.net.Uri.encode(profile.id.value) val encodedProfileId = android.net.Uri.encode(profile.id.value)
DeviceListScreen( DeviceListScreen(
onSocketClick = {
navController.navigateToSocket(
profileId = profile.id,
deviceName = DeviceName("Zigbee插座控制器"),
)
},
onTemperatureHumidityClick = {
navController.navigateToTemperatureHumidity(
profileId = profile.id,
deviceName = DeviceName("温湿度传感器"),
)
},
onInfraredClick = { onInfraredClick = {
navController.navigate("infrared_hub/$encodedProfileId") navController.navigate("infrared_hub/$encodedProfileId")
}, },
@@ -110,6 +126,9 @@ private fun AuthenticatedNavHost(
}, },
) )
} }
deviceDetailGraph(
onBackClick = navController::popBackStack,
)
composable( composable(
route = "infrared_hub/{profileId}", route = "infrared_hub/{profileId}",
arguments = listOf( arguments = listOf(
@@ -9,16 +9,21 @@ import com.flagship.abox.manager.domain.DeviceName
import com.flagship.abox.manager.domain.ProviderProfileId import com.flagship.abox.manager.domain.ProviderProfileId
import com.flagship.abox.manager.ui.socket.SocketRoute import com.flagship.abox.manager.ui.socket.SocketRoute
import com.flagship.abox.manager.ui.temperaturehumidity.TemperatureHumidityRoute import com.flagship.abox.manager.ui.temperaturehumidity.TemperatureHumidityRoute
import java.net.URLEncoder
import java.nio.charset.StandardCharsets
object DeviceDetailRoutes { object DeviceDetailRoutes {
const val SOCKET_PATTERN = "device/socket/{profileId}/{deviceName}" const val SOCKET_PATTERN = "device/socket/{profileId}/{deviceName}"
const val TH_PATTERN = "device/th/{profileId}/{deviceName}" const val TH_PATTERN = "device/th/{profileId}/{deviceName}"
fun socketRoute(profileId: ProviderProfileId, deviceName: DeviceName): String = fun socketRoute(profileId: ProviderProfileId, deviceName: DeviceName): String =
"device/socket/${profileId.value}/${deviceName.value}" "device/socket/${encode(profileId.value)}/${encode(deviceName.value)}"
fun temperatureHumidityRoute(profileId: ProviderProfileId, deviceName: DeviceName): String = fun temperatureHumidityRoute(profileId: ProviderProfileId, deviceName: DeviceName): String =
"device/th/${profileId.value}/${deviceName.value}" "device/th/${encode(profileId.value)}/${encode(deviceName.value)}"
private fun encode(value: String): String =
URLEncoder.encode(value, StandardCharsets.UTF_8.name())
} }
fun NavController.navigateToSocket(profileId: ProviderProfileId, deviceName: DeviceName) { fun NavController.navigateToSocket(profileId: ProviderProfileId, deviceName: DeviceName) {
@@ -16,6 +16,7 @@ import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Card import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults import androidx.compose.material3.CardDefaults
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
@@ -23,12 +24,16 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
@@ -43,6 +48,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.flagship.abox.manager.R import com.flagship.abox.manager.R
import com.flagship.abox.manager.designsystem.ABoxButton import com.flagship.abox.manager.designsystem.ABoxButton
import com.flagship.abox.manager.designsystem.ABoxLoadingIndicator import com.flagship.abox.manager.designsystem.ABoxLoadingIndicator
import com.flagship.abox.manager.domain.DeviceName
import com.flagship.abox.manager.domain.SocketPowerState import com.flagship.abox.manager.domain.SocketPowerState
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Date import java.util.Date
@@ -73,6 +79,7 @@ fun SocketScreen(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val scrollState = rememberScrollState() val scrollState = rememberScrollState()
var showEditNameDialog by remember { mutableStateOf(false) }
Scaffold( Scaffold(
modifier = modifier.fillMaxSize(), modifier = modifier.fillMaxSize(),
@@ -90,6 +97,12 @@ fun SocketScreen(
} }
}, },
actions = { actions = {
IconButton(
onClick = { showEditNameDialog = true },
enabled = !state.isLoading && !state.isOperating,
) {
Text("✎", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold)
}
IconButton( IconButton(
onClick = { onEvent(SocketUiEvent.Refresh) }, onClick = { onEvent(SocketUiEvent.Refresh) },
enabled = !state.isLoading && !state.isOperating, enabled = !state.isLoading && !state.isOperating,
@@ -234,6 +247,42 @@ fun SocketScreen(
} }
} }
} }
if (showEditNameDialog) {
var inputName by remember(state.deviceName) { mutableStateOf(state.deviceName?.value.orEmpty()) }
AlertDialog(
onDismissRequest = { showEditNameDialog = false },
title = { Text(stringResource(R.string.dialog_edit_device_name_title)) },
text = {
OutlinedTextField(
value = inputName,
onValueChange = { inputName = it },
label = { Text(stringResource(R.string.dialog_edit_device_name_hint)) },
singleLine = true,
modifier = Modifier.fillMaxWidth(),
)
},
confirmButton = {
TextButton(
onClick = {
val trimmed = inputName.trim()
if (trimmed.isNotBlank()) {
onEvent(SocketUiEvent.ChangeDeviceName(DeviceName(trimmed)))
}
showEditNameDialog = false
},
enabled = inputName.isNotBlank(),
) {
Text(stringResource(R.string.action_confirm))
}
},
dismissButton = {
TextButton(onClick = { showEditNameDialog = false }) {
Text(stringResource(R.string.action_cancel))
}
},
)
}
} }
@Composable @Composable
@@ -24,6 +24,7 @@ data class SocketUiState(
sealed interface SocketUiEvent { sealed interface SocketUiEvent {
data class Initialize(val profileId: ProviderProfileId, val deviceName: DeviceName) : SocketUiEvent data class Initialize(val profileId: ProviderProfileId, val deviceName: DeviceName) : SocketUiEvent
data class ChangeDeviceName(val deviceName: DeviceName) : SocketUiEvent
data object Refresh : SocketUiEvent data object Refresh : SocketUiEvent
data object TogglePower : SocketUiEvent data object TogglePower : SocketUiEvent
data class SetPower(val targetState: SocketPowerState) : SocketUiEvent data class SetPower(val targetState: SocketPowerState) : SocketUiEvent
@@ -16,6 +16,8 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.net.URLDecoder
import java.nio.charset.StandardCharsets
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
@@ -28,8 +30,8 @@ class SocketViewModel @Inject constructor(
val state: StateFlow<SocketUiState> = _state.asStateFlow() val state: StateFlow<SocketUiState> = _state.asStateFlow()
init { init {
val profileIdString: String? = savedStateHandle["profileId"] val profileIdString: String? = savedStateHandle.get<String>("profileId")?.let { decode(it) }
val deviceNameString: String? = savedStateHandle["deviceName"] val deviceNameString: String? = savedStateHandle.get<String>("deviceName")?.let { decode(it) }
if (!profileIdString.isNullOrBlank() && !deviceNameString.isNullOrBlank()) { if (!profileIdString.isNullOrBlank() && !deviceNameString.isNullOrBlank()) {
onEvent( onEvent(
@@ -41,6 +43,12 @@ class SocketViewModel @Inject constructor(
} }
} }
private fun decode(value: String): String = try {
URLDecoder.decode(value, StandardCharsets.UTF_8.name())
} catch (_: Exception) {
value
}
fun onEvent(event: SocketUiEvent) { fun onEvent(event: SocketUiEvent) {
when (event) { when (event) {
is SocketUiEvent.Initialize -> { is SocketUiEvent.Initialize -> {
@@ -53,6 +61,21 @@ class SocketViewModel @Inject constructor(
loadStatus() loadStatus()
} }
is SocketUiEvent.ChangeDeviceName -> {
val trimmed = event.deviceName.value.trim()
if (trimmed.isNotBlank() && trimmed != _state.value.deviceName?.value) {
_state.update {
it.copy(
deviceName = DeviceName(trimmed),
powerState = null,
errorMessage = null,
lastConfirmedTimeMillis = null,
)
}
loadStatus()
}
}
SocketUiEvent.Refresh -> { SocketUiEvent.Refresh -> {
loadStatus() loadStatus()
} }
@@ -13,17 +13,22 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Card import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults import androidx.compose.material3.CardDefaults
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
@@ -35,6 +40,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.flagship.abox.manager.R import com.flagship.abox.manager.R
import com.flagship.abox.manager.designsystem.ABoxButton import com.flagship.abox.manager.designsystem.ABoxButton
import com.flagship.abox.manager.designsystem.ABoxLoadingIndicator import com.flagship.abox.manager.designsystem.ABoxLoadingIndicator
import com.flagship.abox.manager.domain.DeviceName
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Date import java.util.Date
import java.util.Locale import java.util.Locale
@@ -64,6 +70,7 @@ fun TemperatureHumidityScreen(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val scrollState = rememberScrollState() val scrollState = rememberScrollState()
var showEditNameDialog by remember { mutableStateOf(false) }
Scaffold( Scaffold(
modifier = modifier.fillMaxSize(), modifier = modifier.fillMaxSize(),
@@ -81,6 +88,12 @@ fun TemperatureHumidityScreen(
} }
}, },
actions = { actions = {
IconButton(
onClick = { showEditNameDialog = true },
enabled = !state.isLoading && !state.isRefreshing,
) {
Text("✎", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold)
}
IconButton( IconButton(
onClick = { onEvent(TemperatureHumidityUiEvent.Refresh) }, onClick = { onEvent(TemperatureHumidityUiEvent.Refresh) },
enabled = !state.isLoading && !state.isRefreshing, enabled = !state.isLoading && !state.isRefreshing,
@@ -225,6 +238,42 @@ fun TemperatureHumidityScreen(
} }
} }
} }
if (showEditNameDialog) {
var inputName by remember(state.deviceName) { mutableStateOf(state.deviceName?.value.orEmpty()) }
AlertDialog(
onDismissRequest = { showEditNameDialog = false },
title = { Text(stringResource(R.string.dialog_edit_device_name_title)) },
text = {
OutlinedTextField(
value = inputName,
onValueChange = { inputName = it },
label = { Text(stringResource(R.string.dialog_edit_device_name_hint)) },
singleLine = true,
modifier = Modifier.fillMaxWidth(),
)
},
confirmButton = {
TextButton(
onClick = {
val trimmed = inputName.trim()
if (trimmed.isNotBlank()) {
onEvent(TemperatureHumidityUiEvent.ChangeDeviceName(DeviceName(trimmed)))
}
showEditNameDialog = false
},
enabled = inputName.isNotBlank(),
) {
Text(stringResource(R.string.action_confirm))
}
},
dismissButton = {
TextButton(onClick = { showEditNameDialog = false }) {
Text(stringResource(R.string.action_cancel))
}
},
)
}
} }
@Composable @Composable
@@ -28,6 +28,7 @@ data class TemperatureHumidityUiState(
sealed interface TemperatureHumidityUiEvent { sealed interface TemperatureHumidityUiEvent {
data class Initialize(val profileId: ProviderProfileId, val deviceName: DeviceName) : TemperatureHumidityUiEvent data class Initialize(val profileId: ProviderProfileId, val deviceName: DeviceName) : TemperatureHumidityUiEvent
data class ChangeDeviceName(val deviceName: DeviceName) : TemperatureHumidityUiEvent
data object Refresh : TemperatureHumidityUiEvent data object Refresh : TemperatureHumidityUiEvent
data object ClearError : TemperatureHumidityUiEvent data object ClearError : TemperatureHumidityUiEvent
} }
@@ -15,6 +15,8 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.net.URLDecoder
import java.nio.charset.StandardCharsets
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
@@ -27,8 +29,8 @@ class TemperatureHumidityViewModel @Inject constructor(
val state: StateFlow<TemperatureHumidityUiState> = _state.asStateFlow() val state: StateFlow<TemperatureHumidityUiState> = _state.asStateFlow()
init { init {
val profileIdString: String? = savedStateHandle["profileId"] val profileIdString: String? = savedStateHandle.get<String>("profileId")?.let { decode(it) }
val deviceNameString: String? = savedStateHandle["deviceName"] val deviceNameString: String? = savedStateHandle.get<String>("deviceName")?.let { decode(it) }
if (!profileIdString.isNullOrBlank() && !deviceNameString.isNullOrBlank()) { if (!profileIdString.isNullOrBlank() && !deviceNameString.isNullOrBlank()) {
onEvent( onEvent(
@@ -40,6 +42,12 @@ class TemperatureHumidityViewModel @Inject constructor(
} }
} }
private fun decode(value: String): String = try {
URLDecoder.decode(value, StandardCharsets.UTF_8.name())
} catch (_: Exception) {
value
}
fun onEvent(event: TemperatureHumidityUiEvent) { fun onEvent(event: TemperatureHumidityUiEvent) {
when (event) { when (event) {
is TemperatureHumidityUiEvent.Initialize -> { is TemperatureHumidityUiEvent.Initialize -> {
@@ -52,6 +60,23 @@ class TemperatureHumidityViewModel @Inject constructor(
loadReading(isRefresh = false) loadReading(isRefresh = false)
} }
is TemperatureHumidityUiEvent.ChangeDeviceName -> {
val trimmed = event.deviceName.value.trim()
if (trimmed.isNotBlank() && trimmed != _state.value.deviceName?.value) {
_state.update {
it.copy(
deviceName = DeviceName(trimmed),
temperature = null,
humidity = null,
hasNoData = false,
errorMessage = null,
lastUpdatedTimeMillis = null,
)
}
loadReading(isRefresh = false)
}
}
TemperatureHumidityUiEvent.Refresh -> { TemperatureHumidityUiEvent.Refresh -> {
loadReading(isRefresh = true) loadReading(isRefresh = true)
} }
@@ -82,6 +82,11 @@
<string name="action_refresh">刷新</string> <string name="action_refresh">刷新</string>
<string name="action_retry">重试</string> <string name="action_retry">重试</string>
<string name="action_back">返回</string> <string name="action_back">返回</string>
<string name="action_edit_device_name">修改设备名称</string>
<string name="dialog_edit_device_name_title">修改设备名称</string>
<string name="dialog_edit_device_name_hint">请输入网关中的设备名称</string>
<string name="action_confirm">确定</string>
<string name="action_cancel">取消</string>
<string name="status_loading">加载中…</string> <string name="status_loading">加载中…</string>
<string name="status_device_offline_banner">设备已离线,无法进行通信。</string> <string name="status_device_offline_banner">设备已离线,无法进行通信。</string>
<string name="error_authentication">认证失败,请重新登录。</string> <string name="error_authentication">认证失败,请重新登录。</string>
@@ -82,6 +82,11 @@
<string name="action_refresh">Refresh</string> <string name="action_refresh">Refresh</string>
<string name="action_retry">Retry</string> <string name="action_retry">Retry</string>
<string name="action_back">Back</string> <string name="action_back">Back</string>
<string name="action_edit_device_name">Edit Device Name</string>
<string name="dialog_edit_device_name_title">Edit Device Name</string>
<string name="dialog_edit_device_name_hint">Enter gateway device name</string>
<string name="action_confirm">Confirm</string>
<string name="action_cancel">Cancel</string>
<string name="status_loading">Loading…</string> <string name="status_loading">Loading…</string>
<string name="status_device_offline_banner">Device is offline. Commands cannot be sent.</string> <string name="status_device_offline_banner">Device is offline. Commands cannot be sent.</string>
<string name="error_authentication">Authentication failed. Please log in again.</string> <string name="error_authentication">Authentication failed. Please log in again.</string>
@@ -170,6 +170,20 @@ class SocketViewModelTest {
assertEquals(SocketPowerState.ON, viewModel.state.value.powerState) assertEquals(SocketPowerState.ON, viewModel.state.value.powerState)
} }
@Test
fun changeDeviceName_updatesNameAndReloads() = runTest {
val newDevice = DeviceName("socket-2")
fakeSocketGateway.setPower(profileId, newDevice, SocketPowerState.ON)
val viewModel = SocketViewModel(fakeSocketGateway, SavedStateHandle())
viewModel.onEvent(SocketUiEvent.Initialize(profileId, deviceName))
assertEquals(SocketPowerState.OFF, viewModel.state.value.powerState)
viewModel.onEvent(SocketUiEvent.ChangeDeviceName(newDevice))
assertEquals(newDevice, viewModel.state.value.deviceName)
assertEquals(SocketPowerState.ON, viewModel.state.value.powerState)
}
@Test @Test
fun clearError_resetsErrorMessage() = runTest { fun clearError_resetsErrorMessage() = runTest {
fakeSocketGateway.setError(profileId, deviceName, DomainError.InvalidRequest()) fakeSocketGateway.setError(profileId, deviceName, DomainError.InvalidRequest())
@@ -158,6 +158,28 @@ class TemperatureHumidityViewModelTest {
assertFalse(state.isRefreshing) assertFalse(state.isRefreshing)
} }
@Test
fun changeDeviceName_updatesNameAndReloads() = runTest {
val newDevice = DeviceName("th-2")
fakeGateway.setReading(
TemperatureHumidityReading(
profileId = profileId,
deviceName = newDevice,
temperature = "26.8",
humidity = "55",
),
)
val viewModel = TemperatureHumidityViewModel(fakeGateway, SavedStateHandle())
viewModel.onEvent(TemperatureHumidityUiEvent.Initialize(profileId, deviceName))
assertEquals("23.5", viewModel.state.value.temperature)
viewModel.onEvent(TemperatureHumidityUiEvent.ChangeDeviceName(newDevice))
assertEquals(newDevice, viewModel.state.value.deviceName)
assertEquals("26.8", viewModel.state.value.temperature)
assertEquals("55", viewModel.state.value.humidity)
}
@Test @Test
fun clearError_resetsErrorMessage() = runTest { fun clearError_resetsErrorMessage() = runTest {
fakeGateway.setError(profileId, deviceName, DomainError.Network()) fakeGateway.setError(profileId, deviceName, DomainError.Network())
@@ -75,6 +75,7 @@
<string name="infrared_key_tv_ch_down">电视-频道-</string> <string name="infrared_key_tv_ch_down">电视-频道-</string>
<string name="infrared_key_tv_home">电视-主页</string> <string name="infrared_key_tv_home">电视-主页</string>
<string name="infrared_key_tv_back">电视-返回</string> <string name="infrared_key_tv_back">电视-返回</string>
<string name="infrared_key_tv_menu">电视-菜单</string>
<string name="infrared_control_title">红外设备与环境配置</string> <string name="infrared_control_title">红外设备与环境配置</string>
<string name="infrared_device_config_section">ABSDK 设备名称配置</string> <string name="infrared_device_config_section">ABSDK 设备名称配置</string>
<string name="infrared_device_config_desc">ABSDK 依靠此设备名称(默认为 红外控制器)寻址具体的硬件转发器,请确保名称与主机端保持一致。</string> <string name="infrared_device_config_desc">ABSDK 依靠此设备名称(默认为 红外控制器)寻址具体的硬件转发器,请确保名称与主机端保持一致。</string>