mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 05:07:41 +00:00
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:
parent
b4187540c0
commit
b22a9b8669
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
27
homeassistant/components/launch_library/config_flow.py
Normal file
27
homeassistant/components/launch_library/config_flow.py
Normal 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]})
|
@ -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"
|
||||||
|
@ -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"
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
|
12
homeassistant/components/launch_library/strings.json
Normal file
12
homeassistant/components/launch_library/strings.json
Normal 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%]"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
12
homeassistant/components/launch_library/translations/en.json
Normal file
12
homeassistant/components/launch_library/translations/en.json
Normal 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?"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -166,6 +166,7 @@ FLOWS = [
|
|||||||
"kostal_plenticore",
|
"kostal_plenticore",
|
||||||
"kraken",
|
"kraken",
|
||||||
"kulersky",
|
"kulersky",
|
||||||
|
"launch_library",
|
||||||
"life360",
|
"life360",
|
||||||
"lifx",
|
"lifx",
|
||||||
"litejet",
|
"litejet",
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
1
tests/components/launch_library/__init__.py
Normal file
1
tests/components/launch_library/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
"""Tests for the launch_library component."""
|
64
tests/components/launch_library/test_config_flow.py
Normal file
64
tests/components/launch_library/test_config_flow.py
Normal 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"
|
Loading…
x
Reference in New Issue
Block a user