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:
18
sdk/src/main/graphql/LocationTimeSeries.graphql
Normal file
18
sdk/src/main/graphql/LocationTimeSeries.graphql
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>>
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user