Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement OpenSky library #92814

Merged
merged 5 commits into from
May 24, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -865,6 +865,7 @@ build.json @home-assistant/supervisor
/homeassistant/components/opengarage/ @danielhiversen
/tests/components/opengarage/ @danielhiversen
/homeassistant/components/openhome/ @bazwilliams
/homeassistant/components/opensky/ @joostlek
/homeassistant/components/opentherm_gw/ @mvn23
/tests/components/opentherm_gw/ @mvn23
/homeassistant/components/openuv/ @bachya
Expand Down
5 changes: 3 additions & 2 deletions homeassistant/components/opensky/manifest.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
{
"domain": "opensky",
"name": "OpenSky Network",
"codeowners": [],
"codeowners": ["@joostlek"],
"documentation": "https://www.home-assistant.io/integrations/opensky",
"iot_class": "cloud_polling"
"iot_class": "cloud_polling",
"requirements": ["python-opensky==0.0.6"]
}
192 changes: 155 additions & 37 deletions homeassistant/components/opensky/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@
from __future__ import annotations

from datetime import timedelta
import math

import requests
from python_opensky import BoundingBox, OpenSky
import voluptuous as vol

from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
Expand All @@ -14,14 +15,13 @@
CONF_LONGITUDE,
CONF_NAME,
CONF_RADIUS,
UnitOfLength,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.util import location as util_location
from homeassistant.util.unit_conversion import DistanceConverter
from homeassistant.util.location import AXIS_A, FLATTENING

CONF_ALTITUDE = "altitude"

Expand Down Expand Up @@ -70,6 +70,124 @@
)


def calculate_point(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please move calculate_point to the library

latitude: float, longitude: float, distance: float, degrees: float
) -> tuple[float, float]:
"""Calculate a point from an origin point, direction in degrees and distance."""
# pylint: disable=invalid-name
piD4 = math.atan(1.0)
two_pi = piD4 * 8.0
latitude = latitude * piD4 / 45.0
longitude = longitude * piD4 / 45.0
degrees = degrees * piD4 / 45.0
if degrees < 0.0:
degrees = degrees + two_pi
if degrees > two_pi:
degrees = degrees - two_pi
AXIS_B = AXIS_A * (1.0 - FLATTENING)
TanU1 = (1 - FLATTENING) * math.tan(latitude)
U1 = math.atan(TanU1)
sigma1 = math.atan2(TanU1, math.cos(degrees))
Sinalpha = math.cos(U1) * math.sin(degrees)
cosalpha_sq = 1.0 - Sinalpha * Sinalpha
u2 = cosalpha_sq * (AXIS_A * AXIS_A - AXIS_B * AXIS_B) / (AXIS_B * AXIS_B)
A = 1.0 + (u2 / 16384) * (4096 + u2 * (-768 + u2 * (320 - 175 * u2)))
B = (u2 / 1024) * (256 + u2 * (-128 + u2 * (74 - 47 * u2)))
# Starting with the approx
sigma = distance / (AXIS_B * A)
last_sigma = 2.0 * sigma + 2.0 # something impossible

# Iterate the following 3 eqs until no sig change in sigma
# two_sigma_m , delta_sigma
while abs((last_sigma - sigma) / sigma) > 1.0e-9:
two_sigma_m = 2 * sigma1 + sigma
delta_sigma = (
B
* math.sin(sigma)
* (
math.cos(two_sigma_m)
+ (B / 4)
* (
math.cos(sigma)
* (
-1
+ 2 * math.pow(math.cos(two_sigma_m), 2)
- (B / 6)
* math.cos(two_sigma_m)
* (-3 + 4 * math.pow(math.sin(sigma), 2))
* (-3 + 4 * math.pow(math.cos(two_sigma_m), 2))
)
)
)
)
last_sigma = sigma
sigma = (distance / (AXIS_B * A)) + delta_sigma
phi2 = math.atan2(
(
math.sin(U1) * math.cos(sigma)
+ math.cos(U1) * math.sin(sigma) * math.cos(degrees)
),
(
(1 - FLATTENING)
* math.sqrt(
math.pow(Sinalpha, 2)
+ pow(
math.sin(U1) * math.sin(sigma)
- math.cos(U1) * math.cos(sigma) * math.cos(degrees),
2,
)
)
),
)
lembda = math.atan2(
(math.sin(sigma) * math.sin(degrees)),
(
math.cos(U1) * math.cos(sigma)
- math.sin(U1) * math.sin(sigma) * math.cos(degrees)
),
)
C = (FLATTENING / 16) * cosalpha_sq * (4 + FLATTENING * (4 - 3 * cosalpha_sq))
omega = lembda - (1 - C) * FLATTENING * Sinalpha * (
sigma
+ C
* math.sin(sigma)
* (
math.cos(two_sigma_m)
+ C * math.cos(sigma) * (-1 + 2 * math.pow(math.cos(two_sigma_m), 2))
)
)
lembda2 = longitude + omega
alpha21 = math.atan2(
Sinalpha,
(
-math.sin(U1) * math.sin(sigma)
+ math.cos(U1) * math.cos(sigma) * math.cos(degrees)
),
)
alpha21 = alpha21 + two_pi / 2.0
if alpha21 < 0.0:
alpha21 = alpha21 + two_pi
if alpha21 > two_pi:
alpha21 = alpha21 - two_pi
phi2 = phi2 * 45.0 / piD4
lembda2 = lembda2 * 45.0 / piD4
return phi2, lembda2


