mirror of
https://github.com/home-assistant/core.git
synced 2025-07-16 01:37:08 +00:00
Add Kostal plenticore Installer login support (#133773)
* feat: Add Installer login, Add ManualCharge Switch * remove unnecessary field * replace strings with consts * change to CONF and camel_case * Improve existing code * Add translation string * format code * add service code test * format code * format code * remove manual charge switch * add reconfigure config flow * fix flow * add return type * add reconfigure strings * adjust tests * change string * simlify tests * add reconfigure test * add more tests * Fix --------- Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
This commit is contained in:
parent
e413e9b93b
commit
67b9904740
@ -12,7 +12,7 @@ from homeassistant.const import CONF_BASE, CONF_HOST, CONF_PASSWORD
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .const import DOMAIN
|
||||
from .const import CONF_SERVICE_CODE, DOMAIN
|
||||
from .helper import get_hostname_id
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@ -21,6 +21,7 @@ DATA_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_HOST): str,
|
||||
vol.Required(CONF_PASSWORD): str,
|
||||
vol.Optional(CONF_SERVICE_CODE): str,
|
||||
}
|
||||
)
|
||||
|
||||
@ -32,8 +33,10 @@ async def test_connection(hass: HomeAssistant, data) -> str:
|
||||
"""
|
||||
|
||||
session = async_get_clientsession(hass)
|
||||
async with ApiClient(session, data["host"]) as client:
|
||||
await client.login(data["password"])
|
||||
async with ApiClient(session, data[CONF_HOST]) as client:
|
||||
await client.login(
|
||||
data[CONF_PASSWORD], service_code=data.get(CONF_SERVICE_CODE)
|
||||
)
|
||||
hostname_id = await get_hostname_id(client)
|
||||
values = await client.get_setting_values("scb:network", hostname_id)
|
||||
|
||||
@ -70,3 +73,30 @@ class KostalPlenticoreConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
return self.async_show_form(
|
||||
step_id="user", data_schema=DATA_SCHEMA, errors=errors
|
||||
)
|
||||
|
||||
async def async_step_reconfigure(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Add reconfigure step to allow to reconfigure a config entry."""
|
||||
errors = {}
|
||||
|
||||
if user_input is not None:
|
||||
self._async_abort_entries_match({CONF_HOST: user_input[CONF_HOST]})
|
||||
try:
|
||||
hostname = await test_connection(self.hass, user_input)
|
||||
except AuthenticationException as ex:
|
||||
errors[CONF_PASSWORD] = "invalid_auth"
|
||||
_LOGGER.error("Error response: %s", ex)
|
||||
except (ClientError, TimeoutError):
|
||||
errors[CONF_HOST] = "cannot_connect"
|
||||
except Exception:
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors[CONF_BASE] = "unknown"
|
||||
else:
|
||||
return self.async_update_reload_and_abort(
|
||||
entry=self._get_reconfigure_entry(), title=hostname, data=user_input
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="reconfigure", data_schema=DATA_SCHEMA, errors=errors
|
||||
)
|
||||
|
@ -1,3 +1,4 @@
|
||||
"""Constants for the Kostal Plenticore Solar Inverter integration."""
|
||||
|
||||
DOMAIN = "kostal_plenticore"
|
||||
CONF_SERVICE_CODE = "service_code"
|
||||
|
@ -25,7 +25,7 @@ from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.event import async_call_later
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||
|
||||
from .const import DOMAIN
|
||||
from .const import CONF_SERVICE_CODE, DOMAIN
|
||||
from .helper import get_hostname_id
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@ -60,7 +60,10 @@ class Plenticore:
|
||||
async_get_clientsession(self.hass), host=self.host
|
||||
)
|
||||
try:
|
||||
await self._client.login(self.config_entry.data[CONF_PASSWORD])
|
||||
await self._client.login(
|
||||
self.config_entry.data[CONF_PASSWORD],
|
||||
service_code=self.config_entry.data.get(CONF_SERVICE_CODE),
|
||||
)
|
||||
except AuthenticationException as err:
|
||||
_LOGGER.error(
|
||||
"Authentication exception connecting to %s: %s", self.host, err
|
||||
|
@ -4,7 +4,15 @@
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]",
|
||||
"password": "[%key:common::config_flow::data::password%]"
|
||||
"password": "[%key:common::config_flow::data::password%]",
|
||||
"service_code": "Service code"
|
||||
}
|
||||
},
|
||||
"reconfigure": {
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]",
|
||||
"password": "[%key:common::config_flow::data::password%]",
|
||||
"service_code": "[%key:component::kostal_plenticore::config::step::user::data::service_code%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -14,7 +22,8 @@
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ import pytest
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components.kostal_plenticore.const import DOMAIN
|
||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
|
||||
@ -74,7 +75,7 @@ async def test_form_g1(
|
||||
return_value={"scb:network": {"Hostname": "scb"}}
|
||||
)
|
||||
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
"host": "1.1.1.1",
|
||||
@ -86,15 +87,15 @@ async def test_form_g1(
|
||||
mock_apiclient_class.assert_called_once_with(ANY, "1.1.1.1")
|
||||
mock_apiclient.__aenter__.assert_called_once()
|
||||
mock_apiclient.__aexit__.assert_called_once()
|
||||
mock_apiclient.login.assert_called_once_with("test-password")
|
||||
mock_apiclient.login.assert_called_once_with("test-password", service_code=None)
|
||||
mock_apiclient.get_settings.assert_called_once()
|
||||
mock_apiclient.get_setting_values.assert_called_once_with(
|
||||
"scb:network", "Hostname"
|
||||
)
|
||||
|
||||
assert result2["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result2["title"] == "scb"
|
||||
assert result2["data"] == {
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == "scb"
|
||||
assert result["data"] == {
|
||||
"host": "1.1.1.1",
|
||||
"password": "test-password",
|
||||
}
|
||||
@ -140,7 +141,7 @@ async def test_form_g2(
|
||||
return_value={"scb:network": {"Network:Hostname": "scb"}}
|
||||
)
|
||||
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
"host": "1.1.1.1",
|
||||
@ -152,21 +153,91 @@ async def test_form_g2(
|
||||
mock_apiclient_class.assert_called_once_with(ANY, "1.1.1.1")
|
||||
mock_apiclient.__aenter__.assert_called_once()
|
||||
mock_apiclient.__aexit__.assert_called_once()
|
||||
mock_apiclient.login.assert_called_once_with("test-password")
|
||||
mock_apiclient.login.assert_called_once_with("test-password", service_code=None)
|
||||
mock_apiclient.get_settings.assert_called_once()
|
||||
mock_apiclient.get_setting_values.assert_called_once_with(
|
||||
"scb:network", "Network:Hostname"
|
||||
)
|
||||
|
||||
assert result2["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result2["title"] == "scb"
|
||||
assert result2["data"] == {
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == "scb"
|
||||
assert result["data"] == {
|
||||
"host": "1.1.1.1",
|
||||
"password": "test-password",
|
||||
}
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
async def test_form_g2_with_service_code(
|
||||
hass: HomeAssistant,
|
||||
mock_apiclient_class: type[ApiClient],
|
||||
mock_apiclient: ApiClient,
|
||||
) -> None:
|
||||
"""Test the config flow for G2 models with a Service Code."""
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] == {}
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.kostal_plenticore.async_setup_entry",
|
||||
return_value=True,
|
||||
) as mock_setup_entry:
|
||||
# mock of the context manager instance
|
||||
mock_apiclient.login = AsyncMock()
|
||||
mock_apiclient.get_settings = AsyncMock(
|
||||
return_value={
|
||||
"scb:network": [
|
||||
SettingsData(
|
||||
min="1",
|
||||
max="63",
|
||||
default=None,
|
||||
access="readwrite",
|
||||
unit=None,
|
||||
id="Network:Hostname",
|
||||
type="string",
|
||||
),
|
||||
]
|
||||
}
|
||||
)
|
||||
mock_apiclient.get_setting_values = AsyncMock(
|
||||
# G1 model has the entry id "Hostname"
|
||||
return_value={"scb:network": {"Network:Hostname": "scb"}}
|
||||
)
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
"host": "1.1.1.1",
|
||||
"password": "test-password",
|
||||
"service_code": "test-service-code",
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
mock_apiclient_class.assert_called_once_with(ANY, "1.1.1.1")
|
||||
mock_apiclient.__aenter__.assert_called_once()
|
||||
mock_apiclient.__aexit__.assert_called_once()
|
||||
mock_apiclient.login.assert_called_once_with(
|
||||
"test-password", service_code="test-service-code"
|
||||
)
|
||||
mock_apiclient.get_settings.assert_called_once()
|
||||
mock_apiclient.get_setting_values.assert_called_once_with(
|
||||
"scb:network", "Network:Hostname"
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == "scb"
|
||||
assert result["data"] == {
|
||||
"host": "1.1.1.1",
|
||||
"password": "test-password",
|
||||
"service_code": "test-service-code",
|
||||
}
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
async def test_form_invalid_auth(hass: HomeAssistant) -> None:
|
||||
"""Test we handle invalid auth."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
@ -189,7 +260,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None:
|
||||
|
||||
mock_api_class.return_value = mock_api
|
||||
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
"host": "1.1.1.1",
|
||||
@ -197,8 +268,8 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None:
|
||||
},
|
||||
)
|
||||
|
||||
assert result2["type"] is FlowResultType.FORM
|
||||
assert result2["errors"] == {"password": "invalid_auth"}
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] == {"password": "invalid_auth"}
|
||||
|
||||
|
||||
async def test_form_cannot_connect(hass: HomeAssistant) -> None:
|
||||
@ -223,7 +294,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None:
|
||||
|
||||
mock_api_class.return_value = mock_api
|
||||
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
"host": "1.1.1.1",
|
||||
@ -231,8 +302,8 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None:
|
||||
},
|
||||
)
|
||||
|
||||
assert result2["type"] is FlowResultType.FORM
|
||||
assert result2["errors"] == {"host": "cannot_connect"}
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] == {"host": "cannot_connect"}
|
||||
|
||||
|
||||
async def test_form_unexpected_error(hass: HomeAssistant) -> None:
|
||||
@ -257,7 +328,7 @@ async def test_form_unexpected_error(hass: HomeAssistant) -> None:
|
||||
|
||||
mock_api_class.return_value = mock_api
|
||||
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
"host": "1.1.1.1",
|
||||
@ -265,8 +336,8 @@ async def test_form_unexpected_error(hass: HomeAssistant) -> None:
|
||||
},
|
||||
)
|
||||
|
||||
assert result2["type"] is FlowResultType.FORM
|
||||
assert result2["errors"] == {"base": "unknown"}
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] == {"base": "unknown"}
|
||||
|
||||
|
||||
async def test_already_configured(hass: HomeAssistant) -> None:
|
||||
@ -281,7 +352,7 @@ async def test_already_configured(hass: HomeAssistant) -> None:
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
"host": "1.1.1.1",
|
||||
@ -289,5 +360,197 @@ async def test_already_configured(hass: HomeAssistant) -> None:
|
||||
},
|
||||
)
|
||||
|
||||
assert result2["type"] is FlowResultType.ABORT
|
||||
assert result2["reason"] == "already_configured"
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "already_configured"
|
||||
|
||||
|
||||
async def test_reconfigure(
|
||||
hass: HomeAssistant,
|
||||
mock_apiclient_class: type[ApiClient],
|
||||
mock_apiclient: ApiClient,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test the config flow for G1 models."""
|
||||
|
||||
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"
|
||||
assert result["errors"] == {}
|
||||
|
||||
# mock of the context manager instance
|
||||
mock_apiclient.login = AsyncMock()
|
||||
mock_apiclient.get_settings = AsyncMock(
|
||||
return_value={
|
||||
"scb:network": [
|
||||
SettingsData(
|
||||
min="1",
|
||||
max="63",
|
||||
default=None,
|
||||
access="readwrite",
|
||||
unit=None,
|
||||
id="Hostname",
|
||||
type="string",
|
||||
),
|
||||
]
|
||||
}
|
||||
)
|
||||
mock_apiclient.get_setting_values = AsyncMock(
|
||||
# G1 model has the entry id "Hostname"
|
||||
return_value={"scb:network": {"Hostname": "scb"}}
|
||||
)
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
"host": "1.1.1.1",
|
||||
"password": "test-password",
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
mock_apiclient_class.assert_called_once_with(ANY, "1.1.1.1")
|
||||
mock_apiclient.__aenter__.assert_called_once()
|
||||
mock_apiclient.__aexit__.assert_called_once()
|
||||
mock_apiclient.login.assert_called_once_with("test-password", service_code=None)
|
||||
mock_apiclient.get_settings.assert_called_once()
|
||||
mock_apiclient.get_setting_values.assert_called_once_with("scb:network", "Hostname")
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "reconfigure_successful"
|
||||
|
||||
# changed entry
|
||||
assert mock_config_entry.data[CONF_HOST] == "1.1.1.1"
|
||||
assert mock_config_entry.data[CONF_PASSWORD] == "test-password"
|
||||
|
||||
|
||||
async def test_reconfigure_invalid_auth(
|
||||
hass: HomeAssistant, mock_config_entry: MockConfigEntry
|
||||
) -> None:
|
||||
"""Test we handle invalid auth while reconfiguring."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
result = await mock_config_entry.start_reconfigure_flow(hass)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.kostal_plenticore.config_flow.ApiClient"
|
||||
) as mock_api_class:
|
||||
# mock of the context manager instance
|
||||
mock_api_ctx = MagicMock()
|
||||
mock_api_ctx.login = AsyncMock(
|
||||
side_effect=AuthenticationException(404, "invalid user"),
|
||||
)
|
||||
|
||||
# mock of the return instance of ApiClient
|
||||
mock_api = MagicMock()
|
||||
mock_api.__aenter__.return_value = mock_api_ctx
|
||||
mock_api.__aexit__.return_value = None
|
||||
|
||||
mock_api_class.return_value = mock_api
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
"host": "1.1.1.1",
|
||||
"password": "test-password",
|
||||
},
|
||||
)
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] == {"password": "invalid_auth"}
|
||||
|
||||
|
||||
async def test_reconfigure_cannot_connect(
|
||||
hass: HomeAssistant, mock_config_entry: MockConfigEntry
|
||||
) -> None:
|
||||
"""Test we handle cannot connect error."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
result = await mock_config_entry.start_reconfigure_flow(hass)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.kostal_plenticore.config_flow.ApiClient"
|
||||
) as mock_api_class:
|
||||
# mock of the context manager instance
|
||||
mock_api_ctx = MagicMock()
|
||||
mock_api_ctx.login = AsyncMock(
|
||||
side_effect=TimeoutError(),
|
||||
)
|
||||
|
||||
# mock of the return instance of ApiClient
|
||||
mock_api = MagicMock()
|
||||
mock_api.__aenter__.return_value = mock_api_ctx
|
||||
mock_api.__aexit__.return_value = None
|
||||
|
||||
mock_api_class.return_value = mock_api
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
"host": "1.1.1.1",
|
||||
"password": "test-password",
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] == {"host": "cannot_connect"}
|
||||
|
||||
|
||||
async def test_reconfigure_unexpected_error(
|
||||
hass: HomeAssistant, mock_config_entry: MockConfigEntry
|
||||
) -> None:
|
||||
"""Test we handle unexpected error."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
result = await mock_config_entry.start_reconfigure_flow(hass)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.kostal_plenticore.config_flow.ApiClient"
|
||||
) as mock_api_class:
|
||||
# mock of the context manager instance
|
||||
mock_api_ctx = MagicMock()
|
||||
mock_api_ctx.login = AsyncMock(
|
||||
side_effect=Exception(),
|
||||
)
|
||||
|
||||
# mock of the return instance of ApiClient
|
||||
mock_api = MagicMock()
|
||||
mock_api.__aenter__.return_value = mock_api_ctx
|
||||
mock_api.__aexit__.return_value = None
|
||||
|
||||
mock_api_class.return_value = mock_api
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
"host": "1.1.1.1",
|
||||
"password": "test-password",
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] == {"base": "unknown"}
|
||||
|
||||
|
||||
async def test_reconfigure_already_configured(
|
||||
hass: HomeAssistant, mock_config_entry: MockConfigEntry
|
||||
) -> None:
|
||||
"""Test we handle already configured error."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
MockConfigEntry(
|
||||
domain="kostal_plenticore",
|
||||
data={CONF_HOST: "1.1.1.1", CONF_PASSWORD: "foobar"},
|
||||
unique_id="112233445566",
|
||||
).add_to_hass(hass)
|
||||
|
||||
result = await mock_config_entry.start_reconfigure_flow(hass)
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_HOST: "1.1.1.1",
|
||||
CONF_PASSWORD: "test-password",
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "already_configured"
|
||||
|
Loading…
x
Reference in New Issue
Block a user