feat(map): load device time series for map panel chart

Add LocationTimeSeries GraphQL query and DeviceTimeSeriesRepository.

Extend DevicePanelState with chart data/loading/offset; MapViewModel fetches via location _id and maps with DashboardChartMapper.

Wire MapScreen preview with sample chart data.

Made-with: Cursor
This commit is contained in:
2026-04-06 22:20:04 +02:00
parent 9cbc521a0d
commit d34b3bf70e
8 changed files with 332 additions and 22 deletions

View File

@@ -0,0 +1,18 @@
query LocationTimeSeries($filter: LocationFilter!, $span: TimeSpan!) {
location(filter: $filter) {
_id
timeSeries(filter: $span) {
deviceId
time
Temp
Hum
Press
PMS1
PMS25
PMS10
radRg
PPM
IKAV
}
}
}

View File

@@ -0,0 +1,19 @@
package org.db3.airmq.sdk.dashboard
/**
* Fetches per-location sensor time series for map / device charts.
*/
interface DeviceTimeSeriesRepository {
/**
* @param intervalHours/Days/Minutes bucketing for aggregation (server-defined); pass 0 for unused axes.
*/
suspend fun fetchTimeSeries(
locationId: String,
tFromEpochMillis: Long,
tToEpochMillis: Long,
intervalHours: Int,
intervalDays: Int,
intervalMinutes: Int,
): Result<List<SensorSampleRow>>
}

View File

@@ -0,0 +1,70 @@
package org.db3.airmq.sdk.dashboard
import android.util.Log
import com.apollographql.apollo.ApolloClient
import com.apollographql.apollo.api.Optional
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import java.util.TimeZone
import javax.inject.Inject
import javax.inject.Singleton
import org.db3.airmq.sdk.LocationTimeSeriesQuery
import org.db3.airmq.sdk.type.LocationFilter
import org.db3.airmq.sdk.type.TimeSpan
@Singleton
class DeviceTimeSeriesRepositoryImpl @Inject constructor(
private val apolloClient: ApolloClient,
) : DeviceTimeSeriesRepository {
override suspend fun fetchTimeSeries(
locationId: String,
tFromEpochMillis: Long,
tToEpochMillis: Long,
intervalHours: Int,
intervalDays: Int,
intervalMinutes: Int,
): Result<List<SensorSampleRow>> = runCatching {
val filter = LocationFilter(
_id = Optional.Present(locationId),
)
val span = TimeSpan(
t_from = Optional.Present(formatUtcIso(tFromEpochMillis)),
t_to = Optional.Present(formatUtcIso(tToEpochMillis)),
interval_h = Optional.Present(intervalHours),
interval_d = Optional.Present(intervalDays),
interval_m = Optional.Present(intervalMinutes),
)
Log.d(
TAG,
"LocationTimeSeries locationId=$locationId t_from=${span.t_from} t_to=${span.t_to} " +
"ih=$intervalHours id=$intervalDays im=$intervalMinutes"
)
val response = apolloClient
.query(LocationTimeSeriesQuery(filter = filter, span = span))
.execute()
response.exception?.let { throw it }
response.errors?.firstOrNull()?.let { throw IllegalStateException(it.message) }
val series = response.data?.location?.timeSeries.orEmpty()
val rows = series.mapNotNull { row -> row?.toSensorSampleRow() }
Log.d(TAG, "LocationTimeSeries parsed ${rows.size} rows (raw points=${series.size})")
rows
}
private fun formatUtcIso(epochMillis: Long): String {
val sdf = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US)
sdf.timeZone = TimeZone.getTimeZone("UTC")
return sdf.format(Date(epochMillis))
}
private companion object {
private const val TAG = "DeviceTimeSeriesApi"
}
}

View File

@@ -0,0 +1,19 @@
package org.db3.airmq.sdk.dashboard
import org.db3.airmq.sdk.LocationTimeSeriesQuery
internal fun LocationTimeSeriesQuery.TimeSeries.toSensorSampleRow(): SensorSampleRow? {
val t = GraphqlDateTimeParser.parseToEpochMillis(time) ?: return null
return SensorSampleRow(
epochMillis = t,
temp = Temp?.toFloat(),
hum = Hum?.toFloat(),
press = Press?.toFloat(),
pms1 = PMS1?.toFloat(),
pms25 = PMS25?.toFloat(),
pms10 = PMS10?.toFloat(),
radRg = radRg?.toFloat(),
ppm = PPM?.toFloat(),
ikav = IKAV?.toFloat(),
)
}

View File

@@ -18,6 +18,8 @@ import org.db3.airmq.sdk.auth.SharedPreferencesApiTokenStore
import org.db3.airmq.sdk.auth.SharedPreferencesLocalEmailAuthStore
import org.db3.airmq.sdk.dashboard.DashboardMetricsRepository
import org.db3.airmq.sdk.dashboard.DashboardMetricsRepositoryImpl
import org.db3.airmq.sdk.dashboard.DeviceTimeSeriesRepository
import org.db3.airmq.sdk.dashboard.DeviceTimeSeriesRepositoryImpl
import org.db3.airmq.sdk.map.MapServiceImpl
import org.db3.airmq.sdk.map.MapService
import org.db3.airmq.sdk.settings.SettingsService
@@ -74,6 +76,12 @@ abstract class SDKBindModule {
impl: DashboardMetricsRepositoryImpl
): DashboardMetricsRepository
@Binds
@Singleton
abstract fun bindDeviceTimeSeriesRepository(
impl: DeviceTimeSeriesRepositoryImpl
): DeviceTimeSeriesRepository
@Binds
@Singleton
abstract fun bindSettingsService(impl: SettingsServiceImpl): SettingsService