UPD: Station Model and filtering and aggregating in sql not in python
This commit is contained in:
parent
67772affc3
commit
ed0d2897ce
14
main.py
14
main.py
@ -1,9 +1,11 @@
|
|||||||
import functools
|
import functools
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from typing import List
|
||||||
|
|
||||||
from fastapi import FastAPI, Depends, Query
|
from fastapi import FastAPI, Depends, Query
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from sqlmodel import Session
|
from sqlmodel import Session
|
||||||
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
|
||||||
from db import get_session
|
from db import get_session
|
||||||
from models.measurement import IndoorMeasurementCreateRequest, MeasurementListResponse, OutdoorMeasurementCreateRequest, \
|
from models.measurement import IndoorMeasurementCreateRequest, MeasurementListResponse, OutdoorMeasurementCreateRequest, \
|
||||||
@ -13,9 +15,15 @@ from models.station import StationCreateRequest, StationCreateResponse, Station,
|
|||||||
from services import stationService, measurementService
|
from services import stationService, measurementService
|
||||||
|
|
||||||
app = FastAPI()
|
app = FastAPI()
|
||||||
|
app.add_middleware(
|
||||||
|
CORSMiddleware,
|
||||||
|
allow_origins=["http://localhost:4200"],
|
||||||
|
allow_credentials=True,
|
||||||
|
allow_methods=["*"],
|
||||||
|
allow_headers=["*"],
|
||||||
|
)
|
||||||
|
|
||||||
|
@app.get("/stations/list", response_model=List[StationListResponse], status_code=201)
|
||||||
@app.get("/stations/list")
|
|
||||||
async def list_stations(session: Session = Depends(get_session)):
|
async def list_stations(session: Session = Depends(get_session)):
|
||||||
result = stationService.list_stations(session)
|
result = stationService.list_stations(session)
|
||||||
return [StationListResponse.model_validate(station) for station in result]
|
return [StationListResponse.model_validate(station) for station in result]
|
||||||
@ -48,6 +56,6 @@ async def get_measurements(
|
|||||||
from_timestamp: datetime | None = None,
|
from_timestamp: datetime | None = None,
|
||||||
to_timestamp: datetime | None = None,
|
to_timestamp: datetime | None = None,
|
||||||
resolution: MeasurementResolution = MeasurementResolution.hourly,
|
resolution: MeasurementResolution = MeasurementResolution.hourly,
|
||||||
limit: int = 100,
|
limit: int | None = None,
|
||||||
session: Session = Depends(get_session)):
|
session: Session = Depends(get_session)):
|
||||||
return measurementService.get_measurements(session, station_ids, indoor, from_timestamp, to_timestamp, limit, resolution)
|
return measurementService.get_measurements(session, station_ids, indoor, from_timestamp, to_timestamp, limit, resolution)
|
||||||
|
|||||||
@ -12,6 +12,7 @@ class Station(SQLModel, table=True):
|
|||||||
mac: str = Field(unique=True)
|
mac: str = Field(unique=True)
|
||||||
name: Optional[str] = Field(default=None)
|
name: Optional[str] = Field(default=None)
|
||||||
created_at: datetime = Field(default_factory=utc_now)
|
created_at: datetime = Field(default_factory=utc_now)
|
||||||
|
indoor_only: bool = Field(default=True)
|
||||||
|
|
||||||
class StationCreateRequest(SQLModel):
|
class StationCreateRequest(SQLModel):
|
||||||
name: str
|
name: str
|
||||||
@ -24,7 +25,8 @@ class StationCreateResponse(SQLModel):
|
|||||||
class StationListResponse(SQLModel):
|
class StationListResponse(SQLModel):
|
||||||
id: int
|
id: int
|
||||||
name: str
|
name: str
|
||||||
created_at: datetime
|
mac: str
|
||||||
|
indoor_only: bool
|
||||||
|
|
||||||
class StationUpdateRequest(SQLModel):
|
class StationUpdateRequest(SQLModel):
|
||||||
id: int
|
id: int
|
||||||
|
|||||||
@ -1,6 +1,4 @@
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from itertools import groupby
|
|
||||||
from statistics import mean
|
|
||||||
|
|
||||||
from sqlalchemy import func
|
from sqlalchemy import func
|
||||||
from sqlmodel import Session, select
|
from sqlmodel import Session, select
|
||||||
@ -52,32 +50,22 @@ 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:
|
def period_format(resolution: MeasurementResolution) -> str:
|
||||||
formats = {
|
return {
|
||||||
MeasurementResolution.hourly: "%Y-%m-%d %H",
|
MeasurementResolution.hourly: "%Y-%m-%d %H",
|
||||||
MeasurementResolution.daily: "%Y-%m-%d",
|
MeasurementResolution.daily: "%Y-%m-%d",
|
||||||
MeasurementResolution.weekly: "%Y-%W",
|
MeasurementResolution.weekly: "%Y-%W",
|
||||||
MeasurementResolution.monthly: "%Y-%m",
|
MeasurementResolution.monthly: "%Y-%m",
|
||||||
MeasurementResolution.yearly: "%Y",
|
MeasurementResolution.yearly: "%Y",
|
||||||
}
|
}[resolution]
|
||||||
return m.timestamp.strftime(formats[resolution])
|
from sqlalchemy import func
|
||||||
|
from sqlmodel import select
|
||||||
|
|
||||||
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[IndoorMeasurement | OutdoorMeasurement],
|
||||||
indoor: bool,
|
indoor: bool,
|
||||||
station_ids: list[int] | None,
|
station_ids: list[int] | None,
|
||||||
from_timestamp: datetime | None = None,
|
from_timestamp: datetime | None = None,
|
||||||
@ -85,27 +73,83 @@ def _query_measurements(
|
|||||||
limit: int | None = None,
|
limit: int | None = None,
|
||||||
resolution: MeasurementResolution = MeasurementResolution.hourly,
|
resolution: MeasurementResolution = MeasurementResolution.hourly,
|
||||||
) -> list[StationMeasurementResponse]:
|
) -> list[StationMeasurementResponse]:
|
||||||
|
|
||||||
|
#
|
||||||
|
# RAW
|
||||||
|
#
|
||||||
|
if resolution == MeasurementResolution.raw:
|
||||||
statement = select(model)
|
statement = select(model)
|
||||||
|
|
||||||
if station_ids:
|
if station_ids:
|
||||||
statement = statement.where(model.station_id.in_(station_ids))
|
statement = statement.where(model.station_id.in_(station_ids))
|
||||||
|
|
||||||
if from_timestamp:
|
if from_timestamp:
|
||||||
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())
|
statement = statement.order_by(model.timestamp.desc())
|
||||||
results = session.exec(statement).all()
|
|
||||||
|
if limit:
|
||||||
|
statement = statement.limit(limit)
|
||||||
|
|
||||||
|
rows = session.exec(statement).all()
|
||||||
|
|
||||||
grouped: dict[int, list[MeasurementResponse]] = {}
|
grouped: dict[int, list[MeasurementResponse]] = {}
|
||||||
for m in results:
|
|
||||||
if m.station_id not in grouped:
|
|
||||||
grouped[m.station_id] = []
|
|
||||||
grouped[m.station_id].append(MeasurementResponse.model_validate(m))
|
|
||||||
|
|
||||||
if resolution != MeasurementResolution.raw:
|
for row in rows:
|
||||||
grouped = {
|
grouped.setdefault(row.station_id, []).append(
|
||||||
station_id: aggregate(measurements, resolution)
|
MeasurementResponse.model_validate(row)
|
||||||
for station_id, measurements in grouped.items()
|
)
|
||||||
}
|
|
||||||
|
#
|
||||||
|
# AGGREGATED
|
||||||
|
#
|
||||||
|
else:
|
||||||
|
period = func.strftime(period_format(resolution), model.timestamp)
|
||||||
|
|
||||||
|
columns = [
|
||||||
|
model.station_id.label("station_id"),
|
||||||
|
func.max(model.timestamp).label("timestamp"),
|
||||||
|
func.round(func.avg(model.temperature), 1).label("temperature"),
|
||||||
|
func.round(func.avg(model.humidity), 1).label("humidity"),
|
||||||
|
]
|
||||||
|
|
||||||
|
if model is OutdoorMeasurement:
|
||||||
|
columns.append(
|
||||||
|
func.round(func.avg(model.pressure), 1).label("pressure")
|
||||||
|
)
|
||||||
|
|
||||||
|
statement = (
|
||||||
|
select(*columns)
|
||||||
|
.group_by(model.station_id, period)
|
||||||
|
.order_by(func.max(model.timestamp).desc())
|
||||||
|
)
|
||||||
|
|
||||||
|
if station_ids:
|
||||||
|
statement = statement.where(model.station_id.in_(station_ids))
|
||||||
|
|
||||||
|
if from_timestamp:
|
||||||
|
statement = statement.where(model.timestamp >= from_timestamp)
|
||||||
|
|
||||||
|
if to_timestamp:
|
||||||
|
statement = statement.where(model.timestamp <= to_timestamp)
|
||||||
|
|
||||||
|
print(str(statement.compile(compile_kwargs={"literal_binds": True})))
|
||||||
|
rows = session.exec(statement).all()
|
||||||
|
|
||||||
|
grouped: dict[int, list[MeasurementResponse]] = {}
|
||||||
|
|
||||||
|
for row in rows:
|
||||||
|
grouped.setdefault(row.station_id, []).append(
|
||||||
|
MeasurementResponse(
|
||||||
|
timestamp=row.timestamp,
|
||||||
|
temperature=row.temperature,
|
||||||
|
humidity=row.humidity,
|
||||||
|
pressure=getattr(row, "pressure", None),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
if limit:
|
if limit:
|
||||||
grouped = {
|
grouped = {
|
||||||
@ -113,25 +157,37 @@ def _query_measurements(
|
|||||||
for station_id, measurements in grouped.items()
|
for station_id, measurements in grouped.items()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#
|
||||||
|
# Stations laden
|
||||||
|
#
|
||||||
|
station_ids = list(grouped.keys())
|
||||||
|
|
||||||
|
stations = {
|
||||||
|
s.id: s
|
||||||
|
for s in session.exec(
|
||||||
|
select(Station).where(Station.id.in_(station_ids))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
StationMeasurementResponse(
|
StationMeasurementResponse(
|
||||||
station=StationListResponse.model_validate(session.get(Station, station_id)),
|
station=StationListResponse.model_validate(stations[station_id]),
|
||||||
measurements=measurements,
|
measurements=measurements,
|
||||||
indoor=indoor
|
indoor=indoor,
|
||||||
)
|
)
|
||||||
for station_id, measurements in grouped.items()
|
for station_id, measurements in grouped.items()
|
||||||
]
|
]
|
||||||
|
|
||||||
def get_indoor_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_indoor_measurements(session: Session, station_ids: list[int] | None, from_timestamp: datetime | None = None, to_timestamp: datetime | None = None, limit: int | None = None, resolution: MeasurementResolution = MeasurementResolution.hourly) -> list[StationMeasurementResponse]:
|
||||||
return _query_measurements(session, IndoorMeasurement, True, station_ids, from_timestamp, to_timestamp, limit)
|
return _query_measurements(session, IndoorMeasurement, True, station_ids, from_timestamp, to_timestamp, limit, resolution)
|
||||||
|
|
||||||
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, resolution: MeasurementResolution = MeasurementResolution.hourly) -> 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, resolution)
|
||||||
|
|
||||||
def get_measurements(session: Session, station_ids: list[int] | None, indoor: bool, from_timestamp: datetime | None, to_timestamp: datetime | None,limit: int, resolution: MeasurementResolution ):
|
def get_measurements(session: Session, station_ids: list[int] | None, indoor: bool, from_timestamp: datetime | None, to_timestamp: datetime | None,limit: int | None, 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, resolution)
|
||||||
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, resolution)
|
||||||
return MeasurementListResponse(
|
return MeasurementListResponse(
|
||||||
stations=[
|
stations=[
|
||||||
*[StationMeasurementResponse(station=indoor_result.station, measurements=indoor_result.measurements, indoor=True) for indoor_result in indoor_results],
|
*[StationMeasurementResponse(station=indoor_result.station, measurements=indoor_result.measurements, indoor=True) for indoor_result in indoor_results],
|
||||||
@ -140,14 +196,14 @@ def get_measurements(session: Session, station_ids: list[int] | None, indoor: bo
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
if indoor:
|
if indoor:
|
||||||
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, resolution)
|
||||||
return MeasurementListResponse(
|
return MeasurementListResponse(
|
||||||
stations=[
|
stations=[
|
||||||
*[StationMeasurementResponse(station=indoor_result.station, measurements=indoor_result.measurements, indoor=True) for indoor_result in indoor_results],
|
*[StationMeasurementResponse(station=indoor_result.station, measurements=indoor_result.measurements, indoor=True) for indoor_result in indoor_results],
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
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, resolution)
|
||||||
return MeasurementListResponse(
|
return MeasurementListResponse(
|
||||||
stations=[
|
stations=[
|
||||||
*[StationMeasurementResponse(station=outdoor_result.station, measurements=outdoor_result.measurements, indoor=False) for outdoor_result in outdoor_results],
|
*[StationMeasurementResponse(station=outdoor_result.station, measurements=outdoor_result.measurements, indoor=False) for outdoor_result in outdoor_results],
|
||||||
|
|||||||
@ -99,8 +99,8 @@ def generate_outdoor_rows(station_id: int, start: datetime, end: datetime) -> li
|
|||||||
# ── Main ──────────────────────────────────────────────────────────────────────
|
# ── Main ──────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
def simulate():
|
def simulate():
|
||||||
start = datetime(2024, 1, 1, 0, 0, tzinfo=timezone.utc)
|
start = datetime(2025, 1, 1, 0, 0, tzinfo=timezone.utc)
|
||||||
end = datetime(2024, 12, 31, 23, 59, tzinfo=timezone.utc)
|
end = datetime(2026, 6, 28, 20, 0, tzinfo=timezone.utc)
|
||||||
|
|
||||||
engine = create_engine(f"sqlite:///{DB_PATH}")
|
engine = create_engine(f"sqlite:///{DB_PATH}")
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user