| 1 | """Live satellite-camera tracker. Re-propagates imaging-satellite positions to 'now' every |
| 2 | INTERVAL seconds (locally, from cached orbital elements - no network spam), upserts the latest |
| 3 | position into live_entities, and appends to entity_positions so each satellite has a moving |
| 4 | ground track. Prunes position history beyond RETAIN_MINUTES. |
| 5 | """ |
| 6 | from __future__ import annotations |
| 7 | |
| 8 | import signal |
| 9 | import time |
| 10 | from datetime import datetime, timedelta, timezone |
| 11 | |
| 12 | from sqlalchemy import delete, select |
| 13 | from sqlalchemy.dialects.postgresql import insert as pg_insert |
| 14 | |
| 15 | from app.db.session import SessionLocal |
| 16 | from app.models import EntityPosition, LiveEntity |
| 17 | from app.services.realsources import imaging_satellites |
| 18 | |
| 19 | INTERVAL = 30 |
| 20 | RETAIN_MINUTES = 180 |
| 21 | _running = True |
| 22 | |
| 23 | |
| 24 | def _stop(*_): |
| 25 | global _running |
| 26 | _running = False |
| 27 | |
| 28 | |
| 29 | def main(): |
| 30 | signal.signal(signal.SIGTERM, _stop) |
| 31 | signal.signal(signal.SIGINT, _stop) |
| 32 | db = SessionLocal() |
| 33 | print(f"satellite tracker started (every {INTERVAL}s)", flush=True) |
| 34 | while _running: |
| 35 | try: |
| 36 | sats = imaging_satellites() |
| 37 | now = datetime.now(timezone.utc) |
| 38 | for s in sats: |
| 39 | stmt = pg_insert(LiveEntity).values( |
| 40 | kind="satellite_camera", source=s["source"], identifier=s["identifier"], |
| 41 | label=s["label"], lat=s["lat"], lon=s["lon"], altitude_m=s["altitude_m"], |
| 42 | origin_country=None, attributes=s["attributes"], observed_at=now) |
| 43 | stmt = stmt.on_conflict_do_update( |
| 44 | index_elements=["source", "identifier"], |
| 45 | set_={"lat": s["lat"], "lon": s["lon"], "altitude_m": s["altitude_m"], |
| 46 | "observed_at": now}) |
| 47 | db.execute(stmt) |
| 48 | if sats: |
| 49 | db.execute(EntityPosition.__table__.insert(), |
| 50 | [{"identifier": s["identifier"], "kind": "satellite_camera", |
| 51 | "lat": s["lat"], "lon": s["lon"], "altitude_m": s["altitude_m"], |
| 52 | "observed_at": now} for s in sats]) |
| 53 | db.execute(delete(EntityPosition).where( |
| 54 | EntityPosition.observed_at < now - timedelta(minutes=RETAIN_MINUTES))) |
| 55 | db.commit() |
| 56 | print(f"propagated {len(sats)} satellite cameras @ {now.strftime('%H:%M:%S')}", flush=True) |
| 57 | except Exception as e: |
| 58 | print(f"sat tracker error: {e}", flush=True) |
| 59 | db.rollback() |
| 60 | for _ in range(INTERVAL): |
| 61 | if not _running: |
| 62 | break |
| 63 | time.sleep(1) |
| 64 | db.close() |
| 65 | |
| 66 | |
| 67 | if __name__ == "__main__": |
| 68 | main() |