feat(infrared): add infrared control, learning, and virtual remote module #9

Merged
KremeCN merged 41 commits from feat/moler-infrared-control into main 2026-08-25 14:51:34 +08:00
2 changed files with 99 additions and 0 deletions
Showing only changes of commit 1376702476 - Show all commits
@@ -17,10 +17,12 @@ import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
import kotlin.reflect.KClass
class AbsdkDeviceBridgeTest {
private val profile = ProviderProfileId("abox")
private val socket = DeviceName("socket-1")
private val irDevice = DeviceName("ir-room")
@Test
fun socketControlReturnsConfirmedStatus() = runTest {
@@ -83,6 +85,69 @@ class AbsdkDeviceBridgeTest {
assertTrue((offline as DomainResult.Failure).error is DomainError.DeviceOffline)
}
@Test
fun infraredSendForwardsDeviceAndStableKey() = runTest {
val client = ScriptedAbsdkClient()
val bridge = authenticatedBridge(client)
assertEquals(DomainResult.Success(Unit), bridge.sendKey(profile, irDevice, "app_ir_power"))
assertEquals(listOf("login:demo", "ir-send:ir-room:app_ir_power"), client.calls)
}
@Test
fun infraredLearnForwardsDeviceAndStableKey() = runTest {
val client = ScriptedAbsdkClient()
val bridge = authenticatedBridge(client)
assertEquals(DomainResult.Success(Unit), bridge.learnKey(profile, irDevice, "app_ir_power"))
assertEquals(listOf("login:demo", "ir-learn:ir-room:app_ir_power"), client.calls)
}
@Test
fun infraredErrorCodesAreMappedCorrectlyForSendAndLearn() = runTest {
val errorMappings = mapOf<String, KClass<out DomainError>>(
"20201" to DomainError.InvalidRequest::class,
"20202" to DomainError.InvalidRequest::class,
"20203" to DomainError.DeviceNotFound::class,
"20204" to DomainError.OperationFailed::class,
"20205" to DomainError.OperationFailed::class,
"20206" to DomainError.DeviceOffline::class,
)
for ((code, expectedClass) in errorMappings) {
val client = ScriptedAbsdkClient().apply {
sendResponse = ScriptedAbsdkClient.failure(code, "message for $code")
learnResponse = ScriptedAbsdkClient.failure(code, "message for $code")
}
val bridge = authenticatedBridge(client)
val sendResult = bridge.sendKey(profile, irDevice, "key1")
assertTrue("Expected send error class $expectedClass for code $code", sendResult is DomainResult.Failure)
assertEquals(expectedClass, (sendResult as DomainResult.Failure).error::class)
assertEquals(code, sendResult.error.externalCode)
val learnResult = bridge.learnKey(profile, irDevice, "key1")
assertTrue("Expected learn error class $expectedClass for code $code", learnResult is DomainResult.Failure)
assertEquals(expectedClass, (learnResult as DomainResult.Failure).error::class)
assertEquals(code, learnResult.error.externalCode)
}
}
@Test
fun infraredNullResponseReturnsUnknownDomainError() = runTest {
val client = ScriptedAbsdkClient().apply {
sendResponse = null
learnResponse = null
}
val bridge = authenticatedBridge(client)
val sendResult = bridge.sendKey(profile, irDevice, "key1")
assertTrue(sendResult is DomainResult.Failure)
assertTrue((sendResult as DomainResult.Failure).error is DomainError.Unknown)
val learnResult = bridge.learnKey(profile, irDevice, "key1")
assertTrue(learnResult is DomainResult.Failure)
assertTrue((learnResult as DomainResult.Failure).error is DomainError.Unknown)
}
@Test
fun infraredCallsAndPartialDownloadResultsAreMapped() = runTest {
val codeOne = InfraredCode(DeviceName("ir-1"), "power", "code-1")
@@ -48,4 +48,38 @@ class AbsdkSerializationTest {
assertEquals(1, client.maxConcurrentCalls.get())
}
}
@Test
fun concurrentInfraredAndOtherDeviceCallsDoNotOverlap() = runBlocking {
withTimeout(5_000) {
val profile = ProviderProfileId("abox")
val client = ScriptedAbsdkClient().apply {
socketStatusResponses = ArrayDeque(
listOf(ScriptedAbsdkClient.success(mapOf("status" to "1"))),
)
}
val bridge = AbsdkBridge(client, Dispatchers.IO)
bridge.login(profile, LoginCredentials(Username("demo"), Password("secret")))
client.onCall = {
if (client.calls.last().startsWith("ir-learn") || client.calls.last().startsWith("temperature")) {
Thread.sleep(75)
}
}
val irCall = async {
bridge.learnKey(profile, DeviceName("ir-1"), "app_ir_power")
}
val tempCall = async {
bridge.getReading(profile, DeviceName("th-1"))
}
delay(10)
irCall.await()
tempCall.await()
assertEquals(1, client.maxConcurrentCalls.get())
}
}
}