diff --git a/main.py b/main.py index f5c4948..0423ea7 100644 --- a/main.py +++ b/main.py @@ -2,10 +2,12 @@ import functools from datetime import datetime from fastapi import FastAPI, Depends, Query +from enum import Enum from sqlmodel import 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, \ StationUpdateResponse, StationUpdateRequest from services import stationService, measurementService @@ -45,9 +47,10 @@ async def get_measurements( indoor: bool | None = Query(default=None), from_timestamp: datetime | None = None, to_timestamp: datetime | None = None, + resolution: MeasurementResolution = MeasurementResolution.hourly, limit: int = 100, 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}") async def say_hello(name: str): return {"message": f"Hello {name}"} diff --git a/models/measurement.py b/models/measurement.py index 410d12a..15ec0dc 100644 --- a/models/measurement.py +++ b/models/measurement.py @@ -1,4 +1,5 @@ from datetime import datetime, timezone +from enum import Enum from typing import List from sqlmodel import SQLModel, Field @@ -9,6 +10,13 @@ from models.station import StationListResponse def utc_now() -> datetime: return datetime.now(timezone.utc) +class MeasurementResolution(str, Enum): + hourly = "hourly" + daily = "daily" + weekly = "weekly" + monthly = "monthly" + yearly = "yearly" + class IndoorMeasurement(SQLModel, table=True): id: int | None = Field(default=None, primary_key=True) diff --git a/services/measurementService.py b/services/measurementService.py index 2d3a2d9..32ebb2e 100644 --- a/services/measurementService.py +++ b/services/measurementService.py @@ -1,9 +1,12 @@ from datetime import datetime +from itertools import groupby +from statistics import mean +from sqlalchemy import func from sqlmodel import Session, select from models.measurement import IndoorMeasurement, IndoorMeasurementCreateRequest, OutdoorMeasurementCreateRequest, \ - OutdoorMeasurement, MeasurementListResponse, StationMeasurementResponse, MeasurementResponse + OutdoorMeasurement, MeasurementListResponse, StationMeasurementResponse, MeasurementResponse, MeasurementResolution from models.station import Station, StationCreateRequest, StationListResponse from services import stationService from coolname import generate_slug @@ -49,6 +52,29 @@ def push_outdoor_measurement(session: Session, raw_measurement: OutdoorMeasureme session.commit() 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( session: Session, model: Type[Union[IndoorMeasurement, OutdoorMeasurement]], @@ -56,7 +82,8 @@ def _query_measurements( station_ids: list[int] | None, from_timestamp: datetime | None = None, to_timestamp: datetime | None = None, - limit: int | None = None + limit: int | None = None, + resolution: MeasurementResolution = MeasurementResolution.hourly, ) -> list[StationMeasurementResponse]: statement = select(model) if station_ids: @@ -65,7 +92,7 @@ def _query_measurements( statement = statement.where(model.timestamp >= from_timestamp) if 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() grouped: dict[int, list[MeasurementResponse]] = {} @@ -74,6 +101,17 @@ def _query_measurements( grouped[m.station_id] = [] 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 [ StationMeasurementResponse( 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]: 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: 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)