Add config flow to OpenSky (#96912)

Co-authored-by: Sander <developer@golles.nl>
This commit is contained in:
Joost Lekkerkerker 2023-07-25 20:46:04 +02:00 committed by GitHub
parent b4200cb85e
commit 585d357129
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 443 additions and 19 deletions

View File

@ -894,6 +894,7 @@ build.json @home-assistant/supervisor
/homeassistant/components/openhome/ @bazwilliams
/tests/components/openhome/ @bazwilliams
/homeassistant/components/opensky/ @joostlek
/tests/components/opensky/ @joostlek
/homeassistant/components/opentherm_gw/ @mvn23
/tests/components/opentherm_gw/ @mvn23
/homeassistant/components/openuv/ @bachya

View File

@ -1 +1,27 @@
"""The opensky component."""
from __future__ import annotations
from python_opensky import OpenSky
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import CLIENT, DOMAIN, PLATFORMS
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up opensky from a config entry."""
client = OpenSky(session=async_get_clientsession(hass))
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = {CLIENT: client}
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload opensky config entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)

View File

@ -0,0 +1,77 @@
"""Config flow for OpenSky integration."""
from __future__ import annotations
from typing import Any
import voluptuous as vol
from homeassistant.config_entries import ConfigFlow
from homeassistant.const import (
CONF_LATITUDE,
CONF_LONGITUDE,
CONF_NAME,
CONF_RADIUS,
)
from homeassistant.data_entry_flow import FlowResult
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.typing import ConfigType
from .const import DEFAULT_NAME, DOMAIN
from .sensor import CONF_ALTITUDE, DEFAULT_ALTITUDE
class OpenSkyConfigFlowHandler(ConfigFlow, domain=DOMAIN):
"""Config flow handler for OpenSky."""
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Initialize user input."""
if user_input is not None:
return self.async_create_entry(
title=DEFAULT_NAME,
data={
CONF_LATITUDE: user_input[CONF_LATITUDE],
CONF_LONGITUDE: user_input[CONF_LONGITUDE],
},
options={
CONF_RADIUS: user_input[CONF_RADIUS],
CONF_ALTITUDE: user_input[CONF_ALTITUDE],
},
)
return self.async_show_form(
step_id="user",
data_schema=self.add_suggested_values_to_schema(
vol.Schema(
{
vol.Required(CONF_RADIUS): vol.Coerce(float),
vol.Required(CONF_LATITUDE): cv.latitude,
vol.Required(CONF_LONGITUDE): cv.longitude,
vol.Optional(CONF_ALTITUDE): vol.Coerce(float),
}
),
{
CONF_LATITUDE: self.hass.config.latitude,
CONF_LONGITUDE: self.hass.config.longitude,
CONF_ALTITUDE: DEFAULT_ALTITUDE,
},
),
)
async def async_step_import(self, import_config: ConfigType) -> FlowResult:
"""Import config from yaml."""
entry_data = {
CONF_LATITUDE: import_config.get(CONF_LATITUDE, self.hass.config.latitude),
CONF_LONGITUDE: import_config.get(
CONF_LONGITUDE, self.hass.config.longitude
),
}
self._async_abort_entries_match(entry_data)
return self.async_create_entry(
title=import_config.get(CONF_NAME, DEFAULT_NAME),
data=entry_data,
options={
CONF_RADIUS: import_config[CONF_RADIUS] * 1000,
CONF_ALTITUDE: import_config.get(CONF_ALTITUDE, DEFAULT_ALTITUDE),
},
)

View File

@ -1,6 +1,10 @@
"""OpenSky constants."""
from homeassistant.const import Platform
PLATFORMS = [Platform.SENSOR]
DEFAULT_NAME = "OpenSky"
DOMAIN = "opensky"
CLIENT = "client"
CONF_ALTITUDE = "altitude"
ATTR_ICAO24 = "icao24"

View File

@ -2,6 +2,7 @@
"domain": "opensky",
"name": "OpenSky Network",
"codeowners": ["@joostlek"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/opensky",
"iot_class": "cloud_polling",
"requirements": ["python-opensky==0.0.10"]

View File

@ -7,6 +7,7 @@ from python_opensky import BoundingBox, OpenSky, StateVector
import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.const import (
ATTR_LATITUDE,
ATTR_LONGITUDE,
@ -15,10 +16,10 @@ from homeassistant.const import (
CONF_NAME,
CONF_RADIUS,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from .const import (
@ -26,6 +27,7 @@ from .const import (
ATTR_CALLSIGN,
ATTR_ICAO24,
ATTR_SENSOR,
CLIENT,
CONF_ALTITUDE,
DEFAULT_ALTITUDE,
DOMAIN,
@ -36,6 +38,7 @@ from .const import (
# OpenSky free user has 400 credits, with 4 credits per API call. 100/24 = ~4 requests per hour
SCAN_INTERVAL = timedelta(minutes=15)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
vol.Required(CONF_RADIUS): vol.Coerce(float),
@ -47,27 +50,57 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
)
def setup_platform(
async def async_setup_platform(
hass: HomeAssistant,
config: ConfigType,
add_entities: AddEntitiesCallback,
async_add_entities: AddEntitiesCallback,
discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""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 = OpenSky.get_bounding_box(latitude, longitude, radius * 1000)
session = async_get_clientsession(hass)
opensky = OpenSky(session=session)
add_entities(
"""Set up the OpenSky sensor platform from yaml."""
async_create_issue(
hass,
HOMEASSISTANT_DOMAIN,
f"deprecated_yaml_{DOMAIN}",
breaks_in_ha_version="2024.1.0",
is_fixable=False,
issue_domain=DOMAIN,
severity=IssueSeverity.WARNING,
translation_key="deprecated_yaml",
translation_placeholders={
"domain": DOMAIN,
"integration_title": "OpenSky",
},
)
hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_IMPORT}, data=config
)
)
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Initialize the entries."""
opensky = hass.data[DOMAIN][entry.entry_id][CLIENT]
bounding_box = OpenSky.get_bounding_box(
entry.data[CONF_LATITUDE],
entry.data[CONF_LONGITUDE],
entry.options[CONF_RADIUS],
)
async_add_entities(
[
OpenSkySensor(
hass,
config.get(CONF_NAME, DOMAIN),
entry.title,
opensky,
bounding_box,
config[CONF_ALTITUDE],
entry.options.get(CONF_ALTITUDE, DEFAULT_ALTITUDE),
entry.entry_id,
)
],
True,
@ -83,20 +116,20 @@ class OpenSkySensor(SensorEntity):
def __init__(
self,
hass: HomeAssistant,
name: str,
opensky: OpenSky,
bounding_box: BoundingBox,
altitude: float,
entry_id: str,
) -> None:
"""Initialize the sensor."""
self._altitude = altitude
self._state = 0
self._hass = hass
self._name = name
self._previously_tracked: set[str] = set()
self._opensky = opensky
self._bounding_box = bounding_box
self._attr_unique_id = f"{entry_id}_opensky"
@property
def name(self) -> str:
@ -133,7 +166,7 @@ class OpenSkySensor(SensorEntity):
ATTR_LATITUDE: latitude,
ATTR_ICAO24: icao24,
}
self._hass.bus.fire(event, data)
self.hass.bus.fire(event, data)
async def async_update(self) -> None:
"""Update device state."""

View File

@ -0,0 +1,16 @@
{
"config": {
"step": {
"user": {
"description": "Fill in the location to track.",
"data": {
"name": "[%key:common::config_flow::data::api_key%]",
"radius": "Radius",
"latitude": "[%key:common::config_flow::data::latitude%]",
"longitude": "[%key:common::config_flow::data::longitude%]",
"altitude": "Altitude"
}
}
}
}
}

View File

@ -325,6 +325,7 @@ FLOWS = {
"openexchangerates",
"opengarage",
"openhome",
"opensky",
"opentherm_gw",
"openuv",
"openweathermap",

View File

@ -3976,7 +3976,7 @@
"opensky": {
"name": "OpenSky Network",
"integration_type": "hub",
"config_flow": false,
"config_flow": true,
"iot_class": "cloud_polling"
},
"opentherm_gw": {

View File

@ -1562,6 +1562,9 @@ python-miio==0.5.12
# homeassistant.components.mystrom
python-mystrom==2.2.0
# homeassistant.components.opensky
python-opensky==0.0.10
# homeassistant.components.otbr
# homeassistant.components.thread
python-otbr-api==2.3.0

View File

@ -0,0 +1,9 @@
"""Opensky tests."""
from unittest.mock import patch
def patch_setup_entry() -> bool:
"""Patch interface."""
return patch(
"homeassistant.components.opensky.async_setup_entry", return_value=True
)

View File

@ -0,0 +1,50 @@
"""Configure tests for the OpenSky integration."""
from collections.abc import Awaitable, Callable
from unittest.mock import patch
import pytest
from python_opensky import StatesResponse
from homeassistant.components.opensky.const import CONF_ALTITUDE, DOMAIN
from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_RADIUS
from homeassistant.core import HomeAssistant
from homeassistant.setup import async_setup_component
from tests.common import MockConfigEntry
ComponentSetup = Callable[[MockConfigEntry], Awaitable[None]]
@pytest.fixture(name="config_entry")
def mock_config_entry() -> MockConfigEntry:
"""Create OpenSky entry in Home Assistant."""
return MockConfigEntry(
domain=DOMAIN,
title="OpenSky",
data={
CONF_LATITUDE: 0.0,
CONF_LONGITUDE: 0.0,
},
options={
CONF_RADIUS: 10.0,
CONF_ALTITUDE: 0.0,
},
)
@pytest.fixture(name="setup_integration")
async def mock_setup_integration(
hass: HomeAssistant,
) -> Callable[[MockConfigEntry], Awaitable[None]]:
"""Fixture for setting up the component."""
async def func(mock_config_entry: MockConfigEntry) -> None:
mock_config_entry.add_to_hass(hass)
with patch(
"python_opensky.OpenSky.get_states",
return_value=StatesResponse(states=[], time=0),
):
assert await async_setup_component(hass, DOMAIN, {})
await hass.async_block_till_done()
return func

View File

@ -0,0 +1,155 @@
"""Test OpenSky config flow."""
from typing import Any
import pytest
from homeassistant.components.opensky.const import (
CONF_ALTITUDE,
DEFAULT_NAME,
DOMAIN,
)
from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER
from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, CONF_RADIUS
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType
from . import patch_setup_entry
from tests.common import MockConfigEntry
async def test_full_user_flow(hass: HomeAssistant) -> None:
"""Test the full user configuration flow."""
with patch_setup_entry():
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_USER},
)
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input={
CONF_RADIUS: 10,
CONF_LATITUDE: 0.0,
CONF_LONGITUDE: 0.0,
CONF_ALTITUDE: 0,
},
)
assert result["type"] == FlowResultType.CREATE_ENTRY
assert result["title"] == "OpenSky"
assert result["data"] == {
CONF_LATITUDE: 0.0,
CONF_LONGITUDE: 0.0,
}
assert result["options"] == {
CONF_ALTITUDE: 0.0,
CONF_RADIUS: 10.0,
}
@pytest.mark.parametrize(
("config", "title", "data", "options"),
[
(
{CONF_RADIUS: 10.0},
DEFAULT_NAME,
{
CONF_LATITUDE: 32.87336,
CONF_LONGITUDE: -117.22743,
},
{
CONF_RADIUS: 10000.0,
CONF_ALTITUDE: 0,
},
),
(
{
CONF_RADIUS: 10.0,
CONF_NAME: "My home",
},
"My home",
{
CONF_LATITUDE: 32.87336,
CONF_LONGITUDE: -117.22743,
},
{
CONF_RADIUS: 10000.0,
CONF_ALTITUDE: 0,
},
),
(
{
CONF_RADIUS: 10.0,
CONF_LATITUDE: 10.0,
CONF_LONGITUDE: -100.0,
},
DEFAULT_NAME,
{
CONF_LATITUDE: 10.0,
CONF_LONGITUDE: -100.0,
},
{
CONF_RADIUS: 10000.0,
CONF_ALTITUDE: 0,
},
),
(
{CONF_RADIUS: 10.0, CONF_ALTITUDE: 100.0},
DEFAULT_NAME,
{
CONF_LATITUDE: 32.87336,
CONF_LONGITUDE: -117.22743,
},
{
CONF_RADIUS: 10000.0,
CONF_ALTITUDE: 100.0,
},
),
],
)
async def test_import_flow(
hass: HomeAssistant,
config: dict[str, Any],
title: str,
data: dict[str, Any],
options: dict[str, Any],
) -> None:
"""Test the import flow."""
with patch_setup_entry():
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_IMPORT}, data=config
)
await hass.async_block_till_done()
assert result["type"] == FlowResultType.CREATE_ENTRY
assert result["title"] == title
assert result["options"] == options
assert result["data"] == data
async def test_importing_already_exists_flow(hass: HomeAssistant) -> None:
"""Test the import flow when same location already exists."""
MockConfigEntry(
domain=DOMAIN,
title=DEFAULT_NAME,
data={},
options={
CONF_LATITUDE: 32.87336,
CONF_LONGITUDE: -117.22743,
CONF_RADIUS: 10.0,
CONF_ALTITUDE: 100.0,
},
).add_to_hass(hass)
with patch_setup_entry():
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_IMPORT},
data={
CONF_LATITUDE: 32.87336,
CONF_LONGITUDE: -117.22743,
CONF_RADIUS: 10.0,
CONF_ALTITUDE: 100.0,
},
)
await hass.async_block_till_done()
assert result["type"] == FlowResultType.ABORT
assert result["reason"] == "already_configured"

View File

@ -0,0 +1,28 @@
"""Test OpenSky component setup process."""
from __future__ import annotations
from homeassistant.components.opensky.const import DOMAIN
from homeassistant.core import HomeAssistant
from .conftest import ComponentSetup
from tests.common import MockConfigEntry
async def test_load_unload_entry(
hass: HomeAssistant,
setup_integration: ComponentSetup,
config_entry: MockConfigEntry,
) -> None:
"""Test load and unload entry."""
await setup_integration(config_entry)
entry = hass.config_entries.async_entries(DOMAIN)[0]
state = hass.states.get("sensor.opensky")
assert state
await hass.config_entries.async_remove(entry.entry_id)
await hass.async_block_till_done()
state = hass.states.get("sensor.opensky")
assert not state

View File

@ -0,0 +1,20 @@
"""OpenSky sensor tests."""
from homeassistant.components.opensky.const import DOMAIN
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import CONF_PLATFORM, CONF_RADIUS, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import issue_registry as ir
from homeassistant.setup import async_setup_component
LEGACY_CONFIG = {Platform.SENSOR: [{CONF_PLATFORM: DOMAIN, CONF_RADIUS: 10.0}]}
async def test_legacy_migration(hass: HomeAssistant) -> None:
"""Test migration from yaml to config flow."""
assert await async_setup_component(hass, Platform.SENSOR, LEGACY_CONFIG)
await hass.async_block_till_done()
entries = hass.config_entries.async_entries(DOMAIN)
assert len(entries) == 1
assert entries[0].state is ConfigEntryState.LOADED
issue_registry = ir.async_get(hass)
assert len(issue_registry.issues) == 1