From 9a264ba013c7c9dcc1cb988595643b49b1dceece Mon Sep 17 00:00:00 2001 From: EJClaw Owner Date: Thu, 6 Aug 2026 15:45:08 +0900 Subject: [PATCH] sync source to v0.3.7 (matches released EasyAppliance-v0.3.7.apk, verCode 10) Brings the Gitea source up to date with the released v0.3.5-0.3.7 APKs: real-device washer/aircon presets, current vs pre-start settings, string/number command arg fix, all-bold typography. --- README.md | 51 ------- app/build.gradle.kts | 4 +- .../easyappliance/data/DeviceRepository.kt | 4 + .../kr/tkrmagid/easyappliance/data/Macro.kt | 9 +- .../kr/tkrmagid/easyappliance/data/Models.kt | 7 + .../SmartThingsDeviceRepository.kt | 26 +++- .../smartthings/SmartThingsStatusMapper.kt | 60 +++++++- .../tkrmagid/easyappliance/ui/HomeScreen.kt | 39 +++++ .../tkrmagid/easyappliance/ui/MacroEditor.kt | 137 ++++++++++++------ .../tkrmagid/easyappliance/ui/theme/Type.kt | 6 +- .../kr/tkrmagid/easyappliance/MacroTest.kt | 37 +++++ .../easyappliance/MockDeviceRepositoryTest.kt | 7 +- .../SmartThingsStatusMapperTest.kt | 28 +++- 13 files changed, 308 insertions(+), 107 deletions(-) delete mode 100644 README.md create mode 100644 app/src/test/java/kr/tkrmagid/easyappliance/MacroTest.kt diff --git a/README.md b/README.md deleted file mode 100644 index 9d58ce3..0000000 --- a/README.md +++ /dev/null @@ -1,51 +0,0 @@ -# 쉬운 가전 리모컨 (EasyAppliance) - -SmartThings 가전(세탁기·에어컨 등)을 어르신도 쉽게 쓸 수 있도록 만든 안드로이드 앱. -큰 글씨·고대비·큰 버튼의 직관적 UX로, 등록된 기기 중 한 대를 골라 상태 확인과 -켜기/끄기, 시작 매크로를 제공합니다. - -## 주요 기능 - -- 한 대 선택 조작: 관리자 화면에서 조작할 기기 1대를 선택 -- 숨김 관리자 진입: 화면 위쪽 양쪽 모서리를 동시에 약 1.5초 길게 누르면 진입 - (어르신이 실수로 들어가지 않도록) -- 큰 상태 표시: 운전 중/꺼짐, 남은 시간, 에어컨 온도 등 -- 관리자에서 표시 정보·기능 on/off, 기기 이름·사진 변경(기본 이미지 + 직접 추가) -- 시작 매크로: "시작"을 누르면 물 온도·수위 등 설정을 적용한 뒤 자동 시작 -- 연결 끊김 시 화면 중앙 큰 경고창 + 다시 연결하기(백그라운드 재시도, 복구 시 자동 해제) -- 설정은 저장되어 앱을 껐다 켜도 유지, 앱 복귀/주기적 폴링으로 최신화 -- 폴드/플립 대응 반응형 레이아웃, 삼성 뒤로가기 지원 - -## SmartThings 연결 - -관리자 화면에서 SmartThings Personal Access Token을 입력하면 실제 기기 목록/상태/제어에 -연결됩니다. 토큰이 없으면 샘플(목업) 데이터로 동작합니다. 토큰은 -https://account.smartthings.com/tokens 에서 발급합니다. - -## 빌드 - -Android SDK와 JDK 17+ 필요. - -```bash -./gradlew :app:assembleDebug # 디버그 APK -./gradlew :app:testDebugUnitTest # 단위 테스트 -``` - -릴리즈 서명 빌드는 프로젝트 루트에 `keystore.properties`(gitignore됨)가 있을 때만 -서명됩니다: - -```properties -storeFile=/절대/경로/키스토어.keystore -storePassword=... -keyAlias=... -keyPassword=... -``` - -```bash -./gradlew :app:assembleRelease -``` - -## 기술 스택 - -Kotlin, Jetpack Compose(Material3), DataStore, Retrofit + kotlinx.serialization, Coil. -minSdk 24 / targetSdk 35. diff --git a/app/build.gradle.kts b/app/build.gradle.kts index df18b7a..a9736ad 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -24,8 +24,8 @@ android { applicationId = "kr.tkrmagid.easyappliance" minSdk = 24 targetSdk = 35 - versionCode = (project.findProperty("verCode") as String?)?.toInt() ?: 7 - versionName = (project.findProperty("verName") as String?) ?: "0.3.4" + versionCode = (project.findProperty("verCode") as String?)?.toInt() ?: 10 + versionName = (project.findProperty("verName") as String?) ?: "0.3.7" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } diff --git a/app/src/main/java/kr/tkrmagid/easyappliance/data/DeviceRepository.kt b/app/src/main/java/kr/tkrmagid/easyappliance/data/DeviceRepository.kt index 8515666..1e31c6e 100644 --- a/app/src/main/java/kr/tkrmagid/easyappliance/data/DeviceRepository.kt +++ b/app/src/main/java/kr/tkrmagid/easyappliance/data/DeviceRepository.kt @@ -36,12 +36,16 @@ class MockDeviceRepository : DeviceRepository { "washer-1" -> DeviceStatus( powerOn = on, operatingState = if (on) "운전 중" else "꺼짐", + running = on, + currentSettings = if (on) "물 온도 40도 · 탈수 강 · 헹굼 3회" else null, remainingMinutes = if (on) 32 else null, reservation = null, ) "aircon-1" -> DeviceStatus( powerOn = on, operatingState = if (on) "냉방 중" else "꺼짐", + running = on, + currentSettings = if (on) "냉방 · 바람 자동" else null, currentTemperature = 27, targetTemperature = if (on) 24 else null, ) diff --git a/app/src/main/java/kr/tkrmagid/easyappliance/data/Macro.kt b/app/src/main/java/kr/tkrmagid/easyappliance/data/Macro.kt index 11c91af..950efb2 100644 --- a/app/src/main/java/kr/tkrmagid/easyappliance/data/Macro.kt +++ b/app/src/main/java/kr/tkrmagid/easyappliance/data/Macro.kt @@ -24,4 +24,11 @@ data class MacroStep( data class DeviceMacro( val enabled: Boolean = false, val steps: List = emptyList(), -) +) { + /** + * One-line human summary of the configured start options, e.g. + * "물 온도 40도 · 수위 높음". Empty when nothing is configured. + */ + fun summary(): String = + steps.mapNotNull { it.label.takeIf { l -> l.isNotBlank() } }.joinToString(" · ") +} diff --git a/app/src/main/java/kr/tkrmagid/easyappliance/data/Models.kt b/app/src/main/java/kr/tkrmagid/easyappliance/data/Models.kt index 88cf8c0..18b087d 100644 --- a/app/src/main/java/kr/tkrmagid/easyappliance/data/Models.kt +++ b/app/src/main/java/kr/tkrmagid/easyappliance/data/Models.kt @@ -28,6 +28,13 @@ data class DeviceStatus( val powerOn: Boolean = false, /** Human-readable run state, e.g. "운전 중", "정지", "일시정지". */ val operatingState: String? = null, + /** True when a cycle is actually in progress (run/pause), not just powered. */ + val running: Boolean = false, + /** + * Essential current settings while running, e.g. "물 온도 40도 · 수위 높음". + * Only the few important fields; null when not running / not reported. + */ + val currentSettings: String? = null, /** Remaining minutes for a running cycle, if reported. */ val remainingMinutes: Int? = null, /** Reservation / scheduled-start description, if set. */ diff --git a/app/src/main/java/kr/tkrmagid/easyappliance/data/smartthings/SmartThingsDeviceRepository.kt b/app/src/main/java/kr/tkrmagid/easyappliance/data/smartthings/SmartThingsDeviceRepository.kt index 95282f7..9e6270f 100644 --- a/app/src/main/java/kr/tkrmagid/easyappliance/data/smartthings/SmartThingsDeviceRepository.kt +++ b/app/src/main/java/kr/tkrmagid/easyappliance/data/smartthings/SmartThingsDeviceRepository.kt @@ -49,16 +49,32 @@ class SmartThingsDeviceRepository( StCommand( capability = step.capability, command = step.command, - arguments = step.args.map { it.toJsonElement() }, + arguments = step.args.map { encodeArg(step.command, it) }, ) } + StCommand(capability = "switch", command = "on") api.sendCommands(deviceId, StCommandsRequest(commands)) } - private fun String.toJsonElement(): JsonElement = - toIntOrNull()?.let { JsonPrimitive(it) } - ?: toDoubleOrNull()?.let { JsonPrimitive(it) } - ?: JsonPrimitive(this) + /** + * SmartThings commands are type-strict. Most appliance commands take string + * enums (e.g. washer temperature "40", fan "1") and MUST be sent as strings — + * only setpoint-style commands take a number. So default to string and send a + * number only for the known numeric commands. + */ + private fun encodeArg(command: String, raw: String): JsonElement = + if (command in NUMERIC_COMMANDS) { + raw.toDoubleOrNull()?.let { d -> + if (d % 1.0 == 0.0) JsonPrimitive(d.toInt()) else JsonPrimitive(d) + } ?: JsonPrimitive(raw) + } else { + JsonPrimitive(raw) + } + + private companion object { + val NUMERIC_COMMANDS = setOf( + "setCoolingSetpoint", "setHeatingSetpoint", "setThermostatSetpoint", + ) + } private fun inferType(caps: Set, label: String?, name: String?): DeviceType { val text = "${label.orEmpty()} ${name.orEmpty()}" diff --git a/app/src/main/java/kr/tkrmagid/easyappliance/data/smartthings/SmartThingsStatusMapper.kt b/app/src/main/java/kr/tkrmagid/easyappliance/data/smartthings/SmartThingsStatusMapper.kt index fb29191..c3dc450 100644 --- a/app/src/main/java/kr/tkrmagid/easyappliance/data/smartthings/SmartThingsStatusMapper.kt +++ b/app/src/main/java/kr/tkrmagid/easyappliance/data/smartthings/SmartThingsStatusMapper.kt @@ -37,11 +37,25 @@ object SmartThingsStatusMapper { "stop" -> "정지" else -> if (powerOn) "켜짐" else "꺼짐" } + val running = machine == "run" || machine == "pause" val remaining = numberAttr(main, "samsungce.washerOperatingState", "remainingTime") ?.let { secondsToMinutes(it) } + val temp = attr(main, "custom.washerWaterTemperature", "washerWaterTemperature")?.contentOrNull() + ?.let { washerTempLabel(it) } + val spin = attr(main, "custom.washerSpinLevel", "washerSpinLevel")?.contentOrNull() + ?.let { washerSpinLabel(it) } + val rinse = attr(main, "custom.washerRinseCycles", "washerRinseCycles")?.contentOrNull() + ?.toIntOrNull() + val settings = listOfNotNull( + temp?.let { "물 온도 $it" }, + spin?.let { "탈수 $it" }, + rinse?.let { "헹굼 ${it}회" }, + ).joinToString(" · ").ifBlank { null } return DeviceStatus( powerOn = powerOn, operatingState = state, + running = running, + currentSettings = if (running) settings else null, remainingMinutes = remaining, ) } @@ -50,14 +64,58 @@ object SmartThingsStatusMapper { val current = numberAttr(main, "temperatureMeasurement", "temperature") val target = numberAttr(main, "thermostatCoolingSetpoint", "coolingSetpoint") ?: numberAttr(main, "custom.thermostatSetpointControl", "coolingSetpoint") + val mode = attr(main, "airConditionerMode", "airConditionerMode")?.contentOrNull() + ?.let { acModeLabel(it) } + val fan = attr(main, "airConditionerFanMode", "fanMode")?.contentOrNull() + ?.let { acFanLabel(it) } + val settings = listOfNotNull( + mode, + fan?.let { "바람 $it" }, + ).joinToString(" · ").ifBlank { null } return DeviceStatus( powerOn = powerOn, - operatingState = if (powerOn) "냉방 중" else "꺼짐", + operatingState = if (powerOn) (mode?.let { "$it 중" } ?: "켜짐") else "꺼짐", + running = powerOn, + currentSettings = if (powerOn) settings else null, currentTemperature = current, targetTemperature = if (powerOn) target else null, ) } + private fun washerTempLabel(raw: String): String = when (raw.lowercase()) { + "cold", "tapcold" -> "냉수" + "hot" -> "온수" + "none" -> "없음" + else -> raw.toIntOrNull()?.let { "${it}도" } ?: raw + } + + private fun washerSpinLabel(raw: String): String = when (raw.lowercase()) { + "nospin" -> "탈수없음" + "rinsehold" -> "헹굼정지" + "extralow" -> "매우약" + "low" -> "약" + "medium" -> "중" + "high" -> "강" + "extrahigh" -> "최강" + else -> raw + } + + private fun acModeLabel(raw: String): String = when (raw.lowercase()) { + "cool" -> "냉방" + "dry" -> "제습" + "wind" -> "송풍" + "heat" -> "난방" + "aicomfort" -> "AI쾌적" + "auto" -> "자동" + else -> raw + } + + private fun acFanLabel(raw: String): String = when (raw.lowercase()) { + "auto" -> "자동" + "max" -> "최대" + else -> raw.toIntOrNull()?.let { "${it}단" } ?: raw + } + private fun attr(main: JsonObject, capability: String, attribute: String): JsonPrimitive? = runCatching { main[capability]?.jsonObject?.get(attribute)?.jsonObject?.get("value") as? JsonPrimitive diff --git a/app/src/main/java/kr/tkrmagid/easyappliance/ui/HomeScreen.kt b/app/src/main/java/kr/tkrmagid/easyappliance/ui/HomeScreen.kt index 5615816..8869531 100644 --- a/app/src/main/java/kr/tkrmagid/easyappliance/ui/HomeScreen.kt +++ b/app/src/main/java/kr/tkrmagid/easyappliance/ui/HomeScreen.kt @@ -90,6 +90,18 @@ fun HomeScreen( StatusCard(state) + val status = state.status + val preStart = state.macroFor(device) + ?.takeIf { it.enabled && it.steps.isNotEmpty() } + ?.summary() + if (status?.running == true && !status.currentSettings.isNullOrBlank()) { + Spacer(Modifier.height(16.dp)) + SelectedOptionsCard(title = "현재 작동 설정", summary = status.currentSettings) + } else if (status?.running != true && preStart != null) { + Spacer(Modifier.height(16.dp)) + SelectedOptionsCard(title = "시작 설정", summary = preStart) + } + if (state.settings.showPowerControls) { Spacer(Modifier.height(28.dp)) val macroOn = state.macroFor(device)?.enabled == true @@ -169,6 +181,33 @@ private fun StatusCard(state: HomeUiState) { } } +@Composable +private fun SelectedOptionsCard(title: String, summary: String) { + Card( + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant), + elevation = CardDefaults.cardElevation(defaultElevation = 1.dp), + ) { + Column( + modifier = Modifier.fillMaxWidth().padding(20.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + title, + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.primary, + textAlign = TextAlign.Center, + ) + Spacer(Modifier.height(8.dp)) + Text( + summary, + style = MaterialTheme.typography.titleLarge, + textAlign = TextAlign.Center, + ) + } + } +} + @Composable private fun PowerControls(isOn: Boolean, stacked: Boolean, onTogglePower: (Boolean) -> Unit) { val onButton: @Composable (Modifier) -> Unit = { m -> diff --git a/app/src/main/java/kr/tkrmagid/easyappliance/ui/MacroEditor.kt b/app/src/main/java/kr/tkrmagid/easyappliance/ui/MacroEditor.kt index 93d8cb2..f78807d 100644 --- a/app/src/main/java/kr/tkrmagid/easyappliance/ui/MacroEditor.kt +++ b/app/src/main/java/kr/tkrmagid/easyappliance/ui/MacroEditor.kt @@ -34,23 +34,67 @@ import kr.tkrmagid.easyappliance.data.DeviceMacro import kr.tkrmagid.easyappliance.data.DeviceType import kr.tkrmagid.easyappliance.data.MacroStep +// Capabilities/commands and supported values below match the user's real +// Samsung washer/aircon (read live from the SmartThings API). private const val TEMP_CAP = "custom.washerWaterTemperature" private const val TEMP_CMD = "setWasherWaterTemperature" -private const val LEVEL_CAP = "custom.washerWaterLevel" -private const val LEVEL_CMD = "setWasherWaterLevel" +private const val SPIN_CAP = "custom.washerSpinLevel" +private const val SPIN_CMD = "setWasherSpinLevel" +private const val RINSE_CAP = "custom.washerRinseCycles" +private const val RINSE_CMD = "setWasherRinseCycles" + +private const val AC_MODE_CAP = "airConditionerMode" +private const val AC_MODE_CMD = "setAirConditionerMode" +private const val AC_TEMP_CAP = "thermostatCoolingSetpoint" +private const val AC_TEMP_CMD = "setCoolingSetpoint" +private const val AC_FAN_CAP = "airConditionerFanMode" +private const val AC_FAN_CMD = "setFanMode" private data class Preset(val value: String, val label: String) private val TEMP_PRESETS = listOf( Preset("cold", "냉수"), + Preset("20", "20도"), Preset("30", "30도"), Preset("40", "40도"), Preset("60", "60도"), + Preset("90", "90도"), ) -private val LEVEL_PRESETS = listOf( - Preset("low", "낮음"), - Preset("medium", "중간"), - Preset("high", "높음"), +private val SPIN_PRESETS = listOf( + Preset("noSpin", "탈수없음"), + Preset("low", "약"), + Preset("medium", "중"), + Preset("high", "강"), + Preset("extraHigh", "최강"), +) +private val RINSE_PRESETS = listOf( + Preset("1", "1회"), + Preset("2", "2회"), + Preset("3", "3회"), + Preset("4", "4회"), + Preset("5", "5회"), +) +private val AC_MODE_PRESETS = listOf( + Preset("cool", "냉방"), + Preset("dry", "제습"), + Preset("wind", "송풍"), + Preset("aIComfort", "AI쾌적"), +) +private val AC_TEMP_PRESETS = listOf( + Preset("18", "18도"), + Preset("20", "20도"), + Preset("22", "22도"), + Preset("24", "24도"), + Preset("26", "26도"), + Preset("28", "28도"), +) +private val AC_FAN_PRESETS = listOf( + Preset("auto", "자동"), + Preset("1", "1단"), + Preset("2", "2단"), + Preset("3", "3단"), + Preset("4", "4단"), + Preset("max", "최대"), ) @OptIn(ExperimentalLayoutApi::class) @@ -80,45 +124,24 @@ fun MacroEditor(device: Device, macro: DeviceMacro, onChange: (DeviceMacro) -> U if (!macro.enabled) return@Column if (device.type == DeviceType.WASHER) { - Spacer(Modifier.height(8.dp)) - Text("물 온도", style = MaterialTheme.typography.bodyMedium) - FlowRow(horizontalArrangement = Arrangement.spacedBy(8.dp)) { - TEMP_PRESETS.forEach { p -> - val selected = macro.steps.any { it.capability == TEMP_CAP && it.args == listOf(p.value) } - FilterChip( - selected = selected, - onClick = { - onChange( - macro.upsert( - MacroStep(TEMP_CAP, TEMP_CMD, listOf(p.value), "물 온도 ${p.label}"), - ), - ) - }, - label = { Text(p.label) }, - ) - } - } - Spacer(Modifier.height(8.dp)) - Text("수위(물 높이)", style = MaterialTheme.typography.bodyMedium) - FlowRow(horizontalArrangement = Arrangement.spacedBy(8.dp)) { - LEVEL_PRESETS.forEach { p -> - val selected = macro.steps.any { it.capability == LEVEL_CAP && it.args == listOf(p.value) } - FilterChip( - selected = selected, - onClick = { - onChange( - macro.upsert( - MacroStep(LEVEL_CAP, LEVEL_CMD, listOf(p.value), "수위 ${p.label}"), - ), - ) - }, - label = { Text(p.label) }, - ) - } - } + PresetGroup("물 온도", TEMP_CAP, TEMP_CMD, "물 온도", TEMP_PRESETS, macro, onChange) + PresetGroup("탈수 세기", SPIN_CAP, SPIN_CMD, "탈수", SPIN_PRESETS, macro, onChange) + PresetGroup("헹굼 횟수", RINSE_CAP, RINSE_CMD, "헹굼", RINSE_PRESETS, macro, onChange) Spacer(Modifier.height(4.dp)) Text( - "※ 삼성 세탁기 기준 예시 명령입니다. 실제 기기에서 동작하지 않으면 아래 '직접 추가'로 조정하세요.", + "※ 연결된 세탁기가 실제로 지원하는 설정입니다. 물높이(수위)는 세탁기가 자동으로 맞추므로 직접 설정하지 않습니다.", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + + if (device.type == DeviceType.AIRCONDITIONER) { + PresetGroup("운전 모드", AC_MODE_CAP, AC_MODE_CMD, "", AC_MODE_PRESETS, macro, onChange) + PresetGroup("설정 온도", AC_TEMP_CAP, AC_TEMP_CMD, "설정 온도", AC_TEMP_PRESETS, macro, onChange) + PresetGroup("바람 세기", AC_FAN_CAP, AC_FAN_CMD, "바람 세기", AC_FAN_PRESETS, macro, onChange) + Spacer(Modifier.height(4.dp)) + Text( + "※ 연결된 에어컨이 실제로 지원하는 설정입니다.", style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant, ) @@ -157,6 +180,34 @@ fun MacroEditor(device: Device, macro: DeviceMacro, onChange: (DeviceMacro) -> U } } +@OptIn(ExperimentalLayoutApi::class) +@Composable +private fun PresetGroup( + title: String, + capability: String, + command: String, + labelPrefix: String, + presets: List, + macro: DeviceMacro, + onChange: (DeviceMacro) -> Unit, +) { + Spacer(Modifier.height(8.dp)) + Text(title, style = MaterialTheme.typography.bodyMedium) + FlowRow(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + presets.forEach { p -> + val selected = macro.steps.any { it.capability == capability && it.args == listOf(p.value) } + val stepLabel = if (labelPrefix.isBlank()) p.label else "$labelPrefix ${p.label}" + FilterChip( + selected = selected, + onClick = { + onChange(macro.upsert(MacroStep(capability, command, listOf(p.value), stepLabel))) + }, + label = { Text(p.label) }, + ) + } + } +} + @Composable private fun AdvancedStepAdder(onAdd: (MacroStep) -> Unit) { var cap by remember { mutableStateOf("") } diff --git a/app/src/main/java/kr/tkrmagid/easyappliance/ui/theme/Type.kt b/app/src/main/java/kr/tkrmagid/easyappliance/ui/theme/Type.kt index 8f1f528..ff09f77 100644 --- a/app/src/main/java/kr/tkrmagid/easyappliance/ui/theme/Type.kt +++ b/app/src/main/java/kr/tkrmagid/easyappliance/ui/theme/Type.kt @@ -6,12 +6,14 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.sp // Larger-than-default type scale so text stays readable for older users. +// Every style is Bold so all text stays thick and easy to read. val ElderlyTypography = Typography( displayLarge = TextStyle(fontWeight = FontWeight.Bold, fontSize = 48.sp, lineHeight = 56.sp), headlineLarge = TextStyle(fontWeight = FontWeight.Bold, fontSize = 40.sp, lineHeight = 48.sp), headlineMedium = TextStyle(fontWeight = FontWeight.Bold, fontSize = 32.sp, lineHeight = 40.sp), titleLarge = TextStyle(fontWeight = FontWeight.Bold, fontSize = 28.sp, lineHeight = 34.sp), - bodyLarge = TextStyle(fontWeight = FontWeight.Normal, fontSize = 22.sp, lineHeight = 30.sp), - bodyMedium = TextStyle(fontWeight = FontWeight.Normal, fontSize = 20.sp, lineHeight = 28.sp), + titleMedium = TextStyle(fontWeight = FontWeight.Bold, fontSize = 24.sp, lineHeight = 30.sp), + bodyLarge = TextStyle(fontWeight = FontWeight.Bold, fontSize = 22.sp, lineHeight = 30.sp), + bodyMedium = TextStyle(fontWeight = FontWeight.Bold, fontSize = 20.sp, lineHeight = 28.sp), labelLarge = TextStyle(fontWeight = FontWeight.Bold, fontSize = 24.sp, lineHeight = 30.sp), ) diff --git a/app/src/test/java/kr/tkrmagid/easyappliance/MacroTest.kt b/app/src/test/java/kr/tkrmagid/easyappliance/MacroTest.kt new file mode 100644 index 0000000..4748fb6 --- /dev/null +++ b/app/src/test/java/kr/tkrmagid/easyappliance/MacroTest.kt @@ -0,0 +1,37 @@ +package kr.tkrmagid.easyappliance + +import kr.tkrmagid.easyappliance.data.DeviceMacro +import kr.tkrmagid.easyappliance.data.MacroStep +import org.junit.Assert.assertEquals +import org.junit.Test + +class MacroTest { + + @Test + fun summaryJoinsStepLabels() { + val macro = DeviceMacro( + enabled = true, + steps = listOf( + MacroStep("custom.washerWaterTemperature", "setWasherWaterTemperature", listOf("40"), "물 온도 40도"), + MacroStep("custom.washerWaterLevel", "setWasherWaterLevel", listOf("high"), "수위 높음"), + ), + ) + assertEquals("물 온도 40도 · 수위 높음", macro.summary()) + } + + @Test + fun summaryIsEmptyWhenNoSteps() { + assertEquals("", DeviceMacro(enabled = true).summary()) + } + + @Test + fun summarySkipsBlankLabels() { + val macro = DeviceMacro( + steps = listOf( + MacroStep("thermostatCoolingSetpoint", "setCoolingSetpoint", listOf("24"), "설정 온도 24도"), + MacroStep("airConditionerFanMode", "setFanMode", listOf("high"), ""), + ), + ) + assertEquals("설정 온도 24도", macro.summary()) + } +} diff --git a/app/src/test/java/kr/tkrmagid/easyappliance/MockDeviceRepositoryTest.kt b/app/src/test/java/kr/tkrmagid/easyappliance/MockDeviceRepositoryTest.kt index 1bd20f1..24621df 100644 --- a/app/src/test/java/kr/tkrmagid/easyappliance/MockDeviceRepositoryTest.kt +++ b/app/src/test/java/kr/tkrmagid/easyappliance/MockDeviceRepositoryTest.kt @@ -26,9 +26,14 @@ class MockDeviceRepositoryTest { assertTrue(on.powerOn) assertEquals("운전 중", on.operatingState) assertEquals(32, on.remainingMinutes) + assertTrue(on.running) + assertEquals("물 온도 40도 · 탈수 강 · 헹굼 3회", on.currentSettings) repo.setPower("washer-1", false) - assertFalse(repo.getStatus("washer-1").powerOn) + val off = repo.getStatus("washer-1") + assertFalse(off.powerOn) + assertFalse(off.running) + assertEquals(null, off.currentSettings) } @Test diff --git a/app/src/test/java/kr/tkrmagid/easyappliance/SmartThingsStatusMapperTest.kt b/app/src/test/java/kr/tkrmagid/easyappliance/SmartThingsStatusMapperTest.kt index a41da8b..d45ae2b 100644 --- a/app/src/test/java/kr/tkrmagid/easyappliance/SmartThingsStatusMapperTest.kt +++ b/app/src/test/java/kr/tkrmagid/easyappliance/SmartThingsStatusMapperTest.kt @@ -20,7 +20,10 @@ class SmartThingsStatusMapperTest { {"components":{"main":{ "switch":{"switch":{"value":"on"}}, "washerOperatingState":{"machineState":{"value":"run"}}, - "samsungce.washerOperatingState":{"remainingTime":{"value":1980}} + "samsungce.washerOperatingState":{"remainingTime":{"value":1980}}, + "custom.washerWaterTemperature":{"washerWaterTemperature":{"value":"60"}}, + "custom.washerSpinLevel":{"washerSpinLevel":{"value":"extraHigh"}}, + "custom.washerRinseCycles":{"washerRinseCycles":{"value":"3"}} }}} """.trimIndent(), ) @@ -28,6 +31,26 @@ class SmartThingsStatusMapperTest { assertTrue(result.powerOn) assertEquals("운전 중", result.operatingState) assertEquals(33, result.remainingMinutes) + assertTrue(result.running) + assertEquals("물 온도 60도 · 탈수 최강 · 헹굼 3회", result.currentSettings) + } + + @Test + fun stoppedWasherHasNoCurrentSettings() { + val status = obj( + """ + {"components":{"main":{ + "switch":{"switch":{"value":"on"}}, + "washerOperatingState":{"machineState":{"value":"stop"}}, + "custom.washerWaterTemperature":{"washerWaterTemperature":{"value":"40"}}, + "custom.washerSpinLevel":{"washerSpinLevel":{"value":"high"}} + }}} + """.trimIndent(), + ) + val result = SmartThingsStatusMapper.map(status, DeviceType.WASHER) + assertEquals("정지", result.operatingState) + assertTrue(!result.running) + assertNull(result.currentSettings) } @Test @@ -36,6 +59,8 @@ class SmartThingsStatusMapperTest { """ {"components":{"main":{ "switch":{"switch":{"value":"on"}}, + "airConditionerMode":{"airConditionerMode":{"value":"cool"}}, + "airConditionerFanMode":{"fanMode":{"value":"auto"}}, "temperatureMeasurement":{"temperature":{"value":27}}, "thermostatCoolingSetpoint":{"coolingSetpoint":{"value":24}} }}} @@ -46,6 +71,7 @@ class SmartThingsStatusMapperTest { assertEquals("냉방 중", result.operatingState) assertEquals(27, result.currentTemperature) assertEquals(24, result.targetTemperature) + assertEquals("냉방 · 바람 자동", result.currentSettings) } @Test