Refactor Launch Library to use config flow (#62416)

Co-authored-by: Franck Nijhof <frenck@frenck.nl>
Co-authored-by: Joakim Sørensen <hi@ludeeus.dev>
This commit is contained in:
Simon Hansen 2022-01-08 11:47:16 +01:00 committed by GitHub
parent b4187540c0
commit b22a9b8669
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 269 additions and 39 deletions

View File

@ -576,6 +576,7 @@ omit =
homeassistant/components/lametric/* homeassistant/components/lametric/*
homeassistant/components/lannouncer/notify.py homeassistant/components/lannouncer/notify.py
homeassistant/components/lastfm/sensor.py homeassistant/components/lastfm/sensor.py
homeassistant/components/launch_library/__init__.py
homeassistant/components/launch_library/const.py homeassistant/components/launch_library/const.py
homeassistant/components/launch_library/sensor.py homeassistant/components/launch_library/sensor.py
homeassistant/components/lcn/binary_sensor.py homeassistant/components/lcn/binary_sensor.py

View File

@ -489,7 +489,8 @@ tests/components/kraken/* @eifinger
homeassistant/components/kulersky/* @emlove homeassistant/components/kulersky/* @emlove
tests/components/kulersky/* @emlove tests/components/kulersky/* @emlove
homeassistant/components/lametric/* @robbiet480 homeassistant/components/lametric/* @robbiet480
homeassistant/components/launch_library/* @ludeeus homeassistant/components/launch_library/* @ludeeus @DurgNomis-drol
tests/components/launch_library/* @ludeeus @DurgNomis-drol
homeassistant/components/lcn/* @alengwenus homeassistant/components/lcn/* @alengwenus
tests/components/lcn/* @alengwenus tests/components/lcn/* @alengwenus
homeassistant/components/lg_netcast/* @Drafteed homeassistant/components/lg_netcast/* @Drafteed

View File

@ -1 +1,55 @@
"""The launch_library component.""" """The launch_library component."""
from datetime import timedelta
import logging
from pylaunches import PyLaunches, PyLaunchesException
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
PLATFORMS = [Platform.SENSOR]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up this integration using UI."""
hass.data.setdefault(DOMAIN, {})
session = async_get_clientsession(hass)
launches = PyLaunches(session)
async def async_update():
try:
return await launches.upcoming_launches()
except PyLaunchesException as ex:
raise UpdateFailed(ex) from ex
coordinator = DataUpdateCoordinator(
hass,
_LOGGER,
name=DOMAIN,
update_method=async_update,
update_interval=timedelta(hours=1),
)
await coordinator.async_config_entry_first_refresh()
hass.data[DOMAIN] = coordinator
hass.config_entries.async_setup_platforms(entry, PLATFORMS)
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Handle removal of an entry."""
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
del hass.data[DOMAIN]
return unload_ok

View File

@ -0,0 +1,27 @@
"""Config flow to configure launch library component."""
from homeassistant import config_entries
from homeassistant.const import CONF_NAME
from homeassistant.data_entry_flow import FlowResult
from .const import DOMAIN
class LaunchLibraryFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
"""Config flow for Launch Library component."""
VERSION = 1
async def async_step_user(self, user_input=None) -> FlowResult:
"""Handle a flow initialized by the user."""
# Check if already configured
if self._async_current_entries():
return self.async_abort(reason="single_instance_allowed")
if user_input is not None:
return self.async_create_entry(title="Launch Library", data=user_input)
return self.async_show_form(step_id="user")
async def async_step_import(self, conf: dict) -> FlowResult:
"""Import a configuration from config.yaml."""
return await self.async_step_user(user_input={CONF_NAME: conf[CONF_NAME]})

View File

@ -1,5 +1,7 @@
"""Constants for launch_library.""" """Constants for launch_library."""
DOMAIN = "launch_library"
ATTR_AGENCY = "agency" ATTR_AGENCY = "agency"
ATTR_AGENCY_COUNTRY_CODE = "agency_country_code" ATTR_AGENCY_COUNTRY_CODE = "agency_country_code"
ATTR_LAUNCH_TIME = "launch_time" ATTR_LAUNCH_TIME = "launch_time"

View File

@ -1,8 +1,9 @@
{ {
"domain": "launch_library", "domain": "launch_library",
"name": "Launch Library", "name": "Launch Library",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/launch_library", "documentation": "https://www.home-assistant.io/integrations/launch_library",
"requirements": ["pylaunches==1.2.0"], "requirements": ["pylaunches==1.2.0"],
"codeowners": ["@ludeeus"], "codeowners": ["@ludeeus", "@DurgNomis-drol"],
"iot_class": "cloud_polling" "iot_class": "cloud_polling"
} }

View File

@ -1,19 +1,23 @@
"""A sensor platform that give you information about the next space launch.""" """Support for Launch Library sensors."""
from __future__ import annotations from __future__ import annotations
from datetime import timedelta
import logging import logging
from typing import Any
from pylaunches import PyLaunches, PyLaunchesException from pylaunches.objects.launch import Launch
import voluptuous as vol import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.core import HomeAssistant from homeassistant.const import CONF_NAME
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.core import HomeAssistant, callback
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.helpers.update_coordinator import (
CoordinatorEntity,
DataUpdateCoordinator,
)
from .const import ( from .const import (
ATTR_AGENCY, ATTR_AGENCY,
@ -22,11 +26,11 @@ from .const import (
ATTR_STREAM, ATTR_STREAM,
ATTRIBUTION, ATTRIBUTION,
DEFAULT_NAME, DEFAULT_NAME,
DOMAIN,
) )
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
SCAN_INTERVAL = timedelta(hours=1)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string} {vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string}
@ -39,39 +43,86 @@ async def async_setup_platform(
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
discovery_info: DiscoveryInfoType | None = None, discovery_info: DiscoveryInfoType | None = None,
) -> None: ) -> None:
"""Create the launch sensor.""" """Import Launch Library configuration from yaml."""
name = config[CONF_NAME] _LOGGER.warning(
session = async_get_clientsession(hass) "Configuration of the launch_library platform in YAML is deprecated and will be "
launches = PyLaunches(session) "removed in Home Assistant 2022.4; Your existing configuration "
"has been imported into the UI automatically and can be safely removed "
async_add_entities([LaunchLibrarySensor(launches, name)], True) "from your configuration.yaml file"
)
hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_IMPORT},
data=config,
)
)
class LaunchLibrarySensor(SensorEntity): async def async_setup_entry(
"""Representation of a launch_library Sensor.""" hass: HomeAssistant,
entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the sensor platform."""
_attr_icon = "mdi:rocket" name = entry.data.get(CONF_NAME, DEFAULT_NAME)
def __init__(self, api: PyLaunches, name: str) -> None: coordinator = hass.data[DOMAIN]
"""Initialize the sensor."""
self.api = api async_add_entities(
[
NextLaunchSensor(coordinator, entry.entry_id, name),
]
)
class NextLaunchSensor(CoordinatorEntity, SensorEntity):
"""Representation of the next launch sensor."""
_attr_attribution = ATTRIBUTION
_attr_icon = "mdi:rocket-launch"
_next_launch: Launch | None = None
def __init__(
self, coordinator: DataUpdateCoordinator, entry_id: str, name: str
) -> None:
"""Initialize a Launch Library entity."""
super().__init__(coordinator)
self._attr_name = name self._attr_name = name
self._attr_unique_id = f"{entry_id}_next_launch"
async def async_update(self) -> None: @property
"""Get the latest data.""" def native_value(self) -> str | None:
try: """Return the state of the sensor."""
launches = await self.api.upcoming_launches() if self._next_launch is None:
except PyLaunchesException as exception: return None
_LOGGER.error("Error getting data, %s", exception) return self._next_launch.name
self._attr_available = False
else: @property
if next_launch := next((launch for launch in launches), None): def available(self) -> bool:
self._attr_available = True """Return if the sensor is available."""
self._attr_native_value = next_launch.name return super().available and self._next_launch is not None
self._attr_extra_state_attributes = {
ATTR_LAUNCH_TIME: next_launch.net, @property
ATTR_AGENCY: next_launch.launch_service_provider.name, def extra_state_attributes(self) -> dict[str, Any] | None:
ATTR_AGENCY_COUNTRY_CODE: next_launch.pad.location.country_code, """Return the attributes of the sensor."""
ATTR_STREAM: next_launch.webcast_live, if self._next_launch is None:
ATTR_ATTRIBUTION: ATTRIBUTION, return None
} return {
ATTR_LAUNCH_TIME: self._next_launch.net,
ATTR_AGENCY: self._next_launch.launch_service_provider.name,
ATTR_AGENCY_COUNTRY_CODE: self._next_launch.pad.location.country_code,
ATTR_STREAM: self._next_launch.webcast_live,
}
@callback
def _handle_coordinator_update(self) -> None:
"""Handle updated data from the coordinator."""
self._next_launch = next((launch for launch in self.coordinator.data), None)
super()._handle_coordinator_update()
async def async_added_to_hass(self) -> None:
"""When entity is added to hass."""
await super().async_added_to_hass()
self._handle_coordinator_update()

View File

@ -0,0 +1,12 @@
{
"config": {
"step": {
"user": {
"description": "Do you want to configure the Launch Library?"
}
},
"abort": {
"single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]"
}
}
}

View File

@ -0,0 +1,12 @@
{
"config": {
"abort": {
"single_instance_allowed": "Already configured. Only a single configuration possible."
},
"step": {
"user": {
"description": "Do you want to configure the Launch Library?"
}
}
}
}

View File

@ -166,6 +166,7 @@ FLOWS = [
"kostal_plenticore", "kostal_plenticore",
"kraken", "kraken",
"kulersky", "kulersky",
"launch_library",
"life360", "life360",
"lifx", "lifx",
"litejet", "litejet",

View File

@ -1004,6 +1004,9 @@ pykulersky==0.5.2
# homeassistant.components.lastfm # homeassistant.components.lastfm
pylast==4.2.1 pylast==4.2.1
# homeassistant.components.launch_library
pylaunches==1.2.0
# homeassistant.components.forked_daapd # homeassistant.components.forked_daapd
pylibrespot-java==0.1.0 pylibrespot-java==0.1.0

View File

@ -0,0 +1 @@
"""Tests for the launch_library component."""

View File

@ -0,0 +1,64 @@
"""Test launch_library config flow."""
from unittest.mock import patch
from homeassistant import data_entry_flow
from homeassistant.components.launch_library.const import DEFAULT_NAME, DOMAIN
from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER
from homeassistant.const import CONF_NAME
from tests.common import MockConfigEntry
async def test_import(hass):
"""Test entry will be imported."""
imported_config = {CONF_NAME: DEFAULT_NAME}
with patch(
"homeassistant.components.launch_library.async_setup_entry", return_value=True
):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_IMPORT}, data=imported_config
)
assert result.get("type") == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result.get("result").data == imported_config
async def test_create_entry(hass):
"""Test we can finish a config flow."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
assert result.get("type") == data_entry_flow.RESULT_TYPE_FORM
assert result.get("step_id") == SOURCE_USER
with patch(
"homeassistant.components.launch_library.async_setup_entry", return_value=True
):
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{},
)
assert result.get("type") == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result.get("result").data == {}
async def test_integration_already_exists(hass):
"""Test we only allow a single config flow."""
MockConfigEntry(
domain=DOMAIN,
data={},
).add_to_hass(hass)
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}, data={}
)
assert result.get("type") == data_entry_flow.RESULT_TYPE_ABORT
assert result.get("reason") == "single_instance_allowed"