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 sqlmodel import SQLModel, Session
from models.station import Station
from models.measurement import IndoorMeasurement, OutdoorMeasurement
engine = create_engine("sqlite:///database.db")
SQLModel.metadata.create_all(engine)

19
main.py
View File

@ -1,12 +1,14 @@
import functools
from datetime import datetime
from fastapi import FastAPI, Depends
from fastapi import FastAPI, Depends, Query
from sqlmodel import Session
from db import get_session
from models.measurement import IndoorMeasurementCreateRequest, MeasurementListResponse
from models.station import StationCreateRequest, StationCreateResponse, Station, StationListResponse, \
StationUpdateResponse, StationUpdateRequest
from services import stationService
from services import stationService, measurementService
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)):
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}")
async def say_hello(name: str):
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 sqlmodel import SQLModel, Field
def utc_now() -> datetime:
return datetime.now(timezone.utc)
class Station(SQLModel, table=True):
id: int = Field(default=None, primary_key=True)
mac: str = Field(unique=True)
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):
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],
]
)