From 1c745976a75b1e34ee5a35015c2b20a3cf26e712 Mon Sep 17 00:00:00 2001 From: happyleaves Date: Tue, 11 Apr 2017 19:24:23 -0400 Subject: [PATCH 1/3] opensky sensor --- .coveragerc | 1 + homeassistant/components/sensor/opensky.py | 141 +++++++++++++++++++++ 2 files changed, 142 insertions(+) create mode 100644 homeassistant/components/sensor/opensky.py diff --git a/.coveragerc b/.coveragerc index a8e771c9ad27f6..d4a909db6c7f42 100644 --- a/.coveragerc +++ b/.coveragerc @@ -374,6 +374,7 @@ omit = homeassistant/components/sensor/onewire.py homeassistant/components/sensor/openevse.py homeassistant/components/sensor/openexchangerates.py + homeassistant/components/sensor/opensky.py homeassistant/components/sensor/openweathermap.py homeassistant/components/sensor/pi_hole.py homeassistant/components/sensor/plex.py diff --git a/homeassistant/components/sensor/opensky.py b/homeassistant/components/sensor/opensky.py new file mode 100644 index 00000000000000..f705c447f5cf0a --- /dev/null +++ b/homeassistant/components/sensor/opensky.py @@ -0,0 +1,141 @@ +""" +Sensor for the Open Sky Network. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.opensky/ +""" +import logging +from datetime import timedelta + +import requests +import voluptuous as vol + +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import ( + CONF_NAME, CONF_LATITUDE, CONF_LONGITUDE, + ATTR_ATTRIBUTION, ATTR_LATITUDE, ATTR_LONGITUDE) +from homeassistant.helpers.entity import Entity +from homeassistant.util.location import vincenty +import homeassistant.helpers.config_validation as cv + + +_LOGGER = logging.getLogger(__name__) + +SCAN_INTERVAL = timedelta(seconds=12) # opensky public limit is 10 seconds +DOMAIN = 'opensky' +EVENT_OPENSKY_ENTRY = '{}_entry'.format(DOMAIN) +EVENT_OPENSKY_EXIT = '{}_exit'.format(DOMAIN) +CONF_RADIUS = 'radius' +ATTR_SENSOR = 'sensor' +ATTR_STATES = 'states' +ATTR_ON_GROUND = 'on_ground' +ATTR_CALLSIGN = 'callsign' +UNIT_OF_MEASUREMENT = 'flights' +ICON = 'mdi:airplane' +OPENSKY_ATTRIBUTION = "Information provided by the OpenSky Network "\ + "(https://opensky-network.org)" +OPENSKY_API_URL = 'https://opensky-network.org/api/states/all' +OPENSKY_API_FIELDS = [ + 'icao24', ATTR_CALLSIGN, 'origin_country', 'time_position', + 'time_velocity', ATTR_LONGITUDE, ATTR_LATITUDE, 'altitude', + ATTR_ON_GROUND, 'velocity', 'heading', 'vertical_rate', 'sensors'] + + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_RADIUS): vol.Coerce(float), + vol.Optional(CONF_NAME): cv.string, + vol.Inclusive(CONF_LATITUDE, 'coordinates'): cv.latitude, + vol.Inclusive(CONF_LONGITUDE, 'coordinates'): cv.longitude +}) + + +# pylint: disable=unused-argument +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup the Open Sky platform.""" + session = requests.Session() + latitude = config.get(CONF_LATITUDE, hass.config.latitude) + longitude = config.get(CONF_LONGITUDE, hass.config.longitude) + add_devices([OpenSkySensor( + hass, config.get(CONF_NAME, DOMAIN), session, latitude, longitude, + config.get(CONF_RADIUS))], True) + + +class OpenSkySensor(Entity): + """Open Sky Network Sensor.""" + + def __init__(self, hass, name, session, latitude, longitude, radius): + """Initialize the sensor.""" + self._session = session + self._latitude = latitude + self._longitude = longitude + self._radius = radius + self._state = 0 + self._hass = hass + self._name = name + self._previously_tracked = None + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + def _handle_boundary(self, callsigns, event): + """Handle flights crossing region boundary.""" + for callsign in callsigns: + data = { + ATTR_CALLSIGN: callsign, + ATTR_SENSOR: self._name + } + self._hass.bus.fire(event, data) + + def update(self): + """Update device state.""" + currently_tracked = set() + states = self._session.get(OPENSKY_API_URL).json().get(ATTR_STATES) + for state in states: + data = dict(zip(OPENSKY_API_FIELDS, state)) + missing_location = ( + data.get(ATTR_LONGITUDE) is None or + data.get(ATTR_LATITUDE) is None) + if missing_location: + continue + if data.get(ATTR_ON_GROUND): + continue + distance = vincenty( + (self._latitude, self._longitude), + (data.get(ATTR_LATITUDE), data.get(ATTR_LONGITUDE))) + if distance > self._radius: + continue + callsign = data[ATTR_CALLSIGN].strip() + if callsign == '': + continue + currently_tracked.add(callsign) + if self._previously_tracked is not None: + entries = currently_tracked - self._previously_tracked + exits = self._previously_tracked - currently_tracked + self._handle_boundary(entries, EVENT_OPENSKY_ENTRY) + self._handle_boundary(exits, EVENT_OPENSKY_EXIT) + self._state = len(currently_tracked) + self._previously_tracked = currently_tracked + + @property + def device_state_attributes(self): + """Return the state attributes.""" + return { + ATTR_ATTRIBUTION: OPENSKY_ATTRIBUTION + } + + @property + def unit_of_measurement(self): + """Unit of measurement.""" + return UNIT_OF_MEASUREMENT + + @property + def icon(self): + """Icon.""" + return ICON From 1074b868a4ec47f61d99dfc05922444f93fff7d3 Mon Sep 17 00:00:00 2001 From: happyleaves Date: Thu, 13 Apr 2017 16:55:20 -0400 Subject: [PATCH 2/3] address opensky review comments --- homeassistant/components/sensor/opensky.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/sensor/opensky.py b/homeassistant/components/sensor/opensky.py index f705c447f5cf0a..6f2ee665a01a4f 100644 --- a/homeassistant/components/sensor/opensky.py +++ b/homeassistant/components/sensor/opensky.py @@ -30,8 +30,6 @@ ATTR_STATES = 'states' ATTR_ON_GROUND = 'on_ground' ATTR_CALLSIGN = 'callsign' -UNIT_OF_MEASUREMENT = 'flights' -ICON = 'mdi:airplane' OPENSKY_ATTRIBUTION = "Information provided by the OpenSky Network "\ "(https://opensky-network.org)" OPENSKY_API_URL = 'https://opensky-network.org/api/states/all' @@ -52,20 +50,19 @@ # pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the Open Sky platform.""" - session = requests.Session() latitude = config.get(CONF_LATITUDE, hass.config.latitude) longitude = config.get(CONF_LONGITUDE, hass.config.longitude) add_devices([OpenSkySensor( - hass, config.get(CONF_NAME, DOMAIN), session, latitude, longitude, + hass, config.get(CONF_NAME, DOMAIN), latitude, longitude, config.get(CONF_RADIUS))], True) class OpenSkySensor(Entity): """Open Sky Network Sensor.""" - def __init__(self, hass, name, session, latitude, longitude, radius): + def __init__(self, hass, name, latitude, longitude, radius): """Initialize the sensor.""" - self._session = session + self._session = requests.Session() self._latitude = latitude self._longitude = longitude self._radius = radius @@ -133,9 +130,9 @@ def device_state_attributes(self): @property def unit_of_measurement(self): """Unit of measurement.""" - return UNIT_OF_MEASUREMENT + return 'flights' @property def icon(self): """Icon.""" - return ICON + return 'mdi:airplane' From aacc87b345bc4e7f6337f1ade69075703b1f8502 Mon Sep 17 00:00:00 2001 From: happyleaves Date: Sun, 16 Apr 2017 12:47:48 -0400 Subject: [PATCH 3/3] update opensky distance calc --- homeassistant/components/sensor/opensky.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/sensor/opensky.py b/homeassistant/components/sensor/opensky.py index 6f2ee665a01a4f..17e5f1f351c02f 100644 --- a/homeassistant/components/sensor/opensky.py +++ b/homeassistant/components/sensor/opensky.py @@ -13,9 +13,11 @@ from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( CONF_NAME, CONF_LATITUDE, CONF_LONGITUDE, - ATTR_ATTRIBUTION, ATTR_LATITUDE, ATTR_LONGITUDE) + ATTR_ATTRIBUTION, ATTR_LATITUDE, ATTR_LONGITUDE, + LENGTH_KILOMETERS, LENGTH_METERS) from homeassistant.helpers.entity import Entity -from homeassistant.util.location import vincenty +from homeassistant.util import distance as util_distance +from homeassistant.util import location as util_location import homeassistant.helpers.config_validation as cv @@ -65,7 +67,8 @@ def __init__(self, hass, name, latitude, longitude, radius): self._session = requests.Session() self._latitude = latitude self._longitude = longitude - self._radius = radius + self._radius = util_distance.convert( + radius, LENGTH_KILOMETERS, LENGTH_METERS) self._state = 0 self._hass = hass self._name = name @@ -103,10 +106,10 @@ def update(self): continue if data.get(ATTR_ON_GROUND): continue - distance = vincenty( - (self._latitude, self._longitude), - (data.get(ATTR_LATITUDE), data.get(ATTR_LONGITUDE))) - if distance > self._radius: + distance = util_location.distance( + self._latitude, self._longitude, + data.get(ATTR_LATITUDE), data.get(ATTR_LONGITUDE)) + if distance is None or distance > self._radius: continue callsign = data[ATTR_CALLSIGN].strip() if callsign == '':