|
|
package net.aiterp.git.ykonsole2.infrastructure.indigo1
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import kotlinx.coroutines.future.await import me.lazmaid.kraph.Kraph import net.aiterp.git.ykonsole2.StorageException import net.aiterp.git.ykonsole2.application.logging.log import net.aiterp.git.ykonsole2.domain.models.Device import net.aiterp.git.ykonsole2.domain.models.Program import net.aiterp.git.ykonsole2.domain.models.Workout import net.aiterp.git.ykonsole2.domain.models.WorkoutState import net.aiterp.git.ykonsole2.infrastructure.ExportTarget import java.net.URI import java.net.http.HttpClient import java.net.http.HttpRequest import java.net.http.HttpRequest.BodyPublishers import java.net.http.HttpResponse.BodyHandlers import java.time.LocalDate import java.time.LocalTime import java.time.ZoneId import java.time.ZoneOffset import java.time.temporal.ChronoUnit import java.util.*
class Indigo1( private val endpoint: String, private val clientId: String, private val clientSecret: String, ) : ExportTarget { private val logger = log
override suspend fun isExported(workout: Workout): Boolean { val ids = run { query { fieldObject( "exercises", args = mapOf( "filter" to mapOf( "fromDate" to workout.createdAt.minus(7, ChronoUnit.DAYS).atZone(ZoneOffset.UTC).toLocalDate().toString(), "kindId" to 3, "tags" to listOf(tag("ykonsole:Version", "2"), tag("ykonsole:WorkoutID", workout.id)), ) ) ) { field("id") } } }.data.exercises ?: return false
return ids.isNotEmpty() }
override suspend fun export( workout: Workout, workoutStates: List<WorkoutState>, device: Device?, program: Program?, ) { logger.info("Creating exercise...") val exerciseId = run { mutation { fieldObject( "addExercise", args = mapOf( "options" to mapOf( "kindId" to 3, "partOfDayId" to workout.partOfDayId, "date" to workout.date.toString(), ), ), ) { field("id") } } }.data.addExercise?.id ?: throw StorageException("Failed to create exercise") logger.info("Created exercise with ID $exerciseId")
logger.info("Exporting states for exercise with ID $exerciseId...") for (chunk in workoutStates.chunked(100)) { val calories = chunk.mapNotNull { ws -> if (ws.calories != null) (ws.time.toInt() to ws.calories!!.toInt()) else null } val distance = chunk.mapNotNull { ws -> if (ws.distance != null) (ws.time.toInt() to ws.distance!!.toInt().toDouble() / 1000) else null }
if (calories.isNotEmpty()) { run { mutation { fieldObject( "addMeasurementBatch", args = mapOf( "exerciseId" to exerciseId, "options" to calories.map { mapOf("point" to it.first, "value" to it.second) }, ) ) { field("id") } } } }
if (distance.isNotEmpty()) { run { mutation { fieldObject( "addMetadataBatch", args = mapOf( "exerciseId" to exerciseId, "options" to distance.map { mapOf("point" to it.first, "kindId" to 5, "value" to it.second) }, ) ) { field("id") } } } } }
logger.info("Exporting tags for exercise with ID $exerciseId...") run { mutation { fieldObject( "addTagBatch", args = mapOf( "exerciseId" to exerciseId, "options" to listOfNotNull( tag("ykonsole:Version", "2"), tag("ykonsole:WorkoutID", workout.id), workout.message.takeIf(String::isNotBlank)?.let { tag("ykonsole:ErrorMessage", it) }, tag("ykonsole:DeviceID", workout.deviceId), device?.let { tag("ykonsole:DeviceName", it.name) }, program?.let { tag("ykonsole:ProgramID", it.id) }, program?.let { tag("ykonsole:ProgramName", it.name) }, tag("ykonsole:CreatedAt", "${workout.createdAt}"), ) ) ) { field("id") } } } }
private suspend fun run(func: Kraph.() -> Unit): Output { val query = Kraph { func() }
val request = HttpRequest.newBuilder() .uri(URI.create(endpoint)) .POST(BodyPublishers.ofString(query.toRequestString())) .header("Content-Type", "application/json") .header("Authorization", "Basic ${Base64.getEncoder().encodeToString("$clientId:$clientSecret".toByteArray())}") .build()
val response = HttpClient.newHttpClient() .sendAsync(request, BodyHandlers.ofString()) .await()
return jackson.readValue(response.body(), Output::class.java) }
private val jackson = jacksonObjectMapper()
private data class Output(val data: Data)
private data class Data( val addExercise: ExerciseOutput? = null, val addMeasurementBatch: List<ExerciseOutput>? = null, val addMetadataBatch: List<ExerciseOutput>? = null, val addTagBatch: List<ExerciseOutput>? = null, val exercises: List<ExerciseOutput>? = null, )
private data class ExerciseOutput(val id: Int)
private val Workout.partOfDayId get() = when (LocalTime.ofInstant(createdAt, zone).hour) { in 5..11 -> "M" in 12..17 -> "A" in 18..22 -> "E" else -> "N" } private val Workout.date get() = LocalDate.ofInstant(createdAt, zone)
private val zone = ZoneId.of("Europe/Oslo")
private fun tag(key: String, value: String) = mapOf("key" to key, "value" to value) }
|