diff --git a/code/alembic/versions/ce7ce568b948_update_station_model.py b/code/alembic/versions/ce7ce568b948_update_station_model.py new file mode 100644 index 0000000..a28967a --- /dev/null +++ b/code/alembic/versions/ce7ce568b948_update_station_model.py @@ -0,0 +1,30 @@ +"""update Station model + +Revision ID: ce7ce568b948 +Revises: 5661cd0d79fd +Create Date: 2024-09-28 00:07:13.078189 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = 'ce7ce568b948' +down_revision: Union[str, None] = '5661cd0d79fd' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + pass + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + pass + # ### end Alembic commands ### diff --git a/code/alembic/versions/d37e07dca078_add_location_model.py b/code/alembic/versions/d37e07dca078_add_location_model.py new file mode 100644 index 0000000..92fe487 --- /dev/null +++ b/code/alembic/versions/d37e07dca078_add_location_model.py @@ -0,0 +1,52 @@ +"""Add Location model + +Revision ID: d37e07dca078 +Revises: ce7ce568b948 +Create Date: 2024-09-29 23:06:13.815037 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = 'd37e07dca078' +down_revision: Union[str, None] = 'ce7ce568b948' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('locations', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('lat', sa.Float(), nullable=True), + sa.Column('lon', sa.Float(), nullable=True), + sa.Column('height', sa.Float(), nullable=True), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_locations_id'), 'locations', ['id'], unique=False) + op.add_column('measurements', sa.Column('location_id', sa.Integer(), nullable=True)) + op.create_foreign_key(None, 'measurements', 'locations', ['location_id'], ['id']) + op.add_column('stations', sa.Column('location_id', sa.Integer(), nullable=True)) + op.create_foreign_key(None, 'stations', 'locations', ['location_id'], ['id']) + op.drop_column('stations', 'height') + op.drop_column('stations', 'lat') + op.drop_column('stations', 'lon') + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('stations', sa.Column('lon', sa.DOUBLE_PRECISION(precision=53), autoincrement=False, nullable=True)) + op.add_column('stations', sa.Column('lat', sa.DOUBLE_PRECISION(precision=53), autoincrement=False, nullable=True)) + op.add_column('stations', sa.Column('height', sa.DOUBLE_PRECISION(precision=53), autoincrement=False, nullable=True)) + op.drop_constraint(None, 'stations', type_='foreignkey') + op.drop_column('stations', 'location_id') + op.drop_constraint(None, 'measurements', type_='foreignkey') + op.drop_column('measurements', 'location_id') + op.drop_index(op.f('ix_locations_id'), table_name='locations') + op.drop_table('locations') + # ### end Alembic commands ### diff --git a/code/main.py b/code/main.py index 08a65f3..e227e03 100644 --- a/code/main.py +++ b/code/main.py @@ -9,11 +9,9 @@ from datetime import datetime, timedelta from zoneinfo import ZoneInfo import json -import csv -import io from schemas import StationDataCreate, SensorsCreate -from models import Station, Measurement, Values +from models import Measurement, Station, Values from enums import SensorModel @@ -30,6 +28,25 @@ allow_headers=["*"], ) + +# Helper function to get or create a location +def get_or_create_location(db: Session, lat: float, lon: float, height: float): + # Prüfen, ob bereits eine Location mit lat, lon und height existiert + location = db.query(Location).filter( + Location.lat == lat, + Location.lon == lon, + Location.height == height + ).first() + + # Falls keine existiert, erstelle eine neue Location + if location is None: + location = Location(lat=lat, lon=lon, height=height) + db.add(location) + db.commit() + db.refresh(location) + + return location + # Old endpoints for compatability reason def download_csv(url: str): @@ -69,9 +86,9 @@ async def get_history_station_data( @app.get("/v1/station/current", response_class=Response) async def get_current_station_data( - station_ids: str = None, # Eine Liste von alphanumerischen Station IDs (z.B. "00112233AABB,00112233CCDD") - last_active: int = 3600, # Zeitspanne in Sekunden, die Zeitraum angibt, in welchem Stationen aktiv gewesen sein müssen - output_format: str = "geojson", # Standardmäßig GeoJSON + station_ids: str = None, + last_active: int = 3600, + output_format: str = "geojson", db: Session = Depends(get_db) ): """ @@ -83,39 +100,30 @@ async def get_current_station_data( time_threshold = datetime.now(tz=ZoneInfo("Europe/Vienna")) - timedelta(seconds=last_active) if station_ids: - # Station IDs als Liste von Strings (alphanumerisch, z.B. "00112233AABB") station_id_list = station_ids.split(",") - # Abfrage auf die Stationen basierend auf den alphanumerischen Gerätenamen (device) stations = db.query(Station).filter(Station.device.in_(station_id_list)).all() - else: - # Abfrage auf alle Stationen, die in den letzten last_active Sekunden aktiv waren stations = db.query(Station).filter(Station.last_active >= time_threshold).all() - #stations = db.query(Station).all() + if not stations: raise HTTPException(status_code=404, detail="No stations found") - # Initialisierung der Ausgabe basierend auf dem gewünschten Format if output_format == "geojson": features = [] - for station in stations: - # Finde alle Messungen, die den gleichen Zeitpunkt wie "last_active" haben measurements = db.query(Measurement).filter( Measurement.station_id == station.id, Measurement.time_measured == station.last_active ).all() - # Für jede Messung die zugehörigen Values abfragen sensors = [] - for measurement in measurements: values = db.query(Values).filter(Values.measurement_id == measurement.id).all() sensors.append({ - "sensor_model": measurement.sensor_model, - "values": [{"dimension": value.dimension, "value": value.value} for value in values] - }) - + "sensor_model": measurement.sensor_model, + "values": [{"dimension": value.dimension, "value": value.value} for value in values] + }) + features.append({ "type": "Feature", "geometry": { @@ -123,7 +131,7 @@ async def get_current_station_data( "coordinates": [station.lon, station.lat], }, "properties": { - "device": station.device, + "device": station.device, "time": str(station.last_active), "height": station.height, "sensors": sensors @@ -140,13 +148,11 @@ async def get_current_station_data( elif output_format == "csv": csv_data = "device,lat,lon,last_active,height,sensor_model,dimension,value\n" for station in stations: - # Finde alle Messungen, die den gleichen Zeitpunkt wie "last_active" haben measurements = db.query(Measurement).filter( Measurement.station_id == station.id, Measurement.time_measured == station.last_active ).all() - # Für jede Messung die zugehörigen Values abfragen for measurement in measurements: values = db.query(Values).filter(Values.measurement_id == measurement.id).all() @@ -158,7 +164,7 @@ async def get_current_station_data( else: return Response(content="Invalid output format", media_type="text/plain", status_code=400) - + return Response(content=content, media_type=media_type) @@ -171,46 +177,44 @@ async def create_station_data( # Empfangszeit des Requests erfassen time_received = datetime.now() + # Hole oder erstelle eine Location basierend auf lat, lon, height + location = get_or_create_location(db, station.location.lat, station.location.lon, float(station.location.height)) + # Prüfen, ob die Station bereits existiert db_station = db.query(Station).filter(Station.device == station.device).first() if db_station is None: - # Neue Station anlegen + # Neue Station anlegen und mit der Location verknüpfen db_station = Station( device=station.device, firmware=station.firmware, apikey=station.apikey, - lat=station.location.lat, - lon=station.location.lon, - height=float(station.location.height), - last_active=station.time + last_active=station.time, + location_id=location.id # Verknüpfung zur Location ) db.add(db_station) db.commit() db.refresh(db_station) else: if db_station.apikey != station.apikey: - # Fehler werfen, wenn der APIKEY nicht übereinstimmt raise HTTPException( status_code=401, detail="Invalid API key" ) - # Station existiert bereits, prüfe und aktualisiere ggf. lon, lat, height und firmware - updated = False - if (db_station.lat != station.location.lat or - db_station.lon != station.location.lon or - db_station.height != float(station.location.height) or - db_station.firmware != station.firmware): + # Prüfen, ob die Location-Daten abweichen + if (db_station.location.lat != station.location.lat or + db_station.location.lon != station.location.lon or + db_station.location.height != float(station.location.height)): - # Aktualisierung der Station mit den neuen Werten - db_station.lat = station.location.lat - db_station.lon = station.location.lon - db_station.height = float(station.location.height) - db_station.firmware = station.firmware - updated = True + # Erstelle eine neue Location, wenn die aktuellen Standortdaten abweichen + new_location = get_or_create_location(db, station.location.lat, station.location.lon, float(station.location.height)) + db_station.location_id = new_location.id # Verknüpfe die Station mit der neuen Location + db.commit() - if updated: + # Prüfe und aktualisiere die Firmware, falls sie abweicht + if db_station.firmware != station.firmware: + db_station.firmware = station.firmware db.commit() # Durch alle Sensoren iterieren @@ -223,7 +227,6 @@ async def create_station_data( ).first() if existing_measurement: - # Fehler werfen, wenn die Messung bereits existiert raise HTTPException( status_code=422, detail="Measurement already in Database" @@ -233,8 +236,9 @@ async def create_station_data( db_measurement = Measurement( sensor_model=sensor_data.type, station_id=db_station.id, - time_measured=station.time, # Die Zeit der Messung (aus den Daten) - time_received=time_received # Empfangszeit des Requests + time_measured=station.time, + time_received=time_received, + location_id=db_station.location_id # Verknüpfe die Messung mit der neuen Location ) db.add(db_measurement) db.commit() diff --git a/code/models.py b/code/models.py index dd7a13e..c664c1e 100644 --- a/code/models.py +++ b/code/models.py @@ -3,6 +3,18 @@ from database import Base +class Location(Base): + __tablename__ = "locations" + + id = Column(Integer, primary_key=True, index=True) + lat = Column(Float) + lon = Column(Float) + height = Column(Float) + # Relationships: + stations = relationship("Station", back_populates="location") + measurements = relationship("Measurement", back_populates="location") + + class Station(Base): __tablename__ = "stations" @@ -10,10 +22,10 @@ class Station(Base): device = Column(String, index=True, unique=True) firmware = Column(String) apikey = Column(String) - lat = Column(Float) - lon = Column(Float) last_active = Column(DateTime) - height = Column(Float) + # Relationships: + location_id = Column(Integer, ForeignKey('locations.id')) + location = relationship("Location", back_populates="stations") measurements = relationship("Measurement", back_populates="station") @@ -24,6 +36,9 @@ class Measurement(Base): time_received = Column(DateTime) time_measured = Column(DateTime) sensor_model = Column(Integer) + # Relationships: + location_id = Column(Integer, ForeignKey('locations.id')) + location = relationship("Location", back_populates="measurements") station_id = Column(Integer, ForeignKey('stations.id')) station = relationship("Station", back_populates="measurements") values = relationship("Values", back_populates="measurement") @@ -35,6 +50,7 @@ class Values(Base): id = Column(Integer, primary_key=True, index=True) dimension = Column(Integer) value = Column(Float) + # Relationships: measurement_id = Column(Integer, ForeignKey('measurements.id')) measurement = relationship("Measurement", back_populates="values")