Add reconfigure to LG webOS TV (#135360)

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
pull/135379/head^2
Shay Levy 2025-01-11 17:16:35 +02:00 committed by GitHub
parent 19f460614e
commit a745e079e9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 184 additions and 72 deletions

View File

@ -20,7 +20,7 @@ from homeassistant.const import CONF_CLIENT_SECRET, CONF_HOST
from homeassistant.core import callback
from homeassistant.helpers import config_validation as cv
from . import async_control_connect, update_client_key
from . import async_control_connect
from .const import CONF_SOURCES, DEFAULT_NAME, DOMAIN, WEBOSTV_EXCEPTIONS
from .helpers import async_get_sources
@ -53,14 +53,11 @@ class FlowHandler(ConfigFlow, domain=DOMAIN):
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle a flow initialized by the user."""
errors: dict[str, str] = {}
if user_input is not None:
self._host = user_input[CONF_HOST]
return await self.async_step_pairing()
return self.async_show_form(
step_id="user", data_schema=DATA_SCHEMA, errors=errors
)
return self.async_show_form(step_id="user", data_schema=DATA_SCHEMA)
async def async_step_pairing(
self, user_input: dict[str, Any] | None = None
@ -69,13 +66,13 @@ class FlowHandler(ConfigFlow, domain=DOMAIN):
self._async_abort_entries_match({CONF_HOST: self._host})
self.context["title_placeholders"] = {"name": self._name}
errors = {}
errors: dict[str, str] = {}
if user_input is not None:
try:
client = await async_control_connect(self._host, None)
except WebOsTvPairError:
return self.async_abort(reason="error_pairing")
errors["base"] = "error_pairing"
except WEBOSTV_EXCEPTIONS:
errors["base"] = "cannot_connect"
else:
@ -130,20 +127,56 @@ class FlowHandler(ConfigFlow, domain=DOMAIN):
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Dialog that informs the user that reauth is required."""
errors: dict[str, str] = {}
if user_input is not None:
try:
client = await async_control_connect(self._host, None)
except WebOsTvPairError:
return self.async_abort(reason="error_pairing")
errors["base"] = "error_pairing"
except WEBOSTV_EXCEPTIONS:
return self.async_abort(reason="reauth_unsuccessful")
errors["base"] = "cannot_connect"
else:
reauth_entry = self._get_reauth_entry()
data = {CONF_HOST: self._host, CONF_CLIENT_SECRET: client.client_key}
return self.async_update_reload_and_abort(reauth_entry, data=data)
reauth_entry = self._get_reauth_entry()
update_client_key(self.hass, reauth_entry, client)
await self.hass.config_entries.async_reload(reauth_entry.entry_id)
return self.async_abort(reason="reauth_successful")
return self.async_show_form(step_id="reauth_confirm", errors=errors)
return self.async_show_form(step_id="reauth_confirm")
async def async_step_reconfigure(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle reconfiguration of the integration."""
errors: dict[str, str] = {}
reconfigure_entry = self._get_reconfigure_entry()
if user_input is not None:
host = user_input[CONF_HOST]
client_key = reconfigure_entry.data.get(CONF_CLIENT_SECRET)
try:
client = await async_control_connect(host, client_key)
except WebOsTvPairError:
errors["base"] = "error_pairing"
except WEBOSTV_EXCEPTIONS:
errors["base"] = "cannot_connect"
else:
await self.async_set_unique_id(client.hello_info["deviceUUID"])
self._abort_if_unique_id_mismatch(reason="wrong_device")
data = {CONF_HOST: host, CONF_CLIENT_SECRET: client.client_key}
return self.async_update_reload_and_abort(reconfigure_entry, data=data)
return self.async_show_form(
step_id="reconfigure",
data_schema=vol.Schema(
{
vol.Required(
CONF_HOST, default=reconfigure_entry.data.get(CONF_HOST)
): cv.string
}
),
errors=errors,
)
class OptionsFlowHandler(OptionsFlow):

View File

@ -7,9 +7,7 @@ rules:
status: exempt
comment: The integration does not use common patterns.
config-flow-test-coverage: done
config-flow:
status: todo
comment: make reauth flow more graceful
config-flow: done
dependency-transparency: done
docs-actions:
status: todo
@ -66,7 +64,7 @@ rules:
icon-translations:
status: exempt
comment: The only entity can use the device class.
reconfiguration-flow: todo
reconfiguration-flow: done
repair-issues:
status: exempt
comment: The integration does not have anything to repair.

View File

@ -8,7 +8,7 @@
"host": "[%key:common::config_flow::data::host%]"
},
"data_description": {
"host": "Hostname or IP address of your webOS TV."
"host": "Hostname or IP address of your LG webOS TV."
}
},
"pairing": {
@ -18,17 +18,26 @@
"reauth_confirm": {
"title": "[%key:component::webostv::config::step::pairing::title%]",
"description": "[%key:component::webostv::config::step::pairing::description%]"
},
"reconfigure": {
"data": {
"host": "[%key:common::config_flow::data::host%]"
},
"data_description": {
"host": "[%key:component::webostv::config::step::user::data_description::host%]"
}
}
},
"error": {
"cannot_connect": "Failed to connect, please turn on your TV or check the IP address"
"cannot_connect": "Failed to connect, please turn on your TV and try again.",
"error_pairing": "Pairing failed, make sure to accept the pairing request on the TV and try again."
},
"abort": {
"error_pairing": "Connected to LG webOS TV but not paired",
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
"reauth_unsuccessful": "Re-authentication was unsuccessful, please turn on your TV and try again."
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]",
"wrong_device": "The configured device is not the same found on this Hostname or IP address."
}
},
"options": {
@ -38,6 +47,9 @@
"description": "Select enabled sources",
"data": {
"sources": "Sources list"
},
"data_description": {
"sources": "List of sources to enable"
}
}
},

View File

@ -1,7 +1,5 @@
"""Test the WebOS Tv config flow."""
from unittest.mock import AsyncMock
from aiowebostv import WebOsTvPairError
import pytest
@ -105,7 +103,7 @@ async def test_options_flow_cannot_retrieve(hass: HomeAssistant, client) -> None
"""Test options config flow cannot retrieve sources."""
entry = await setup_webostv(hass)
client.connect = AsyncMock(side_effect=ConnectionRefusedError())
client.connect.side_effect = ConnectionRefusedError
result = await hass.config_entries.options.async_init(entry.entry_id)
await hass.async_block_till_done()
@ -113,7 +111,7 @@ async def test_options_flow_cannot_retrieve(hass: HomeAssistant, client) -> None
assert result["errors"] == {"base": "cannot_retrieve"}
# recover
client.connect = AsyncMock(return_value=True)
client.connect.side_effect = None
result = await hass.config_entries.options.async_configure(
result["flow_id"],
user_input=None,
@ -139,7 +137,7 @@ async def test_form_cannot_connect(hass: HomeAssistant, client) -> None:
data=MOCK_USER_CONFIG,
)
client.connect = AsyncMock(side_effect=ConnectionRefusedError())
client.connect.side_effect = ConnectionRefusedError
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={}
)
@ -148,7 +146,7 @@ async def test_form_cannot_connect(hass: HomeAssistant, client) -> None:
assert result["errors"] == {"base": "cannot_connect"}
# recover
client.connect = AsyncMock(return_value=True)
client.connect.side_effect = None
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={}
)
@ -165,13 +163,22 @@ async def test_form_pairexception(hass: HomeAssistant, client) -> None:
data=MOCK_USER_CONFIG,
)
client.connect = AsyncMock(side_effect=WebOsTvPairError("error"))
client.connect.side_effect = WebOsTvPairError
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={}
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "error_pairing"
assert result["type"] is FlowResultType.FORM
assert result["errors"] == {"base": "error_pairing"}
# recover
client.connect.side_effect = None
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={}
)
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["title"] == TV_NAME
async def test_entry_already_configured(hass: HomeAssistant, client) -> None:
@ -267,9 +274,7 @@ async def test_form_abort_uuid_configured(hass: HomeAssistant, client) -> None:
assert entry.data[CONF_HOST] == "new_host"
async def test_reauth_successful(
hass: HomeAssistant, client, monkeypatch: pytest.MonkeyPatch
) -> None:
async def test_reauth_successful(hass: HomeAssistant, client) -> None:
"""Test that the reauthorization is successful."""
entry = await setup_webostv(hass)
@ -282,7 +287,7 @@ async def test_reauth_successful(
assert result["step_id"] == "reauth_confirm"
assert entry.data[CONF_CLIENT_SECRET] == CLIENT_KEY
monkeypatch.setattr(client, "client_key", "new_key")
client.client_key = "new_key"
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={}
)
@ -293,15 +298,13 @@ async def test_reauth_successful(
@pytest.mark.parametrize(
("side_effect", "reason"),
("side_effect", "error"),
[
(WebOsTvPairError, "error_pairing"),
(ConnectionRefusedError, "reauth_unsuccessful"),
(ConnectionRefusedError, "cannot_connect"),
],
)
async def test_reauth_errors(
hass: HomeAssistant, client, monkeypatch: pytest.MonkeyPatch, side_effect, reason
) -> None:
async def test_reauth_errors(hass: HomeAssistant, client, side_effect, error) -> None:
"""Test reauthorization errors."""
entry = await setup_webostv(hass)
@ -318,5 +321,88 @@ async def test_reauth_errors(
result["flow_id"], user_input={}
)
assert result["type"] is FlowResultType.FORM
assert result["errors"] == {"base": error}
client.connect.side_effect = None
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={}
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == reason
assert result["reason"] == "reauth_successful"
async def test_reconfigure_successful(hass: HomeAssistant, client) -> None:
"""Test that the reconfigure is successful."""
entry = await setup_webostv(hass)
result = await entry.start_reconfigure_flow(hass)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "reconfigure"
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input={CONF_HOST: "new_host"},
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "reconfigure_successful"
assert entry.data[CONF_HOST] == "new_host"
@pytest.mark.parametrize(
("side_effect", "error"),
[
(WebOsTvPairError, "error_pairing"),
(ConnectionRefusedError, "cannot_connect"),
],
)
async def test_reconfigure_errors(
hass: HomeAssistant, client, side_effect, error
) -> None:
"""Test reconfigure errors."""
entry = await setup_webostv(hass)
result = await entry.start_reconfigure_flow(hass)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "reconfigure"
client.connect.side_effect = side_effect
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input={CONF_HOST: "new_host"},
)
assert result["type"] is FlowResultType.FORM
assert result["errors"] == {"base": error}
client.connect.side_effect = None
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input={CONF_HOST: "new_host"},
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "reconfigure_successful"
async def test_reconfigure_wrong_device(hass: HomeAssistant, client) -> None:
"""Test abort if reconfigure host is wrong webOS TV device."""
entry = await setup_webostv(hass)
result = await entry.start_reconfigure_flow(hass)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "reconfigure"
client.hello_info = {"deviceUUID": "wrong_uuid"}
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input={CONF_HOST: "new_host"},
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "wrong_device"

View File

@ -1,9 +1,6 @@
"""The tests for the LG webOS TV platform."""
from unittest.mock import Mock
from aiowebostv import WebOsTvPairError
import pytest
from homeassistant.components.media_player import ATTR_INPUT_SOURCE_LIST
from homeassistant.components.webostv.const import CONF_SOURCES, DOMAIN
@ -15,12 +12,10 @@ from . import setup_webostv
from .const import ENTITY_ID
async def test_reauth_setup_entry(
hass: HomeAssistant, client, monkeypatch: pytest.MonkeyPatch
) -> None:
async def test_reauth_setup_entry(hass: HomeAssistant, client) -> None:
"""Test reauth flow triggered by setup entry."""
monkeypatch.setattr(client, "is_connected", Mock(return_value=False))
monkeypatch.setattr(client, "connect", Mock(side_effect=WebOsTvPairError))
client.is_connected.return_value = False
client.connect.side_effect = WebOsTvPairError
entry = await setup_webostv(hass)
assert entry.state is ConfigEntryState.SETUP_ERROR
@ -37,11 +32,9 @@ async def test_reauth_setup_entry(
assert flow["context"].get("entry_id") == entry.entry_id
async def test_key_update_setup_entry(
hass: HomeAssistant, client, monkeypatch: pytest.MonkeyPatch
) -> None:
async def test_key_update_setup_entry(hass: HomeAssistant, client) -> None:
"""Test key update from setup entry."""
monkeypatch.setattr(client, "client_key", "new_key")
client.client_key = "new_key"
entry = await setup_webostv(hass)
assert entry.state is ConfigEntryState.LOADED

View File

@ -1,6 +1,6 @@
"""The tests for the WebOS TV notify platform."""
from unittest.mock import Mock, call
from unittest.mock import call
from aiowebostv import WebOsTvPairError
import pytest
@ -74,14 +74,12 @@ async def test_notify(hass: HomeAssistant, client) -> None:
)
async def test_notify_not_connected(
hass: HomeAssistant, client, monkeypatch: pytest.MonkeyPatch
) -> None:
async def test_notify_not_connected(hass: HomeAssistant, client) -> None:
"""Test sending a message when client is not connected."""
await setup_webostv(hass)
assert hass.services.has_service(NOTIFY_DOMAIN, SERVICE_NAME)
monkeypatch.setattr(client, "is_connected", Mock(return_value=False))
client.is_connected.return_value = False
await hass.services.async_call(
NOTIFY_DOMAIN,
SERVICE_NAME,
@ -99,16 +97,13 @@ async def test_notify_not_connected(
async def test_icon_not_found(
hass: HomeAssistant,
caplog: pytest.LogCaptureFixture,
client,
monkeypatch: pytest.MonkeyPatch,
hass: HomeAssistant, caplog: pytest.LogCaptureFixture, client
) -> None:
"""Test notify icon not found error."""
await setup_webostv(hass)
assert hass.services.has_service(NOTIFY_DOMAIN, SERVICE_NAME)
monkeypatch.setattr(client, "send_message", Mock(side_effect=FileNotFoundError))
client.send_message.side_effect = FileNotFoundError
await hass.services.async_call(
NOTIFY_DOMAIN,
SERVICE_NAME,
@ -134,19 +129,14 @@ async def test_icon_not_found(
],
)
async def test_connection_errors(
hass: HomeAssistant,
caplog: pytest.LogCaptureFixture,
client,
monkeypatch: pytest.MonkeyPatch,
side_effect,
error,
hass: HomeAssistant, caplog: pytest.LogCaptureFixture, client, side_effect, error
) -> None:
"""Test connection errors scenarios."""
await setup_webostv(hass)
assert hass.services.has_service("notify", SERVICE_NAME)
monkeypatch.setattr(client, "is_connected", Mock(return_value=False))
monkeypatch.setattr(client, "connect", Mock(side_effect=side_effect))
client.is_connected.return_value = False
client.connect.side_effect = side_effect
await hass.services.async_call(
NOTIFY_DOMAIN,
SERVICE_NAME,
@ -159,7 +149,7 @@ async def test_connection_errors(
blocking=True,
)
assert client.mock_calls[0] == call.connect()
assert client.connect.call_count == 1
assert client.connect.call_count == 2
client.send_message.assert_not_called()
assert error in caplog.text