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.core import callback
from homeassistant.helpers import config_validation as cv 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 .const import CONF_SOURCES, DEFAULT_NAME, DOMAIN, WEBOSTV_EXCEPTIONS
from .helpers import async_get_sources from .helpers import async_get_sources
@ -53,14 +53,11 @@ class FlowHandler(ConfigFlow, domain=DOMAIN):
self, user_input: dict[str, Any] | None = None self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult: ) -> ConfigFlowResult:
"""Handle a flow initialized by the user.""" """Handle a flow initialized by the user."""
errors: dict[str, str] = {}
if user_input is not None: if user_input is not None:
self._host = user_input[CONF_HOST] self._host = user_input[CONF_HOST]
return await self.async_step_pairing() return await self.async_step_pairing()
return self.async_show_form( return self.async_show_form(step_id="user", data_schema=DATA_SCHEMA)
step_id="user", data_schema=DATA_SCHEMA, errors=errors
)
async def async_step_pairing( async def async_step_pairing(
self, user_input: dict[str, Any] | None = None 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._async_abort_entries_match({CONF_HOST: self._host})
self.context["title_placeholders"] = {"name": self._name} self.context["title_placeholders"] = {"name": self._name}
errors = {} errors: dict[str, str] = {}
if user_input is not None: if user_input is not None:
try: try:
client = await async_control_connect(self._host, None) client = await async_control_connect(self._host, None)
except WebOsTvPairError: except WebOsTvPairError:
return self.async_abort(reason="error_pairing") errors["base"] = "error_pairing"
except WEBOSTV_EXCEPTIONS: except WEBOSTV_EXCEPTIONS:
errors["base"] = "cannot_connect" errors["base"] = "cannot_connect"
else: else:
@ -130,20 +127,56 @@ class FlowHandler(ConfigFlow, domain=DOMAIN):
self, user_input: dict[str, Any] | None = None self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult: ) -> ConfigFlowResult:
"""Dialog that informs the user that reauth is required.""" """Dialog that informs the user that reauth is required."""
errors: dict[str, str] = {}
if user_input is not None: if user_input is not None:
try: try:
client = await async_control_connect(self._host, None) client = await async_control_connect(self._host, None)
except WebOsTvPairError: except WebOsTvPairError:
return self.async_abort(reason="error_pairing") errors["base"] = "error_pairing"
except WEBOSTV_EXCEPTIONS: 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() return self.async_show_form(step_id="reauth_confirm", errors=errors)
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") 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): class OptionsFlowHandler(OptionsFlow):

View File

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

View File

@ -8,7 +8,7 @@
"host": "[%key:common::config_flow::data::host%]" "host": "[%key:common::config_flow::data::host%]"
}, },
"data_description": { "data_description": {
"host": "Hostname or IP address of your webOS TV." "host": "Hostname or IP address of your LG webOS TV."
} }
}, },
"pairing": { "pairing": {
@ -18,17 +18,26 @@
"reauth_confirm": { "reauth_confirm": {
"title": "[%key:component::webostv::config::step::pairing::title%]", "title": "[%key:component::webostv::config::step::pairing::title%]",
"description": "[%key:component::webostv::config::step::pairing::description%]" "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": { "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": { "abort": {
"error_pairing": "Connected to LG webOS TV but not paired",
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]", "already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", "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": { "options": {
@ -38,6 +47,9 @@
"description": "Select enabled sources", "description": "Select enabled sources",
"data": { "data": {
"sources": "Sources list" "sources": "Sources list"
},
"data_description": {
"sources": "List of sources to enable"
} }
} }
}, },

View File

@ -1,7 +1,5 @@
"""Test the WebOS Tv config flow.""" """Test the WebOS Tv config flow."""
from unittest.mock import AsyncMock
from aiowebostv import WebOsTvPairError from aiowebostv import WebOsTvPairError
import pytest import pytest
@ -105,7 +103,7 @@ async def test_options_flow_cannot_retrieve(hass: HomeAssistant, client) -> None
"""Test options config flow cannot retrieve sources.""" """Test options config flow cannot retrieve sources."""
entry = await setup_webostv(hass) 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) result = await hass.config_entries.options.async_init(entry.entry_id)
await hass.async_block_till_done() 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"} assert result["errors"] == {"base": "cannot_retrieve"}
# recover # recover
client.connect = AsyncMock(return_value=True) client.connect.side_effect = None
result = await hass.config_entries.options.async_configure( result = await hass.config_entries.options.async_configure(
result["flow_id"], result["flow_id"],
user_input=None, user_input=None,
@ -139,7 +137,7 @@ async def test_form_cannot_connect(hass: HomeAssistant, client) -> None:
data=MOCK_USER_CONFIG, data=MOCK_USER_CONFIG,
) )
client.connect = AsyncMock(side_effect=ConnectionRefusedError()) client.connect.side_effect = ConnectionRefusedError
result = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={} 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"} assert result["errors"] == {"base": "cannot_connect"}
# recover # recover
client.connect = AsyncMock(return_value=True) client.connect.side_effect = None
result = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={} result["flow_id"], user_input={}
) )
@ -165,13 +163,22 @@ async def test_form_pairexception(hass: HomeAssistant, client) -> None:
data=MOCK_USER_CONFIG, 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 = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={} result["flow_id"], user_input={}
) )
assert result["type"] is FlowResultType.ABORT assert result["type"] is FlowResultType.FORM
assert result["reason"] == "error_pairing" 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: 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" assert entry.data[CONF_HOST] == "new_host"
async def test_reauth_successful( async def test_reauth_successful(hass: HomeAssistant, client) -> None:
hass: HomeAssistant, client, monkeypatch: pytest.MonkeyPatch
) -> None:
"""Test that the reauthorization is successful.""" """Test that the reauthorization is successful."""
entry = await setup_webostv(hass) entry = await setup_webostv(hass)
@ -282,7 +287,7 @@ async def test_reauth_successful(
assert result["step_id"] == "reauth_confirm" assert result["step_id"] == "reauth_confirm"
assert entry.data[CONF_CLIENT_SECRET] == CLIENT_KEY 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 = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={} result["flow_id"], user_input={}
) )
@ -293,15 +298,13 @@ async def test_reauth_successful(
@pytest.mark.parametrize( @pytest.mark.parametrize(
("side_effect", "reason"), ("side_effect", "error"),
[ [
(WebOsTvPairError, "error_pairing"), (WebOsTvPairError, "error_pairing"),
(ConnectionRefusedError, "reauth_unsuccessful"), (ConnectionRefusedError, "cannot_connect"),
], ],
) )
async def test_reauth_errors( async def test_reauth_errors(hass: HomeAssistant, client, side_effect, error) -> None:
hass: HomeAssistant, client, monkeypatch: pytest.MonkeyPatch, side_effect, reason
) -> None:
"""Test reauthorization errors.""" """Test reauthorization errors."""
entry = await setup_webostv(hass) entry = await setup_webostv(hass)
@ -318,5 +321,88 @@ async def test_reauth_errors(
result["flow_id"], user_input={} 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["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.""" """The tests for the LG webOS TV platform."""
from unittest.mock import Mock
from aiowebostv import WebOsTvPairError from aiowebostv import WebOsTvPairError
import pytest
from homeassistant.components.media_player import ATTR_INPUT_SOURCE_LIST from homeassistant.components.media_player import ATTR_INPUT_SOURCE_LIST
from homeassistant.components.webostv.const import CONF_SOURCES, DOMAIN from homeassistant.components.webostv.const import CONF_SOURCES, DOMAIN
@ -15,12 +12,10 @@ from . import setup_webostv
from .const import ENTITY_ID from .const import ENTITY_ID
async def test_reauth_setup_entry( async def test_reauth_setup_entry(hass: HomeAssistant, client) -> None:
hass: HomeAssistant, client, monkeypatch: pytest.MonkeyPatch
) -> None:
"""Test reauth flow triggered by setup entry.""" """Test reauth flow triggered by setup entry."""
monkeypatch.setattr(client, "is_connected", Mock(return_value=False)) client.is_connected.return_value = False
monkeypatch.setattr(client, "connect", Mock(side_effect=WebOsTvPairError)) client.connect.side_effect = WebOsTvPairError
entry = await setup_webostv(hass) entry = await setup_webostv(hass)
assert entry.state is ConfigEntryState.SETUP_ERROR 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 assert flow["context"].get("entry_id") == entry.entry_id
async def test_key_update_setup_entry( async def test_key_update_setup_entry(hass: HomeAssistant, client) -> None:
hass: HomeAssistant, client, monkeypatch: pytest.MonkeyPatch
) -> None:
"""Test key update from setup entry.""" """Test key update from setup entry."""
monkeypatch.setattr(client, "client_key", "new_key") client.client_key = "new_key"
entry = await setup_webostv(hass) entry = await setup_webostv(hass)
assert entry.state is ConfigEntryState.LOADED assert entry.state is ConfigEntryState.LOADED

View File

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