Skip to main content

Command Service — Data Layer

Covers the command data pipeline: Retrofit API → DTOs → Mapper → Repository.


CommandApi

Retrofit interface at data/remote/api/CommandApi.kt. All methods are suspend.

interface CommandApi {

@GET("api/commands/types")
suspend fun getCommandTypes(@Query("deviceId") deviceId: Int): CommandTypesDto

@GET("api/commands/definitions")
suspend fun getAllDefinitions(): List<CommandDefinitionDto>

@GET("api/commands/definitions")
suspend fun getDefinitions(@Query("model") model: String): List<CommandDefinitionDto>

@POST("api/commands/send")
suspend fun sendCommand(@Body request: SendCommandRequest): CommandSendResultDto

@GET("api/commands/status/{entryId}")
suspend fun getCommandStatus(@Path("entryId") entryId: String): CommandStatusDto

@GET("api/commands/history/device/{deviceId}")
suspend fun getHistory(@Path("deviceId") deviceId: Int): CommandHistoryResponseDto
}
MethodHTTPPathParams / BodyReturns
getCommandTypesGETapi/commands/types@Query deviceId: IntCommandTypesDto
getAllDefinitionsGETapi/commands/definitionsList<CommandDefinitionDto>
getDefinitionsGETapi/commands/definitions@Query model: StringList<CommandDefinitionDto>
sendCommandPOSTapi/commands/send@Body SendCommandRequestCommandSendResultDto
getCommandStatusGETapi/commands/status/{entryId}@Path entryId: StringCommandStatusDto
getHistoryGETapi/commands/history/device/{deviceId}@Path deviceId: IntCommandHistoryResponseDto

getAllDefinitions and getDefinitions share the same endpoint path; the latter adds a model query parameter to filter results.


Command DTOs

All DTOs live in data/remote/dto/CommandDtos.kt.

Request Models

data class SendCommandRequest(
val deviceId: Int,
val type: String,
val attributes: Map<String, Any>? = null,
val channel: String = "tcp"
)

Response Models

data class CommandTypesDto(
val deviceId: Int? = null,
val model: String? = null,
val commands: List<String>? = null
)

data class AllCommandTypesDto(val types: List<String>)

data class CommandSendResultDto(
val entryId: String,
val status: String,
val queuedAt: String? = null
)

data class CommandStatusDto(
val entryId: String,
val status: String,
val result: String? = null
)

Command Definition Models

data class CommandDefinitionDto(
val id: Int? = null,
val model: String? = null,
val commandType: String? = null,
val type: String? = null,
val label: String,
val description: String? = null,
val defaultValue: String? = null,
val hasParams: Boolean = false,
val active: Boolean = true,
val category: String? = null,
val paramConfig: ParamConfigDto? = null,
val channels: List<String>? = null,
val parameters: List<CommandParameterDto>? = null
) {
val resolvedType: String get() = commandType ?: type ?: ""
}

data class CommandParameterDto(
val key: String,
val label: String,
val type: String,
val required: Boolean = false,
val defaultValue: Any? = null
)

data class ParamConfigDto(
val type: String? = null,
val min: Double? = null,
val max: Double? = null,
val step: Double? = null,
val unit: String? = null,
val options: List<String>? = null,
val placeholder: String? = null,
val fields: List<ParamFieldDto>? = null
)

data class ParamFieldDto(
val name: String,
val label: String,
val type: String? = null,
val min: Double? = null,
val max: Double? = null,
val unit: String? = null,
val options: List<String>? = null,
val defaultValue: String? = null,
val placeholder: String? = null
)

CommandDefinitionDto.resolvedType is a computed property that prefers commandType over type, falling back to "".

Command / History Models

data class CommandDto(
val id: Int,
val deviceId: Int,
val type: String,
val description: String? = null,
val textChannel: Boolean = false,
val attributes: Map<String, Any>? = null,
val status: String? = null,
val sentAt: String? = null,
val completedAt: String? = null
)

data class CommandHistoryEntryDto(
val id: Int,
val entryId: String?,
val commandType: String,
val rawCommand: String?,
val status: String,
val responseData: String?,
val createdAt: String,
val updatedAt: String?
)

data class CommandHistoryResponseDto(
val items: List<CommandHistoryEntryDto>,
val total: Int
)

CommandMapper

data/mappers/CommandMapper.kt@Singleton, injected with @Inject constructor().

Public Methods

