mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 13:17:32 +00:00
Add DHCP discovery to balboa (#136762)
This commit is contained in:
parent
fa6df1cc25
commit
35e3952770
@ -10,7 +10,7 @@ from pybalboa.exceptions import SpaConnectionError
|
|||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult
|
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.core import callback
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
from homeassistant.helpers.device_registry import format_mac
|
from homeassistant.helpers.device_registry import format_mac
|
||||||
@ -18,6 +18,7 @@ from homeassistant.helpers.schema_config_entry_flow import (
|
|||||||
SchemaFlowFormStep,
|
SchemaFlowFormStep,
|
||||||
SchemaOptionsFlowHandler,
|
SchemaOptionsFlowHandler,
|
||||||
)
|
)
|
||||||
|
from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo
|
||||||
|
|
||||||
from .const import CONF_SYNC_TIME, DOMAIN
|
from .const import CONF_SYNC_TIME, DOMAIN
|
||||||
|
|
||||||
@ -55,7 +56,8 @@ class BalboaSpaClientFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||||||
|
|
||||||
VERSION = 1
|
VERSION = 1
|
||||||
|
|
||||||
_host: str | None
|
_host: str
|
||||||
|
_model: str
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@callback
|
@callback
|
||||||
@ -63,6 +65,43 @@ class BalboaSpaClientFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||||||
"""Get the options flow for this handler."""
|
"""Get the options flow for this handler."""
|
||||||
return SchemaOptionsFlowHandler(config_entry, OPTIONS_FLOW)
|
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(
|
async def async_step_user(
|
||||||
self, user_input: dict[str, Any] | None = None
|
self, user_input: dict[str, Any] | None = None
|
||||||
) -> ConfigFlowResult:
|
) -> ConfigFlowResult:
|
||||||
@ -78,7 +117,9 @@ class BalboaSpaClientFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||||||
_LOGGER.exception("Unexpected exception")
|
_LOGGER.exception("Unexpected exception")
|
||||||
errors["base"] = "unknown"
|
errors["base"] = "unknown"
|
||||||
else:
|
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()
|
self._abort_if_unique_id_configured()
|
||||||
return self.async_create_entry(title=info["title"], data=user_input)
|
return self.async_create_entry(title=info["title"], data=user_input)
|
||||||
|
|
||||||
|
@ -3,6 +3,14 @@
|
|||||||
"name": "Balboa Spa Client",
|
"name": "Balboa Spa Client",
|
||||||
"codeowners": ["@garbled1", "@natekspencer"],
|
"codeowners": ["@garbled1", "@natekspencer"],
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
|
"dhcp": [
|
||||||
|
{
|
||||||
|
"registered_devices": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"macaddress": "001527*"
|
||||||
|
}
|
||||||
|
],
|
||||||
"documentation": "https://www.home-assistant.io/integrations/balboa",
|
"documentation": "https://www.home-assistant.io/integrations/balboa",
|
||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
"loggers": ["pybalboa"],
|
"loggers": ["pybalboa"],
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"config": {
|
"config": {
|
||||||
|
"flow_title": "{model}",
|
||||||
"step": {
|
"step": {
|
||||||
"user": {
|
"user": {
|
||||||
"description": "Connect to the Balboa Wi-Fi device",
|
"description": "Connect to the Balboa Wi-Fi device",
|
||||||
@ -9,6 +10,9 @@
|
|||||||
"data_description": {
|
"data_description": {
|
||||||
"host": "Hostname or IP address of your Balboa Spa Wi-Fi Device. For example, 192.168.1.58."
|
"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": {
|
"error": {
|
||||||
|
8
homeassistant/generated/dhcp.py
generated
8
homeassistant/generated/dhcp.py
generated
@ -61,6 +61,14 @@ DHCP: Final[list[dict[str, str | bool]]] = [
|
|||||||
"hostname": "axis-e82725*",
|
"hostname": "axis-e82725*",
|
||||||
"macaddress": "E82725*",
|
"macaddress": "E82725*",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"domain": "balboa",
|
||||||
|
"registered_devices": True,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"domain": "balboa",
|
||||||
|
"macaddress": "001527*",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"domain": "blink",
|
"domain": "blink",
|
||||||
"hostname": "blink*",
|
"hostname": "blink*",
|
||||||
|
@ -3,19 +3,23 @@
|
|||||||
from unittest.mock import MagicMock, patch
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
from pybalboa.exceptions import SpaConnectionError
|
from pybalboa.exceptions import SpaConnectionError
|
||||||
|
import pytest
|
||||||
|
|
||||||
from homeassistant import config_entries
|
from homeassistant import config_entries
|
||||||
from homeassistant.components.balboa.const import CONF_SYNC_TIME, DOMAIN
|
from homeassistant.components.balboa.const import CONF_SYNC_TIME, DOMAIN
|
||||||
from homeassistant.const import CONF_HOST
|
from homeassistant.const import CONF_HOST
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.data_entry_flow import FlowResultType
|
from homeassistant.data_entry_flow import FlowResultType
|
||||||
|
from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo
|
||||||
|
|
||||||
from tests.common import MockConfigEntry
|
from tests.common import MockConfigEntry
|
||||||
|
|
||||||
TEST_DATA = {
|
TEST_HOST = "1.1.1.1"
|
||||||
CONF_HOST: "1.1.1.1",
|
TEST_DATA = {CONF_HOST: TEST_HOST}
|
||||||
}
|
TEST_MAC = "ef:ef:ef:c0:ff:ee"
|
||||||
TEST_ID = "FakeBalboa"
|
TEST_DHCP_SERVICE_INFO = DhcpServiceInfo(
|
||||||
|
ip=TEST_HOST, macaddress=TEST_MAC.replace(":", ""), hostname="fakespa"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def test_form(hass: HomeAssistant, client: MagicMock) -> None:
|
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:
|
async def test_already_configured(hass: HomeAssistant, client: MagicMock) -> None:
|
||||||
"""Test when provided credentials are already configured."""
|
"""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(
|
result = await hass.config_entries.flow.async_init(
|
||||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
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:
|
async def test_options_flow(hass: HomeAssistant, client: MagicMock) -> None:
|
||||||
"""Test specifying non default settings using options flow."""
|
"""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)
|
config_entry.add_to_hass(hass)
|
||||||
|
|
||||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
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 result["type"] is FlowResultType.CREATE_ENTRY
|
||||||
assert dict(config_entry.options) == {CONF_SYNC_TIME: True}
|
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…
x
Reference in New Issue
Block a user