ADD: Measurements

This commit is contained in:
sebastian 2026-06-28 16:06:26 +02:00
parent 078fed7155
commit 6518faaff1
5 changed files with 193 additions and 4 deletions

3
db.py
View File

@ -1,6 +1,9 @@
from sqlalchemy import create_engine from sqlalchemy import create_engine
from sqlmodel import SQLModel, Session from sqlmodel import SQLModel, Session
from models.station import Station
from models.measurement import IndoorMeasurement, OutdoorMeasurement
engine = create_engine("sqlite:///database.db") engine = create_engine("sqlite:///database.db")
SQLModel.metadata.create_all(engine) SQLModel.metadata.create_all(engine)

19
main.py
View File

@ -1,12 +1,14 @@
import functools import functools
from datetime import datetime
from fastapi import FastAPI, Depends from fastapi import FastAPI, Depends, Query
from sqlmodel import Session from sqlmodel import Session
from db import get_session from db import get_session
from models.measurement import IndoorMeasurementCreateRequest, MeasurementListResponse
from models.station import StationCreateRequest, StationCreateResponse, Station, StationListResponse, \ from models.station import StationCreateRequest, StationCreateResponse, Station, StationListResponse, \
StationUpdateResponse, StationUpdateRequest StationUpdateResponse, StationUpdateRequest
from services import stationService from services import stationService, measurementService
app = FastAPI() app = FastAPI()
@ -30,6 +32,19 @@ async def update_station(station_data: StationUpdateRequest, session: Session =
async def delete_station(station_id: int, session: Session = Depends(get_session)): async def delete_station(station_id: int, session: Session = Depends(get_session)):
stationService.delete_station(session, station_id) stationService.delete_station(session, station_id)
@app.post("/measurements/indoor", status_code=204)
async def create_indoor_measurement(data: IndoorMeasurementCreateRequest, session: Session = Depends(get_session)):
measurementService.push_indoor_measurement(session, data)
@app.get("/measurements", response_model=MeasurementListResponse, status_code=200)
async def get_measurements(
station_ids: list[int] | None = Query(default=None),
indoor: bool | None = Query(default=None),
from_timestamp: datetime | None = None,
to_timestamp: datetime | None = None,
limit: int = 100,
session: Session = Depends(get_session)):
return measurementService.get_measurements(session, station_ids, indoor, from_timestamp, to_timestamp, limit)
@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}"}

52
models/measurement.py Normal file
View File

@ -0,0 +1,52 @@
from datetime import datetime, timezone
from typing import List
from sqlmodel import SQLModel, Field
from models.station import StationListResponse
def utc_now() -> datetime:
return datetime.now(timezone.utc)
class IndoorMeasurement(SQLModel, table=True):
id: int | None = Field(default=None, primary_key=True)
station_id: int = Field(foreign_key="station.id")
timestamp: datetime = Field(default_factory=utc_now)
temperature: float | None= Field(default=None)
humidity: float| None = Field(default=None)
class OutdoorMeasurement(SQLModel, table=True):
id: int | None = Field(default=None, primary_key=True)
station_id : int= Field(foreign_key="station.id")
temperature: float| None = Field(default=None)
humidity: float | None = Field(default=None)
pressure: float| None = Field(default=None)
timestamp: datetime = Field(default_factory=utc_now)
class IndoorMeasurementCreateRequest(SQLModel):
mac: str
temperature: float
humidity: float
class OutdoorMeasurementCreateRequest(SQLModel):
mac: str
temperature: float
humidity: float | None = None
pressure: float | None = None
class MeasurementResponse(SQLModel):
temperature: float
humidity: float | None = None
pressure: float | None = None
timestamp: datetime
class StationMeasurementResponse(SQLModel):
station: StationListResponse
measurements: list[MeasurementResponse]
indoor: bool
class MeasurementListResponse(SQLModel):
stations: list[StationMeasurementResponse]

View File

