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