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