@ -1,14 +1,17 @@
from datetime import datetime from datetime import datetime, timezone
from typing import Optional from typing import Optional
from sqlmodel import SQLModel, Field from sqlmodel import SQLModel, Field
def utc_now() -> datetime:
return datetime.now(timezone.utc)
class Station(SQLModel, table=True): class Station(SQLModel, table=True):
id: int = Field(default=None, primary_key=True) id: int = Field(default=None, primary_key=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=datetime.now) created_at: datetime = Field(default_factory=utc_now)
class StationCreateRequest(SQLModel): class StationCreateRequest(SQLModel):
name: str name: str

View File

@ -0,0 +1,116 @@
from datetime import datetime
from sqlmodel import Session, select
from models.measurement import IndoorMeasurement, IndoorMeasurementCreateRequest, OutdoorMeasurementCreateRequest, \
OutdoorMeasurement, MeasurementListResponse, StationMeasurementResponse, MeasurementResponse
from models.station import Station, StationCreateRequest, StationListResponse
from services import stationService
from coolname import generate_slug
def push_indoor_measurement(session: Session, raw_measurement: IndoorMeasurementCreateRequest):
statement = select(Station).where(Station.mac == raw_measurement.mac)
station = session.exec(statement).first()
if not station:
station = stationService.create_station(session, StationCreateRequest(
mac=raw_measurement.mac,
name=generate_slug(2)
))
measurement = IndoorMeasurement(
station_id=station.id,
temperature=raw_measurement.temperature,
humidity=raw_measurement.humidity
)
session.add(IndoorMeasurement.model_validate(measurement))
session.commit()
def push_outdoor_measurement(session: Session, raw_measurement: OutdoorMeasurementCreateRequest):
statement = select(Station).where(Station.mac == raw_measurement.mac)
station = session.exec(statement).first()
if not station:
station = stationService.create_station(session, StationCreateRequest(
mac=raw_measurement.mac,
name=generate_slug(2)
))
measurement = OutdoorMeasurement(
station_id=station.id,
temperature=raw_measurement.temperature,
humidity=raw_measurement.humidity,
pressure=raw_measurement.pressure
)
session.add(OutdoorMeasurement.model_validate(measurement))
session.commit()
from typing import Type, Union
def _query_measurements(
session: Session,
model: Type[Union[IndoorMeasurement, OutdoorMeasurement]],
indoor: bool,
station_ids: list[int] | None,
from_timestamp: datetime | None = None,
to_timestamp: datetime | None = None,
limit: int | None = None
) -> list[StationMeasurementResponse]:
statement = select(model)
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)
statement = statement.order_by(model.timestamp.desc()).limit(limit)
results = session.exec(statement).all()
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))
return [
StationMeasurementResponse(
station=StationListResponse.model_validate(session.get(Station, station_id)),
measurements=measurements,
indoor=indoor
)
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]:
return _query_measurements(session, IndoorMeasurement, True, station_ids, from_timestamp, to_timestamp, limit)
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):
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)
return MeasurementListResponse(
stations=[
*[StationMeasurementResponse(station=indoor_result.station, measurements=indoor_result.measurements, indoor=True) for indoor_result in indoor_results],
*[StationMeasurementResponse(station=outdoor_result.station, measurements=outdoor_result.measurements, indoor=False) for outdoor_result in outdoor_results],
]
)
else:
if indoor:
indoor_results = get_indoor_measurements(session, station_ids, from_timestamp, to_timestamp, limit)
return MeasurementListResponse(
stations=[
*[StationMeasurementResponse(station=indoor_result.station, measurements=indoor_result.measurements, indoor=True) for indoor_result in indoor_results],
]
)
else:
outdoor_results = get_outdoor_measurements(session, station_ids, from_timestamp, to_timestamp, limit)
return MeasurementListResponse(
stations=[
*[StationMeasurementResponse(station=outdoor_result.station, measurements=outdoor_result.measurements, indoor=False) for outdoor_result in outdoor_results],
]
)