Add a reconfigure flow to LaMetric (#184160)

This commit is contained in:
Franck Nijhof
2026-10-04 08:51:13 +02:00
committed by GitHub
parent 09403997a3
commit 46bc023e99
4 changed files with 160 additions and 1 deletions
@@ -7,6 +7,7 @@ from typing import Any, override
from demetriek import (
CloudDevice,
LaMetricAuthenticationError,
LaMetricCloud,
LaMetricConnectionError,
LaMetricDevice,
@@ -118,6 +119,57 @@ class LaMetricFlowHandler(AbstractOAuth2FlowHandler, domain=DOMAIN):
"""Handle initiation of re-authentication with LaMetric."""
return await self.async_step_choice_enter_manual_or_fetch_cloud()
async def async_step_reconfigure(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle reconfiguration of the host and API key of a device."""
errors: dict[str, str] = {}
reconfigure_entry = self._get_reconfigure_entry()
if user_input is not None:
lametric = LaMetricDevice(
host=user_input[CONF_HOST],
api_key=user_input[CONF_API_KEY],
session=async_get_clientsession(self.hass),
)
try:
device = await lametric.device()
except LaMetricAuthenticationError:
errors["base"] = "invalid_auth"
except LaMetricConnectionError as ex:
LOGGER.error("Error connecting to LaMetric: %s", ex)
errors["base"] = "cannot_connect"
except Exception: # noqa: BLE001
LOGGER.exception("Unexpected error occurred")
errors["base"] = "unknown"
else:
await self.async_set_unique_id(device.serial_number)
self._abort_if_unique_id_mismatch()
return self.async_update_reload_and_abort(
reconfigure_entry,
data_updates={
CONF_HOST: user_input[CONF_HOST],
CONF_API_KEY: user_input[CONF_API_KEY],
},
)
return self.async_show_form(
step_id="reconfigure",
data_schema=self.add_suggested_values_to_schema(
probatio.Schema(
{
probatio.Required(CONF_HOST): TextSelector(),
probatio.Required(probatio.Secret(CONF_API_KEY)): TextSelector(
TextSelectorConfig(type=TextSelectorType.PASSWORD)
),
}
),
{CONF_HOST: reconfigure_entry.data[CONF_HOST]},
),
description_placeholders={"devices_url": DEVICES_URL},
errors=errors,
)
async def async_step_choice_enter_manual_or_fetch_cloud(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
@@ -68,7 +68,7 @@ rules:
entity-translations: done
exception-translations: done
icon-translations: done
reconfiguration-flow: todo
reconfiguration-flow: done
repair-issues:
status: exempt
comment: |
@@ -7,10 +7,12 @@
"missing_configuration": "The LaMetric integration is not configured. Please follow the documentation.",
"no_devices": "The authorized user has no LaMetric devices",
"reauth_device_not_found": "The device you are trying to re-authenticate is not found in this LaMetric account",
"unique_id_mismatch": "The device at this address is not the LaMetric device you are reconfiguring.",
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"step": {
@@ -47,6 +49,16 @@
"implementation": "[%key:common::config_flow::description::implementation%]"
},
"title": "[%key:common::config_flow::title::oauth2_pick_implementation%]"
},
"reconfigure": {
"data": {
"api_key": "[%key:common::config_flow::data::api_key%]",
"host": "[%key:common::config_flow::data::host%]"
},
"data_description": {
"api_key": "[%key:component::lametric::config::step::manual_entry::data_description::api_key%]",
"host": "[%key:component::lametric::config::step::manual_entry::data_description::host%]"
}
}
}
},
@@ -4,6 +4,7 @@ from http import HTTPStatus
from unittest.mock import MagicMock
from demetriek import (
LaMetricAuthenticationError,
LaMetricConnectionError,
LaMetricConnectionTimeoutError,
LaMetricError,
@@ -927,3 +928,97 @@ async def test_reauth_manual_sky(
notification: Notification = mock_lametric.notify.mock_calls[0][2]["notification"]
assert notification.model.sound is None
@pytest.mark.usefixtures("mock_setup_entry")
async def test_reconfigure(
hass: HomeAssistant,
mock_lametric: MagicMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test changing the host and API key of a configured device."""
mock_config_entry.add_to_hass(hass)
result = await mock_config_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: "127.0.0.42", CONF_API_KEY: "new-api-key"},
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "reconfigure_successful"
assert mock_config_entry.data == {
CONF_HOST: "127.0.0.42",
CONF_API_KEY: "new-api-key",
CONF_MAC: "AA:BB:CC:DD:EE:FF",
}
# Reconfiguring only checks the device, it does not send a notification.
assert len(mock_lametric.device.mock_calls) == 1
assert len(mock_lametric.notify.mock_calls) == 0
@pytest.mark.usefixtures("mock_setup_entry")
async def test_reconfigure_other_device(
hass: HomeAssistant,
mock_lametric: MagicMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test reconfiguring towards another device is refused."""
mock_config_entry.add_to_hass(hass)
mock_lametric.device.return_value.serial_number = "SA000000000000000000"
result = await mock_config_entry.start_reconfigure_flow(hass)
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input={CONF_HOST: "127.0.0.42", CONF_API_KEY: "new-api-key"},
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "unique_id_mismatch"
assert mock_config_entry.data[CONF_HOST] == "127.0.0.2"
@pytest.mark.usefixtures("mock_setup_entry")
@pytest.mark.parametrize(
("side_effect", "error"),
[
(LaMetricAuthenticationError, "invalid_auth"),
(LaMetricConnectionError, "cannot_connect"),
(Exception, "unknown"),
],
)
async def test_reconfigure_errors(
hass: HomeAssistant,
mock_lametric: MagicMock,
mock_config_entry: MockConfigEntry,
side_effect: type[Exception],
error: str,
) -> None:
"""Test errors while reconfiguring, and recovering from them."""
mock_config_entry.add_to_hass(hass)
device = mock_lametric.device.return_value
mock_lametric.device.side_effect = side_effect
result = await mock_config_entry.start_reconfigure_flow(hass)
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input={CONF_HOST: "127.0.0.42", CONF_API_KEY: "new-api-key"},
)
assert result["type"] is FlowResultType.FORM
assert result["errors"] == {"base": error}
mock_lametric.device.side_effect = None
mock_lametric.device.return_value = device
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input={CONF_HOST: "127.0.0.42", CONF_API_KEY: "new-api-key"},
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "reconfigure_successful"