mirror of
https://github.com/home-assistant/core.git
synced 2026-09-28 02:18:10 -04:00
Add zeroconf discovery to Velux integration (#181284)
This commit is contained in:
@@ -4,6 +4,7 @@ from collections.abc import Mapping
|
||||
from typing import Any, override
|
||||
|
||||
from pyvlx import PyVLX, PyVLXException
|
||||
from pyvlx.discovery import sanitize_hostname
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigEntryState, ConfigFlow, ConfigFlowResult
|
||||
@@ -11,6 +12,7 @@ from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME, CONF_PASSWORD
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.device_registry import format_mac
|
||||
from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo
|
||||
from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
|
||||
|
||||
from .const import DOMAIN, LOGGER, PYVLX_FROM_CONFIG_FLOW
|
||||
|
||||
@@ -128,6 +130,34 @@ class VeluxConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
},
|
||||
)
|
||||
|
||||
def _is_already_configured_without_unique_id(self) -> bool:
|
||||
"""Checks if a config entry already exists for the given host without a unique_id configured.
|
||||
|
||||
If yes, it updates the entry with the unique_id and discovery data and returns True.
|
||||
If no, it returns False.
|
||||
|
||||
Comparing the host is the best we can do, it will fail if the user configured manually
|
||||
with a different name, but the gateway does not provide a good unique ID other than the
|
||||
announced name, which does not exist if configured manually.
|
||||
"""
|
||||
for entry in self.hass.config_entries.async_entries(DOMAIN):
|
||||
if (
|
||||
entry.data[CONF_HOST] == self.discovery_data[CONF_HOST]
|
||||
and entry.unique_id is None
|
||||
and entry.state is ConfigEntryState.LOADED
|
||||
):
|
||||
LOGGER.info(
|
||||
"Config entry for host %s exists without unique_id, updating entry",
|
||||
self.discovery_data[CONF_HOST],
|
||||
)
|
||||
self.hass.config_entries.async_update_entry(
|
||||
entry=entry,
|
||||
unique_id=self.discovery_data[CONF_NAME],
|
||||
data={**entry.data, **self.discovery_data},
|
||||
)
|
||||
return True
|
||||
return False
|
||||
|
||||
@override
|
||||
async def async_step_dhcp(
|
||||
self, discovery_info: DhcpServiceInfo
|
||||
@@ -146,18 +176,27 @@ class VeluxConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
)
|
||||
|
||||
# Abort if config_entry already exists without unique_id configured.
|
||||
for entry in self.hass.config_entries.async_entries(DOMAIN):
|
||||
if (
|
||||
entry.data[CONF_HOST] == self.discovery_data[CONF_HOST]
|
||||
and entry.unique_id is None
|
||||
and entry.state is ConfigEntryState.LOADED
|
||||
):
|
||||
self.hass.config_entries.async_update_entry(
|
||||
entry=entry,
|
||||
unique_id=self.discovery_data[CONF_NAME],
|
||||
data={**entry.data, **self.discovery_data},
|
||||
)
|
||||
return self.async_abort(reason="already_configured")
|
||||
if self._is_already_configured_without_unique_id():
|
||||
return self.async_abort(reason="already_configured")
|
||||
self._async_abort_entries_match({CONF_HOST: self.discovery_data[CONF_HOST]})
|
||||
return await self.async_step_discovery_confirm()
|
||||
|
||||
@override
|
||||
async def async_step_zeroconf(
|
||||
self, discovery_info: ZeroconfServiceInfo
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle discovery by zeroconf."""
|
||||
self.discovery_data[CONF_HOST] = discovery_info.host
|
||||
self.discovery_data[CONF_NAME] = sanitize_hostname(discovery_info.name)
|
||||
|
||||
self.context["title_placeholders"] = {CONF_NAME: self.discovery_data[CONF_NAME]}
|
||||
await self.async_set_unique_id(self.discovery_data[CONF_NAME])
|
||||
self._abort_if_unique_id_configured(
|
||||
updates={CONF_HOST: self.discovery_data[CONF_HOST]}
|
||||
)
|
||||
# Abort if config_entry already exists without unique_id configured.
|
||||
if self._is_already_configured_without_unique_id():
|
||||
return self.async_abort(reason="already_configured")
|
||||
self._async_abort_entries_match({CONF_HOST: self.discovery_data[CONF_HOST]})
|
||||
return await self.async_step_discovery_confirm()
|
||||
|
||||
|
||||
@@ -14,5 +14,11 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["pyvlx"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["pyvlx==0.2.36"]
|
||||
"requirements": ["pyvlx==0.2.36"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"name": "velux_klf_lan_*",
|
||||
"type": "_http._tcp.local."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ rules:
|
||||
# Gold
|
||||
devices: done
|
||||
diagnostics: done
|
||||
discovery-update-info: todo
|
||||
discovery-update-info: done
|
||||
discovery: done
|
||||
docs-data-update: todo
|
||||
docs-examples: todo
|
||||
|
||||
Generated
+4
@@ -695,6 +695,10 @@ ZEROCONF = {
|
||||
"vendor": "tailwind",
|
||||
},
|
||||
},
|
||||
{
|
||||
"domain": "velux",
|
||||
"name": "velux_klf_lan_*",
|
||||
},
|
||||
],
|
||||
"_hue._tcp.local.": [
|
||||
{
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""Test the Velux config flow."""
|
||||
|
||||
from ipaddress import ip_address
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
import pytest
|
||||
@@ -7,11 +8,17 @@ from pyvlx import PyVLXException
|
||||
|
||||
from homeassistant.components.velux import DOMAIN
|
||||
from homeassistant.components.velux.const import PYVLX_FROM_CONFIG_FLOW
|
||||
from homeassistant.config_entries import SOURCE_DHCP, SOURCE_USER, ConfigEntryState
|
||||
from homeassistant.config_entries import (
|
||||
SOURCE_DHCP,
|
||||
SOURCE_USER,
|
||||
SOURCE_ZEROCONF,
|
||||
ConfigEntryState,
|
||||
)
|
||||
from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME, CONF_PASSWORD
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo
|
||||
from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
@@ -21,6 +28,26 @@ DHCP_DISCOVERY = DhcpServiceInfo(
|
||||
macaddress="64618400abcd",
|
||||
)
|
||||
|
||||
ZEROCONF_DISCOVERY = ZeroconfServiceInfo(
|
||||
ip_address=ip_address("127.0.0.1"),
|
||||
ip_addresses=[ip_address("127.0.0.1")],
|
||||
hostname="VELUX_KLF_LAN_ABCD.local.",
|
||||
name="VELUX_KLF_LAN_ABCD._http._tcp.local.",
|
||||
port=80,
|
||||
type="_http._tcp.local.",
|
||||
properties={},
|
||||
)
|
||||
|
||||
ZEROCONF_DISCOVERY_NEW_IP = ZeroconfServiceInfo(
|
||||
ip_address=ip_address("127.0.0.2"),
|
||||
ip_addresses=[ip_address("127.0.0.2")],
|
||||
hostname="VELUX_KLF_LAN_ABCD.local.",
|
||||
name="VELUX_KLF_LAN_ABCD._http._tcp.local.",
|
||||
port=80,
|
||||
type="_http._tcp.local.",
|
||||
properties={},
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_setup_entry")
|
||||
async def test_user_flow(hass: HomeAssistant, mock_pyvlx: AsyncMock) -> None:
|
||||
@@ -310,6 +337,92 @@ async def test_dhcp_discovery_errors(
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_setup_entry")
|
||||
async def test_zeroconf_discovery(hass: HomeAssistant, mock_pyvlx: AsyncMock) -> None:
|
||||
"""Test we can set up from zeroconf discovery."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_ZEROCONF},
|
||||
data=ZEROCONF_DISCOVERY,
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "discovery_confirm"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={CONF_PASSWORD: "NotAStrongPassword"},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == "VELUX_KLF_ABCD"
|
||||
assert result["data"] == {
|
||||
CONF_HOST: "127.0.0.1",
|
||||
CONF_NAME: "VELUX_KLF_ABCD",
|
||||
CONF_PASSWORD: "NotAStrongPassword",
|
||||
}
|
||||
assert result["result"].unique_id == "VELUX_KLF_ABCD"
|
||||
|
||||
mock_pyvlx.connect.assert_awaited_once()
|
||||
mock_pyvlx.disconnect.assert_not_awaited()
|
||||
assert hass.data[PYVLX_FROM_CONFIG_FLOW]["127.0.0.1"] is mock_pyvlx
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_setup_entry")
|
||||
async def test_zeroconf_discovery_already_configured(
|
||||
hass: HomeAssistant,
|
||||
mock_discovered_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test zeroconf discovery when already configured."""
|
||||
mock_discovered_config_entry.add_to_hass(hass)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_ZEROCONF},
|
||||
data=ZEROCONF_DISCOVERY,
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "already_configured"
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_setup_entry")
|
||||
async def test_zeroconf_discovery_updates_host(
|
||||
hass: HomeAssistant,
|
||||
mock_discovered_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test zeroconf discovery updates the host for an existing unique ID."""
|
||||
mock_discovered_config_entry.add_to_hass(hass)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_ZEROCONF},
|
||||
data=ZEROCONF_DISCOVERY_NEW_IP,
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "already_configured"
|
||||
assert mock_discovered_config_entry.data[CONF_HOST] == "127.0.0.2"
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_setup_entry")
|
||||
async def test_zeroconf_discovery_manual_entry_same_host(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test zeroconf discovery when a manual entry has the same host."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_ZEROCONF},
|
||||
data=ZEROCONF_DISCOVERY,
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "already_configured"
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_setup_entry")
|
||||
async def test_dhcp_discovery_already_configured(
|
||||
hass: HomeAssistant,
|
||||
@@ -350,6 +463,28 @@ async def test_dhcp_discover_unique_id(
|
||||
assert mock_config_entry.unique_id == "VELUX_KLF_ABCD"
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_setup_entry")
|
||||
async def test_zeroconf_discover_unique_id(
|
||||
hass: HomeAssistant, mock_config_entry: MockConfigEntry
|
||||
) -> None:
|
||||
"""Test zeroconf discovery updates a loaded entry without a unique ID."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
assert await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
|
||||
assert mock_config_entry.state is ConfigEntryState.LOADED
|
||||
assert mock_config_entry.unique_id is None
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_ZEROCONF},
|
||||
data=ZEROCONF_DISCOVERY,
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "already_configured"
|
||||
assert mock_config_entry.unique_id == "VELUX_KLF_ABCD"
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_setup_entry")
|
||||
async def test_dhcp_discovery_not_loaded(
|
||||
hass: HomeAssistant, mock_pyvlx: AsyncMock, mock_config_entry: MockConfigEntry
|
||||
|
||||
Reference in New Issue
Block a user