Ajout Android CLI
This commit is contained in:
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package iotmonitor
|
||||
|
||||
import java.time.Instant
|
||||
|
||||
/**
|
||||
* CLI minimale : `ingest` ou `measures`.
|
||||
*
|
||||
* Aucun secret en dur : tout vient de l'environnement (ou renvoie une
|
||||
* erreur lisible). L'état « connecté » est dérivé du code de statut HTTP.
|
||||
*/
|
||||
fun main(args: Array<String>) {
|
||||
val command = args.firstOrNull()
|
||||
?: run {
|
||||
println("usage: ingest | measures --device <id>")
|
||||
return
|
||||
}
|
||||
|
||||
val baseUrl = System.getenv("IOT_BACKEND_URL")
|
||||
?: run {
|
||||
println("erreur : IOT_BACKEND_URL manquant (ex. http://127.0.0.1:5000)")
|
||||
return
|
||||
}
|
||||
|
||||
val client = BackendClient(baseUrl)
|
||||
|
||||
when (command) {
|
||||
"ingest" -> runIngest(client)
|
||||
"measures" -> runMeasures(client, args)
|
||||
else -> println("commande inconnue : $command")
|
||||
}
|
||||
}
|
||||
|
||||
private fun runIngest(client: BackendClient) {
|
||||
val deviceId = System.getenv("IOT_DEVICE_ID")
|
||||
?: run {
|
||||
println("erreur : IOT_DEVICE_ID manquant")
|
||||
return
|
||||
}
|
||||
val secret = System.getenv("IOT_DEVICE_SECRET")
|
||||
?: run {
|
||||
println("erreur : IOT_DEVICE_SECRET manquant (ne jamais mettre en dur)")
|
||||
return
|
||||
}
|
||||
|
||||
val measure = Measure(
|
||||
device_id = deviceId,
|
||||
timestamp = Instant.now().toString(),
|
||||
light_value = (0..1023).random().toLong(), // valeur brute simulée
|
||||
)
|
||||
|
||||
val status = client.ingest(deviceId, secret, measure)
|
||||
println("état : ${if (status in 200..299) "connecté (accepté)" else "refusé (HTTP $status)"}")
|
||||
}
|
||||
|
||||
private fun runMeasures(client: BackendClient, args: Array<String>) {
|
||||
val deviceId = args.getOrNull(args.indexOf("--device") + 1)
|
||||
?: run {
|
||||
println("usage: measures --device <id>")
|
||||
return
|
||||
}
|
||||
val token = System.getenv("IOT_USER_TOKEN")
|
||||
?: run {
|
||||
println("erreur : IOT_USER_TOKEN manquant")
|
||||
return
|
||||
}
|
||||
|
||||
val measures = client.listMeasures(token, deviceId)
|
||||
if (measures.isEmpty()) {
|
||||
println("état : aucune mesure retournée ou accès refusé")
|
||||
} else {
|
||||
println("état : connecté — ${measures.size} mesure(s)")
|
||||
measures.forEach { m ->
|
||||
println(" device=${m.device_id} time=${m.timestamp} value=${m.light_value}")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package iotmonitor
|
||||
|
||||
/**
|
||||
* Modèle de données minimal (seulement les champs exposés par l'API,
|
||||
* pas de sur-exposition — module 04).
|
||||
*/
|
||||
data class Measure(
|
||||
val device_id: String,
|
||||
val timestamp: String,
|
||||
val light_value: Long,
|
||||
)
|
||||
Reference in New Issue
Block a user