def get_bounding_box(latitude: float, longitude: float, radius: float) -> BoundingBox:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please move get_bounding_box to the library

"""Get bounding box from radius and a point."""
north = calculate_point(latitude, longitude, radius, 0)
east = calculate_point(latitude, longitude, radius, 90)
south = calculate_point(latitude, longitude, radius, 180)
west = calculate_point(latitude, longitude, radius, 270)
return BoundingBox(
min_latitude=min(north[0], south[0]) + latitude,
max_latitude=max(north[0], south[0]) + latitude,
min_longitude=min(east[1], west[1]) + longitude,
max_longitude=max(east[1], west[1]) + longitude,
)


def setup_platform(
hass: HomeAssistant,
config: ConfigType,
Expand All @@ -79,14 +197,17 @@ def setup_platform(
"""Set up the Open Sky platform."""
latitude = config.get(CONF_LATITUDE, hass.config.latitude)
longitude = config.get(CONF_LONGITUDE, hass.config.longitude)
radius = config.get(CONF_RADIUS, 0)
bounding_box = get_bounding_box(latitude, longitude, radius)
session = async_get_clientsession(hass)
opensky = OpenSky(session=session)
add_entities(
[
OpenSkySensor(
hass,
config.get(CONF_NAME, DOMAIN),
latitude,
longitude,
config.get(CONF_RADIUS),
opensky,
bounding_box,
config.get(CONF_ALTITUDE),
joostlek marked this conversation as resolved.
Show resolved Hide resolved
)
],
Expand All @@ -101,19 +222,22 @@ class OpenSkySensor(SensorEntity):
"Information provided by the OpenSky Network (https://opensky-network.org)"
)

def __init__(self, hass, name, latitude, longitude, radius, altitude):
def __init__(
self,
hass: HomeAssistant,
name: str,
opensky: OpenSky,
bounding_box: BoundingBox,
altitude: float | None,
joostlek marked this conversation as resolved.
Show resolved Hide resolved
) -> None:
"""Initialize the sensor."""
self._session = requests.Session()
self._latitude = latitude
self._longitude = longitude
self._radius = DistanceConverter.convert(
radius, UnitOfLength.KILOMETERS, UnitOfLength.METERS
)
self._altitude = altitude
self._altitude = altitude or 0
joostlek marked this conversation as resolved.
Show resolved Hide resolved
self._state = 0
self._hass = hass
self._name = name
self._previously_tracked = None
self._previously_tracked: set[str] = set()
self._opensky = opensky
self._bounding_box = bounding_box

@property
def name(self):
Expand All @@ -129,10 +253,10 @@ def _handle_boundary(self, flights, event, metadata):
"""Handle flights crossing region boundary."""
for flight in flights:
if flight in metadata:
altitude = metadata[flight].get(ATTR_ALTITUDE)
longitude = metadata[flight].get(ATTR_LONGITUDE)
latitude = metadata[flight].get(ATTR_LATITUDE)
icao24 = metadata[flight].get(ATTR_ICAO24)
altitude = metadata[flight].barometric_altitude
longitude = metadata[flight].longitude
latitude = metadata[flight].latitude
icao24 = metadata[flight].icao24
else:
# Assume Flight has landed if missing.
altitude = 0
Expand All @@ -150,33 +274,27 @@ def _handle_boundary(self, flights, event, metadata):
}
self._hass.bus.fire(event, data)

def update(self) -> None:
async def async_update(self) -> None:
"""Update device state."""
currently_tracked = set()
flight_metadata = {}
states = self._session.get(OPENSKY_API_URL).json().get(ATTR_STATES)
for state in states:
flight = dict(zip(OPENSKY_API_FIELDS, state))
callsign = flight[ATTR_CALLSIGN].strip()
response = await self._opensky.get_states(bounding_box=self._bounding_box)
for flight in response.states:
if not flight.callsign:
continue
callsign = flight.callsign.strip()
if callsign != "":
flight_metadata[callsign] = flight
else:
continue
if (
(longitude := flight.get(ATTR_LONGITUDE)) is None
or (latitude := flight.get(ATTR_LATITUDE)) is None
or flight.get(ATTR_ON_GROUND)
flight.longitude is None
or flight.latitude is None
or flight.on_ground
or flight.barometric_altitude is None
):
continue
distance = util_location.distance(
self._latitude,
self._longitude,
latitude,
longitude,
)
if distance is None or distance > self._radius:
continue
altitude = flight.get(ATTR_ALTITUDE)
altitude = flight.barometric_altitude
if altitude > self._altitude and self._altitude != 0:
continue
currently_tracked.add(callsign)
Expand Down
3 changes: 3 additions & 0 deletions requirements_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2088,6 +2088,9 @@ python-mystrom==1.1.2
# homeassistant.components.nest
python-nest==4.2.0

# homeassistant.components.opensky
python-opensky==0.0.6

# homeassistant.components.otbr
# homeassistant.components.thread
python-otbr-api==1.0.9
Expand Down