Ajout Android CLI

This commit is contained in:
2026-09-25 15:16:08 +02:00
parent 7a49e73ed1
commit 1faace10d2
7 changed files with 186 additions and 0 deletions
@@ -0,0 +1,80 @@
package iotmonitor
import java.net.URI
import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpResponse
import java.time.Duration
/**
* Logique réseau minimale. Réutilisable telle quelle dans une MainActivity.
*
* Règles de sécurité (modules 03/04) :
* - le secret de device passe par en-tête, jamais loggé ;
* - on n'imprime que l'identité et le résultat ;
* - les erreurs sont remontées sans détail d'implémentation.
*/
class BackendClient(
private val baseUrl: String,
private val http: HttpClient = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(5))
.build(),
) {
/** Envoie une mesure en tant que device. Retourne le code de statut HTTP. */
fun ingest(deviceId: String, deviceSecret: String, measure: Measure): Int {
val body = buildIngestBody(measure)
val request = HttpRequest.newBuilder()
.uri(URI.create("$baseUrl/api/ingest"))
.header("Content-Type", "application/json")
.header("X-Device-ID", deviceId)
.header("X-Device-Secret", deviceSecret)
.POST(HttpRequest.BodyPublishers.ofString(body))
.build()
val response = http.send(request, HttpResponse.BodyHandlers.ofString())
// Le secret n'est jamais imprimé.
println("[ingest] device=$deviceId -> HTTP ${response.statusCode()}")
return response.statusCode()
}
/**
* Liste les mesures d'un device en tant qu'utilisateur.
* L'autorisation est décidée par le backend (module 03) : 403 => refus.
*/
fun listMeasures(userToken: String, deviceId: String): List<Measure> {
val request = HttpRequest.newBuilder()
.uri(URI.create("$baseUrl/api/measures?device_id=$deviceId"))
.header("Authorization", "Bearer $userToken")
.GET()
.build()
val response = http.send(request, HttpResponse.BodyHandlers.ofString())
println("[measures] device=$deviceId -> HTTP ${response.statusCode()}")
if (response.statusCode() != 200) {
return emptyList()
}
return parseMeasures(response.body())
}
private fun buildIngestBody(measure: Measure): String =
"""{"device_id":"${measure.device_id}","timestamp":"${measure.timestamp}", "light_value":${measure.light_value}}"""
private fun parseMeasures(body: String): List<Measure> {
// Parse très simple, sans dépendance JSON : on extrait chaque objet
// entre accolades (ordre des champs indifférent), puis les valeurs
// par clé. Suffisant pour le format minimal du backend.
val objectRegex = Regex("""\{[^{}]*\}""")
val fieldRegex = Regex(""""([a-z_]+)":"?([^",\}]+)"?""")
return objectRegex.findAll(body).mapNotNull { objMatch ->
val fields = fieldRegex.findAll(objMatch.value)
.associate { m -> m.groupValues[1] to m.groupValues[2] }
val deviceId = fields["device_id"] ?: return@mapNotNull null
val timestamp = fields["timestamp"] ?: return@mapNotNull null
val light = fields["light_value"]?.toLongOrNull() ?: return@mapNotNull null
Measure(device_id = deviceId, timestamp = timestamp, light_value = light)
}.toList()
}
}