diff --git a/modules/healthconnectonfhir/src/main/java/edu/stanford/healthconnectonfhir/HealthConnectToFHIR.kt b/modules/healthconnectonfhir/src/main/java/edu/stanford/healthconnectonfhir/HealthConnectToFHIR.kt index 354aab9cb..1c853975b 100644 --- a/modules/healthconnectonfhir/src/main/java/edu/stanford/healthconnectonfhir/HealthConnectToFHIR.kt +++ b/modules/healthconnectonfhir/src/main/java/edu/stanford/healthconnectonfhir/HealthConnectToFHIR.kt @@ -5,6 +5,7 @@ import androidx.health.connect.client.records.BloodPressureRecord import androidx.health.connect.client.records.BodyTemperatureRecord import androidx.health.connect.client.records.HeartRateRecord import androidx.health.connect.client.records.HeightRecord +import androidx.health.connect.client.records.Record import androidx.health.connect.client.records.StepsRecord import androidx.health.connect.client.records.WeightRecord import org.hl7.fhir.r4.model.CodeableConcept @@ -14,21 +15,30 @@ import org.hl7.fhir.r4.model.Period import org.hl7.fhir.r4.model.Quantity import java.util.Date -fun T.toObservation( - code: String, - display: String, +fun T.createObservation( + categories: List? = null, + codings: List, unit: String, valueExtractor: T.() -> Double, periodExtractor: T.() -> Pair ): Observation { val observation = Observation() observation.status = Observation.ObservationStatus.FINAL - observation.code = CodeableConcept().addCoding( - Coding() - .setSystem("http://loinc.org") - .setCode(code) - .setDisplay(display) - ) + + if (categories != null) { + val categoryConcept = CodeableConcept() + categories.forEach { coding -> + categoryConcept.addCoding(coding) + } + observation.addCategory(categoryConcept) + } + + val codeableConcept = CodeableConcept() + codings.forEach { coding -> + codeableConcept.addCoding(coding) + } + + observation.code = codeableConcept val period = Period() val (start, end) = this.periodExtractor() @@ -42,9 +52,13 @@ fun T.toObservation( } inline fun StepsRecord.toObservation(): Observation { - return this.toObservation( - code = "55423-8", - display = "Number of steps", + return this.createObservation( + categories = listOf( + Coding().setSystem("http://terminology.hl7.org/CodeSystem/observation-category").setCode("activity").setDisplay("Activity") + ), + codings = listOf( + Coding().setSystem("http://loinc.org").setCode("55423-8").setDisplay("Number of steps") + ), unit = "steps", valueExtractor = { count.toDouble() }, periodExtractor = { Date.from(startTime) to Date.from(endTime) } @@ -52,28 +66,71 @@ inline fun StepsRecord.toObservation(): Observation { } inline fun WeightRecord.toObservation(): Observation { - return this.toObservation( - code = "29463-7", - display = "Body weight", + return this.createObservation( + categories = listOf( + Coding().setSystem("http://terminology.hl7.org/CodeSystem/observation-category").setCode("vital-signs").setDisplay("Vital Signs") + ), + codings = listOf( + Coding().setSystem("http://loinc.org").setCode("29463-7").setDisplay("Body weight") + ), unit = "g", valueExtractor = { weight.inGrams }, periodExtractor = { Date.from(time) to Date.from(time) } ) } +inline fun HeightRecord.toObservation(): Observation { + return this.createObservation( + categories = listOf( + Coding().setSystem("http://terminology.hl7.org/CodeSystem/observation-category").setCode("vital-signs").setDisplay("Vital Signs") + ), + codings = listOf( + Coding().setSystem("http://loinc.org").setCode("8302-2").setDisplay("Body height") + ), + unit = "m", + valueExtractor = { height.inMeters }, + periodExtractor = { Date.from(time) to Date.from(time) } + ) +} + inline fun ActiveCaloriesBurnedRecord.toObservation(): Observation { - return this.toObservation( - code = "41981-2", - display = "Calories burned", + return this.createObservation( + categories = listOf( + Coding().setSystem("http://terminology.hl7.org/CodeSystem/observation-category").setCode("activity").setDisplay("Activity") + ), + codings = listOf( + Coding().setSystem("http://loinc.org").setCode("41981-2").setDisplay("Calories burned") + ), unit = "kcal", valueExtractor = { energy.inCalories }, periodExtractor = { Date.from(startTime) to Date.from(endTime) } ) } +inline fun BodyTemperatureRecord.toObservation(): Observation { + return createObservation( + categories = listOf( + Coding().setSystem("http://terminology.hl7.org/CodeSystem/observation-category").setCode("vital-signs").setDisplay("Vital Signs") + ), + codings = listOf( + Coding().setSystem("http://loinc.org").setCode("8310-5").setDisplay("Body temperature") + ), + unit = "°C", + valueExtractor = { temperature.inCelsius }, + periodExtractor = { Date.from(time) to Date.from(time) } + ) +} + inline fun BloodPressureRecord.toObservation(): Observation { val observation = Observation() observation.status = Observation.ObservationStatus.FINAL + + observation.category = listOf( + CodeableConcept().addCoding( + Coding().setSystem("http://terminology.hl7.org/CodeSystem/observation-category").setCode("vital-signs").setDisplay("Vital Signs") + ) + ) + observation.code = CodeableConcept().addCoding( Coding() .setSystem("http://loinc.org") @@ -110,20 +167,17 @@ inline fun BloodPressureRecord.toObservation(): Observation { return observation } -inline fun HeightRecord.toObservation(): Observation { - return this.toObservation( - code = "8302-2", - display = "Body height", - unit = "m", - valueExtractor = { height.inMeters }, - periodExtractor = { Date.from(time) to Date.from(time) } - ) -} - inline fun HeartRateRecord.toObservations(): List { return samples.map { sample -> val observation = Observation() observation.status = Observation.ObservationStatus.FINAL + + observation.category = listOf( + CodeableConcept().addCoding( + Coding().setSystem("http://terminology.hl7.org/CodeSystem/observation-category").setCode("vital-signs").setDisplay("Vital Signs") + ) + ) + observation.code = CodeableConcept().addCoding( Coding() .setSystem("http://loinc.org") @@ -140,14 +194,4 @@ inline fun HeartRateRecord.toObservations(): List { observation } -} - -inline fun BodyTemperatureRecord.toObservation(): Observation { - return this.toObservation( - code = "8310-5", - display = "Body temperature", - unit = "°C", - valueExtractor = { temperature.inCelsius }, - periodExtractor = { Date.from(time) to Date.from(time) } - ) } \ No newline at end of file diff --git a/modules/healthconnectonfhir/src/test/java/edu/stanford/healthconnectonfhir/CreateObservationTests.kt b/modules/healthconnectonfhir/src/test/java/edu/stanford/healthconnectonfhir/CreateObservationTests.kt index 74e8d2701..8b6308d00 100644 --- a/modules/healthconnectonfhir/src/test/java/edu/stanford/healthconnectonfhir/CreateObservationTests.kt +++ b/modules/healthconnectonfhir/src/test/java/edu/stanford/healthconnectonfhir/CreateObservationTests.kt @@ -20,6 +20,7 @@ import org.junit.Test import org.junit.Assert.* import java.time.Instant +import java.time.ZoneOffset import java.util.Date /** @@ -42,8 +43,8 @@ class CreateObservationTests { count = 1000, startTime = Instant.parse("2023-05-18T10:15:30.00Z"), endTime = Instant.parse("2023-05-18T11:15:30.00Z"), - startZoneOffset = null, - endZoneOffset = null + startZoneOffset = ZoneOffset.UTC, + endZoneOffset = ZoneOffset.UTC ) val observation = stepsRecord.toObservation() @@ -52,6 +53,7 @@ class CreateObservationTests { assertEquals(Observation.ObservationStatus.FINAL, observation.status) assertEquals("55423-8", observation.code.codingFirstRep.code) + assertEquals("activity", observation.categoryFirstRep.codingFirstRep.code) assertEquals( Date.from(Instant.parse("2023-05-18T10:15:30.00Z")), (observation.effective as Period).start @@ -69,7 +71,7 @@ class CreateObservationTests { val weightRecord = WeightRecord( weight = Mass.kilograms(75.0), time = Instant.parse("2023-05-18T10:15:30.00Z"), - zoneOffset = null + zoneOffset = ZoneOffset.UTC ) val observation = weightRecord.toObservation() @@ -78,6 +80,7 @@ class CreateObservationTests { assertEquals(Observation.ObservationStatus.FINAL, observation.status) assertEquals("29463-7", observation.code.codingFirstRep.code) + assertEquals("vital-signs", observation.categoryFirstRep.codingFirstRep.code) assertEquals( Date.from(Instant.parse("2023-05-18T10:15:30.00Z")), (observation.effective as Period).start @@ -96,8 +99,8 @@ class CreateObservationTests { energy = Energy.calories(250.0), startTime = Instant.parse("2023-05-18T10:15:30.00Z"), endTime = Instant.parse("2023-05-18T11:15:30.00Z"), - startZoneOffset = null, - endZoneOffset = null + startZoneOffset = ZoneOffset.UTC, + endZoneOffset = ZoneOffset.UTC ) val observation = activeCaloriesBurnedRecord.toObservation() @@ -106,6 +109,7 @@ class CreateObservationTests { assertEquals(Observation.ObservationStatus.FINAL, observation.status) assertEquals("41981-2", observation.code.codingFirstRep.code) + assertEquals("activity", observation.categoryFirstRep.codingFirstRep.code) assertEquals( Date.from(Instant.parse("2023-05-18T10:15:30.00Z")), (observation.effective as Period).start @@ -124,7 +128,7 @@ class CreateObservationTests { time = Instant.parse("2023-05-18T10:15:30.00Z"), systolic = Pressure.millimetersOfMercury(120.0), diastolic = Pressure.millimetersOfMercury(80.0), - zoneOffset = null + zoneOffset = ZoneOffset.UTC ) val observation = bloodPressureRecord.toObservation() @@ -133,6 +137,7 @@ class CreateObservationTests { assertEquals(Observation.ObservationStatus.FINAL, observation.status) assertEquals("85354-9", observation.code.codingFirstRep.code) + assertEquals("vital-signs", observation.categoryFirstRep.codingFirstRep.code) assertEquals( Date.from(Instant.parse("2023-05-18T10:15:30.00Z")), (observation.effective as Period).start @@ -152,7 +157,7 @@ class CreateObservationTests { val heightRecord = HeightRecord( time = Instant.parse("2023-05-18T10:15:30.00Z"), height = Length.meters(1.75), - zoneOffset = null + zoneOffset = ZoneOffset.UTC ) val observation = heightRecord.toObservation() @@ -161,6 +166,7 @@ class CreateObservationTests { assertEquals(Observation.ObservationStatus.FINAL, observation.status) assertEquals("8302-2", observation.code.codingFirstRep.code) + assertEquals("vital-signs", observation.categoryFirstRep.codingFirstRep.code) assertEquals( Date.from(Instant.parse("2023-05-18T10:15:30.00Z")), (observation.effective as Period).start @@ -178,7 +184,7 @@ class CreateObservationTests { val bodyTemperatureRecord = BodyTemperatureRecord( time = Instant.parse("2023-05-18T10:15:30.00Z"), temperature = Temperature.celsius(37.5), - zoneOffset = null + zoneOffset = ZoneOffset.UTC ) val observation = bodyTemperatureRecord.toObservation() @@ -187,6 +193,7 @@ class CreateObservationTests { assertEquals(Observation.ObservationStatus.FINAL, observation.status) assertEquals("8310-5", observation.code.codingFirstRep.code) + assertEquals("vital-signs", observation.categoryFirstRep.codingFirstRep.code) assertEquals( Date.from(Instant.parse("2023-05-18T10:15:30.00Z")), (observation.effective as Period).start