mirror of
https://github.com/home-assistant/core.git
synced 2025-06-25 01:21:51 +02:00
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:
@ -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/*
|
||||
|
@ -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
|
||||
|
84
homeassistant/components/faa_delays/__init__.py
Normal file
84
homeassistant/components/faa_delays/__init__.py
Normal 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
|
93
homeassistant/components/faa_delays/binary_sensor.py
Normal file
93
homeassistant/components/faa_delays/binary_sensor.py
Normal 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
|
62
homeassistant/components/faa_delays/config_flow.py
Normal file
62
homeassistant/components/faa_delays/config_flow.py
Normal 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
|
||||
)
|
28
homeassistant/components/faa_delays/const.py
Normal file
28
homeassistant/components/faa_delays/const.py
Normal 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",
|
||||
},
|
||||
}
|
8
homeassistant/components/faa_delays/manifest.json
Normal file
8
homeassistant/components/faa_delays/manifest.json
Normal 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"]
|
||||
}
|
21
homeassistant/components/faa_delays/strings.json
Normal file
21
homeassistant/components/faa_delays/strings.json
Normal 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."
|
||||
}
|
||||
}
|
||||
}
|
19
homeassistant/components/faa_delays/translations/en.json
Normal file
19
homeassistant/components/faa_delays/translations/en.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -64,6 +64,7 @@ FLOWS = [
|
||||
"enocean",
|
||||
"epson",
|
||||
"esphome",
|
||||
"faa_delays",
|
||||
"fireservicerota",
|
||||
"flick_electric",
|
||||
"flo",
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
1
tests/components/faa_delays/__init__.py
Normal file
1
tests/components/faa_delays/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
"""Tests for the FAA Delays integration."""
|
120
tests/components/faa_delays/test_config_flow.py
Normal file
120
tests/components/faa_delays/test_config_flow.py
Normal 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"}
|
Reference in New Issue
Block a user