Remove YAML support from Hyperion integration (#45690)

This commit is contained in:
Dermot Duffy 2021-01-29 00:05:00 -08:00 committed by GitHub
parent 4f3b10d661
commit c66a892233
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 6 additions and 383 deletions

View File

@ -278,10 +278,6 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
}
)
# Must only listen for option updates after the setup is complete, as otherwise
# the YAML->ConfigEntry migration code triggers an options update, which causes a
# reload -- which clashes with the initial load (causing entity_id / unique_id
# clashes).
async def setup_then_listen() -> None:
await asyncio.gather(
*[

View File

@ -140,14 +140,6 @@ class HyperionConfigFlow(ConfigFlow, domain=DOMAIN):
return await self.async_step_auth()
return await self.async_step_confirm()
async def async_step_import(self, import_data: ConfigType) -> Dict[str, Any]:
"""Handle a flow initiated by a YAML config import."""
self._data.update(import_data)
async with self._create_client(raw_connection=True) as hyperion_client:
if not hyperion_client:
return self.async_abort(reason="cannot_connect")
return await self._advance_to_auth_step_if_necessary(hyperion_client)
async def async_step_reauth(
self,
config_data: ConfigType,

View File

@ -2,47 +2,30 @@
from __future__ import annotations
import logging
import re
from types import MappingProxyType
from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple
from hyperion import client, const
import voluptuous as vol
from homeassistant import data_entry_flow
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
ATTR_EFFECT,
ATTR_HS_COLOR,
DOMAIN as LIGHT_DOMAIN,
PLATFORM_SCHEMA,
SUPPORT_BRIGHTNESS,
SUPPORT_COLOR,
SUPPORT_EFFECT,
LightEntity,
)
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import callback
from homeassistant.exceptions import PlatformNotReady
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.dispatcher import (
async_dispatcher_connect,
async_dispatcher_send,
)
from homeassistant.helpers.entity_registry import async_get_registry
from homeassistant.helpers.typing import (
ConfigType,
DiscoveryInfoType,
HomeAssistantType,
)
from homeassistant.helpers.typing import HomeAssistantType
import homeassistant.util.color as color_util
from . import (
create_hyperion_client,
get_hyperion_unique_id,
listen_for_instance_updates,
)
from . import get_hyperion_unique_id, listen_for_instance_updates
from .const import (
CONF_INSTANCE_CLIENTS,
CONF_PRIORITY,
@ -73,8 +56,6 @@ CONF_EFFECT_LIST = "effect_list"
# showing a solid color. This is the same method used by WLED.
KEY_EFFECT_SOLID = "Solid"
KEY_ENTRY_ID_YAML = "YAML"
DEFAULT_COLOR = [255, 255, 255]
DEFAULT_BRIGHTNESS = 255
DEFAULT_EFFECT = KEY_EFFECT_SOLID
@ -85,144 +66,11 @@ DEFAULT_EFFECT_LIST: List[str] = []
SUPPORT_HYPERION = SUPPORT_COLOR | SUPPORT_BRIGHTNESS | SUPPORT_EFFECT
# Usage of YAML for configuration of the Hyperion component is deprecated.
PLATFORM_SCHEMA = vol.All(
cv.deprecated(CONF_HDMI_PRIORITY),
cv.deprecated(CONF_HOST),
cv.deprecated(CONF_PORT),
cv.deprecated(CONF_DEFAULT_COLOR),
cv.deprecated(CONF_NAME),
cv.deprecated(CONF_PRIORITY),
cv.deprecated(CONF_EFFECT_LIST),
PLATFORM_SCHEMA.extend(
{
vol.Required(CONF_HOST): cv.string,
vol.Required(CONF_PORT, default=DEFAULT_PORT): cv.port,
vol.Optional(CONF_DEFAULT_COLOR, default=DEFAULT_COLOR): vol.All(
list,
vol.Length(min=3, max=3),
[vol.All(vol.Coerce(int), vol.Range(min=0, max=255))],
),
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_PRIORITY, default=DEFAULT_PRIORITY): cv.positive_int,
vol.Optional(
CONF_HDMI_PRIORITY, default=DEFAULT_HDMI_PRIORITY
): cv.positive_int,
vol.Optional(CONF_EFFECT_LIST, default=DEFAULT_EFFECT_LIST): vol.All(
cv.ensure_list, [cv.string]
),
}
),
)
ICON_LIGHTBULB = "mdi:lightbulb"
ICON_EFFECT = "mdi:lava-lamp"
ICON_EXTERNAL_SOURCE = "mdi:television-ambient-light"
async def async_setup_platform(
hass: HomeAssistantType,
config: ConfigType,
async_add_entities: Callable,
discovery_info: Optional[DiscoveryInfoType] = None,
) -> None:
"""Set up Hyperion platform.."""
# This is the entrypoint for the old YAML-style Hyperion integration. The goal here
# is to auto-convert the YAML configuration into a config entry, with no human
# interaction, preserving the entity_id. This should be possible, as the YAML
# configuration did not support any of the things that should otherwise require
# human interaction in the config flow (e.g. it did not support auth).
host = config[CONF_HOST]
port = config[CONF_PORT]
instance = 0 # YAML only supports a single instance.
# First, connect to the server and get the server id (which will be unique_id on a config_entry
# if there is one).
async with create_hyperion_client(host, port) as hyperion_client:
if not hyperion_client:
raise PlatformNotReady
hyperion_id = await hyperion_client.async_sysinfo_id()
if not hyperion_id:
raise PlatformNotReady
future_unique_id = get_hyperion_unique_id(
hyperion_id, instance, TYPE_HYPERION_LIGHT
)
# Possibility 1: Already converted.
# There is already a config entry with the unique id reporting by the
# server. Nothing to do here.
for entry in hass.config_entries.async_entries(domain=DOMAIN):
if entry.unique_id == hyperion_id:
return
# Possibility 2: Upgraded to the new Hyperion component pre-config-flow.
# No config entry for this unique_id, but have an entity_registry entry
# with an old-style unique_id:
# <host>:<port>-<instance> (instance will always be 0, as YAML
# configuration does not support multiple
# instances)
# The unique_id needs to be updated, then the config_flow should do the rest.
registry = await async_get_registry(hass)
for entity_id, entity in registry.entities.items():
if entity.config_entry_id is not None or entity.platform != DOMAIN:
continue
result = re.search(rf"([^:]+):(\d+)-{instance}", entity.unique_id)
if result and result.group(1) == host and int(result.group(2)) == port:
registry.async_update_entity(entity_id, new_unique_id=future_unique_id)
break
else:
# Possibility 3: This is the first upgrade to the new Hyperion component.
# No config entry and no entity_registry entry, in which case the CONF_NAME
# variable will be used as the preferred name. Rather than pollute the config
# entry with a "suggested name" type variable, instead create an entry in the
# registry that will subsequently be used when the entity is created with this
# unique_id.
# This also covers the case that should not occur in the wild (no config entry,
# but new style unique_id).
registry.async_get_or_create(
domain=LIGHT_DOMAIN,
platform=DOMAIN,
unique_id=future_unique_id,
suggested_object_id=config[CONF_NAME],
)
async def migrate_yaml_to_config_entry_and_options(
host: str, port: int, priority: int
) -> None:
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_IMPORT},
data={
CONF_HOST: host,
CONF_PORT: port,
},
)
if (
result["type"] != data_entry_flow.RESULT_TYPE_CREATE_ENTRY
or result.get("result") is None
):
_LOGGER.warning(
"Could not automatically migrate Hyperion YAML to a config entry."
)
return
config_entry = result.get("result")
options = {**config_entry.options, CONF_PRIORITY: config[CONF_PRIORITY]}
hass.config_entries.async_update_entry(config_entry, options=options)
_LOGGER.info(
"Successfully migrated Hyperion YAML configuration to a config entry."
)
# Kick off a config flow to create the config entry.
hass.async_create_task(
migrate_yaml_to_config_entry_and_options(host, port, config[CONF_PRIORITY])
)
async def async_setup_entry(
hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities: Callable
) -> bool:

