You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

177 lines
5.8 KiB

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)
}