Remove webostv deprecated YAML import (#69043)

* webostv: remove deprecated YAML import

* Remove unused CUSTOMIZE_SCHEMA and WEBOSTV_CONFIG_FILE
This commit is contained in:
Shay Levy 2022-04-01 11:05:59 +03:00 committed by GitHub
parent 68d563c630
commit 4a921ac67f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 8 additions and 430 deletions

View file

@ -1,33 +1,24 @@
"""Support for LG webOS Smart TV."""
from __future__ import annotations
import asyncio
from collections.abc import Callable
from contextlib import suppress
import json
import logging
import os
from pickle import loads
from typing import Any
from aiowebostv import WebOsClient, WebOsTvPairError
import sqlalchemy as db
import voluptuous as vol
from homeassistant.components import notify as hass_notify
from homeassistant.components.automation import AutomationActionType
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
ATTR_COMMAND,
ATTR_ENTITY_ID,
CONF_CLIENT_SECRET,
CONF_CUSTOMIZE,
CONF_HOST,
CONF_ICON,
CONF_NAME,
CONF_UNIQUE_ID,
EVENT_HOMEASSISTANT_STOP,
Platform,
)
from homeassistant.core import (
Context,
@ -37,7 +28,7 @@ from homeassistant.core import (
ServiceCall,
callback,
)
from homeassistant.helpers import config_validation as cv, discovery, entity_registry
from homeassistant.helpers import config_validation as cv, discovery
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.typing import ConfigType
@ -46,46 +37,17 @@ from .const import (
ATTR_CONFIG_ENTRY_ID,
ATTR_PAYLOAD,
ATTR_SOUND_OUTPUT,
CONF_ON_ACTION,
CONF_SOURCES,
DATA_CONFIG_ENTRY,
DATA_HASS_CONFIG,
DEFAULT_NAME,
DOMAIN,
PLATFORMS,
SERVICE_BUTTON,
SERVICE_COMMAND,
SERVICE_SELECT_SOUND_OUTPUT,
WEBOSTV_CONFIG_FILE,
WEBOSTV_EXCEPTIONS,
)
CUSTOMIZE_SCHEMA = vol.Schema(
{vol.Optional(CONF_SOURCES, default=[]): vol.All(cv.ensure_list, [cv.string])}
)
CONFIG_SCHEMA = vol.Schema(
vol.All(
cv.deprecated(DOMAIN),
{
DOMAIN: vol.All(
cv.ensure_list,
[
vol.Schema(
{
vol.Optional(CONF_CUSTOMIZE, default={}): CUSTOMIZE_SCHEMA,
vol.Required(CONF_HOST): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_ON_ACTION): cv.SCRIPT_SCHEMA,
vol.Optional(CONF_ICON): cv.string,
}
)
],
)
},
),
extra=vol.ALLOW_EXTRA,
)
CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False)
CALL_SCHEMA = vol.Schema({vol.Required(ATTR_ENTITY_ID): cv.comp_entity_ids})
@ -109,113 +71,12 @@ SERVICE_TO_METHOD = {
_LOGGER = logging.getLogger(__name__)
def read_client_keys(config_file: str) -> dict[str, str]:
"""Read legacy client keys from file."""
if not os.path.isfile(config_file):
return {}
# Try to parse the file as being JSON
with open(config_file, encoding="utf8") as json_file:
try:
client_keys = json.load(json_file)
if isinstance(client_keys, dict):
return client_keys
return {}
except (json.JSONDecodeError, UnicodeDecodeError):
pass
# If the file is not JSON, read it as Sqlite DB
engine = db.create_engine(f"sqlite:///{config_file}")
table = db.Table("unnamed", db.MetaData(), autoload=True, autoload_with=engine)
results = engine.connect().execute(db.select([table])).fetchall()
db_client_keys = {k: loads(v) for k, v in results}
return db_client_keys
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the LG WebOS TV platform."""
hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN].setdefault(DATA_CONFIG_ENTRY, {})
hass.data[DOMAIN][DATA_HASS_CONFIG] = config
if DOMAIN not in config:
return True
config_file = hass.config.path(WEBOSTV_CONFIG_FILE)
if not (
client_keys := await hass.async_add_executor_job(read_client_keys, config_file)
):
_LOGGER.debug("No pairing keys, Not importing webOS Smart TV YAML config")
return True
async def async_migrate_task(
entity_id: str, conf: dict[str, str], key: str
) -> None:
_LOGGER.debug("Migrating webOS Smart TV entity %s unique_id", entity_id)
client = WebOsClient(conf[CONF_HOST], key)
tries = 0
while not client.is_connected():
try:
await client.connect()
except WEBOSTV_EXCEPTIONS:
if tries == 0:
_LOGGER.warning(
"Please make sure webOS TV %s is turned on to complete "
"the migration of configuration.yaml to the UI",
entity_id,
)
wait_time = 2 ** min(tries, 4) * 5
tries += 1
await asyncio.sleep(wait_time)
except WebOsTvPairError:
return
ent_reg = entity_registry.async_get(hass)
if not (
new_entity_id := ent_reg.async_get_entity_id(
Platform.MEDIA_PLAYER, DOMAIN, key
)
):
_LOGGER.debug(
"Not updating webOSTV Smart TV entity %s unique_id, entity missing",
entity_id,
)
return
uuid = client.hello_info["deviceUUID"]
ent_reg.async_update_entity(new_entity_id, new_unique_id=uuid)
await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_IMPORT},
data={
**conf,
CONF_CLIENT_SECRET: key,
CONF_UNIQUE_ID: uuid,
},
)
ent_reg = entity_registry.async_get(hass)
tasks = []
for conf in config[DOMAIN]:
host = conf[CONF_HOST]
if (key := client_keys.get(host)) is None:
_LOGGER.debug(
"Not importing webOS Smart TV host %s without pairing key", host
)
continue
if entity_id := ent_reg.async_get_entity_id(Platform.MEDIA_PLAYER, DOMAIN, key):
tasks.append(asyncio.create_task(async_migrate_task(entity_id, conf, key)))
async def async_tasks_cancel(_event: Event) -> None:
"""Cancel config flow import tasks."""
for task in tasks:
if not task.done():
task.cancel()
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, async_tasks_cancel)
return True

View file

@ -10,13 +10,7 @@ import voluptuous as vol
from homeassistant import config_entries, data_entry_flow
from homeassistant.components import ssdp
from homeassistant.const import (
CONF_CLIENT_SECRET,
CONF_CUSTOMIZE,
CONF_HOST,
CONF_NAME,
CONF_UNIQUE_ID,
)
from homeassistant.const import CONF_CLIENT_SECRET, CONF_HOST, CONF_NAME
from homeassistant.core import callback
from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers import config_validation as cv
@ -55,28 +49,6 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
"""Get the options flow for this handler."""
return OptionsFlowHandler(config_entry)
async def async_step_import(self, import_info: dict[str, Any]) -> FlowResult:
"""Set the config entry up from yaml."""
self._host = import_info[CONF_HOST]
self._name = import_info.get(CONF_NAME) or import_info[CONF_HOST]
await self.async_set_unique_id(
import_info[CONF_UNIQUE_ID], raise_on_progress=False
)
data = {
CONF_HOST: self._host,
CONF_CLIENT_SECRET: import_info[CONF_CLIENT_SECRET],
}
self._abort_if_unique_id_configured()
options: dict[str, list[str]] | None = None
if sources := import_info.get(CONF_CUSTOMIZE, {}).get(CONF_SOURCES):
if not isinstance(sources, list):
sources = [s.strip() for s in sources.split(",")]
options = {CONF_SOURCES: sources}
_LOGGER.debug("WebOS Smart TV host %s imported from YAML config", self._host)
return self.async_create_entry(title=self._name, data=data, options=options)
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:

View file

@ -33,5 +33,3 @@ WEBOSTV_EXCEPTIONS = (
asyncio.TimeoutError,
asyncio.CancelledError,
)
WEBOSTV_CONFIG_FILE = "webostv.conf"

View file

@ -3,7 +3,7 @@
"name": "LG webOS Smart TV",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/webostv",
"requirements": ["aiowebostv==0.2.0", "sqlalchemy==1.4.32"],
"requirements": ["aiowebostv==0.2.0"],
"codeowners": ["@bendavid", "@thecode"],
"ssdp": [{ "st": "urn:lge-com:service:webos-second-screen:1" }],
"quality_scale": "platinum",

View file

@ -2194,7 +2194,6 @@ spotipy==2.19.0
# homeassistant.components.recorder
# homeassistant.components.sql
# homeassistant.components.webostv
sqlalchemy==1.4.32
# homeassistant.components.srp_energy

View file

@ -1419,7 +1419,6 @@ spotipy==2.19.0
# homeassistant.components.recorder
# homeassistant.components.sql
# homeassistant.components.webostv
sqlalchemy==1.4.32
# homeassistant.components.srp_energy

View file

@ -1,17 +1,10 @@
"""Tests for the WebOS TV integration."""
from pickle import dumps
from unittest.mock import patch
import sqlalchemy as db
from sqlalchemy import create_engine
from homeassistant.components.media_player import DOMAIN as MP_DOMAIN
from homeassistant.components.webostv.const import DOMAIN
from homeassistant.const import CONF_CLIENT_SECRET, CONF_HOST
from homeassistant.helpers import entity_registry
from homeassistant.setup import async_setup_component
from .const import CLIENT_KEY, FAKE_UUID, HOST, MOCK_CLIENT_KEYS, TV_NAME
from .const import CLIENT_KEY, FAKE_UUID, HOST, TV_NAME
from tests.common import MockConfigEntry
@ -29,53 +22,11 @@ async def setup_webostv(hass, unique_id=FAKE_UUID):
)
entry.add_to_hass(hass)
with patch(
"homeassistant.components.webostv.read_client_keys",
return_value=MOCK_CLIENT_KEYS,
):
await async_setup_component(
hass,
DOMAIN,
{DOMAIN: {CONF_HOST: HOST}},
)
await hass.async_block_till_done()
return entry
async def setup_legacy_component(hass, create_entity=True):
"""Initialize webostv component with legacy entity."""
if create_entity:
ent_reg = entity_registry.async_get(hass)
assert ent_reg.async_get_or_create(MP_DOMAIN, DOMAIN, CLIENT_KEY)
assert await async_setup_component(
await async_setup_component(
hass,
DOMAIN,
{DOMAIN: {CONF_HOST: HOST}},
)
await hass.async_block_till_done()
def create_memory_sqlite_engine(url):
"""Create fake db keys file in memory."""
mem_eng = create_engine("sqlite:///:memory:")
table = db.Table(
"unnamed",
db.MetaData(),
db.Column("key", db.String),
db.Column("value", db.String),
)
table.create(mem_eng)
query = db.insert(table).values(key=HOST, value=dumps(CLIENT_KEY))
connection = mem_eng.connect()
connection.execute(query)
return mem_eng
def is_entity_unique_id_updated(hass):
"""Check if entity has new unique_id from UUID."""
ent_reg = entity_registry.async_get(hass)
return ent_reg.async_get_entity_id(
MP_DOMAIN, DOMAIN, FAKE_UUID
) and not ent_reg.async_get_entity_id(MP_DOMAIN, DOMAIN, CLIENT_KEY)
return entry

View file

@ -11,7 +11,6 @@ from homeassistant.components.webostv.const import CONF_SOURCES, DOMAIN, LIVE_TV
from homeassistant.config_entries import SOURCE_SSDP
from homeassistant.const import (
CONF_CLIENT_SECRET,
CONF_CUSTOMIZE,
CONF_HOST,
CONF_ICON,
CONF_NAME,
@ -46,66 +45,6 @@ MOCK_DISCOVERY_INFO = ssdp.SsdpServiceInfo(
)
async def test_import(hass, client):
"""Test we can import yaml config."""
assert client
with patch("homeassistant.components.webostv.async_setup_entry", return_value=True):
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={CONF_SOURCE: config_entries.SOURCE_IMPORT},
data=MOCK_YAML_CONFIG,
)
assert result["type"] == RESULT_TYPE_CREATE_ENTRY
assert result["title"] == TV_NAME
assert result["data"][CONF_HOST] == MOCK_YAML_CONFIG[CONF_HOST]
assert result["data"][CONF_CLIENT_SECRET] == MOCK_YAML_CONFIG[CONF_CLIENT_SECRET]
assert result["result"].unique_id == MOCK_YAML_CONFIG[CONF_UNIQUE_ID]
with patch("homeassistant.components.webostv.async_setup_entry", return_value=True):
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={CONF_SOURCE: config_entries.SOURCE_IMPORT},
data=MOCK_YAML_CONFIG,
)
assert result["type"] == RESULT_TYPE_ABORT
assert result["reason"] == "already_configured"
@pytest.mark.parametrize(
"sources",
[
["Live TV", "Input01", "Input02"],
"Live TV, Input01 , Input02",
"Live TV,Input01 ,Input02",
],
)
async def test_import_sources(hass, client, sources):
"""Test import yaml config with sources list/csv."""
assert client
with patch("homeassistant.components.webostv.async_setup_entry", return_value=True):
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={CONF_SOURCE: config_entries.SOURCE_IMPORT},
data={
**MOCK_YAML_CONFIG,
CONF_CUSTOMIZE: {
CONF_SOURCES: sources,
},
},
)
assert result["type"] == RESULT_TYPE_CREATE_ENTRY
assert result["title"] == TV_NAME
assert result["data"][CONF_HOST] == MOCK_YAML_CONFIG[CONF_HOST]
assert result["data"][CONF_CLIENT_SECRET] == MOCK_YAML_CONFIG[CONF_CLIENT_SECRET]
assert result["options"][CONF_SOURCES] == ["Live TV", "Input01", "Input02"]
assert result["result"].unique_id == MOCK_YAML_CONFIG[CONF_UNIQUE_ID]
async def test_form(hass, client):
"""Test we get the form."""
assert client

View file

@ -1,141 +0,0 @@
"""The tests for the WebOS TV platform."""
from unittest.mock import Mock, mock_open, patch
from aiowebostv import WebOsTvPairError
from homeassistant.components.media_player import DOMAIN as MP_DOMAIN
from homeassistant.components.webostv import DOMAIN
from . import (
create_memory_sqlite_engine,
is_entity_unique_id_updated,
setup_legacy_component,
)
from .const import MOCK_JSON
async def test_missing_keys_file_abort(hass, client, caplog):
"""Test abort import when no pairing keys file."""
with patch(
"homeassistant.components.webostv.os.path.isfile", Mock(return_value=False)
):
await setup_legacy_component(hass)
assert "No pairing keys, Not importing" in caplog.text
assert not is_entity_unique_id_updated(hass)
async def test_empty_json_abort(hass, client, caplog):
"""Test abort import when keys file is empty."""
m_open = mock_open(read_data="[]")
with patch(
"homeassistant.components.webostv.os.path.isfile", Mock(return_value=True)
), patch("homeassistant.components.webostv.open", m_open, create=True):
await setup_legacy_component(hass)
assert "No pairing keys, Not importing" in caplog.text
assert not is_entity_unique_id_updated(hass)
async def test_valid_json_migrate_not_needed(hass, client, caplog):
"""Test import from valid json entity already migrated or removed."""
m_open = mock_open(read_data=MOCK_JSON)
with patch(
"homeassistant.components.webostv.os.path.isfile", Mock(return_value=True)
), patch("homeassistant.components.webostv.open", m_open, create=True):
await setup_legacy_component(hass, False)
assert "Migrating webOS Smart TV entity" not in caplog.text
assert not is_entity_unique_id_updated(hass)
async def test_valid_json_missing_host_key(hass, client, caplog):
"""Test import from valid json missing host key."""
m_open = mock_open(read_data='{"1.2.3.5": "other-key"}')
with patch(
"homeassistant.components.webostv.os.path.isfile", Mock(return_value=True)
), patch("homeassistant.components.webostv.open", m_open, create=True):
await setup_legacy_component(hass)
assert "Not importing webOS Smart TV host" in caplog.text
assert not is_entity_unique_id_updated(hass)
async def test_not_connected_import(hass, client, caplog, monkeypatch):
"""Test import while device is not connected."""
m_open = mock_open(read_data=MOCK_JSON)
monkeypatch.setattr(client, "is_connected", Mock(return_value=False))
monkeypatch.setattr(client, "connect", Mock(side_effect=OSError))
with patch(
"homeassistant.components.webostv.os.path.isfile", Mock(return_value=True)
), patch("homeassistant.components.webostv.open", m_open, create=True):
await setup_legacy_component(hass)
assert f"Please make sure webOS TV {MP_DOMAIN}.{DOMAIN}" in caplog.text
assert not is_entity_unique_id_updated(hass)
async def test_pair_error_import_abort(hass, client, caplog, monkeypatch):
"""Test abort import if device is not paired."""
m_open = mock_open(read_data=MOCK_JSON)
monkeypatch.setattr(client, "is_connected", Mock(return_value=False))
monkeypatch.setattr(client, "connect", Mock(side_effect=WebOsTvPairError))
with patch(
"homeassistant.components.webostv.os.path.isfile", Mock(return_value=True)
), patch("homeassistant.components.webostv.open", m_open, create=True):
await setup_legacy_component(hass)
assert f"Please make sure webOS TV {MP_DOMAIN}.{DOMAIN}" not in caplog.text
assert not is_entity_unique_id_updated(hass)
async def test_entity_removed_import_abort(hass, client_entity_removed, caplog):
"""Test abort import if entity removed by user during import."""
m_open = mock_open(read_data=MOCK_JSON)
with patch(
"homeassistant.components.webostv.os.path.isfile", Mock(return_value=True)
), patch("homeassistant.components.webostv.open", m_open, create=True):
await setup_legacy_component(hass)
assert "Not updating webOSTV Smart TV entity" in caplog.text
assert not is_entity_unique_id_updated(hass)
async def test_json_import(hass, client, caplog, monkeypatch):
"""Test import from json keys file."""
m_open = mock_open(read_data=MOCK_JSON)
monkeypatch.setattr(client, "is_connected", Mock(return_value=True))
monkeypatch.setattr(client, "connect", Mock(return_value=True))
with patch(
"homeassistant.components.webostv.os.path.isfile", Mock(return_value=True)
), patch("homeassistant.components.webostv.open", m_open, create=True):
await setup_legacy_component(hass)
assert "imported from YAML config" in caplog.text
assert is_entity_unique_id_updated(hass)
async def test_sqlite_import(hass, client, caplog, monkeypatch):
"""Test import from sqlite keys file."""
m_open = mock_open(read_data="will raise JSONDecodeError")
monkeypatch.setattr(client, "is_connected", Mock(return_value=True))
monkeypatch.setattr(client, "connect", Mock(return_value=True))
with patch(
"homeassistant.components.webostv.os.path.isfile", Mock(return_value=True)
), patch("homeassistant.components.webostv.open", m_open, create=True), patch(
"homeassistant.components.webostv.db.create_engine",
side_effect=create_memory_sqlite_engine,
):
await setup_legacy_component(hass)
assert "imported from YAML config" in caplog.text
assert is_entity_unique_id_updated(hass)