View File

@ -9,7 +9,6 @@ from unittest.mock import AsyncMock, Mock, patch
from hyperion import const
from homeassistant.components.hyperion.const import CONF_PRIORITY, DOMAIN
from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, CONF_PORT
from homeassistant.helpers.typing import HomeAssistantType
@ -24,8 +23,6 @@ TEST_ID = "default"
TEST_SYSINFO_ID = "f9aab089-f85a-55cf-b7c1-222a72faebe9"
TEST_SYSINFO_VERSION = "2.0.0-alpha.8"
TEST_PRIORITY = 180
TEST_YAML_NAME = f"{TEST_HOST}_{TEST_PORT}_{TEST_INSTANCE}"
TEST_YAML_ENTITY_ID = f"{LIGHT_DOMAIN}.{TEST_YAML_NAME}"
TEST_ENTITY_ID_1 = "light.test_instance_1"
TEST_ENTITY_ID_2 = "light.test_instance_2"
TEST_ENTITY_ID_3 = "light.test_instance_3"

View File

@ -14,12 +14,7 @@ from homeassistant.components.hyperion.const import (
DOMAIN,
)
from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN
from homeassistant.config_entries import (
SOURCE_IMPORT,
SOURCE_REAUTH,
SOURCE_SSDP,
SOURCE_USER,
)
from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_SSDP, SOURCE_USER
from homeassistant.const import (
ATTR_ENTITY_ID,
CONF_HOST,
@ -606,56 +601,6 @@ async def test_ssdp_abort_duplicates(hass: HomeAssistantType) -> None:
assert result_2["reason"] == "already_in_progress"
async def test_import_success(hass: HomeAssistantType) -> None:
"""Check an import flow from the old-style YAML."""
client = create_mock_client()
with patch(
"homeassistant.components.hyperion.client.HyperionClient", return_value=client
):
result = await _init_flow(
hass,
source=SOURCE_IMPORT,
data={
CONF_HOST: TEST_HOST,
CONF_PORT: TEST_PORT,
},
)
await hass.async_block_till_done()
# No human interaction should be required.
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result["handler"] == DOMAIN
assert result["title"] == TEST_TITLE
assert result["data"] == {
CONF_HOST: TEST_HOST,
CONF_PORT: TEST_PORT,
}
async def test_import_cannot_connect(hass: HomeAssistantType) -> None:
"""Check an import flow that cannot connect."""
client = create_mock_client()
client.async_client_connect = AsyncMock(return_value=False)
with patch(
"homeassistant.components.hyperion.client.HyperionClient", return_value=client
):
result = await _init_flow(
hass,
source=SOURCE_IMPORT,
data={
CONF_HOST: TEST_HOST,
CONF_PORT: TEST_PORT,
},
)
await hass.async_block_till_done()
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
assert result["reason"] == "cannot_connect"
async def test_options(hass: HomeAssistantType) -> None:
"""Check an options flow."""

View File

@ -1,21 +1,12 @@
"""Tests for the Hyperion integration."""
import logging
from types import MappingProxyType
from typing import Optional
from unittest.mock import AsyncMock, Mock, call, patch
from hyperion import const
from homeassistant import setup
from homeassistant.components.hyperion import (
get_hyperion_unique_id,
light as hyperion_light,
)
from homeassistant.components.hyperion.const import (
DEFAULT_ORIGIN,
DOMAIN,
TYPE_HYPERION_LIGHT,
)
from homeassistant.components.hyperion import light as hyperion_light
from homeassistant.components.hyperion.const import DEFAULT_ORIGIN, DOMAIN
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
ATTR_EFFECT,
@ -43,7 +34,6 @@ import homeassistant.util.color as color_util
from . import (
TEST_AUTH_NOT_REQUIRED_RESP,
TEST_AUTH_REQUIRED_RESP,
TEST_CONFIG_ENTRY_OPTIONS,
TEST_ENTITY_ID_1,
TEST_ENTITY_ID_2,
TEST_ENTITY_ID_3,
@ -56,8 +46,6 @@ from . import (
TEST_PRIORITY,
TEST_PRIORITY_LIGHT_ENTITY_ID_1,
TEST_SYSINFO_ID,
TEST_YAML_ENTITY_ID,
TEST_YAML_NAME,
add_test_config_entry,
call_registered_callback,
create_mock_client,
@ -69,28 +57,6 @@ _LOGGER = logging.getLogger(__name__)
COLOR_BLACK = color_util.COLORS["black"]
async def _setup_entity_yaml(hass: HomeAssistantType, client: AsyncMock = None) -> None:
"""Add a test Hyperion entity to hass."""
client = client or create_mock_client()
with patch(
"homeassistant.components.hyperion.client.HyperionClient", return_value=client
):
assert await setup.async_setup_component(
hass,
LIGHT_DOMAIN,
{
LIGHT_DOMAIN: {
"platform": "hyperion",
"name": TEST_YAML_NAME,
"host": TEST_HOST,
"port": TEST_PORT,
"priority": TEST_PRIORITY,
}
},
)
await hass.async_block_till_done()
def _get_config_entry_from_unique_id(
hass: HomeAssistantType, unique_id: str
) -> Optional[ConfigEntry]:
@ -100,127 +66,6 @@ def _get_config_entry_from_unique_id(
return None
async def test_setup_yaml_already_converted(hass: HomeAssistantType) -> None:
"""Test an already converted YAML style config."""
# This tests "Possibility 1" from async_setup_platform()
# Add a pre-existing config entry.
add_test_config_entry(hass)
client = create_mock_client()
await _setup_entity_yaml(hass, client=client)
assert client.async_client_disconnect.called
# Setup should be skipped for the YAML config as there is a pre-existing config
# entry.
assert hass.states.get(TEST_YAML_ENTITY_ID) is None
async def test_setup_yaml_old_style_unique_id(hass: HomeAssistantType) -> None:
"""Test an already converted YAML style config."""
# This tests "Possibility 2" from async_setup_platform()
old_unique_id = f"{TEST_HOST}:{TEST_PORT}-0"
# Add a pre-existing registry entry.
registry = await async_get_registry(hass)
registry.async_get_or_create(
domain=LIGHT_DOMAIN,
platform=DOMAIN,
unique_id=old_unique_id,
suggested_object_id=TEST_YAML_NAME,
)
client = create_mock_client()
await _setup_entity_yaml(hass, client=client)
assert client.async_client_disconnect.called
# The entity should have been created with the same entity_id.
assert hass.states.get(TEST_YAML_ENTITY_ID) is not None
# The unique_id should have been updated in the registry (rather than the one
# specified above).
assert registry.async_get(TEST_YAML_ENTITY_ID).unique_id == get_hyperion_unique_id(
TEST_SYSINFO_ID, 0, TYPE_HYPERION_LIGHT
)
assert registry.async_get_entity_id(LIGHT_DOMAIN, DOMAIN, old_unique_id) is None
# There should be a config entry with the correct server unique_id.
entry = _get_config_entry_from_unique_id(hass, TEST_SYSINFO_ID)
assert entry
assert entry.options == MappingProxyType(TEST_CONFIG_ENTRY_OPTIONS)
async def test_setup_yaml_new_style_unique_id_wo_config(
hass: HomeAssistantType,
) -> None:
"""Test an a new unique_id without a config entry."""
# Note: This casde should not happen in the wild, as no released version of Home
# Assistant should this combination, but verify correct behavior for defense in
# depth.
new_unique_id = get_hyperion_unique_id(TEST_SYSINFO_ID, 0, TYPE_HYPERION_LIGHT)
entity_id_to_preserve = "light.magic_entity"
# Add a pre-existing registry entry.
registry = await async_get_registry(hass)
registry.async_get_or_create(
domain=LIGHT_DOMAIN,
platform=DOMAIN,
unique_id=new_unique_id,
suggested_object_id=entity_id_to_preserve.split(".")[1],
)
client = create_mock_client()
await _setup_entity_yaml(hass, client=client)
assert client.async_client_disconnect.called
# The entity should have been created with the same entity_id.
assert hass.states.get(entity_id_to_preserve) is not None
# The unique_id should have been updated in the registry (rather than the one
# specified above).
assert registry.async_get(entity_id_to_preserve).unique_id == new_unique_id
# There should be a config entry with the correct server unique_id.
entry = _get_config_entry_from_unique_id(hass, TEST_SYSINFO_ID)
assert entry
assert entry.options == MappingProxyType(TEST_CONFIG_ENTRY_OPTIONS)
async def test_setup_yaml_no_registry_entity(hass: HomeAssistantType) -> None:
"""Test an already converted YAML style config."""
# This tests "Possibility 3" from async_setup_platform()
registry = await async_get_registry(hass)
# Add a pre-existing config entry.
client = create_mock_client()
await _setup_entity_yaml(hass, client=client)
assert client.async_client_disconnect.called
# The entity should have been created with the same entity_id.
assert hass.states.get(TEST_YAML_ENTITY_ID) is not None
# The unique_id should have been updated in the registry (rather than the one
# specified above).
assert registry.async_get(TEST_YAML_ENTITY_ID).unique_id == get_hyperion_unique_id(
TEST_SYSINFO_ID, 0, TYPE_HYPERION_LIGHT
)
# There should be a config entry with the correct server unique_id.
entry = _get_config_entry_from_unique_id(hass, TEST_SYSINFO_ID)
assert entry
assert entry.options == MappingProxyType(TEST_CONFIG_ENTRY_OPTIONS)
async def test_setup_yaml_not_ready(hass: HomeAssistantType) -> None:
"""Test the component not being ready."""
client = create_mock_client()
client.async_client_connect = AsyncMock(return_value=False)
await _setup_entity_yaml(hass, client=client)
assert client.async_client_disconnect.called
assert hass.states.get(TEST_YAML_ENTITY_ID) is None
async def test_setup_config_entry(hass: HomeAssistantType) -> None:
"""Test setting up the component via config entries."""
await setup_test_config_entry(hass, hyperion_client=create_mock_client())