Add DHCP discovery to balboa (#136762)
parent
fa6df1cc25
commit
35e3952770
|
@ -10,7 +10,7 @@ from pybalboa.exceptions import SpaConnectionError
|
|||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import CONF_HOST
|
||||
from homeassistant.const import CONF_HOST, CONF_MODEL
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.device_registry import format_mac
|
||||
|
@ -18,6 +18,7 @@ from homeassistant.helpers.schema_config_entry_flow import (
|
|||
SchemaFlowFormStep,
|
||||
SchemaOptionsFlowHandler,
|
||||
)
|
||||
from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo
|
||||
|
||||
from .const import CONF_SYNC_TIME, DOMAIN
|
||||
|
||||
|
@ -55,7 +56,8 @@ class BalboaSpaClientFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||
|
||||
VERSION = 1
|
||||
|
||||
_host: str | None
|
||||
_host: str
|
||||
_model: str
|
||||
|
||||
@staticmethod
|
||||
@callback
|
||||
|
@ -63,6 +65,43 @@ class BalboaSpaClientFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||
"""Get the options flow for this handler."""
|
||||
return SchemaOptionsFlowHandler(config_entry, OPTIONS_FLOW)
|
||||
|
||||
async def async_step_dhcp(
|
||||
self, discovery_info: DhcpServiceInfo
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle DHCP discovery."""
|
||||
await self.async_set_unique_id(format_mac(discovery_info.macaddress))
|
||||
self._abort_if_unique_id_configured(updates={CONF_HOST: discovery_info.ip})
|
||||
self._async_abort_entries_match({CONF_HOST: discovery_info.ip})
|
||||
|
||||
error = None
|
||||
try:
|
||||
info = await validate_input({CONF_HOST: discovery_info.ip})
|
||||
except CannotConnect:
|
||||
error = "cannot_connect"
|
||||
except Exception:
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
error = "unknown"
|
||||
if not error:
|
||||
self._host = discovery_info.ip
|
||||
self._model = info["title"]
|
||||
self.context["title_placeholders"] = {CONF_MODEL: self._model}
|
||||
return await self.async_step_discovery_confirm()
|
||||
return self.async_abort(reason=error)
|
||||
|
||||
async def async_step_discovery_confirm(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Allow the user to confirm adding the device."""
|
||||
if user_input is not None:
|
||||
data = {CONF_HOST: self._host}
|
||||
return self.async_create_entry(title=self._model, data=data)
|
||||
|
||||
self._set_confirm_only()
|
||||
return self.async_show_form(
|
||||
step_id="discovery_confirm",
|
||||
description_placeholders={CONF_HOST: self._host},
|
||||
)
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
|
@ -78,7 +117,9 @@ class BalboaSpaClientFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
else:
|
||||
await self.async_set_unique_id(info["formatted_mac"])
|
||||
await self.async_set_unique_id(
|
||||
info["formatted_mac"], raise_on_progress=False
|
||||
)
|
||||
self._abort_if_unique_id_configured()
|
||||
return self.async_create_entry(title=info["title"], data=user_input)
|
||||
|
||||
|
|
|
@ -3,6 +3,14 @@
|
|||
"name": "Balboa Spa Client",
|
||||
"codeowners": ["@garbled1", "@natekspencer"],
|
||||
"config_flow": true,
|
||||
"dhcp": [
|
||||
{
|
||||
"registered_devices": true
|
||||
},
|
||||
{
|
||||
"macaddress": "001527*"
|
||||
}
|
||||
],
|
||||
"documentation": "https://www.home-assistant.io/integrations/balboa",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["pybalboa"],
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
{
|
||||
"config": {
|
||||
"flow_title": "{model}",
|
||||
"step": {
|
||||
"user": {
|
||||
"description": "Connect to the Balboa Wi-Fi device",
|
||||
|
@ -9,6 +10,9 @@
|
|||
"data_description": {
|
||||
"host": "Hostname or IP address of your Balboa Spa Wi-Fi Device. For example, 192.168.1.58."
|
||||
}
|
||||
},
|
||||
"confirm_discovery": {
|
||||
"description": "Do you want to set up the spa at {host}?"
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
|
|
|
@ -61,6 +61,14 @@ DHCP: Final[list[dict[str, str | bool]]] = [
|
|||
"hostname": "axis-e82725*",
|
||||
"macaddress": "E82725*",
|
||||
},
|
||||
{
|
||||
"domain": "balboa",
|
||||
"registered_devices": True,
|
||||
},
|
||||
{
|
||||
"domain": "balboa",
|
||||
"macaddress": "001527*",
|
||||
},
|
||||
{
|
||||
"domain": "blink",
|
||||
"hostname": "blink*",
|
||||
|
|
|
@ -3,19 +3,23 @@
|
|||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from pybalboa.exceptions import SpaConnectionError
|
||||
import pytest
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components.balboa.const import CONF_SYNC_TIME, DOMAIN
|
||||
from homeassistant.const import CONF_HOST
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
TEST_DATA = {
|
||||
CONF_HOST: "1.1.1.1",
|
||||
}
|
||||
TEST_ID = "FakeBalboa"
|
||||
TEST_HOST = "1.1.1.1"
|
||||
TEST_DATA = {CONF_HOST: TEST_HOST}
|
||||
TEST_MAC = "ef:ef:ef:c0:ff:ee"
|
||||
TEST_DHCP_SERVICE_INFO = DhcpServiceInfo(
|
||||
ip=TEST_HOST, macaddress=TEST_MAC.replace(":", ""), hostname="fakespa"
|
||||
)
|
||||
|
||||
|
||||
async def test_form(hass: HomeAssistant, client: MagicMock) -> None:
|
||||
|
@ -107,7 +111,7 @@ async def test_unknown_error(hass: HomeAssistant, client: MagicMock) -> None:
|
|||
|
||||
async def test_already_configured(hass: HomeAssistant, client: MagicMock) -> None:
|
||||
"""Test when provided credentials are already configured."""
|
||||
MockConfigEntry(domain=DOMAIN, data=TEST_DATA, unique_id=TEST_ID).add_to_hass(hass)
|
||||
MockConfigEntry(domain=DOMAIN, data=TEST_DATA, unique_id=TEST_MAC).add_to_hass(hass)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
|
@ -138,7 +142,7 @@ async def test_already_configured(hass: HomeAssistant, client: MagicMock) -> Non
|
|||
|
||||
async def test_options_flow(hass: HomeAssistant, client: MagicMock) -> None:
|
||||
"""Test specifying non default settings using options flow."""
|
||||
config_entry = MockConfigEntry(domain=DOMAIN, data=TEST_DATA, unique_id=TEST_ID)
|
||||
config_entry = MockConfigEntry(domain=DOMAIN, data=TEST_DATA, unique_id=TEST_MAC)
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
|
@ -161,3 +165,111 @@ async def test_options_flow(hass: HomeAssistant, client: MagicMock) -> None:
|
|||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert dict(config_entry.options) == {CONF_SYNC_TIME: True}
|
||||
|
||||
|
||||
async def test_dhcp_discovery(hass: HomeAssistant, client: MagicMock) -> None:
|
||||
"""Test we can process the discovery from dhcp."""
|
||||
with patch(
|
||||
"homeassistant.components.balboa.config_flow.SpaClient.__aenter__",
|
||||
return_value=client,
|
||||
):
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_DHCP},
|
||||
data=TEST_DHCP_SERVICE_INFO,
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == "FakeSpa"
|
||||
assert result["data"] == TEST_DATA
|
||||
assert result["result"].unique_id == TEST_MAC
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_DHCP},
|
||||
data=TEST_DHCP_SERVICE_INFO,
|
||||
)
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "already_configured"
|
||||
|
||||
|
||||
async def test_dhcp_discovery_updates_host(
|
||||
hass: HomeAssistant, client: MagicMock
|
||||
) -> None:
|
||||
"""Test dhcp discovery updates host and aborts."""
|
||||
entry = MockConfigEntry(domain=DOMAIN, data=TEST_DATA, unique_id=TEST_MAC)
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
updated_ip = "1.1.1.2"
|
||||
TEST_DHCP_SERVICE_INFO.ip = updated_ip
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_DHCP},
|
||||
data=TEST_DHCP_SERVICE_INFO,
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "already_configured"
|
||||
|
||||
assert entry.data[CONF_HOST] == updated_ip
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("side_effect", "reason"),
|
||||
[
|
||||
(SpaConnectionError, "cannot_connect"),
|
||||
(Exception, "unknown"),
|
||||
],
|
||||
)
|
||||
async def test_dhcp_discovery_failed(
|
||||
hass: HomeAssistant, client: MagicMock, side_effect: Exception, reason: str
|
||||
) -> None:
|
||||
"""Test failed setup from dhcp."""
|
||||
with patch(
|
||||
"homeassistant.components.balboa.config_flow.SpaClient.__aenter__",
|
||||
return_value=client,
|
||||
side_effect=side_effect(),
|
||||
):
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_DHCP},
|
||||
data=TEST_DHCP_SERVICE_INFO,
|
||||
)
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == reason
|
||||
|
||||
|
||||
async def test_dhcp_discovery_manual_user_setup(
|
||||
hass: HomeAssistant, client: MagicMock
|
||||
) -> None:
|
||||
"""Test dhcp discovery with manual user setup."""
|
||||
with patch(
|
||||
"homeassistant.components.balboa.config_flow.SpaClient.__aenter__",
|
||||
return_value=client,
|
||||
):
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_DHCP},
|
||||
data=TEST_DHCP_SERVICE_INFO,
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_USER},
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
TEST_DATA,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["data"] == TEST_DATA
|
||||
|
|
Loading…
Reference in New Issue