ADD: Fetch Measurements

This commit is contained in:
sebastian 2026-06-28 16:55:51 +02:00
parent 706edeb429
commit 32fc8a031d
3 changed files with 55 additions and 6 deletions

View File

@ -2,10 +2,12 @@ import functools
from datetime import datetime from datetime import datetime
from fastapi import FastAPI, Depends, Query from fastapi import FastAPI, Depends, Query
from enum import Enum
from sqlmodel import Session from sqlmodel import Session
from db import get_session from db import get_session
from models.measurement import IndoorMeasurementCreateRequest, MeasurementListResponse, OutdoorMeasurementCreateRequest from models.measurement import IndoorMeasurementCreateRequest, MeasurementListResponse, OutdoorMeasurementCreateRequest, \
MeasurementResolution
from models.station import StationCreateRequest, StationCreateResponse, Station, StationListResponse, \ from models.station import StationCreateRequest, StationCreateResponse, Station, StationListResponse, \
StationUpdateResponse, StationUpdateRequest StationUpdateResponse, StationUpdateRequest
from services import stationService, measurementService from services import stationService, measurementService
@ -45,9 +47,10 @@ async def get_measurements(
indoor: bool | None = Query(default=None), indoor: bool | None = Query(default=None),
from_timestamp: datetime | None = None, from_timestamp: datetime | None = None,
to_timestamp: datetime | None = None, to_timestamp: datetime | None = None,
resolution: MeasurementResolution = MeasurementResolution.hourly,
limit: int = 100, limit: int = 100,
session: Session = Depends(get_session)): session: Session = Depends(get_session)):
return measurementService.get_measurements(session, station_ids, indoor, from_timestamp, to_timestamp, limit) return measurementService.get_measurements(session, station_ids, indoor, from_timestamp, to_timestamp, limit, resolution)
@app.get("/hello/{name}") @app.get("/hello/{name}")
async def say_hello(name: str): async def say_hello(name: str):
return {"message": f"Hello {name}"} return {"message": f"Hello {name}"}

View File

@ -1,4 +1,5 @@
from datetime import datetime, timezone from datetime import datetime, timezone
from enum import Enum
from typing import List from typing import List
from sqlmodel import SQLModel, Field from sqlmodel import SQLModel, Field
@ -9,6 +10,13 @@ from models.station import StationListResponse
def utc_now() -> datetime: def utc_now() -> datetime:
return datetime.now(timezone.utc) return datetime.now(timezone.utc)
class MeasurementResolution(str, Enum):
hourly = "hourly"
daily = "daily"
weekly = "weekly"
monthly = "monthly"
yearly = "yearly"
class IndoorMeasurement(SQLModel, table=True): class IndoorMeasurement(SQLModel, table=True):
id: int | None = Field(default=None, primary_key=True) id: int | None = Field(default=None, primary_key=True)

View File

