Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add reconfigure to LG webOS TV #135360

Merged
merged 4 commits into from
Jan 11, 2025
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 49 additions & 14 deletions homeassistant/components/webostv/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
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

Expand Down Expand Up @@ -53,14 +53,11 @@ async def async_step_user(
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
Expand All @@ -69,13 +66,13 @@ async def async_step_pairing(
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:
Expand Down Expand Up @@ -130,20 +127,58 @@ async def async_step_reauth_confirm(
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)

return self.async_show_form(step_id="reauth_confirm", errors=errors)

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()

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")
if user_input is not None:
host = user_input[CONF_HOST]
client_key = reconfigure_entry.data.get(CONF_CLIENT_SECRET)

return self.async_show_form(step_id="reauth_confirm")
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):
Expand Down
6 changes: 2 additions & 4 deletions homeassistant/components/webostv/quality_scale.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
20 changes: 16 additions & 4 deletions homeassistant/components/webostv/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand All @@ -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": {
Expand All @@ -38,6 +47,9 @@
"description": "Select enabled sources",
"data": {
"sources": "Sources list"
},
"data_description": {
"sources": "List of sources to enable"
}
}
},
Expand Down
124 changes: 105 additions & 19 deletions tests/components/webostv/test_config_flow.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
"""Test the WebOS Tv config flow."""

from unittest.mock import AsyncMock

from aiowebostv import WebOsTvPairError
import pytest

Expand Down Expand Up @@ -105,15 +103,15 @@ 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()

assert result["type"] is FlowResultType.FORM
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,
Expand All @@ -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={}
)
Expand All @@ -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={}
)
Expand All @@ -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:
Expand Down Expand Up @@ -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)

Expand All @@ -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={}
)
Expand All @@ -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)

Expand All @@ -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"] == "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"] == reason
assert result["reason"] == "wrong_device"
Loading
Loading