"""Define an API endpoint for requests related to sensors.""" from __future__ import annotations from dataclasses import dataclass from datetime import datetime from aiopurpleair.endpoints import APIEndpointsBase from aiopurpleair.models.sensors import ( GetSensorRequest, GetSensorResponse, GetSensorsRequest, GetSensorsResponse, LocationType, SensorModel, ) from aiopurpleair.util.geo import GeoLocation @dataclass class NearbySensorResult: """Define a nearby sensor result.""" sensor: SensorModel distance: float class SensorsEndpoints(APIEndpointsBase): """Define the API manager object.""" async def async_get_sensor( self, sensor_index: int, *, fields: list[str] | None = None, read_key: str | None = None, ) -> GetSensorResponse: """Get all sensors. Args: sensor_index: The sensor index to get data for. fields: The optional sensor data fields to include. read_key: An optional read key for private sensors. Returns: An API response payload in the form of a Pydantic model. """ response: GetSensorResponse = await self._async_endpoint_request_with_models( f"/sensors/{sensor_index}", ( ("fields", fields), ("read_key", read_key), ), GetSensorRequest, GetSensorResponse, ) return response async def async_get_sensors( # pylint: disable=too-many-arguments self, fields: list[str], *, location_type: LocationType | None = None, max_age: int | None = None, modified_since_utc: datetime | None = None, nw_latitude: float | None = None, nw_longitude: float | None = None, read_keys: list[str] | None = None, se_latitude: float | None = None, se_longitude: float | None = None, sensor_indices: list[int] | None = None, ) -> GetSensorsResponse: """Get all sensors. Args: fields: The sensor data fields to include. location_type: An optional LocationType to filter by. max_age: Filter results modified within these seconds. modified_since_utc: Filter results modified since a datetime. nw_latitude: The latitude of the NE corner of an optional bounding box. nw_longitude: The longitude of the NE corner of an optional bounding box. read_keys: Optional read keys for private sensors. se_latitude: The latitude of the SE corner of an optional bounding box. se_longitude: The longitude of the SE corner of an optional bounding box. sensor_indices: Filter results by sensor index. Returns: An API response payload in the form of a Pydantic model. """ response: GetSensorsResponse = await self._async_endpoint_request_with_models( "/sensors", ( ("fields", fields), ("location_type", location_type), ("max_age", max_age), ("modified_since", modified_since_utc), ("nwlat", nw_latitude), ("nwlng", nw_longitude), ("read_keys", read_keys), ("selat", se_latitude), ("selng", se_longitude), ("show_only", sensor_indices), ), GetSensorsRequest, GetSensorsResponse, ) return response async def async_get_nearby_sensors( # pylint: disable=too-many-arguments self, fields: list[str], latitude: float, longitude: float, distance_km: float, *, limit_results: int | None = None, ) -> list[NearbySensorResult]: """Get sensors near a coordinate pair within a distance (in kilometers). The resulting list of sensors is ordered from nearest to furthest within the bounding box defined by the distance. Args: fields: The sensor data fields to include. latitude: The latitude of the "search center." longitude: The longitude of the "search center." distance_km: The radius of the "search center." limit_results: The number of results to limit. Returns: A sorted list of NearbySensorResult objects (containing both the sensor and the distance). """ center = GeoLocation.from_degrees(latitude, longitude) nw_coordinate_pair, se_coordinate_pair = center.bounding_box(distance_km) # Ensure that latitude and longitude are included in the fields no matter what: fields.extend( field for field in ("latitude", "longitude") if field not in fields ) sensors_response = await self.async_get_sensors( fields, nw_latitude=nw_coordinate_pair.latitude_degrees, nw_longitude=nw_coordinate_pair.longitude_degrees, se_latitude=se_coordinate_pair.latitude_degrees, se_longitude=se_coordinate_pair.longitude_degrees, ) sorted_results = await self._async_get_sorted_results(sensors_response, center) if limit_results: return sorted_results[:limit_results] return sorted_results async def _async_get_sorted_results( self, sensors_response: GetSensorsResponse, center: GeoLocation ) -> list[NearbySensorResult]: """Sort the results by distance.""" withgeo_results = [ sensor for sensor in sensors_response.data.values() if sensor.latitude is not None and sensor.longitude is not None ] nearby_results = [ NearbySensorResult( sensor=sensor, distance=center.distance_to( GeoLocation.from_degrees( float(sensor.latitude) if sensor.latitude is not None else 0.0, float(sensor.longitude) if sensor.longitude is not None else 0.0, ) ), ) for sensor in withgeo_results ] return sorted(nearby_results, key=lambda result: result.distance)