feat(infrared): add infrared control, learning, and virtual remote module #9
+46
@@ -0,0 +1,46 @@
|
||||
package com.flagship.abox.manager.domain.infrared
|
||||
|
||||
import com.flagship.abox.manager.domain.DeviceName
|
||||
import com.flagship.abox.manager.domain.DomainError
|
||||
import com.flagship.abox.manager.domain.DomainResult
|
||||
import com.flagship.abox.manager.domain.ProviderProfileId
|
||||
|
||||
fun interface InfraredKeyIdGenerator { fun nextId(): InfraredKeyId }
|
||||
fun interface EpochMillisClock { fun now(): Long }
|
||||
|
||||
class InfraredKeyFactory(
|
||||
private val idGenerator: InfraredKeyIdGenerator,
|
||||
private val clock: EpochMillisClock,
|
||||
) {
|
||||
fun create(
|
||||
profileId: ProviderProfileId,
|
||||
deviceName: DeviceName,
|
||||
displayName: String,
|
||||
): DomainResult<SavedInfraredKey> {
|
||||
val normalizedName = displayName.trim()
|
||||
if (profileId.value.isBlank() || deviceName.value.isBlank() || normalizedName.isBlank()) {
|
||||
return DomainResult.Failure(
|
||||
DomainError.InvalidRequest(detail = "Profile, device name, and display name are required"),
|
||||
)
|
||||
}
|
||||
val id = idGenerator.nextId()
|
||||
val normalizedId = id.value.lowercase().filter { it in 'a'..'z' || it in '0'..'9' }
|
||||
if (normalizedId.isBlank()) {
|
||||
return DomainResult.Failure(DomainError.InvalidRequest(detail = "Infrared key id is invalid"))
|
||||
}
|
||||
val now = clock.now()
|
||||
return DomainResult.Success(
|
||||
SavedInfraredKey(
|
||||
id = id,
|
||||
profileId = profileId,
|
||||
deviceName = deviceName,
|
||||
displayName = normalizedName,
|
||||
sdkKey = "app_ir_${normalizedId.take(40)}",
|
||||
status = InfraredKeyStatus.LEARNING,
|
||||
lastErrorCode = null,
|
||||
createdAtEpochMillis = now,
|
||||
updatedAtEpochMillis = now,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.flagship.abox.manager.domain.infrared
|
||||
|
||||
import com.flagship.abox.manager.domain.DeviceName
|
||||
import com.flagship.abox.manager.domain.ProviderProfileId
|
||||
|
||||
@JvmInline
|
||||
value class InfraredKeyId(val value: String)
|
||||
|
||||
data class InfraredKeyRef(
|
||||
val profileId: ProviderProfileId,
|
||||
val keyId: InfraredKeyId,
|
||||
)
|
||||
|
||||
enum class InfraredKeyStatus { LEARNING, READY, FAILED }
|
||||
|
||||
data class SavedInfraredKey(
|
||||
val id: InfraredKeyId,
|
||||
val profileId: ProviderProfileId,
|
||||
val deviceName: DeviceName,
|
||||
val displayName: String,
|
||||
val sdkKey: String,
|
||||
val status: InfraredKeyStatus,
|
||||
val lastErrorCode: String?,
|
||||
val createdAtEpochMillis: Long,
|
||||
val updatedAtEpochMillis: Long,
|
||||
)
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
package com.flagship.abox.manager.domain.infrared
|
||||
|
||||
import com.flagship.abox.manager.domain.DeviceName
|
||||
import com.flagship.abox.manager.domain.DomainResult
|
||||
import com.flagship.abox.manager.domain.ProviderProfileId
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface InfraredKeyRepository {
|
||||
fun observeKeys(profileId: ProviderProfileId, deviceName: DeviceName): Flow<DomainResult<List<SavedInfraredKey>>>
|
||||
suspend fun getKey(ref: InfraredKeyRef): DomainResult<SavedInfraredKey?>
|
||||
suspend fun insert(key: SavedInfraredKey): DomainResult<Unit>
|
||||
suspend fun update(key: SavedInfraredKey): DomainResult<Unit>
|
||||
suspend fun delete(ref: InfraredKeyRef): DomainResult<Unit>
|
||||
suspend fun findByStatus(profileId: ProviderProfileId, status: InfraredKeyStatus): DomainResult<List<SavedInfraredKey>>
|
||||
}
|
||||
|
||||
interface InfraredDeviceConfigurationRepository {
|
||||
fun observeDeviceName(profileId: ProviderProfileId): Flow<DomainResult<DeviceName?>>
|
||||
suspend fun setDeviceName(profileId: ProviderProfileId, deviceName: DeviceName): DomainResult<Unit>
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
package com.flagship.abox.manager.domain.infrared
|
||||
|
||||
import com.flagship.abox.manager.domain.DeviceName
|
||||
import com.flagship.abox.manager.domain.DomainResult
|
||||
import com.flagship.abox.manager.domain.ProviderProfileId
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
class InfraredKeyFactoryTest {
|
||||
private val id = InfraredKeyId("01J5MOLERABC123")
|
||||
private val factory = InfraredKeyFactory(
|
||||
idGenerator = InfraredKeyIdGenerator { id },
|
||||
clock = EpochMillisClock { 1_724_435_200_000L },
|
||||
)
|
||||
|
||||
@Test
|
||||
fun createsLearningKeyWithStableAsciiSdkKey() {
|
||||
val result = factory.create(
|
||||
ProviderProfileId("abox"),
|
||||
DeviceName("ir-1"),
|
||||
"空调开关",
|
||||
) as DomainResult.Success
|
||||
|
||||
assertEquals(id, result.value.id)
|
||||
assertEquals("app_ir_01j5molerabc123", result.value.sdkKey)
|
||||
assertEquals(InfraredKeyStatus.LEARNING, result.value.status)
|
||||
assertEquals("空调开关", result.value.displayName)
|
||||
assertEquals(1_724_435_200_000L, result.value.createdAtEpochMillis)
|
||||
assertEquals(result.value.createdAtEpochMillis, result.value.updatedAtEpochMillis)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun rejectsBlankDeviceOrDisplayName() {
|
||||
assertTrue(factory.create(ProviderProfileId("abox"), DeviceName(""), "Power") is DomainResult.Failure)
|
||||
assertTrue(factory.create(ProviderProfileId("abox"), DeviceName("ir-1"), " ") is DomainResult.Failure)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user