MethodInputOutput
toDomain(dto: CommandDto)CommandDtoCommand
toDomain(dtos: List<CommandDto>)List<CommandDto>List<Command>
toTypesResponse(dto: CommandTypesDto)CommandTypesDtoCommandTypesResponse
toAllTypesResponse(dto: AllCommandTypesDto)AllCommandTypesDtoAllCommandTypesResponse
toDefinition(dto: CommandDefinitionDto)CommandDefinitionDtoCommandDefinition
toSendResult(dto: CommandSendResultDto)CommandSendResultDtoCommandSendResult
toStatusResult(dto: CommandStatusDto)CommandStatusDtoCommandStatusResult
toHistoryEntry(dto: CommandHistoryEntryDto)CommandHistoryEntryDtoCommandHistoryEntry
toHistoryResponse(dto: CommandHistoryResponseDto)CommandHistoryResponseDtoCommandHistoryResponse

Key Mapping Logic

toDomain(CommandDto) — maps every DTO field to the domain Command. Notable transformations:

  • statusCommandStatus.fromString(dto.status) (handles nullUNKNOWN)
  • attributes → defaults to emptyMap() when null
  • sentAt, completedAt → parsed via Instant.parse(), remains null on absent values

toTypesResponse — maps CommandTypesDto to CommandTypesResponse. The commands field defaults to emptyList() when null.

toDefinition — maps CommandDefinitionDto to CommandDefinition:

  • type → uses dto.resolvedType (prefers commandType over type)
  • categoryCommandCategory.fromString(dto.category) (defaults to OTHER)
  • channels → defaults to listOf("tcp") when null
  • parameters → defaults to emptyList() when null; each entry mapped to CommandParameter
  • paramConfig → delegated to private toParamConfig(), which recursively maps ParamFieldDto via toParamField()

toSendResult — maps CommandSendResultDto to CommandSendResult. Parses status via CommandStatus.fromString() and queuedAt via Instant.parse().

toStatusResult — maps CommandStatusDto to CommandStatusResult. Parses status via CommandStatus.fromString().

toHistoryEntry — maps CommandHistoryEntryDto to CommandHistoryEntry. createdAt is always parsed; updatedAt is parsed only when non-null.

toHistoryResponse — maps each item via toHistoryEntry() and passes through total.

Private Helpers

MethodInputOutput
toParamConfig(dto: ParamConfigDto)ParamConfigDtoParamConfig
toParamField(dto: ParamFieldDto)ParamFieldDtoParamField
parseInstant(dateString: String)StringInstant

parseInstant attempts Instant.parse() and falls back to Instant.now() on failure, logging a warning via Logger.warn().


CommandRepository

Interface at domain/repositories/CommandRepository.kt. Defines the domain contract.

interface CommandRepository {
suspend fun getCommandTypes(deviceId: Int): CommandTypesResponse
suspend fun getAllCommandTypes(): AllCommandTypesResponse
suspend fun getDefinitions(model: String): List<CommandDefinition>
suspend fun send(
deviceId: Int,
type: String,
attributes: Map<String, Any>? = null
): CommandSendResult
suspend fun getStatus(entryId: String): CommandStatusResult
suspend fun getHistory(deviceId: Int): CommandHistoryResponse
}

CommandRepositoryImpl

data/repositories/CommandRepositoryImpl.kt@Singleton.

Constructor (2 dependencies)

@Singleton
class CommandRepositoryImpl @Inject constructor(
private val commandApi: CommandApi,
private val commandMapper: CommandMapper
) : CommandRepository

Method Implementations

MethodStrategy
getCommandTypes(deviceId)Calls commandApi.getCommandTypes(deviceId), maps via commandMapper.toTypesResponse().
getAllCommandTypes()Calls commandApi.getAllDefinitions(), extracts distinct non-empty resolvedType values, wraps in AllCommandTypesResponse. Does not use commandMapper.toAllTypesResponse().
getDefinitions(model)Calls commandApi.getDefinitions(model), maps each DTO via commandMapper.toDefinition(). Wraps mapping failures in CommandMappingException.
send(deviceId, type, attributes)Builds a SendCommandRequest, calls commandApi.sendCommand(), maps via commandMapper.toSendResult().
getStatus(entryId)Calls commandApi.getCommandStatus(entryId), maps via commandMapper.toStatusResult().
getHistory(deviceId)Calls commandApi.getHistory(deviceId), maps via commandMapper.toHistoryResponse().

Error Handling

All methods follow a consistent pattern:

  • HttpExceptionmapHttpException()NetworkException("HTTP error: {code}").
  • IOExceptionNetworkException with a contextual message.
  • getDefinitions additionally catches mapping exceptions per-DTO and re-throws as CommandMappingException(commandType, message, cause).

Data Flow

API Request:   ViewModel → CommandRepository → CommandApi → Backend
API Response: Backend → CommandApi → CommandMapper → Domain entity → ViewModel