"""Asynchronous Python client for the OpenSky API.""" from __future__ import annotations from dataclasses import dataclass from typing import TYPE_CHECKING, Any from .const import AircraftCategory, PositionSource from .exceptions import OpenSkyCoordinateError from .util import to_enum if TYPE_CHECKING: from typing_extensions import Self @dataclass(slots=True) class StatesResponse: """Represents the states response.""" states: list[StateVector] time: int @classmethod def from_api(cls, data: dict[str, Any]) -> Self: """Initialize from the API.""" return cls( time=data["time"], states=[ StateVector.from_api(vector_data) for vector_data in data["states"] ], ) @dataclass(slots=True) # pylint: disable-next=too-many-instance-attributes class StateVector: """Represents the state of a vehicle at a particular time. Attributes ---------- icao24: ICAO24 address of the transmitter in hex string representation. callsign: Callsign of the vehicle. origin_country: Inferred through the ICAO24 address. time_position: Seconds since epoch of last position report. Can be None if there was no position report received by OpenSky within 15s before. last_contact: Seconds since epoch of last received message from this transponder. longitude: In ellipsoidal coordinates (WGS-84) and degrees. latitude: In ellipsoidal coordinates (WGS-84) and degrees. geo_altitude: Geometric altitude in meters. on_ground: True if aircraft is on ground (sends ADS-B surface position reports). velocity: Over ground in m/s. true_track: In decimal degrees (0 is north). vertical_rate: In m/s, incline is positive, decline negative. sensors: Serial numbers of sensors which received messages from the vehicle within the validity period of this state vector. barometric_altitude: Barometric altitude in meters. transponder_code: Transponder code aka Squawk. special_purpose_indicator: Special purpose indicator. position_source: Origin of this state's position. category: Aircraft category. """ icao24: str callsign: str | None origin_country: str time_position: int | None last_contact: int longitude: float | None latitude: float | None geo_altitude: float | None on_ground: bool velocity: float | None true_track: float | None vertical_rate: float | None sensors: list[int] | None barometric_altitude: float | None transponder_code: str | None special_purpose_indicator: bool position_source: PositionSource category: AircraftCategory @classmethod def from_api(cls, data: dict[str, Any]) -> Self: """Initialize from the API.""" return cls( icao24=data["icao24"], callsign=data.get("callsign"), origin_country=data["origin_country"], time_position=data.get("time_position"), last_contact=data["last_contact"], longitude=data.get("longitude"), latitude=data.get("latitude"), geo_altitude=data.get("geo_altitude"), on_ground=data["on_ground"], velocity=data.get("velocity"), true_track=data.get("true_track"), vertical_rate=data.get("vertical_rate"), sensors=data.get("sensors", []), barometric_altitude=data.get("baro_altitude"), transponder_code=data.get("squawk"), special_purpose_indicator=data["spi"], position_source=to_enum( PositionSource, data["position_source"], PositionSource.UNKNOWN, ), category=to_enum( AircraftCategory, data["category"], AircraftCategory.NO_INFORMATION, ), ) @dataclass(slots=True) class BoundingBox: """Bounding box for retrieving state vectors.""" min_latitude: float max_latitude: float min_longitude: float max_longitude: float def validate(self) -> None: """Validate if the latitude and longitude are correct.""" self._check_latitude(self.min_latitude) self._check_latitude(self.max_latitude) self._check_longitude(self.min_longitude) self._check_longitude(self.max_longitude) @staticmethod def _check_latitude(degrees: float) -> None: if degrees < -90 or degrees > 90: msg = f"Invalid latitude {degrees}! Must be in [-90, 90]." raise OpenSkyCoordinateError(msg) @staticmethod def _check_longitude(degrees: float) -> None: if degrees < -180 or degrees > 180: msg = f"Invalid longitude {degrees}! Must be in [-180, 180]." raise OpenSkyCoordinateError(msg)