mirror of
https://github.com/elisspace/core.git
synced 2026-10-05 16:56:19 +00:00
Add config flow to geo_json_events (#87062)
* migrated geo_json_events integration to config flow * improve test coverage * code reformatting * fix tests * fix entity manager * changes after review * improve test coverage and fixed form * remove unused code * remove commented out code * changes after review * make title prettier * fixed tests * simplified code * changes after review * fix test * push deprecation out * changes after review * changes after review * changes after review * changes after review * changes after review * removed scan interval from user flow and import flow
This commit is contained in:
@@ -1 +1,57 @@
|
||||
"""The geo_json_events component."""
|
||||
"""The GeoJSON events component."""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_registry import (
|
||||
async_entries_for_config_entry,
|
||||
async_get,
|
||||
)
|
||||
|
||||
from .const import DOMAIN, PLATFORMS
|
||||
from .manager import GeoJsonFeedEntityManager
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
|
||||
"""Set up the GeoJSON events component as config entry."""
|
||||
feeds = hass.data.setdefault(DOMAIN, {})
|
||||
# Create feed entity manager for all platforms.
|
||||
manager = GeoJsonFeedEntityManager(hass, config_entry)
|
||||
feeds[config_entry.entry_id] = manager
|
||||
_LOGGER.debug("Feed entity manager added for %s", config_entry.entry_id)
|
||||
await remove_orphaned_entities(hass, config_entry.entry_id)
|
||||
await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
|
||||
await manager.async_init()
|
||||
return True
|
||||
|
||||
|
||||
async def remove_orphaned_entities(hass: HomeAssistant, entry_id: str) -> None:
|
||||
"""Remove orphaned geo_location entities.
|
||||
|
||||
This is needed because when fetching data from the external feed this integration is
|
||||
determining which entities need to be added, updated or removed by comparing the
|
||||
current with the previous data. After a restart of Home Assistant the integration
|
||||
has no previous data to compare against, and thus all entities managed by this
|
||||
integration are removed after startup.
|
||||
"""
|
||||
entity_registry = async_get(hass)
|
||||
orphaned_entries = async_entries_for_config_entry(entity_registry, entry_id)
|
||||
if orphaned_entries is not None:
|
||||
for entry in orphaned_entries:
|
||||
if entry.domain == Platform.GEO_LOCATION:
|
||||
_LOGGER.debug("Removing orphaned entry %s", entry.entity_id)
|
||||
entity_registry.async_remove(entry.entity_id)
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload the GeoJSON events config entry."""
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
if unload_ok:
|
||||
manager: GeoJsonFeedEntityManager = hass.data[DOMAIN].pop(entry.entry_id)
|
||||
await manager.async_stop()
|
||||
return unload_ok
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
"""Config flow to configure the GeoJSON events integration."""
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.const import (
|
||||
CONF_LATITUDE,
|
||||
CONF_LOCATION,
|
||||
CONF_LONGITUDE,
|
||||
CONF_RADIUS,
|
||||
CONF_URL,
|
||||
UnitOfLength,
|
||||
)
|
||||
from homeassistant.data_entry_flow import FlowResult
|
||||
from homeassistant.helpers import config_validation as cv, selector
|
||||
from homeassistant.util.unit_conversion import DistanceConverter
|
||||
|
||||
from .const import DEFAULT_RADIUS_IN_KM, DEFAULT_RADIUS_IN_M, DOMAIN
|
||||
|
||||
DATA_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_URL): cv.string,
|
||||
vol.Required(CONF_LOCATION): selector.LocationSelector(
|
||||
selector.LocationSelectorConfig(radius=True, icon="")
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class GeoJsonEventsFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a GeoJSON events config flow."""
|
||||
|
||||
async def async_step_import(self, import_config: dict[str, Any]) -> FlowResult:
|
||||
"""Import a config entry from configuration.yaml."""
|
||||
url: str = import_config[CONF_URL]
|
||||
latitude: float = import_config.get(CONF_LATITUDE, self.hass.config.latitude)
|
||||
longitude: float = import_config.get(CONF_LONGITUDE, self.hass.config.longitude)
|
||||
self._async_abort_entries_match(
|
||||
{
|
||||
CONF_URL: url,
|
||||
CONF_LATITUDE: latitude,
|
||||
CONF_LONGITUDE: longitude,
|
||||
}
|
||||
)
|
||||
return self.async_create_entry(
|
||||
title=f"{url} ({latitude}, {longitude})",
|
||||
data={
|
||||
CONF_URL: url,
|
||||
CONF_LATITUDE: latitude,
|
||||
CONF_LONGITUDE: longitude,
|
||||
CONF_RADIUS: import_config.get(CONF_RADIUS, DEFAULT_RADIUS_IN_KM),
|
||||
},
|
||||
)
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Handle the start of the config flow."""
|
||||
if not user_input:
|
||||
suggested_values: Mapping[str, Any] = {
|
||||
CONF_LOCATION: {
|
||||
CONF_LATITUDE: self.hass.config.latitude,
|
||||
CONF_LONGITUDE: self.hass.config.longitude,
|
||||
CONF_RADIUS: DEFAULT_RADIUS_IN_M,
|
||||
}
|
||||
}
|
||||
data_schema = self.add_suggested_values_to_schema(
|
||||
DATA_SCHEMA, suggested_values
|
||||
)
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=data_schema,
|
||||
)
|
||||
|
||||
url: str = user_input[CONF_URL]
|
||||
location: dict[str, Any] = user_input[CONF_LOCATION]
|
||||
latitude: float = location[CONF_LATITUDE]
|
||||
longitude: float = location[CONF_LONGITUDE]
|
||||
self._async_abort_entries_match(
|
||||
{
|
||||
CONF_URL: url,
|
||||
CONF_LATITUDE: latitude,
|
||||
CONF_LONGITUDE: longitude,
|
||||
}
|
||||
)
|
||||
return self.async_create_entry(
|
||||
title=f"{url} ({latitude}, {longitude})",
|
||||
data={
|
||||
CONF_URL: url,
|
||||
CONF_LATITUDE: latitude,
|
||||
CONF_LONGITUDE: longitude,
|
||||
CONF_RADIUS: DistanceConverter.convert(
|
||||
location[CONF_RADIUS],
|
||||
UnitOfLength.METERS,
|
||||
UnitOfLength.KILOMETERS,
|
||||
),
|
||||
},
|
||||
)
|
||||
@@ -4,11 +4,16 @@ from __future__ import annotations
|
||||
from datetime import timedelta
|
||||
from typing import Final
|
||||
|
||||
from homeassistant.const import Platform
|
||||
|
||||
DOMAIN: Final = "geo_json_events"
|
||||
|
||||
PLATFORMS: Final = [Platform.GEO_LOCATION]
|
||||
|
||||
ATTR_EXTERNAL_ID: Final = "external_id"
|
||||
DEFAULT_RADIUS_IN_KM: Final = 20.0
|
||||
DEFAULT_SCAN_INTERVAL: Final = timedelta(minutes=5)
|
||||
DEFAULT_RADIUS_IN_M: Final = 20000.0
|
||||
DEFAULT_UPDATE_INTERVAL: Final = timedelta(seconds=300)
|
||||
SOURCE: Final = "geo_json_events"
|
||||
|
||||
SIGNAL_DELETE_ENTITY: Final = "geo_json_events_delete_{}"
|
||||
|
||||
@@ -2,42 +2,41 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from aio_geojson_generic_client import GenericFeedManager
|
||||
from aio_geojson_generic_client.feed_entry import GenericFeedEntry
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.geo_location import PLATFORM_SCHEMA, GeolocationEvent
|
||||
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
|
||||
from homeassistant.const import (
|
||||
CONF_LATITUDE,
|
||||
CONF_LONGITUDE,
|
||||
CONF_RADIUS,
|
||||
CONF_SCAN_INTERVAL,
|
||||
CONF_URL,
|
||||
EVENT_HOMEASSISTANT_START,
|
||||
UnitOfLength,
|
||||
)
|
||||
from homeassistant.core import Event, HomeAssistant, callback
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
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 . import GeoJsonFeedEntityManager
|
||||
from .const import (
|
||||
ATTR_EXTERNAL_ID,
|
||||
DEFAULT_RADIUS_IN_KM,
|
||||
DEFAULT_SCAN_INTERVAL,
|
||||
DOMAIN,
|
||||
SIGNAL_DELETE_ENTITY,
|
||||
SIGNAL_UPDATE_ENTITY,
|
||||
SOURCE,
|
||||
)
|
||||
from .manager import GeoJsonFeedEntityManager
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# Deprecated.
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||
{
|
||||
vol.Required(CONF_URL): cv.string,
|
||||
@@ -48,28 +47,15 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_platform(
|
||||
hass: HomeAssistant,
|
||||
config: ConfigType,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
discovery_info: DiscoveryInfoType | None = None,
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
||||
) -> None:
|
||||
"""Set up the GeoJSON Events platform."""
|
||||
url: str = config[CONF_URL]
|
||||
scan_interval: timedelta = config.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL)
|
||||
coordinates: tuple[float, float] = (
|
||||
config.get(CONF_LATITUDE, hass.config.latitude),
|
||||
config.get(CONF_LONGITUDE, hass.config.longitude),
|
||||
)
|
||||
radius_in_km: float = config[CONF_RADIUS]
|
||||
# Initialize the entity manager.
|
||||
manager = GeoJsonFeedEntityManager(
|
||||
hass, scan_interval, coordinates, url, radius_in_km
|
||||
)
|
||||
manager: GeoJsonFeedEntityManager = hass.data[DOMAIN][entry.entry_id]
|
||||
|
||||
@callback
|
||||
def async_add_geolocation(
|
||||
feed_manager: GenericFeedManager,
|
||||
feed_manager: GeoJsonFeedEntityManager,
|
||||
external_id: str,
|
||||
) -> None:
|
||||
"""Add geolocation entity from feed."""
|
||||
@@ -77,15 +63,36 @@ async def async_setup_platform(
|
||||
_LOGGER.debug("Adding geolocation %s", new_entity)
|
||||
async_add_entities([new_entity], True)
|
||||
|
||||
async_dispatcher_connect(hass, manager.signal_new_entity, async_add_geolocation)
|
||||
manager.listeners.append(
|
||||
async_dispatcher_connect(hass, manager.signal_new_entity, async_add_geolocation)
|
||||
)
|
||||
# Do not wait for update here so that the setup can be completed and because an
|
||||
# update will fetch data from the feed via HTTP and then process that data.
|
||||
entry.async_create_task(hass, manager.async_update())
|
||||
_LOGGER.debug("Geolocation setup done")
|
||||
|
||||
await manager.async_init()
|
||||
|
||||
async def start_feed_manager(event: Event) -> None:
|
||||
"""Start feed manager."""
|
||||
await manager.async_update()
|
||||
|
||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, start_feed_manager)
|
||||
async def async_setup_platform(
|
||||
hass: HomeAssistant,
|
||||
config: ConfigType,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
discovery_info: DiscoveryInfoType | None = None,
|
||||
) -> None:
|
||||
"""Set up the GeoJSON Events platform."""
|
||||
async_create_issue(
|
||||
hass,
|
||||
DOMAIN,
|
||||
"deprecated_yaml",
|
||||
breaks_in_ha_version="2023.8.0",
|
||||
is_fixable=False,
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key="deprecated_yaml",
|
||||
)
|
||||
hass.async_create_task(
|
||||
hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_IMPORT}, data=config
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class GeoJsonLocationEvent(GeolocationEvent):
|
||||
@@ -95,10 +102,15 @@ class GeoJsonLocationEvent(GeolocationEvent):
|
||||
_attr_source = SOURCE
|
||||
_attr_unit_of_measurement = UnitOfLength.KILOMETERS
|
||||
|
||||
def __init__(self, feed_manager: GenericFeedManager, external_id: str) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
feed_manager: GeoJsonFeedEntityManager,
|
||||
external_id: str,
|
||||
) -> None:
|
||||
"""Initialize entity with data from feed entry."""
|
||||
self._feed_manager = feed_manager
|
||||
self._external_id = external_id
|
||||
self._attr_unique_id = f"{feed_manager.entry_id}_{external_id}"
|
||||
self._remove_signal_delete: Callable[[], None]
|
||||
self._remove_signal_update: Callable[[], None]
|
||||
|
||||
|
||||
@@ -1,18 +1,26 @@
|
||||
"""Entity manager for generic GeoJSON events."""
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from collections.abc import Callable
|
||||
from datetime import datetime
|
||||
import logging
|
||||
|
||||
from aio_geojson_generic_client import GenericFeedManager
|
||||
from aio_geojson_generic_client.feed_entry import GenericFeedEntry
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_RADIUS, CONF_URL
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import aiohttp_client
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
from homeassistant.helpers.event import async_track_time_interval
|
||||
|
||||
from .const import DOMAIN, SIGNAL_DELETE_ENTITY, SIGNAL_UPDATE_ENTITY
|
||||
from .const import (
|
||||
DEFAULT_UPDATE_INTERVAL,
|
||||
DOMAIN,
|
||||
SIGNAL_DELETE_ENTITY,
|
||||
SIGNAL_UPDATE_ENTITY,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -23,27 +31,28 @@ class GeoJsonFeedEntityManager:
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
scan_interval: timedelta,
|
||||
coordinates: tuple[float, float],
|
||||
url: str,
|
||||
radius_in_km: float,
|
||||
config_entry: ConfigEntry,
|
||||
) -> None:
|
||||
"""Initialize the GeoJSON Feed Manager."""
|
||||
|
||||
self._hass = hass
|
||||
self._hass: HomeAssistant = hass
|
||||
self.entry_id: str = config_entry.entry_id
|
||||
websession = aiohttp_client.async_get_clientsession(hass)
|
||||
self._feed_manager = GenericFeedManager(
|
||||
self._feed_manager: GenericFeedManager = GenericFeedManager(
|
||||
websession,
|
||||
self._generate_entity,
|
||||
self._update_entity,
|
||||
self._remove_entity,
|
||||
coordinates,
|
||||
url,
|
||||
filter_radius=radius_in_km,
|
||||
(
|
||||
config_entry.data[CONF_LATITUDE],
|
||||
config_entry.data[CONF_LONGITUDE],
|
||||
),
|
||||
config_entry.data[CONF_URL],
|
||||
filter_radius=config_entry.data[CONF_RADIUS],
|
||||
)
|
||||
self._scan_interval = scan_interval
|
||||
self.signal_new_entity = (
|
||||
f"{DOMAIN}_new_geolocation_{coordinates}-{url}-{radius_in_km}"
|
||||
self._track_time_remove_callback: Callable[[], None] | None = None
|
||||
self.listeners: list[Callable[[], None]] = []
|
||||
self.signal_new_entity: str = (
|
||||
f"{DOMAIN}_new_geolocation_{config_entry.entry_id}"
|
||||
)
|
||||
|
||||
async def async_init(self) -> None:
|
||||
@@ -54,7 +63,10 @@ class GeoJsonFeedEntityManager:
|
||||
await self.async_update()
|
||||
|
||||
# Trigger updates at regular intervals.
|
||||
async_track_time_interval(self._hass, update, self._scan_interval)
|
||||
self._track_time_remove_callback = async_track_time_interval(
|
||||
self._hass, update, DEFAULT_UPDATE_INTERVAL
|
||||
)
|
||||
|
||||
_LOGGER.debug("Feed entity manager initialized")
|
||||
|
||||
async def async_update(self) -> None:
|
||||
@@ -62,6 +74,15 @@ class GeoJsonFeedEntityManager:
|
||||
await self._feed_manager.update()
|
||||
_LOGGER.debug("Feed entity manager updated")
|
||||
|
||||
async def async_stop(self) -> None:
|
||||
"""Stop this feed entity manager from refreshing."""
|
||||
for unsub_dispatcher in self.listeners:
|
||||
unsub_dispatcher()
|
||||
self.listeners = []
|
||||
if self._track_time_remove_callback:
|
||||
self._track_time_remove_callback()
|
||||
_LOGGER.debug("Feed entity manager stopped")
|
||||
|
||||
def get_entry(self, external_id: str) -> GenericFeedEntry | None:
|
||||
"""Get feed entry by external id."""
|
||||
return self._feed_manager.feed_entries.get(external_id)
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"domain": "geo_json_events",
|
||||
"name": "GeoJSON",
|
||||
"codeowners": ["@exxamalte"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/geo_json_events",
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Fill in your GeoJSON feed details.",
|
||||
"data": {
|
||||
"url": "[%key:common::config_flow::data::url%]",
|
||||
"location": "[%key:common::config_flow::data::location%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]"
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"deprecated_yaml": {
|
||||
"title": "The GeoJSON feed YAML configuration is being removed",
|
||||
"description": "Configuring a GeoJSON feed using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the GeoJSON feed YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue."
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user