Add FAA Delays Integration (#41347)

Co-authored-by: Franck Nijhof <frenck@frenck.nl>
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
Nathan Tilley
2021-02-24 15:11:20 -05:00
committed by GitHub
parent ba51ada494
commit 8d2606134d
14 changed files with 446 additions and 0 deletions

View File

@ -272,6 +272,8 @@ omit =
homeassistant/components/evohome/*
homeassistant/components/ezviz/*
homeassistant/components/familyhub/camera.py
homeassistant/components/faa_delays/__init__.py
homeassistant/components/faa_delays/binary_sensor.py
homeassistant/components/fastdotcom/*
homeassistant/components/ffmpeg/camera.py
homeassistant/components/fibaro/*

View File

@ -146,6 +146,7 @@ homeassistant/components/esphome/* @OttoWinter
homeassistant/components/essent/* @TheLastProject
homeassistant/components/evohome/* @zxdavb
homeassistant/components/ezviz/* @baqs
homeassistant/components/faa_delays/* @ntilley905
homeassistant/components/fastdotcom/* @rohankapoorcom
homeassistant/components/file/* @fabaff
homeassistant/components/filter/* @dgomes

View File

@ -0,0 +1,84 @@
"""The FAA Delays integration."""
import asyncio
from datetime import timedelta
import logging
from aiohttp import ClientConnectionError
from async_timeout import timeout
from faadelays import Airport
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_ID
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import aiohttp_client
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
PLATFORMS = ["binary_sensor"]
async def async_setup(hass: HomeAssistant, config: dict):
"""Set up the FAA Delays component."""
hass.data[DOMAIN] = {}
return True
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Set up FAA Delays from a config entry."""
code = entry.data[CONF_ID]
coordinator = FAADataUpdateCoordinator(hass, code)
await coordinator.async_refresh()
if not coordinator.last_update_success:
raise ConfigEntryNotReady
hass.data[DOMAIN][entry.entry_id] = coordinator
for component in PLATFORMS:
hass.async_create_task(
hass.config_entries.async_forward_entry_setup(entry, component)
)
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Unload a config entry."""
unload_ok = all(
await asyncio.gather(
*[
hass.config_entries.async_forward_entry_unload(entry, component)
for component in PLATFORMS
]
)
)
if unload_ok:
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok
class FAADataUpdateCoordinator(DataUpdateCoordinator):
"""Class to manage fetching FAA API data from a single endpoint."""
def __init__(self, hass, code):
"""Initialize the coordinator."""
super().__init__(
hass, _LOGGER, name=DOMAIN, update_interval=timedelta(minutes=1)
)
self.session = aiohttp_client.async_get_clientsession(hass)
self.data = Airport(code, self.session)
self.code = code
async def _async_update_data(self):
try:
with timeout(10):
await self.data.update()
except ClientConnectionError as err:
raise UpdateFailed(err) from err
return self.data

View File

@ -0,0 +1,93 @@
"""Platform for FAA Delays sensor component."""
from homeassistant.components.binary_sensor import BinarySensorEntity
from homeassistant.const import ATTR_ICON, ATTR_NAME
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN, FAA_BINARY_SENSORS
async def async_setup_entry(hass, entry, async_add_entities):
"""Set up a FAA sensor based on a config entry."""
coordinator = hass.data[DOMAIN][entry.entry_id]
binary_sensors = []
for kind, attrs in FAA_BINARY_SENSORS.items():
name = attrs[ATTR_NAME]
icon = attrs[ATTR_ICON]
binary_sensors.append(
FAABinarySensor(coordinator, kind, name, icon, entry.entry_id)
)
async_add_entities(binary_sensors)
class FAABinarySensor(CoordinatorEntity, BinarySensorEntity):
"""Define a binary sensor for FAA Delays."""
def __init__(self, coordinator, sensor_type, name, icon, entry_id):
"""Initialize the sensor."""
super().__init__(coordinator)
self.coordinator = coordinator
self._entry_id = entry_id
self._icon = icon
self._name = name
self._sensor_type = sensor_type
self._id = self.coordinator.data.iata
self._attrs = {}
@property
def name(self):
"""Return the name of the sensor."""
return f"{self._id} {self._name}"
@property
def icon(self):
"""Return the icon."""
return self._icon
@property
def is_on(self):
"""Return the status of the sensor."""
if self._sensor_type == "GROUND_DELAY":
return self.coordinator.data.ground_delay.status
if self._sensor_type == "GROUND_STOP":
return self.coordinator.data.ground_stop.status
if self._sensor_type == "DEPART_DELAY":
return self.coordinator.data.depart_delay.status
if self._sensor_type == "ARRIVE_DELAY":
return self.coordinator.data.arrive_delay.status
if self._sensor_type == "CLOSURE":
return self.coordinator.data.closure.status
return None
@property
def unique_id(self):
"""Return a unique, Home Assistant friendly identifier for this entity."""
return f"{self._id}_{self._sensor_type}"
@property
def device_state_attributes(self):
"""Return attributes for sensor."""
if self._sensor_type == "GROUND_DELAY":
self._attrs["average"] = self.coordinator.data.ground_delay.average
self._attrs["reason"] = self.coordinator.data.ground_delay.reason
elif self._sensor_type == "GROUND_STOP":
self._attrs["endtime"] = self.coordinator.data.ground_stop.endtime
self._attrs["reason"] = self.coordinator.data.ground_stop.reason
elif self._sensor_type == "DEPART_DELAY":
self._attrs["minimum"] = self.coordinator.data.depart_delay.minimum
self._attrs["maximum"] = self.coordinator.data.depart_delay.maximum
self._attrs["trend"] = self.coordinator.data.depart_delay.trend
self._attrs["reason"] = self.coordinator.data.depart_delay.reason
elif self._sensor_type == "ARRIVE_DELAY":
self._attrs["minimum"] = self.coordinator.data.arrive_delay.minimum
self._attrs["maximum"] = self.coordinator.data.arrive_delay.maximum
self._attrs["trend"] = self.coordinator.data.arrive_delay.trend
self._attrs["reason"] = self.coordinator.data.arrive_delay.reason
elif self._sensor_type == "CLOSURE":
self._attrs["begin"] = self.coordinator.data.closure.begin
self._attrs["end"] = self.coordinator.data.closure.end
self._attrs["reason"] = self.coordinator.data.closure.reason
return self._attrs

View File

@ -0,0 +1,62 @@
"""Config flow for FAA Delays integration."""
import logging
from aiohttp import ClientConnectionError
import faadelays
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.const import CONF_ID
from homeassistant.helpers import aiohttp_client
from .const import DOMAIN # pylint:disable=unused-import
_LOGGER = logging.getLogger(__name__)
DATA_SCHEMA = vol.Schema({vol.Required(CONF_ID): str})
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for FAA Delays."""
VERSION = 1
CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
async def async_step_user(self, user_input=None):
"""Handle the initial step."""
errors = {}
if user_input is not None:
await self.async_set_unique_id(user_input[CONF_ID])
self._abort_if_unique_id_configured()
websession = aiohttp_client.async_get_clientsession(self.hass)
data = faadelays.Airport(user_input[CONF_ID], websession)
try:
await data.update()
except faadelays.InvalidAirport:
_LOGGER.error("Airport code %s is invalid", user_input[CONF_ID])
errors[CONF_ID] = "invalid_airport"
except ClientConnectionError:
_LOGGER.error("Error connecting to FAA API")
errors["base"] = "cannot_connect"
except Exception as error: # pylint: disable=broad-except
_LOGGER.exception("Unexpected exception: %s", error)
errors["base"] = "unknown"
if not errors:
_LOGGER.debug(
"Creating entry with id: %s, name: %s",
user_input[CONF_ID],
data.name,
)
return self.async_create_entry(title=data.name, data=user_input)
return self.async_show_form(
step_id="user", data_schema=DATA_SCHEMA, errors=errors
)

View File

@ -0,0 +1,28 @@
"""Constants for the FAA Delays integration."""
from homeassistant.const import ATTR_ICON, ATTR_NAME
DOMAIN = "faa_delays"
FAA_BINARY_SENSORS = {
"GROUND_DELAY": {
ATTR_NAME: "Ground Delay",
ATTR_ICON: "mdi:airport",
},
"GROUND_STOP": {
ATTR_NAME: "Ground Stop",
ATTR_ICON: "mdi:airport",
},
"DEPART_DELAY": {
ATTR_NAME: "Departure Delay",
ATTR_ICON: "mdi:airplane-takeoff",
},
"ARRIVE_DELAY": {
ATTR_NAME: "Arrival Delay",
ATTR_ICON: "mdi:airplane-landing",
},
"CLOSURE": {
ATTR_NAME: "Closure",
ATTR_ICON: "mdi:airplane:off",
},
}

View File

@ -0,0 +1,8 @@
{
"domain": "faa_delays",
"name": "FAA Delays",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/faadelays",
"requirements": ["faadelays==0.0.6"],
"codeowners": ["@ntilley905"]
}

View File

@ -0,0 +1,21 @@
{
"config": {
"step": {
"user": {
"title": "FAA Delays",
"description": "Enter a US Airport Code in IATA Format",
"data": {
"id": "Airport"
}
}
},
"error": {
"invalid_airport": "Airport code is not valid",
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"abort": {
"already_configured": "This airport is already configured."
}
}
}

View File

@ -0,0 +1,19 @@
{
"config": {
"abort": {
"already_configured": "This airport is already configured."
},
"error": {
"invalid_airport": "Airport code is not valid"
},
"step": {
"user": {
"title": "FAA Delays",
"description": "Enter a US Airport Code in IATA Format",
"data": {
"id": "Airport"
}
}
}
}
}

View File

@ -64,6 +64,7 @@ FLOWS = [
"enocean",
"epson",
"esphome",
"faa_delays",
"fireservicerota",
"flick_electric",
"flo",

View File

@ -574,6 +574,9 @@ eternalegypt==0.0.12
# homeassistant.components.evohome
evohome-async==0.3.5.post1
# homeassistant.components.faa_delays
faadelays==0.0.6
# homeassistant.components.dlib_face_detect
# homeassistant.components.dlib_face_identify
# face_recognition==1.2.3

View File

@ -302,6 +302,9 @@ ephem==3.7.7.0
# homeassistant.components.epson
epson-projector==0.2.3
# homeassistant.components.faa_delays
faadelays==0.0.6
# homeassistant.components.feedreader
feedparser==6.0.2

View File

@ -0,0 +1 @@
"""Tests for the FAA Delays integration."""

View File

@ -0,0 +1,120 @@
"""Test the FAA Delays config flow."""
from unittest.mock import patch
from aiohttp import ClientConnectionError
import faadelays
from homeassistant import config_entries, data_entry_flow, setup
from homeassistant.components.faa_delays.const import DOMAIN
from homeassistant.const import CONF_ID
from homeassistant.exceptions import HomeAssistantError
from tests.common import MockConfigEntry
async def mock_valid_airport(self, *args, **kwargs):
"""Return a valid airport."""
self.name = "Test airport"
async def test_form(hass):
"""Test we get the form."""
await setup.async_setup_component(hass, "persistent_notification", {})
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == "form"
assert result["errors"] == {}
with patch.object(faadelays.Airport, "update", new=mock_valid_airport), patch(
"homeassistant.components.faa_delays.async_setup", return_value=True
) as mock_setup, patch(
"homeassistant.components.faa_delays.async_setup_entry",
return_value=True,
) as mock_setup_entry:
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
"id": "test",
},
)
assert result2["type"] == "create_entry"
assert result2["title"] == "Test airport"
assert result2["data"] == {
"id": "test",
}
await hass.async_block_till_done()
assert len(mock_setup.mock_calls) == 1
assert len(mock_setup_entry.mock_calls) == 1
async def test_duplicate_error(hass):
"""Test that we handle a duplicate configuration."""
conf = {CONF_ID: "test"}
MockConfigEntry(domain=DOMAIN, unique_id="test", data=conf).add_to_hass(hass)
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}, data=conf
)
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
assert result["reason"] == "already_configured"
async def test_form_invalid_airport(hass):
"""Test we handle invalid airport."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
with patch(
"faadelays.Airport.update",
side_effect=faadelays.InvalidAirport,
):
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
"id": "test",
},
)
assert result2["type"] == "form"
assert result2["errors"] == {CONF_ID: "invalid_airport"}
async def test_form_cannot_connect(hass):
"""Test we handle a connection error."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
with patch("faadelays.Airport.update", side_effect=ClientConnectionError):
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
"id": "test",
},
)
assert result2["type"] == "form"
assert result2["errors"] == {"base": "cannot_connect"}
async def test_form_unexpected_exception(hass):
"""Test we handle an unexpected exception."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
with patch("faadelays.Airport.update", side_effect=HomeAssistantError):
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
"id": "test",
},
)
assert result2["type"] == "form"
assert result2["errors"] == {"base": "unknown"}