feat: add ABSDK 0630 bridge (#3)
## Summary - add a pure Kotlin/JVM anti-corruption layer for ABSDK 0630 - serialize blocking SDK calls and enforce one provider-scoped session - map `ABRet`, raw maps, SDK exceptions, and error codes into typed domain results - implement session, socket, temperature/humidity, and infrared gateways - document bytecode-confirmed protocol facts, SDK limitations, and unresolved behavior - align JVM module bytecode with the Android Java 17 target ## Scope boundaries - the bridge is not wired into the Android app in this PR; debug continues using Fakes - `setHostInfo` is intentionally not called or implemented as a provider flow - logout clears only the bridge-local session because the JAR exposes no logout or token setter - no real credentials, network calls, or hardware were used in tests - `docs/requirement_spec.md` and `docs/project_plan.md` are unchanged ## Verification ```text gradlew.bat --stop gradlew.bat clean :absdk-bridge:test :domain:test :testing:fakes:test :android:app:lintDebug :android:app:assembleDebug :android:app:assembleRelease build ``` Result: `BUILD SUCCESSFUL` The public JAR API was also rechecked with `javap`, including the actual `getThStatus(String)` method, absence of a public logout API, `ABRet.getDicDatas()`, `IrInfo.getStatus()`, and `downloadIrCodes(List<IrInfo>)`. Reviewed-on: #3 Co-authored-by: KremeCN <3265494849@qq.com>
This commit was merged in pull request #3.
This commit is contained in:
@@ -0,0 +1,344 @@
|
||||
package com.flagship.abox.manager.absdk.bridge
|
||||
|
||||
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.InfraredCode
|
||||
import com.flagship.abox.manager.domain.InfraredCodeDownloadResult
|
||||
import com.flagship.abox.manager.domain.InfraredGateway
|
||||
import com.flagship.abox.manager.domain.LoginCredentials
|
||||
import com.flagship.abox.manager.domain.ProviderProfileId
|
||||
import com.flagship.abox.manager.domain.Session
|
||||
import com.flagship.abox.manager.domain.SessionGateway
|
||||
import com.flagship.abox.manager.domain.SessionValidity
|
||||
import com.flagship.abox.manager.domain.SocketGateway
|
||||
import com.flagship.abox.manager.domain.SocketPowerState
|
||||
import com.flagship.abox.manager.domain.SocketStatus
|
||||
import com.flagship.abox.manager.domain.TemperatureHumidityGateway
|
||||
import com.flagship.abox.manager.domain.TemperatureHumidityReading
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class AbsdkBridge internal constructor(
|
||||
private val client: AbsdkClient,
|
||||
private val dispatcher: CoroutineDispatcher,
|
||||
) : SessionGateway, SocketGateway, TemperatureHumidityGateway, InfraredGateway {
|
||||
private val mutex = Mutex()
|
||||
private val activeSession = MutableStateFlow<Session?>(null)
|
||||
|
||||
override fun observeSession(profileId: ProviderProfileId): Flow<Session?> =
|
||||
activeSession.map { session -> session?.takeIf { it.profileId == profileId } }
|
||||
|
||||
override suspend fun login(
|
||||
profileId: ProviderProfileId,
|
||||
credentials: LoginCredentials,
|
||||
): DomainResult<Session> = serialized {
|
||||
val currentSession = activeSession.value
|
||||
if (currentSession != null && currentSession.profileId != profileId) {
|
||||
return@serialized DomainResult.Failure(
|
||||
DomainError.InvalidRequest(detail = "Exit the active provider session before logging in"),
|
||||
)
|
||||
}
|
||||
when (val response = call { client.login(credentials.username.value, credentials.password.value) }) {
|
||||
is ClientCall.Failed -> DomainResult.Failure(response.error)
|
||||
is ClientCall.Completed -> {
|
||||
if (!response.response.isSuccess()) {
|
||||
DomainResult.Failure(AbsdkErrorMapper.fromResponse(response.response))
|
||||
} else {
|
||||
DomainResult.Success(
|
||||
Session(profileId, credentials.username).also { activeSession.value = it },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun validateSession(
|
||||
profileId: ProviderProfileId,
|
||||
): DomainResult<SessionValidity> = serialized {
|
||||
if (!hasActiveSession(profileId)) {
|
||||
return@serialized DomainResult.Success(SessionValidity.INVALID)
|
||||
}
|
||||
when (val response = call(client::validateToken)) {
|
||||
is ClientCall.Failed -> DomainResult.Failure(response.error)
|
||||
is ClientCall.Completed -> {
|
||||
if (!response.response.isSuccess()) {
|
||||
val error = AbsdkErrorMapper.fromResponse(response.response)
|
||||
if (error is DomainError.TokenInvalid) activeSession.value = null
|
||||
DomainResult.Failure(error)
|
||||
} else {
|
||||
when (response.response?.data?.stringValue(STATUS_KEY)) {
|
||||
ENABLED_STATUS -> DomainResult.Success(SessionValidity.VALID)
|
||||
DISABLED_STATUS -> DomainResult.Success(SessionValidity.INVALID).also {
|
||||
activeSession.value = null
|
||||
}
|
||||
else -> DomainResult.Failure(DomainError.NoData(detail = "Missing token status"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun logout(profileId: ProviderProfileId): DomainResult<Unit> = serialized {
|
||||
if (activeSession.value?.profileId == profileId) activeSession.value = null
|
||||
DomainResult.Success(Unit)
|
||||
}
|
||||
|
||||
override suspend fun getStatus(
|
||||
profileId: ProviderProfileId,
|
||||
deviceName: DeviceName,
|
||||
): DomainResult<SocketStatus> = serialized {
|
||||
withActiveSession(profileId) {
|
||||
when (val result = call { client.getSocketStatus(deviceName.value) }) {
|
||||
is ClientCall.Failed -> DomainResult.Failure(result.error)
|
||||
is ClientCall.Completed -> socketStatus(profileId, deviceName, result.response)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun setPower(
|
||||
profileId: ProviderProfileId,
|
||||
deviceName: DeviceName,
|
||||
powerState: SocketPowerState,
|
||||
): DomainResult<SocketStatus> = serialized {
|
||||
withActiveSession(profileId) {
|
||||
when (val control = call {
|
||||
client.setSocketPower(deviceName.value, powerState.toSdkStatus())
|
||||
}) {
|
||||
is ClientCall.Failed -> DomainResult.Failure(control.error)
|
||||
is ClientCall.Completed -> {
|
||||
if (!control.response.isSuccess()) {
|
||||
DomainResult.Failure(AbsdkErrorMapper.fromResponse(control.response))
|
||||
} else {
|
||||
when (val confirmation = call {
|
||||
client.getSocketStatus(deviceName.value)
|
||||
}) {
|
||||
is ClientCall.Failed -> DomainResult.Failure(confirmation.error)
|
||||
is ClientCall.Completed -> socketStatus(
|
||||
profileId,
|
||||
deviceName,
|
||||
confirmation.response,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getReading(
|
||||
profileId: ProviderProfileId,
|
||||
deviceName: DeviceName,
|
||||
): DomainResult<TemperatureHumidityReading> = serialized {
|
||||
withActiveSession(profileId) {
|
||||
when (val result = call { client.getTemperatureHumidity(deviceName.value) }) {
|
||||
is ClientCall.Failed -> DomainResult.Failure(result.error)
|
||||
is ClientCall.Completed -> {
|
||||
val response = result.response
|
||||
if (!response.isSuccess()) {
|
||||
DomainResult.Failure(AbsdkErrorMapper.fromResponse(response))
|
||||
} else {
|
||||
val temperature = response?.data?.stringValue(TEMPERATURE_KEY)
|
||||
val humidity = response?.data?.stringValue(HUMIDITY_KEY)
|
||||
if (temperature.isNullOrBlank() || humidity.isNullOrBlank()) {
|
||||
DomainResult.Failure(DomainError.NoData(detail = "Missing temperature or humidity"))
|
||||
} else {
|
||||
DomainResult.Success(
|
||||
TemperatureHumidityReading(
|
||||
profileId = profileId,
|
||||
deviceName = deviceName,
|
||||
temperature = temperature,
|
||||
humidity = humidity,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun sendKey(
|
||||
profileId: ProviderProfileId,
|
||||
deviceName: DeviceName,
|
||||
key: String,
|
||||
): DomainResult<Unit> = serialized {
|
||||
withActiveSession(profileId) {
|
||||
unitResult(call { client.sendInfrared(deviceName.value, key) })
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun learnKey(
|
||||
profileId: ProviderProfileId,
|
||||
deviceName: DeviceName,
|
||||
key: String,
|
||||
): DomainResult<Unit> = serialized {
|
||||
withActiveSession(profileId) {
|
||||
unitResult(call { client.learnInfrared(deviceName.value, key) })
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun downloadCodes(
|
||||
profileId: ProviderProfileId,
|
||||
codes: List<InfraredCode>,
|
||||
): DomainResult<List<InfraredCodeDownloadResult>> = serialized {
|
||||
withActiveSession(profileId) {
|
||||
when (val result = call {
|
||||
client.downloadInfrared(
|
||||
codes.map { AbsdkDownloadCode(it.deviceName.value, it.key, it.code) },
|
||||
)
|
||||
}) {
|
||||
is ClientCall.Failed -> DomainResult.Failure(result.error)
|
||||
is ClientCall.Completed -> parseDownloadResult(codes, result.response)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun <T> serialized(block: suspend () -> T): T = withContext(dispatcher) {
|
||||
mutex.withLock { block() }
|
||||
}
|
||||
|
||||
private inline fun <T> withActiveSession(
|
||||
profileId: ProviderProfileId,
|
||||
block: () -> DomainResult<T>,
|
||||
): DomainResult<T> {
|
||||
if (!hasActiveSession(profileId)) {
|
||||
return DomainResult.Failure(
|
||||
DomainError.TokenInvalid(detail = "No active ABSDK session for this provider"),
|
||||
)
|
||||
}
|
||||
return block().also { result ->
|
||||
if (result is DomainResult.Failure && result.error is DomainError.TokenInvalid) {
|
||||
activeSession.value = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun hasActiveSession(profileId: ProviderProfileId): Boolean =
|
||||
activeSession.value?.profileId == profileId
|
||||
|
||||
private fun socketStatus(
|
||||
profileId: ProviderProfileId,
|
||||
deviceName: DeviceName,
|
||||
response: AbsdkResponse?,
|
||||
): DomainResult<SocketStatus> {
|
||||
if (!response.isSuccess()) {
|
||||
return DomainResult.Failure(AbsdkErrorMapper.fromResponse(response))
|
||||
}
|
||||
return when (val status = response?.data?.stringValue(STATUS_KEY)) {
|
||||
DISABLED_STATUS -> DomainResult.Success(
|
||||
SocketStatus(profileId, deviceName, SocketPowerState.OFF),
|
||||
)
|
||||
ENABLED_STATUS -> DomainResult.Success(
|
||||
SocketStatus(profileId, deviceName, SocketPowerState.ON),
|
||||
)
|
||||
else -> DomainResult.Failure(
|
||||
DomainError.NoData(detail = "Missing or invalid socket status: $status"),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseDownloadResult(
|
||||
codes: List<InfraredCode>,
|
||||
response: AbsdkResponse?,
|
||||
): DomainResult<List<InfraredCodeDownloadResult>> {
|
||||
val validResponse = response?.takeIf { it.code in IR_DOWNLOAD_RESULT_CODES }
|
||||
?: return DomainResult.Failure(AbsdkErrorMapper.fromResponse(response))
|
||||
val results = validResponse.data[IR_RESULTS_KEY] as? List<*>
|
||||
?: return DomainResult.Failure(
|
||||
DomainError.OperationFailed(
|
||||
externalCode = validResponse.code,
|
||||
detail = "Missing infrared download results",
|
||||
),
|
||||
)
|
||||
if (results.size != codes.size || results.any { it !is AbsdkIrResult }) {
|
||||
return DomainResult.Failure(
|
||||
DomainError.OperationFailed(
|
||||
externalCode = validResponse.code,
|
||||
detail = "Malformed infrared download result",
|
||||
),
|
||||
)
|
||||
}
|
||||
val typedResults = results.filterIsInstance<AbsdkIrResult>()
|
||||
val orderedResults = codes.zip(typedResults)
|
||||
if (orderedResults.any { (code, result) ->
|
||||
result.deviceName != code.deviceName.value || result.key != code.key
|
||||
}
|
||||
) {
|
||||
return DomainResult.Failure(
|
||||
DomainError.OperationFailed(
|
||||
externalCode = validResponse.code,
|
||||
detail = "Infrared download results do not match their requests",
|
||||
),
|
||||
)
|
||||
}
|
||||
return DomainResult.Success(
|
||||
orderedResults.map { (code, result) ->
|
||||
InfraredCodeDownloadResult(code, successful = result.status == ENABLED_STATUS)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun unitResult(result: ClientCall): DomainResult<Unit> = when (result) {
|
||||
is ClientCall.Failed -> DomainResult.Failure(result.error)
|
||||
is ClientCall.Completed -> if (result.response.isSuccess()) {
|
||||
DomainResult.Success(Unit)
|
||||
} else {
|
||||
DomainResult.Failure(AbsdkErrorMapper.fromResponse(result.response))
|
||||
}
|
||||
}
|
||||
|
||||
private fun call(block: () -> AbsdkResponse?): ClientCall = try {
|
||||
ClientCall.Completed(block())
|
||||
} catch (error: CancellationException) {
|
||||
throw error
|
||||
} catch (error: Exception) {
|
||||
ClientCall.Failed(AbsdkErrorMapper.fromException(error))
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val STATUS_KEY = "status"
|
||||
private const val TEMPERATURE_KEY = "temperature"
|
||||
private const val HUMIDITY_KEY = "humidity"
|
||||
private const val IR_RESULTS_KEY = "ret"
|
||||
private const val DISABLED_STATUS = "0"
|
||||
private const val ENABLED_STATUS = "1"
|
||||
|
||||
@OptIn(CleartextAbsdkTransport::class)
|
||||
private val processInstance: AbsdkBridge by lazy(LazyThreadSafetyMode.SYNCHRONIZED) {
|
||||
AbsdkBridge(
|
||||
client = VendorAbsdkClient.create(),
|
||||
dispatcher = Dispatchers.IO,
|
||||
)
|
||||
}
|
||||
|
||||
@CleartextAbsdkTransport
|
||||
fun create(): AbsdkBridge = processInstance
|
||||
}
|
||||
}
|
||||
|
||||
private const val SUCCESS_CODE = "00000"
|
||||
private const val PARTIAL_IR_DOWNLOAD_CODE = "20204"
|
||||
private val IR_DOWNLOAD_RESULT_CODES = setOf(SUCCESS_CODE, PARTIAL_IR_DOWNLOAD_CODE)
|
||||
|
||||
private sealed interface ClientCall {
|
||||
data class Completed(val response: AbsdkResponse?) : ClientCall
|
||||
|
||||
data class Failed(val error: DomainError) : ClientCall
|
||||
}
|
||||
|
||||
private fun AbsdkResponse?.isSuccess(): Boolean = this?.code == SUCCESS_CODE
|
||||
|
||||
private fun Map<String, Any?>.stringValue(key: String): String? = this[key] as? String
|
||||
|
||||
private fun SocketPowerState.toSdkStatus(): String = when (this) {
|
||||
SocketPowerState.OFF -> "0"
|
||||
SocketPowerState.ON -> "1"
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.flagship.abox.manager.absdk.bridge
|
||||
|
||||
internal data class AbsdkResponse(
|
||||
val code: String?,
|
||||
val message: String?,
|
||||
val data: Map<String, Any?>,
|
||||
)
|
||||
|
||||
internal data class AbsdkIrResult(
|
||||
val deviceName: String?,
|
||||
val key: String?,
|
||||
val status: String?,
|
||||
)
|
||||
|
||||
internal interface AbsdkClient {
|
||||
fun login(username: String, password: String): AbsdkResponse?
|
||||
|
||||
fun validateToken(): AbsdkResponse?
|
||||
|
||||
fun getSocketStatus(deviceName: String): AbsdkResponse?
|
||||
|
||||
fun setSocketPower(deviceName: String, status: String): AbsdkResponse?
|
||||
|
||||
fun getTemperatureHumidity(deviceName: String): AbsdkResponse?
|
||||
|
||||
fun sendInfrared(deviceName: String, key: String): AbsdkResponse?
|
||||
|
||||
fun learnInfrared(deviceName: String, key: String): AbsdkResponse?
|
||||
|
||||
fun downloadInfrared(codes: List<AbsdkDownloadCode>): AbsdkResponse?
|
||||
}
|
||||
|
||||
internal data class AbsdkDownloadCode(
|
||||
val deviceName: String,
|
||||
val key: String,
|
||||
val code: String,
|
||||
)
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
package com.flagship.abox.manager.absdk.bridge
|
||||
|
||||
import com.flagship.abox.manager.domain.DomainError
|
||||
import java.io.IOException
|
||||
import java.net.SocketException
|
||||
import java.net.SocketTimeoutException
|
||||
|
||||
internal object AbsdkErrorMapper {
|
||||
private val tokenInvalidCodes = setOf("10001", "10002", "10003")
|
||||
private val authenticationCodes = setOf("10006", "20001", "20002", "20003", "20004", "20005")
|
||||
private val invalidRequestCodes = setOf(
|
||||
"10004", "10005", "20101",
|
||||
"20201", "20202",
|
||||
"20301", "20302", "20303",
|
||||
"20501",
|
||||
"20601", "20602",
|
||||
"20701", "20702", "20703", "20704", "20705",
|
||||
"20801", "20804",
|
||||
)
|
||||
private val deviceNotFoundCodes = setOf("20203", "20304", "20502", "20603", "20706", "20802")
|
||||
private val deviceOfflineCodes = setOf("20206", "20306", "20504", "20605", "20805")
|
||||
private val operationFailedCodes = setOf("00001", "20204", "20205", "20305", "20503", "20604", "20707", "20806")
|
||||
|
||||
fun fromResponse(response: AbsdkResponse?): DomainError {
|
||||
if (response == null) {
|
||||
return DomainError.Unknown(detail = "ABSDK returned no response")
|
||||
}
|
||||
return fromCode(response.code, response.message)
|
||||
}
|
||||
|
||||
fun fromCode(code: String?, message: String?): DomainError = when (code) {
|
||||
in tokenInvalidCodes -> DomainError.TokenInvalid(code, message)
|
||||
in authenticationCodes -> DomainError.Authentication(code, message)
|
||||
"20000" -> DomainError.Network(code, message)
|
||||
in invalidRequestCodes -> DomainError.InvalidRequest(code, message)
|
||||
in deviceNotFoundCodes -> DomainError.DeviceNotFound(code, message)
|
||||
in deviceOfflineCodes -> DomainError.DeviceOffline(code, message)
|
||||
"20803" -> DomainError.NoData(code, message)
|
||||
in operationFailedCodes -> DomainError.OperationFailed(code, message)
|
||||
else -> DomainError.Unknown(code, message)
|
||||
}
|
||||
|
||||
fun fromException(error: Throwable): DomainError = when (error) {
|
||||
is SocketTimeoutException,
|
||||
is SocketException,
|
||||
is IOException,
|
||||
-> DomainError.Network(detail = error.message ?: error::class.java.simpleName)
|
||||
|
||||
else -> DomainError.Unknown(detail = error.message ?: error::class.java.simpleName)
|
||||
}
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
package com.flagship.abox.manager.absdk.bridge
|
||||
|
||||
/**
|
||||
* Marks construction of the original ABSDK 0630 client, whose base endpoint is hardcoded to HTTP.
|
||||
*
|
||||
* Opting in acknowledges that transport confidentiality and integrity are not provided by the
|
||||
* vendor JAR. App integration must remain disabled until the deployment has an approved transport
|
||||
* control and the security limitation is presented to the responsible integrator.
|
||||
*/
|
||||
@RequiresOptIn(
|
||||
message = "ABSDK 0630 uses a hardcoded cleartext HTTP transport. " +
|
||||
"Do not enable it without an approved transport control.",
|
||||
level = RequiresOptIn.Level.ERROR,
|
||||
)
|
||||
@Retention(AnnotationRetention.BINARY)
|
||||
@Target(
|
||||
AnnotationTarget.CLASS,
|
||||
AnnotationTarget.CONSTRUCTOR,
|
||||
AnnotationTarget.FUNCTION,
|
||||
AnnotationTarget.PROPERTY,
|
||||
)
|
||||
annotation class CleartextAbsdkTransport
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
package com.flagship.abox.manager.absdk.bridge
|
||||
|
||||
import com.dhc.absdk.ABRet
|
||||
import com.dhc.absdk.ABSDK
|
||||
import com.dhc.absdk.IrInfo
|
||||
|
||||
@CleartextAbsdkTransport
|
||||
internal class VendorAbsdkClient private constructor(
|
||||
private val sdk: ABSDK,
|
||||
) : AbsdkClient {
|
||||
override fun login(username: String, password: String): AbsdkResponse? =
|
||||
sdk.loginWithUsername(username, password).toSnapshot()
|
||||
|
||||
override fun validateToken(): AbsdkResponse? = sdk.isValidateToken().toSnapshot()
|
||||
|
||||
override fun getSocketStatus(deviceName: String): AbsdkResponse? =
|
||||
sdk.getSockStatus(deviceName).toSnapshot()
|
||||
|
||||
override fun setSocketPower(deviceName: String, status: String): AbsdkResponse? =
|
||||
sdk.sockCtrl(deviceName, status).toSnapshot()
|
||||
|
||||
override fun getTemperatureHumidity(deviceName: String): AbsdkResponse? =
|
||||
sdk.getThStatus(deviceName).toSnapshot()
|
||||
|
||||
override fun sendInfrared(deviceName: String, key: String): AbsdkResponse? =
|
||||
sdk.sendIr(deviceName, key).toSnapshot()
|
||||
|
||||
override fun learnInfrared(deviceName: String, key: String): AbsdkResponse? =
|
||||
sdk.studyIrByIrDevName(deviceName, key).toSnapshot()
|
||||
|
||||
override fun downloadInfrared(codes: List<AbsdkDownloadCode>): AbsdkResponse? {
|
||||
val response = sdk.downloadIrCodes(
|
||||
codes.map { code ->
|
||||
IrInfo().apply {
|
||||
irDevName = code.deviceName
|
||||
key = code.key
|
||||
this.code = code.code
|
||||
}
|
||||
},
|
||||
) ?: return null
|
||||
val irResults = (response.dicDatas?.get(IR_RESULTS_KEY) as? List<*>)
|
||||
?.mapNotNull { (it as? IrInfo)?.toSnapshot() }
|
||||
.orEmpty()
|
||||
val data = response.dicDatas.orEmpty() + (IR_RESULTS_KEY to irResults)
|
||||
return AbsdkResponse(response.code, response.msg, data)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val IR_RESULTS_KEY = "ret"
|
||||
|
||||
fun create(): VendorAbsdkClient = VendorAbsdkClient(ABSDK.getInstance())
|
||||
}
|
||||
}
|
||||
|
||||
private fun ABRet?.toSnapshot(): AbsdkResponse? = this?.let {
|
||||
AbsdkResponse(
|
||||
code = code,
|
||||
message = msg,
|
||||
data = dicDatas.orEmpty().mapValues { entry -> entry.value },
|
||||
)
|
||||
}
|
||||
|
||||
private fun IrInfo.toSnapshot(): AbsdkIrResult = AbsdkIrResult(
|
||||
deviceName = irDevName,
|
||||
key = key,
|
||||
status = status,
|
||||
)
|
||||
+174
@@ -0,0 +1,174 @@
|
||||
package com.flagship.abox.manager.absdk.bridge
|
||||
|
||||
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.InfraredCode
|
||||
import com.flagship.abox.manager.domain.LoginCredentials
|
||||
import com.flagship.abox.manager.domain.Password
|
||||
import com.flagship.abox.manager.domain.ProviderProfileId
|
||||
import com.flagship.abox.manager.domain.SocketPowerState
|
||||
import com.flagship.abox.manager.domain.Username
|
||||
import java.io.IOException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
class AbsdkDeviceBridgeTest {
|
||||
private val profile = ProviderProfileId("abox")
|
||||
private val socket = DeviceName("socket-1")
|
||||
|
||||
@Test
|
||||
fun socketControlReturnsConfirmedStatus() = runTest {
|
||||
val client = ScriptedAbsdkClient().apply {
|
||||
socketStatusResponses = ArrayDeque(
|
||||
listOf(ScriptedAbsdkClient.success(mapOf("status" to "1"))),
|
||||
)
|
||||
}
|
||||
val bridge = authenticatedBridge(client)
|
||||
|
||||
val result = bridge.setPower(profile, socket, SocketPowerState.ON)
|
||||
|
||||
assertEquals(SocketPowerState.ON, (result as DomainResult.Success).value.powerState)
|
||||
assertEquals(
|
||||
listOf("login:demo", "socket-power:socket-1:1", "socket-status:socket-1"),
|
||||
client.calls,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun socketSdkExceptionIsMapped() = runTest {
|
||||
val client = ScriptedAbsdkClient()
|
||||
val bridge = authenticatedBridge(client)
|
||||
client.failure = IOException("socket unavailable")
|
||||
|
||||
val result = bridge.getStatus(profile, socket)
|
||||
|
||||
assertTrue((result as DomainResult.Failure).error is DomainError.Network)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun socketMalformedStatusReturnsNoData() = runTest {
|
||||
val client = ScriptedAbsdkClient().apply {
|
||||
socketStatusResponses = ArrayDeque(
|
||||
listOf(ScriptedAbsdkClient.success(mapOf("status" to "unknown"))),
|
||||
)
|
||||
}
|
||||
val bridge = authenticatedBridge(client)
|
||||
|
||||
val result = bridge.getStatus(profile, socket)
|
||||
|
||||
assertTrue((result as DomainResult.Failure).error is DomainError.NoData)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun temperatureMapsValuesAndErrors() = runTest {
|
||||
val client = ScriptedAbsdkClient()
|
||||
val bridge = authenticatedBridge(client)
|
||||
|
||||
val success = bridge.getReading(profile, DeviceName("th-1")) as DomainResult.Success
|
||||
assertEquals("23.5", success.value.temperature)
|
||||
assertEquals("45", success.value.humidity)
|
||||
|
||||
client.temperatureResponse = ScriptedAbsdkClient.success(mapOf("temperature" to "23"))
|
||||
val missing = bridge.getReading(profile, DeviceName("th-1"))
|
||||
assertTrue((missing as DomainResult.Failure).error is DomainError.NoData)
|
||||
|
||||
client.temperatureResponse = ScriptedAbsdkClient.failure("20805", "offline")
|
||||
val offline = bridge.getReading(profile, DeviceName("th-1"))
|
||||
assertTrue((offline as DomainResult.Failure).error is DomainError.DeviceOffline)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun infraredCallsAndPartialDownloadResultsAreMapped() = runTest {
|
||||
val codeOne = InfraredCode(DeviceName("ir-1"), "power", "code-1")
|
||||
val codeTwo = InfraredCode(DeviceName("ir-1"), "volume", "code-2")
|
||||
val client = ScriptedAbsdkClient().apply {
|
||||
downloadResponse = AbsdkResponse(
|
||||
code = "20204",
|
||||
message = "partial failure",
|
||||
data = mapOf(
|
||||
"ret" to listOf(
|
||||
AbsdkIrResult("ir-1", "power", "1"),
|
||||
AbsdkIrResult("ir-1", "volume", "0"),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
val bridge = authenticatedBridge(client)
|
||||
|
||||
assertEquals(DomainResult.Success(Unit), bridge.sendKey(profile, DeviceName("ir-1"), "power"))
|
||||
assertEquals(DomainResult.Success(Unit), bridge.learnKey(profile, DeviceName("ir-1"), "power"))
|
||||
val result = bridge.downloadCodes(profile, listOf(codeOne, codeTwo)) as DomainResult.Success
|
||||
|
||||
assertEquals(listOf(false, true).sorted(), result.value.map { it.successful }.sorted())
|
||||
assertEquals(
|
||||
listOf(
|
||||
AbsdkDownloadCode("ir-1", "power", "code-1"),
|
||||
AbsdkDownloadCode("ir-1", "volume", "code-2"),
|
||||
),
|
||||
client.downloadedCodes,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun infraredTokenErrorCannotBeMaskedByValidResults() = runTest {
|
||||
val code = InfraredCode(DeviceName("ir-1"), "power", "code-1")
|
||||
val client = ScriptedAbsdkClient().apply {
|
||||
downloadResponse = AbsdkResponse(
|
||||
code = "10002",
|
||||
message = "expired",
|
||||
data = mapOf(
|
||||
"ret" to listOf(AbsdkIrResult("ir-1", "power", "1")),
|
||||
),
|
||||
)
|
||||
}
|
||||
val bridge = authenticatedBridge(client)
|
||||
|
||||
val result = bridge.downloadCodes(profile, listOf(code))
|
||||
|
||||
assertTrue((result as DomainResult.Failure).error is DomainError.TokenInvalid)
|
||||
assertEquals(null, bridge.observeSession(profile).first())
|
||||
val second = bridge.sendKey(profile, DeviceName("ir-1"), "power")
|
||||
assertTrue((second as DomainResult.Failure).error is DomainError.TokenInvalid)
|
||||
assertEquals(listOf("login:demo", "ir-download"), client.calls)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun infraredMalformedResultsFailTheBatch() = runTest {
|
||||
val code = InfraredCode(DeviceName("ir-1"), "power", "code-1")
|
||||
val client = ScriptedAbsdkClient().apply {
|
||||
downloadResponse = ScriptedAbsdkClient.success(
|
||||
mapOf("ret" to listOf(AbsdkIrResult("another-device", "power", "1"))),
|
||||
)
|
||||
}
|
||||
val bridge = authenticatedBridge(client)
|
||||
|
||||
val result = bridge.downloadCodes(profile, listOf(code))
|
||||
|
||||
assertTrue((result as DomainResult.Failure).error is DomainError.OperationFailed)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun ioExceptionsStayInsideTheBridge() = runTest {
|
||||
val client = ScriptedAbsdkClient().apply { failure = IOException("offline") }
|
||||
val bridge = AbsdkBridge(client, Dispatchers.Unconfined)
|
||||
|
||||
val result = bridge.login(
|
||||
profile,
|
||||
LoginCredentials(Username("demo"), Password("secret")),
|
||||
)
|
||||
|
||||
assertTrue((result as DomainResult.Failure).error is DomainError.Network)
|
||||
assertFalse(result.error.detail.orEmpty().contains("secret"))
|
||||
}
|
||||
|
||||
private suspend fun authenticatedBridge(client: ScriptedAbsdkClient): AbsdkBridge =
|
||||
AbsdkBridge(client, Dispatchers.Unconfined).also {
|
||||
it.login(profile, LoginCredentials(Username("demo"), Password("secret")))
|
||||
}
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
package com.flagship.abox.manager.absdk.bridge
|
||||
|
||||
import com.flagship.abox.manager.domain.DomainError
|
||||
import java.io.IOException
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
class AbsdkErrorMapperTest {
|
||||
@Test
|
||||
fun mapsKnownCodeGroups() {
|
||||
val cases = mapOf(
|
||||
"10002" to DomainError.TokenInvalid::class,
|
||||
"10004" to DomainError.InvalidRequest::class,
|
||||
"10006" to DomainError.Authentication::class,
|
||||
"20000" to DomainError.Network::class,
|
||||
"20004" to DomainError.Authentication::class,
|
||||
"20101" to DomainError.InvalidRequest::class,
|
||||
"20203" to DomainError.DeviceNotFound::class,
|
||||
"20205" to DomainError.OperationFailed::class,
|
||||
"20206" to DomainError.DeviceOffline::class,
|
||||
"20501" to DomainError.InvalidRequest::class,
|
||||
"20502" to DomainError.DeviceNotFound::class,
|
||||
"20503" to DomainError.OperationFailed::class,
|
||||
"20504" to DomainError.DeviceOffline::class,
|
||||
"20803" to DomainError.NoData::class,
|
||||
"20805" to DomainError.DeviceOffline::class,
|
||||
)
|
||||
|
||||
cases.forEach { (code, expectedType) ->
|
||||
val error = AbsdkErrorMapper.fromCode(code, "detail")
|
||||
assertEquals(code, error.externalCode)
|
||||
assertEquals("detail", error.detail)
|
||||
assertEquals(expectedType, error::class)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun mapsUnknownAndNullResponses() {
|
||||
assertTrue(AbsdkErrorMapper.fromCode("99999", null) is DomainError.Unknown)
|
||||
assertTrue(AbsdkErrorMapper.fromResponse(null) is DomainError.Unknown)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun mapsIoExceptionsToNetworkErrors() {
|
||||
assertTrue(AbsdkErrorMapper.fromException(IOException("offline")) is DomainError.Network)
|
||||
assertTrue(AbsdkErrorMapper.fromException(IllegalStateException("broken")) is DomainError.Unknown)
|
||||
}
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
package com.flagship.abox.manager.absdk.bridge
|
||||
|
||||
import org.junit.Assert.assertSame
|
||||
import org.junit.Test
|
||||
|
||||
@OptIn(CleartextAbsdkTransport::class)
|
||||
class AbsdkFactoryTest {
|
||||
@Test
|
||||
fun productionFactoryReturnsOneProcessInstance() {
|
||||
assertSame(AbsdkBridge.create(), AbsdkBridge.create())
|
||||
}
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
package com.flagship.abox.manager.absdk.bridge
|
||||
|
||||
import com.dhc.absdk.ABRet
|
||||
import com.dhc.absdk.ABSDK
|
||||
import com.dhc.absdk.IrInfo
|
||||
import java.lang.reflect.ParameterizedType
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNotNull
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Test
|
||||
|
||||
class AbsdkPublicApiContractTest {
|
||||
@Test
|
||||
fun usesActualTemperatureHumidityMethodName() {
|
||||
assertNotNull(ABSDK::class.java.getMethod("getThStatus", String::class.java))
|
||||
assertNull(
|
||||
ABSDK::class.java.methods.find { it.name == "getThStatusByDevName" },
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun sdkHasNoPublicLogoutApi() {
|
||||
assertNull(ABSDK::class.java.methods.find { it.name.equals("logout", ignoreCase = true) })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun responseAndInfraredAccessorsKeepExpectedTypes() {
|
||||
assertEquals(String::class.java, ABRet::class.java.getMethod("getCode").returnType)
|
||||
assertEquals(String::class.java, ABRet::class.java.getMethod("getMsg").returnType)
|
||||
assertEquals(Map::class.java, ABRet::class.java.getMethod("getDicDatas").returnType)
|
||||
val dataType = ABRet::class.java.getMethod("getDicDatas").genericReturnType as ParameterizedType
|
||||
assertEquals(String::class.java, dataType.actualTypeArguments[0])
|
||||
assertEquals(Any::class.java, dataType.actualTypeArguments[1])
|
||||
assertEquals(String::class.java, IrInfo::class.java.getMethod("getStatus").returnType)
|
||||
}
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
package com.flagship.abox.manager.absdk.bridge
|
||||
|
||||
import com.flagship.abox.manager.domain.DeviceName
|
||||
import com.flagship.abox.manager.domain.LoginCredentials
|
||||
import com.flagship.abox.manager.domain.Password
|
||||
import com.flagship.abox.manager.domain.ProviderProfileId
|
||||
import com.flagship.abox.manager.domain.SocketPowerState
|
||||
import com.flagship.abox.manager.domain.Username
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withTimeout
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
|
||||
class AbsdkSerializationTest {
|
||||
@Test
|
||||
fun concurrentCallsDoNotEnterTheSdkTogether() = runBlocking {
|
||||
withTimeout(5_000) {
|
||||
val profile = ProviderProfileId("abox")
|
||||
val client = ScriptedAbsdkClient().apply {
|
||||
socketStatusResponses = ArrayDeque(
|
||||
listOf(
|
||||
ScriptedAbsdkClient.success(mapOf("status" to "1")),
|
||||
ScriptedAbsdkClient.success(mapOf("status" to "0")),
|
||||
),
|
||||
)
|
||||
}
|
||||
val bridge = AbsdkBridge(client, Dispatchers.IO)
|
||||
bridge.login(profile, LoginCredentials(Username("demo"), Password("secret")))
|
||||
|
||||
client.onCall = {
|
||||
if (client.calls.last().startsWith("socket-power")) {
|
||||
Thread.sleep(75)
|
||||
}
|
||||
}
|
||||
val first = async {
|
||||
bridge.setPower(profile, DeviceName("socket-1"), SocketPowerState.ON)
|
||||
}
|
||||
val second = async {
|
||||
bridge.setPower(profile, DeviceName("socket-2"), SocketPowerState.OFF)
|
||||
}
|
||||
delay(10)
|
||||
|
||||
first.await()
|
||||
second.await()
|
||||
assertEquals(1, client.maxConcurrentCalls.get())
|
||||
}
|
||||
}
|
||||
}
|
||||
+115
@@ -0,0 +1,115 @@
|
||||
package com.flagship.abox.manager.absdk.bridge
|
||||
|
||||
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.LoginCredentials
|
||||
import com.flagship.abox.manager.domain.Password
|
||||
import com.flagship.abox.manager.domain.ProviderProfileId
|
||||
import com.flagship.abox.manager.domain.SessionValidity
|
||||
import com.flagship.abox.manager.domain.Username
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
class AbsdkSessionBridgeTest {
|
||||
private val profile = ProviderProfileId("abox")
|
||||
private val otherProfile = ProviderProfileId("other")
|
||||
private val credentials = LoginCredentials(Username("demo"), Password("secret"))
|
||||
|
||||
@Test
|
||||
fun loginPublishesSessionAndLogoutClearsOnlyLocalState() = runTest {
|
||||
val client = ScriptedAbsdkClient()
|
||||
val bridge = bridge(client)
|
||||
|
||||
val login = bridge.login(profile, credentials)
|
||||
assertTrue(login is DomainResult.Success)
|
||||
assertEquals(Username("demo"), bridge.observeSession(profile).first()?.username)
|
||||
|
||||
assertEquals(DomainResult.Success(Unit), bridge.logout(profile))
|
||||
assertNull(bridge.observeSession(profile).first())
|
||||
|
||||
val deviceResult = bridge.getStatus(profile, DeviceName("socket"))
|
||||
assertTrue((deviceResult as DomainResult.Failure).error is DomainError.TokenInvalid)
|
||||
assertEquals(listOf("login:demo"), client.calls)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun failedLoginDoesNotPublishSession() = runTest {
|
||||
val client = ScriptedAbsdkClient().apply {
|
||||
loginResponse = ScriptedAbsdkClient.failure("20004", "invalid")
|
||||
}
|
||||
val bridge = bridge(client)
|
||||
|
||||
val result = bridge.login(profile, credentials)
|
||||
|
||||
assertTrue((result as DomainResult.Failure).error is DomainError.Authentication)
|
||||
assertNull(bridge.observeSession(profile).first())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun rejectsSecondProfileUntilLogout() = runTest {
|
||||
val client = ScriptedAbsdkClient()
|
||||
val bridge = bridge(client)
|
||||
bridge.login(profile, credentials)
|
||||
|
||||
val result = bridge.login(otherProfile, credentials)
|
||||
|
||||
assertTrue((result as DomainResult.Failure).error is DomainError.InvalidRequest)
|
||||
assertEquals(listOf("login:demo"), client.calls)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun validatesAndClearsInvalidSession() = runTest {
|
||||
val client = ScriptedAbsdkClient().apply {
|
||||
validationResponse = ScriptedAbsdkClient.success(mapOf("status" to "0"))
|
||||
}
|
||||
val bridge = bridge(client)
|
||||
bridge.login(profile, credentials)
|
||||
|
||||
val result = bridge.validateSession(profile)
|
||||
|
||||
assertEquals(DomainResult.Success(SessionValidity.INVALID), result)
|
||||
assertNull(bridge.observeSession(profile).first())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun tokenErrorClearsSession() = runTest {
|
||||
val client = ScriptedAbsdkClient().apply {
|
||||
validationResponse = ScriptedAbsdkClient.failure("10002", "expired")
|
||||
}
|
||||
val bridge = bridge(client)
|
||||
bridge.login(profile, credentials)
|
||||
|
||||
val result = bridge.validateSession(profile)
|
||||
|
||||
assertTrue((result as DomainResult.Failure).error is DomainError.TokenInvalid)
|
||||
assertNull(bridge.observeSession(profile).first())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun deviceTokenErrorClearsSessionAndBlocksFurtherCalls() = runTest {
|
||||
val client = ScriptedAbsdkClient().apply {
|
||||
socketStatusResponses = ArrayDeque(
|
||||
listOf(ScriptedAbsdkClient.failure("10002", "expired")),
|
||||
)
|
||||
}
|
||||
val bridge = bridge(client)
|
||||
bridge.login(profile, credentials)
|
||||
|
||||
val first = bridge.getStatus(profile, DeviceName("socket"))
|
||||
|
||||
assertTrue((first as DomainResult.Failure).error is DomainError.TokenInvalid)
|
||||
assertNull(bridge.observeSession(profile).first())
|
||||
val second = bridge.getStatus(profile, DeviceName("socket"))
|
||||
assertTrue((second as DomainResult.Failure).error is DomainError.TokenInvalid)
|
||||
assertEquals(listOf("login:demo", "socket-status:socket"), client.calls)
|
||||
}
|
||||
|
||||
private fun bridge(client: ScriptedAbsdkClient): AbsdkBridge =
|
||||
AbsdkBridge(client, Dispatchers.Unconfined)
|
||||
}
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
package com.flagship.abox.manager.absdk.bridge
|
||||
|
||||
import java.util.concurrent.CopyOnWriteArrayList
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
|
||||
internal class ScriptedAbsdkClient : AbsdkClient {
|
||||
val calls = CopyOnWriteArrayList<String>()
|
||||
val downloadedCodes = CopyOnWriteArrayList<AbsdkDownloadCode>()
|
||||
val activeCalls = AtomicInteger(0)
|
||||
val maxConcurrentCalls = AtomicInteger(0)
|
||||
|
||||
var loginResponse: AbsdkResponse? = success()
|
||||
var validationResponse: AbsdkResponse? = success(mapOf("status" to "1"))
|
||||
var socketStatusResponses = ArrayDeque(listOf(success(mapOf("status" to "0"))))
|
||||
var socketControlResponse: AbsdkResponse? = success()
|
||||
var temperatureResponse: AbsdkResponse? = success(
|
||||
mapOf("temperature" to "23.5", "humidity" to "45"),
|
||||
)
|
||||
var sendResponse: AbsdkResponse? = success()
|
||||
var learnResponse: AbsdkResponse? = success()
|
||||
var downloadResponse: AbsdkResponse? = success(mapOf("ret" to emptyList<AbsdkIrResult>()))
|
||||
var failure: Exception? = null
|
||||
var onCall: (() -> Unit)? = null
|
||||
|
||||
override fun login(username: String, password: String): AbsdkResponse? =
|
||||
record("login:$username") { loginResponse }
|
||||
|
||||
override fun validateToken(): AbsdkResponse? = record("validate") { validationResponse }
|
||||
|
||||
override fun getSocketStatus(deviceName: String): AbsdkResponse? =
|
||||
record("socket-status:$deviceName") {
|
||||
socketStatusResponses.removeFirstOrNull() ?: error("No socket status response scripted")
|
||||
}
|
||||
|
||||
override fun setSocketPower(deviceName: String, status: String): AbsdkResponse? =
|
||||
record("socket-power:$deviceName:$status") { socketControlResponse }
|
||||
|
||||
override fun getTemperatureHumidity(deviceName: String): AbsdkResponse? =
|
||||
record("temperature:$deviceName") { temperatureResponse }
|
||||
|
||||
override fun sendInfrared(deviceName: String, key: String): AbsdkResponse? =
|
||||
record("ir-send:$deviceName:$key") { sendResponse }
|
||||
|
||||
override fun learnInfrared(deviceName: String, key: String): AbsdkResponse? =
|
||||
record("ir-learn:$deviceName:$key") { learnResponse }
|
||||
|
||||
override fun downloadInfrared(codes: List<AbsdkDownloadCode>): AbsdkResponse? =
|
||||
record("ir-download") {
|
||||
downloadedCodes += codes
|
||||
downloadResponse
|
||||
}
|
||||
|
||||
private fun <T> record(name: String, block: () -> T): T {
|
||||
failure?.let { throw it }
|
||||
calls += name
|
||||
val concurrent = activeCalls.incrementAndGet()
|
||||
maxConcurrentCalls.updateAndGet { previous -> maxOf(previous, concurrent) }
|
||||
return try {
|
||||
onCall?.invoke()
|
||||
block()
|
||||
} finally {
|
||||
activeCalls.decrementAndGet()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun success(data: Map<String, Any?> = emptyMap()): AbsdkResponse =
|
||||
AbsdkResponse("00000", null, data)
|
||||
|
||||
fun failure(code: String, message: String = "failure"): AbsdkResponse =
|
||||
AbsdkResponse(code, message, emptyMap())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user