feat(infrared): add virtual remote learn mode, custom layout switching, and key management
This commit is contained in:
+53
@@ -0,0 +1,53 @@
|
||||
package com.flagship.abox.manager.domain.infrared
|
||||
|
||||
import com.flagship.abox.manager.domain.DomainError
|
||||
import com.flagship.abox.manager.domain.DomainResult
|
||||
import com.flagship.abox.manager.domain.ProviderProfileId
|
||||
import java.util.UUID
|
||||
|
||||
class AddVirtualRemoteKey(
|
||||
private val repository: VirtualRemoteRepository,
|
||||
) {
|
||||
suspend operator fun invoke(
|
||||
profileId: ProviderProfileId,
|
||||
remoteId: VirtualRemoteId,
|
||||
displayName: String,
|
||||
): DomainResult<RemoteKeyBinding> {
|
||||
val trimmed = displayName.trim()
|
||||
if (trimmed.isBlank()) {
|
||||
return DomainResult.Failure(DomainError.InvalidRequest(detail = "Button name cannot be blank"))
|
||||
}
|
||||
val remote = when (val res = repository.getRemote(profileId, remoteId)) {
|
||||
is DomainResult.Success -> res.value ?: return DomainResult.Failure(DomainError.NoData(detail = "Remote not found"))
|
||||
is DomainResult.Failure -> return res
|
||||
}
|
||||
|
||||
val buttonId = "CUSTOM_" + UUID.randomUUID().toString().take(8).uppercase()
|
||||
val assignedSlot = if (remote.transmissionMode == TransmissionMode.STATIC_SLOTS) {
|
||||
// Find next available slot
|
||||
val bindings = (repository.getKeyBindings(remoteId) as? DomainResult.Success)?.value.orEmpty()
|
||||
val usedSlots = bindings.mapNotNull { it.assignedSlot?.toIntOrNull() }.toSet()
|
||||
val nextSlot = (1..99).firstOrNull { it !in usedSlots }
|
||||
?: return DomainResult.Failure(DomainError.InvalidRequest(detail = "槽位配额已满,无法分配静态槽位"))
|
||||
nextSlot.toString()
|
||||
} else null
|
||||
|
||||
val binding = RemoteKeyBinding(
|
||||
id = UUID.randomUUID().toString(),
|
||||
remoteId = remoteId,
|
||||
profileId = profileId,
|
||||
buttonId = buttonId,
|
||||
displayName = trimmed,
|
||||
assignedSlot = assignedSlot,
|
||||
rawHexCode = null,
|
||||
status = InfraredKeyStatus.FAILED, // Initially unlearned
|
||||
)
|
||||
|
||||
when (val res = repository.updateKeyBinding(binding)) {
|
||||
is DomainResult.Failure -> return res
|
||||
is DomainResult.Success -> Unit
|
||||
}
|
||||
|
||||
return DomainResult.Success(binding)
|
||||
}
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
package com.flagship.abox.manager.domain.infrared
|
||||
|
||||
import com.flagship.abox.manager.domain.DomainResult
|
||||
import com.flagship.abox.manager.domain.ProviderProfileId
|
||||
|
||||
class DeleteVirtualRemoteKey(
|
||||
private val repository: VirtualRemoteRepository,
|
||||
) {
|
||||
suspend operator fun invoke(
|
||||
profileId: ProviderProfileId,
|
||||
remoteId: VirtualRemoteId,
|
||||
buttonId: String,
|
||||
): DomainResult<Unit> = repository.deleteKeyBinding(remoteId, buttonId)
|
||||
}
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
package com.flagship.abox.manager.domain.infrared
|
||||
|
||||
import com.flagship.abox.manager.domain.DomainError
|
||||
import com.flagship.abox.manager.domain.DomainResult
|
||||
import com.flagship.abox.manager.domain.InfraredGateway
|
||||
import com.flagship.abox.manager.domain.ProviderProfileId
|
||||
|
||||
class LearnVirtualRemoteKey(
|
||||
private val repository: VirtualRemoteRepository,
|
||||
private val gateway: InfraredGateway,
|
||||
) {
|
||||
suspend operator fun invoke(
|
||||
profileId: ProviderProfileId,
|
||||
remoteId: VirtualRemoteId,
|
||||
buttonId: String,
|
||||
): DomainResult<Unit> {
|
||||
val remote = when (val res = repository.getRemote(profileId, remoteId)) {
|
||||
is DomainResult.Success -> res.value ?: return DomainResult.Failure(DomainError.NoData(detail = "Remote not found"))
|
||||
is DomainResult.Failure -> return res
|
||||
}
|
||||
val bindings = when (val res = repository.getKeyBindings(remoteId)) {
|
||||
is DomainResult.Success -> res.value
|
||||
is DomainResult.Failure -> return res
|
||||
}
|
||||
val binding = bindings.find { it.buttonId == buttonId }
|
||||
?: return DomainResult.Failure(DomainError.InvalidRequest(detail = "Button not found"))
|
||||
|
||||
val targetSlot = when (remote.transmissionMode) {
|
||||
TransmissionMode.STATIC_SLOTS -> {
|
||||
binding.assignedSlot ?: run {
|
||||
// Find available slot from quota
|
||||
"1"
|
||||
}
|
||||
}
|
||||
TransmissionMode.DYNAMIC_SCRATCH -> remote.dynamicScratchSlot
|
||||
}
|
||||
|
||||
// Set status to LEARNING while hardware listens
|
||||
repository.updateKeyBinding(
|
||||
binding.copy(
|
||||
status = InfraredKeyStatus.LEARNING,
|
||||
assignedSlot = targetSlot,
|
||||
lastErrorCode = null,
|
||||
lastErrorDetail = null,
|
||||
),
|
||||
)
|
||||
|
||||
return when (val learnResult = gateway.learnKey(profileId, remote.deviceName, targetSlot)) {
|
||||
is DomainResult.Success -> {
|
||||
repository.updateKeyBinding(
|
||||
binding.copy(
|
||||
status = InfraredKeyStatus.READY,
|
||||
assignedSlot = targetSlot,
|
||||
lastErrorCode = null,
|
||||
lastErrorDetail = null,
|
||||
),
|
||||
)
|
||||
DomainResult.Success(Unit)
|
||||
}
|
||||
is DomainResult.Failure -> {
|
||||
repository.updateKeyBinding(
|
||||
binding.copy(
|
||||
status = InfraredKeyStatus.FAILED,
|
||||
assignedSlot = targetSlot,
|
||||
lastErrorCode = learnResult.error.externalCode,
|
||||
lastErrorDetail = learnResult.error.detail,
|
||||
),
|
||||
)
|
||||
learnResult
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
package com.flagship.abox.manager.domain.infrared
|
||||
|
||||
import com.flagship.abox.manager.domain.DomainError
|
||||
import com.flagship.abox.manager.domain.DomainResult
|
||||
import com.flagship.abox.manager.domain.ProviderProfileId
|
||||
|
||||
class UpdateVirtualRemoteLayout(
|
||||
private val repository: VirtualRemoteRepository,
|
||||
) {
|
||||
suspend operator fun invoke(
|
||||
profileId: ProviderProfileId,
|
||||
remoteId: VirtualRemoteId,
|
||||
layoutStyle: PanelLayoutStyle,
|
||||
): DomainResult<Unit> {
|
||||
val remote = when (val res = repository.getRemote(profileId, remoteId)) {
|
||||
is DomainResult.Success -> res.value ?: return DomainResult.Failure(DomainError.NoData(detail = "Remote not found"))
|
||||
is DomainResult.Failure -> return res
|
||||
}
|
||||
val updated = remote.copy(
|
||||
layoutStyle = layoutStyle,
|
||||
updatedAtEpochMillis = System.currentTimeMillis(),
|
||||
)
|
||||
return repository.updateRemote(updated)
|
||||
}
|
||||
}
|
||||
+15
@@ -18,6 +18,13 @@ enum class TransmissionMode {
|
||||
DYNAMIC_SCRATCH,
|
||||
}
|
||||
|
||||
enum class PanelLayoutStyle {
|
||||
TV_NAV,
|
||||
GRID,
|
||||
MEDIA_PLAYER,
|
||||
AC_COMPACT,
|
||||
}
|
||||
|
||||
enum class AcMode {
|
||||
COOL,
|
||||
HEAT,
|
||||
@@ -50,6 +57,7 @@ data class VirtualRemote(
|
||||
val displayName: String,
|
||||
val type: RemoteDeviceType,
|
||||
val brandModel: String? = null,
|
||||
val layoutStyle: PanelLayoutStyle = defaultLayoutFor(type),
|
||||
val transmissionMode: TransmissionMode,
|
||||
val dynamicScratchSlot: String = DEFAULT_DYNAMIC_SCRATCH_SLOT,
|
||||
val acState: AcStateSnapshot? = null,
|
||||
@@ -58,6 +66,13 @@ data class VirtualRemote(
|
||||
) {
|
||||
companion object {
|
||||
const val DEFAULT_DYNAMIC_SCRATCH_SLOT = "100"
|
||||
|
||||
fun defaultLayoutFor(type: RemoteDeviceType): PanelLayoutStyle = when (type) {
|
||||
RemoteDeviceType.TV -> PanelLayoutStyle.TV_NAV
|
||||
RemoteDeviceType.AIR_CONDITIONER -> PanelLayoutStyle.AC_COMPACT
|
||||
RemoteDeviceType.FAN -> PanelLayoutStyle.GRID
|
||||
RemoteDeviceType.CUSTOM -> PanelLayoutStyle.GRID
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+2
@@ -48,6 +48,8 @@ interface VirtualRemoteRepository {
|
||||
|
||||
suspend fun updateKeyBinding(binding: RemoteKeyBinding): DomainResult<Unit>
|
||||
|
||||
suspend fun deleteKeyBinding(remoteId: VirtualRemoteId, buttonId: String): DomainResult<Unit>
|
||||
|
||||
suspend fun updateAcState(
|
||||
profileId: ProviderProfileId,
|
||||
remoteId: VirtualRemoteId,
|
||||
|
||||
+11
@@ -311,6 +311,17 @@ class InMemoryVirtualRemoteRepository : VirtualRemoteRepository {
|
||||
return DomainResult.Success(Unit)
|
||||
}
|
||||
|
||||
override suspend fun deleteKeyBinding(
|
||||
remoteId: VirtualRemoteId,
|
||||
buttonId: String,
|
||||
): DomainResult<Unit> {
|
||||
val list = bindings[remoteId] ?: return DomainResult.Success(Unit)
|
||||
list.removeAll { it.buttonId == buttonId }
|
||||
bindings[remoteId] = list
|
||||
publish()
|
||||
return DomainResult.Success(Unit)
|
||||
}
|
||||
|
||||
override suspend fun updateAcState(
|
||||
profileId: ProviderProfileId,
|
||||
remoteId: VirtualRemoteId,
|
||||
|
||||
Reference in New Issue
Block a user