@ -1,9 +1,12 @@
from datetime import datetime from datetime import datetime
from itertools import groupby
from statistics import mean
from sqlalchemy import func
from sqlmodel import Session, select from sqlmodel import Session, select
from models.measurement import IndoorMeasurement, IndoorMeasurementCreateRequest, OutdoorMeasurementCreateRequest, \ from models.measurement import IndoorMeasurement, IndoorMeasurementCreateRequest, OutdoorMeasurementCreateRequest, \
OutdoorMeasurement, MeasurementListResponse, StationMeasurementResponse, MeasurementResponse OutdoorMeasurement, MeasurementListResponse, StationMeasurementResponse, MeasurementResponse, MeasurementResolution
from models.station import Station, StationCreateRequest, StationListResponse from models.station import Station, StationCreateRequest, StationListResponse
from services import stationService from services import stationService
from coolname import generate_slug from coolname import generate_slug
@ -49,6 +52,29 @@ def push_outdoor_measurement(session: Session, raw_measurement: OutdoorMeasureme
session.commit() session.commit()
from typing import Type, Union from typing import Type, Union
def aggregate(measurements: list[MeasurementResponse], resolution: MeasurementResolution) -> list[MeasurementResponse]:
def period_key(m: MeasurementResponse) -> str:
formats = {
MeasurementResolution.hourly: "%Y-%m-%d %H",
MeasurementResolution.daily: "%Y-%m-%d",
MeasurementResolution.weekly: "%Y-%W",
MeasurementResolution.monthly: "%Y-%m",
MeasurementResolution.yearly: "%Y",
}
return m.timestamp.strftime(formats[resolution])
result = []
sorted_measurements = sorted(measurements, key=period_key)
for _, group in groupby(sorted_measurements, key=period_key):
group_list = list(group)
result.append(MeasurementResponse(
timestamp=group_list[0].timestamp,
temperature=round(mean(m.temperature for m in group_list if m.temperature is not None), 1),
humidity=round(mean(m.humidity for m in group_list if m.humidity is not None), 1) if any(m.humidity for m in group_list) else None,
pressure=round(mean(m.pressure for m in group_list if m.pressure is not None), 1) if any(m.pressure for m in group_list) else None,
))
return result
def _query_measurements( def _query_measurements(
session: Session, session: Session,
model: Type[Union[IndoorMeasurement, OutdoorMeasurement]], model: Type[Union[IndoorMeasurement, OutdoorMeasurement]],
@ -56,7 +82,8 @@ def _query_measurements(
station_ids: list[int] | None, station_ids: list[int] | None,
from_timestamp: datetime | None = None, from_timestamp: datetime | None = None,
to_timestamp: datetime | None = None, to_timestamp: datetime | None = None,
limit: int | None = None limit: int | None = None,
resolution: MeasurementResolution = MeasurementResolution.hourly,
) -> list[StationMeasurementResponse]: ) -> list[StationMeasurementResponse]:
statement = select(model) statement = select(model)
if station_ids: if station_ids:
@ -65,7 +92,7 @@ def _query_measurements(
statement = statement.where(model.timestamp >= from_timestamp) statement = statement.where(model.timestamp >= from_timestamp)
if to_timestamp: if to_timestamp:
statement = statement.where(model.timestamp <= to_timestamp) statement = statement.where(model.timestamp <= to_timestamp)
statement = statement.order_by(model.timestamp.desc()).limit(limit) statement = statement.order_by(model.timestamp.desc())
results = session.exec(statement).all() results = session.exec(statement).all()
grouped: dict[int, list[MeasurementResponse]] = {} grouped: dict[int, list[MeasurementResponse]] = {}
@ -74,6 +101,17 @@ def _query_measurements(
grouped[m.station_id] = [] grouped[m.station_id] = []
grouped[m.station_id].append(MeasurementResponse.model_validate(m)) grouped[m.station_id].append(MeasurementResponse.model_validate(m))
grouped = {
station_id: aggregate(measurements, resolution)
for station_id, measurements in grouped.items()
}
if limit:
grouped = {
station_id: measurements[:limit]
for station_id, measurements in grouped.items()
}
return [ return [
StationMeasurementResponse( StationMeasurementResponse(
station=StationListResponse.model_validate(session.get(Station, station_id)), station=StationListResponse.model_validate(session.get(Station, station_id)),
@ -89,7 +127,7 @@ def get_indoor_measurements(session: Session, station_ids: list[int] | None, fro
def get_outdoor_measurements(session: Session, station_ids: list[int] | None, from_timestamp: datetime | None = None, to_timestamp: datetime | None = None, limit: int | None = None) -> list[StationMeasurementResponse]: def get_outdoor_measurements(session: Session, station_ids: list[int] | None, from_timestamp: datetime | None = None, to_timestamp: datetime | None = None, limit: int | None = None) -> list[StationMeasurementResponse]:
return _query_measurements(session, OutdoorMeasurement, False, station_ids, from_timestamp, to_timestamp, limit) return _query_measurements(session, OutdoorMeasurement, False, station_ids, from_timestamp, to_timestamp, limit)
def get_measurements(session: Session, station_ids: list[int] | None, indoor: bool | None, from_timestamp: datetime | None = None, to_timestamp: datetime | None = None, limit: int | None = None): def get_measurements(session: Session, station_ids: list[int] | None, indoor: bool, from_timestamp: datetime | None, to_timestamp: datetime | None,limit: int, resolution: MeasurementResolution ):
if indoor is None: if indoor is None:
indoor_results = get_indoor_measurements(session, station_ids, from_timestamp, to_timestamp, limit) indoor_results = get_indoor_measurements(session, station_ids, from_timestamp, to_timestamp, limit)
outdoor_results = get_outdoor_measurements(session, station_ids, from_timestamp, to_timestamp, limit) outdoor_results = get_outdoor_measurements(session, station_ids, from_timestamp, to_timestamp, limit)