mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge.git
synced 2026-07-31 07:44:24 +02:00
Health Connect: Use per-timestamp timezone offset for DST correctness
Replace single sync-time ZoneOffset with ZoneId, resolving the correct offset per record timestamp via ZoneId.rules.getOffset(instant). Fixes wrong offset metadata on records crossing DST boundaries (e.g. sleep sessions starting in CET and ending in CEST). Also fixes leading space in device model name after manufacturer prefix strip.
This commit is contained in:
+4
-4
@@ -238,7 +238,7 @@ class HealthConnectUtils {
|
||||
val recordsSyncedByType = mutableMapOf<String, Int>() // Track records per data type
|
||||
val dataTypesWithErrors = mutableSetOf<String>() // Track which data types had errors
|
||||
|
||||
val offset = ZonedDateTime.now(TimeZone.getDefault().toZoneId()).offset
|
||||
val zoneId = TimeZone.getDefault().toZoneId()
|
||||
val manager = GBApplication.app().deviceManager
|
||||
val syncIntervalInSeconds: Long = 24 * 60 * 60 // 1 day slices
|
||||
val lookBackInSeconds: Long = 24 * 60 * 60
|
||||
@@ -263,7 +263,7 @@ class HealthConnectUtils {
|
||||
val manufacturer = deviceCoordinator.manufacturer
|
||||
var deviceName = context.getString(deviceCoordinator.deviceNameResource)
|
||||
if (deviceName.startsWith(manufacturer) && deviceName != manufacturer) {
|
||||
deviceName = deviceName.replace(manufacturer, "");
|
||||
deviceName = deviceName.replace(manufacturer, "").trim();
|
||||
}
|
||||
val device = Device(
|
||||
type = when (deviceCoordinator.getDeviceKind(gbDevice)) {
|
||||
@@ -393,7 +393,7 @@ class HealthConnectUtils {
|
||||
healthConnectClient = healthConnectClient,
|
||||
gbDevice = gbDevice,
|
||||
metadata = metadata,
|
||||
offset = offset,
|
||||
offset = zoneId,
|
||||
currentSliceStartTs = currentSliceStartTs,
|
||||
currentSliceEndTs = currentSliceEndTs,
|
||||
grantedPermissions = grantedPermissions,
|
||||
@@ -575,7 +575,7 @@ class HealthConnectUtils {
|
||||
healthConnectClient: HealthConnectClient,
|
||||
gbDevice: GBDevice,
|
||||
metadata: Metadata,
|
||||
offset: java.time.ZoneOffset,
|
||||
offset: java.time.ZoneId,
|
||||
currentSliceStartTs: Instant,
|
||||
currentSliceEndTs: Instant,
|
||||
grantedPermissions: Set<String>,
|
||||
|
||||
+3
-2
@@ -25,6 +25,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.healthconnect.HealthConnectUtils
|
||||
import org.slf4j.Logger
|
||||
import java.time.Instant
|
||||
import java.time.ZoneId
|
||||
import java.time.ZoneOffset
|
||||
import java.time.temporal.ChronoUnit
|
||||
import kotlin.reflect.KClass
|
||||
@@ -44,7 +45,7 @@ internal abstract class AbstractActivitySampleSyncer<TRecord : Record> : Activit
|
||||
healthConnectClient: HealthConnectClient,
|
||||
gbDevice: GBDevice,
|
||||
metadata: Metadata,
|
||||
offset: ZoneOffset,
|
||||
offset: ZoneId,
|
||||
sliceStartBoundary: Instant,
|
||||
sliceEndBoundary: Instant,
|
||||
grantedPermissions: Set<String>,
|
||||
@@ -89,7 +90,7 @@ internal abstract class AbstractActivitySampleSyncer<TRecord : Record> : Activit
|
||||
return@forEach
|
||||
}
|
||||
|
||||
val record = convertSample(sample = currentSample, offset, metadata, deviceName)
|
||||
val record = convertSample(sample = currentSample, offset.rules.getOffset(endTs), metadata, deviceName)
|
||||
if (record == null) {
|
||||
skippedCount++
|
||||
return@forEach
|
||||
|
||||
+4
-2
@@ -29,6 +29,7 @@ import nodomain.freeyourgadget.gadgetbridge.util.healthconnect.HealthConnectUtil
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.healthconnect.SyncException
|
||||
import org.slf4j.Logger
|
||||
import java.time.Instant
|
||||
import java.time.ZoneId
|
||||
import java.time.ZoneOffset
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
@@ -59,7 +60,7 @@ internal abstract class AbstractTimeSampleSyncer<TSample : TimeSample, TRecord :
|
||||
healthConnectClient: HealthConnectClient,
|
||||
gbDevice: GBDevice,
|
||||
metadata: Metadata,
|
||||
offset: ZoneOffset,
|
||||
offset: ZoneId,
|
||||
sliceStartBoundary: Instant,
|
||||
sliceEndBoundary: Instant,
|
||||
grantedPermissions: Set<String>
|
||||
@@ -114,9 +115,10 @@ internal abstract class AbstractTimeSampleSyncer<TSample : TimeSample, TRecord :
|
||||
}
|
||||
return@filter true
|
||||
}.mapNotNull { sample ->
|
||||
val sampleOffset = offset.rules.getOffset(Instant.ofEpochMilli(sample.timestamp))
|
||||
convertSample(
|
||||
sample,
|
||||
offset,
|
||||
sampleOffset,
|
||||
metadata,
|
||||
deviceName
|
||||
)?.also {
|
||||
|
||||
+4
-4
@@ -22,7 +22,7 @@ import androidx.health.connect.client.records.metadata.Metadata
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample
|
||||
import java.time.Instant
|
||||
import java.time.ZoneOffset
|
||||
import java.time.ZoneId
|
||||
|
||||
/**
|
||||
* Statistics returned by a syncer after processing a slice.
|
||||
@@ -46,7 +46,7 @@ internal sealed interface HealthConnectSyncer {
|
||||
healthConnectClient: HealthConnectClient,
|
||||
gbDevice: GBDevice,
|
||||
metadata: Metadata,
|
||||
offset: ZoneOffset,
|
||||
offset: ZoneId,
|
||||
sliceStartBoundary: Instant,
|
||||
sliceEndBoundary: Instant,
|
||||
grantedPermissions: Set<String>
|
||||
@@ -62,7 +62,7 @@ internal interface ActivitySampleSyncer {
|
||||
healthConnectClient: HealthConnectClient,
|
||||
gbDevice: GBDevice,
|
||||
metadata: Metadata,
|
||||
offset: ZoneOffset,
|
||||
offset: ZoneId,
|
||||
sliceStartBoundary: Instant,
|
||||
sliceEndBoundary: Instant,
|
||||
grantedPermissions: Set<String>,
|
||||
@@ -79,7 +79,7 @@ internal interface ContextualActivitySampleSyncer {
|
||||
healthConnectClient: HealthConnectClient,
|
||||
gbDevice: GBDevice,
|
||||
metadata: Metadata,
|
||||
offset: ZoneOffset,
|
||||
offset: ZoneId,
|
||||
sliceStartBoundary: Instant,
|
||||
sliceEndBoundary: Instant,
|
||||
grantedPermissions: Set<String>,
|
||||
|
||||
+4
-3
@@ -26,6 +26,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.healthconnect.HealthConnectUtils
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.time.Instant
|
||||
import java.time.ZoneId
|
||||
import java.time.ZoneOffset
|
||||
import java.time.ZonedDateTime
|
||||
|
||||
@@ -36,7 +37,7 @@ internal object HeartRateSyncer : ActivitySampleSyncer {
|
||||
healthConnectClient: HealthConnectClient,
|
||||
gbDevice: GBDevice,
|
||||
metadata: Metadata,
|
||||
offset: ZoneOffset,
|
||||
offset: ZoneId,
|
||||
sliceStartBoundary: Instant,
|
||||
sliceEndBoundary: Instant,
|
||||
grantedPermissions: Set<String>,
|
||||
@@ -99,7 +100,7 @@ internal object HeartRateSyncer : ActivitySampleSyncer {
|
||||
recordEndTime,
|
||||
currentHcSamples.size
|
||||
)
|
||||
heartRateRecordList.add(HeartRateRecord(recordStartTime, offset, recordEndTime, offset, ArrayList(currentHcSamples), metadata))
|
||||
heartRateRecordList.add(HeartRateRecord(recordStartTime, offset.rules.getOffset(recordStartTime), recordEndTime, offset.rules.getOffset(recordEndTime), ArrayList(currentHcSamples), metadata))
|
||||
} else {
|
||||
LOG.warn("Skipping HeartRateRecord for device '$deviceName' from $recordStartTime to $recordEndTime due to invalid duration even after adjustment.")
|
||||
}
|
||||
@@ -126,7 +127,7 @@ internal object HeartRateSyncer : ActivitySampleSyncer {
|
||||
recordEndTime,
|
||||
currentHcSamples.size
|
||||
)
|
||||
heartRateRecordList.add(HeartRateRecord(recordStartTime, offset, recordEndTime, offset, ArrayList(currentHcSamples), metadata))
|
||||
heartRateRecordList.add(HeartRateRecord(recordStartTime, offset.rules.getOffset(recordStartTime), recordEndTime, offset.rules.getOffset(recordEndTime), ArrayList(currentHcSamples), metadata))
|
||||
} else {
|
||||
LOG.warn("Skipping final HeartRateRecord for device '$deviceName' from $recordStartTime to $recordEndTime due to invalid duration even after adjustment.")
|
||||
}
|
||||
|
||||
+4
-1
@@ -52,6 +52,7 @@ import nodomain.freeyourgadget.gadgetbridge.util.Prefs
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.healthconnect.HealthConnectUtils
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.time.Instant
|
||||
import java.time.ZoneId
|
||||
import java.time.ZoneOffset
|
||||
import java.util.Date
|
||||
|
||||
@@ -69,7 +70,7 @@ internal object RecordedWorkoutSyncer {
|
||||
healthConnectClient: HealthConnectClient,
|
||||
gbDevice: GBDevice,
|
||||
metadata: Metadata,
|
||||
offset: ZoneOffset,
|
||||
zoneId: ZoneId,
|
||||
sliceStartBoundary: Instant,
|
||||
sliceEndBoundary: Instant,
|
||||
grantedPermissions: Set<String>,
|
||||
@@ -116,6 +117,8 @@ internal object RecordedWorkoutSyncer {
|
||||
workoutsProcessedInThisSlice++
|
||||
LOG.info("Processing workout for device '$deviceName' (Type: ${workout.activityKind}, Start: $workoutStartInstant, End: $workoutEndInstant).")
|
||||
|
||||
val offset = zoneId.rules.getOffset(workoutStartInstant)
|
||||
|
||||
val recordsToInsert = mutableListOf<Record>()
|
||||
val activityKind = ActivityKind.fromCode(workout.activityKind)
|
||||
val exerciseType = WorkoutSyncerUtils.mapActivityKindToExerciseType(activityKind)
|
||||
|
||||
+5
-5
@@ -28,7 +28,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.healthconnect.HealthConnectUtils
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.time.Instant
|
||||
import java.time.ZoneOffset
|
||||
import java.time.ZoneId
|
||||
|
||||
private val LOG = LoggerFactory.getLogger("SleepSyncer")
|
||||
|
||||
@@ -38,7 +38,7 @@ internal object SleepSyncer : ContextualActivitySampleSyncer {
|
||||
healthConnectClient: HealthConnectClient,
|
||||
gbDevice: GBDevice,
|
||||
metadata: Metadata,
|
||||
offset: ZoneOffset,
|
||||
offset: ZoneId,
|
||||
sliceStartBoundary: Instant,
|
||||
sliceEndBoundary: Instant,
|
||||
grantedPermissions: Set<String>,
|
||||
@@ -114,7 +114,7 @@ internal object SleepSyncer : ContextualActivitySampleSyncer {
|
||||
sortedDeviceSamples: List<ActivitySample>,
|
||||
sliceStartBoundary: Instant,
|
||||
sliceEndBoundary: Instant,
|
||||
offset: ZoneOffset,
|
||||
offset: ZoneId,
|
||||
metadata: Metadata,
|
||||
context: Context,
|
||||
deviceName: String
|
||||
@@ -182,9 +182,9 @@ internal object SleepSyncer : ContextualActivitySampleSyncer {
|
||||
|
||||
return SleepSessionRecord(
|
||||
startTime = recordFinalStartTime,
|
||||
startZoneOffset = offset,
|
||||
startZoneOffset = offset.rules.getOffset(recordFinalStartTime),
|
||||
endTime = recordFinalEndTime,
|
||||
endZoneOffset = offset,
|
||||
endZoneOffset = offset.rules.getOffset(recordFinalEndTime),
|
||||
title = context.getString(nodomain.freeyourgadget.gadgetbridge.R.string.health_connect_sleep_session_title, deviceName),
|
||||
notes = context.getString(nodomain.freeyourgadget.gadgetbridge.R.string.health_connect_sleep_session_notes, deviceName),
|
||||
stages = stages,
|
||||
|
||||
+5
-5
@@ -31,7 +31,7 @@ import nodomain.freeyourgadget.gadgetbridge.util.healthconnect.HealthConnectUtil
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.healthconnect.SyncException
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.time.Instant
|
||||
import java.time.ZoneOffset
|
||||
import java.time.ZoneId
|
||||
import java.time.temporal.ChronoUnit
|
||||
|
||||
private val LOG = LoggerFactory.getLogger("TemperatureSyncer")
|
||||
@@ -47,7 +47,7 @@ internal object TemperatureSyncer : HealthConnectSyncer {
|
||||
healthConnectClient: HealthConnectClient,
|
||||
gbDevice: GBDevice,
|
||||
metadata: Metadata,
|
||||
offset: ZoneOffset,
|
||||
offset: ZoneId,
|
||||
sliceStartBoundary: Instant,
|
||||
sliceEndBoundary: Instant,
|
||||
grantedPermissions: Set<String>
|
||||
@@ -108,7 +108,7 @@ internal object TemperatureSyncer : HealthConnectSyncer {
|
||||
recordsToInsert.add(
|
||||
BodyTemperatureRecord(
|
||||
time = timestamp,
|
||||
zoneOffset = offset,
|
||||
zoneOffset = offset.rules.getOffset(timestamp),
|
||||
temperature = Temperature.celsius(sampleTemp),
|
||||
measurementLocation = sample.temperatureLocation,
|
||||
metadata = metadata
|
||||
@@ -178,9 +178,9 @@ internal object TemperatureSyncer : HealthConnectSyncer {
|
||||
recordsToInsert.add(
|
||||
SkinTemperatureRecord(
|
||||
startTime = recordStartTime,
|
||||
startZoneOffset = offset,
|
||||
startZoneOffset = offset.rules.getOffset(recordStartTime),
|
||||
endTime = recordEndTime,
|
||||
endZoneOffset = offset,
|
||||
endZoneOffset = offset.rules.getOffset(recordEndTime),
|
||||
deltas = deltas,
|
||||
baseline = Temperature.celsius(baselineForCurrentRecord),
|
||||
metadata = metadata
|
||||
|
||||
Reference in New Issue
Block a user