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
}
| Method | HTTP | Path | Params / Body | Returns |
|---|---|---|---|---|
getCommandTypes | GET | api/commands/types | @Query deviceId: Int | CommandTypesDto |
getAllDefinitions | GET | api/commands/definitions | — | List<CommandDefinitionDto> |
getDefinitions | GET | api/commands/definitions | @Query model: String | List<CommandDefinitionDto> |
sendCommand | POST | api/commands/send | @Body SendCommandRequest | CommandSendResultDto |
getCommandStatus | GET | api/commands/status/{entryId} | @Path entryId: String | CommandStatusDto |
getHistory | GET | api/commands/history/device/{deviceId} | @Path deviceId: Int | CommandHistoryResponseDto |
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
| Method | Input | Output |
|---|---|---|
toDomain(dto: CommandDto) | CommandDto | Command |
toDomain(dtos: List<CommandDto>) | List<CommandDto> | List<Command> |
toTypesResponse(dto: CommandTypesDto) | CommandTypesDto | CommandTypesResponse |
toAllTypesResponse(dto: AllCommandTypesDto) | AllCommandTypesDto | AllCommandTypesResponse |
toDefinition(dto: CommandDefinitionDto) | CommandDefinitionDto | CommandDefinition |
toSendResult(dto: CommandSendResultDto) | CommandSendResultDto | CommandSendResult |
toStatusResult(dto: CommandStatusDto) | CommandStatusDto | CommandStatusResult |
toHistoryEntry(dto: CommandHistoryEntryDto) | CommandHistoryEntryDto | CommandHistoryEntry |
toHistoryResponse(dto: CommandHistoryResponseDto) | CommandHistoryResponseDto | CommandHistoryResponse |
Key Mapping Logic
toDomain(CommandDto) — maps every DTO field to the domain Command. Notable transformations:
status→CommandStatus.fromString(dto.status)(handlesnull→UNKNOWN)attributes→ defaults toemptyMap()whennullsentAt,completedAt→ parsed viaInstant.parse(), remainsnullon absent values
toTypesResponse — maps CommandTypesDto to CommandTypesResponse. The commands field defaults to emptyList() when null.
toDefinition — maps CommandDefinitionDto to CommandDefinition:
type→ usesdto.resolvedType(preferscommandTypeovertype)category→CommandCategory.fromString(dto.category)(defaults toOTHER)channels→ defaults tolistOf("tcp")whennullparameters→ defaults toemptyList()whennull; each entry mapped toCommandParameterparamConfig→ delegated to privatetoParamConfig(), which recursively mapsParamFieldDtoviatoParamField()
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
| Method | Input | Output |
|---|---|---|
toParamConfig(dto: ParamConfigDto) | ParamConfigDto | ParamConfig |
toParamField(dto: ParamFieldDto) | ParamFieldDto | ParamField |
parseInstant(dateString: String) | String | Instant |
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
| Method | Strategy |
|---|---|
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:
HttpException→mapHttpException()→NetworkException("HTTP error: {code}").IOException→NetworkExceptionwith a contextual message.getDefinitionsadditionally catches mapping exceptions per-DTO and re-throws asCommandMappingException(commandType, message, cause).
Data Flow
API Request: ViewModel → CommandRepository → CommandApi → Backend
API Response: Backend → CommandApi → CommandMapper → Domain entity → ViewModel