Airthings DHCP discovery (#144280)

* Add DHCP to Airthings manifest

* Update manifest

* Update manifest

* Add tests

* Fix pr comments

* fix naming for all tests

* Fix pr comment
This commit is contained in:
Ståle Storø Hauknes 2025-05-09 18:49:22 +02:00 committed by GitHub
parent e29fc37bb1
commit ad7cfe49c8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 116 additions and 13 deletions

View File

@ -3,6 +3,19 @@
"name": "Airthings", "name": "Airthings",
"codeowners": ["@danielhiversen", "@LaStrada"], "codeowners": ["@danielhiversen", "@LaStrada"],
"config_flow": true, "config_flow": true,
"dhcp": [
{
"hostname": "airthings-view"
},
{
"hostname": "airthings-hub",
"macaddress": "D0141190*"
},
{
"hostname": "airthings-hub",
"macaddress": "70B3D52A0*"
}
],
"documentation": "https://www.home-assistant.io/integrations/airthings", "documentation": "https://www.home-assistant.io/integrations/airthings",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["airthings"], "loggers": ["airthings"],

View File

@ -8,6 +8,20 @@ from __future__ import annotations
from typing import Final from typing import Final
DHCP: Final[list[dict[str, str | bool]]] = [ DHCP: Final[list[dict[str, str | bool]]] = [
{
"domain": "airthings",
"hostname": "airthings-view",
},
{
"domain": "airthings",
"hostname": "airthings-hub",
"macaddress": "D0141190*",
},
{
"domain": "airthings",
"hostname": "airthings-hub",
"macaddress": "70B3D52A0*",
},
{ {
"domain": "airzone", "domain": "airzone",
"macaddress": "E84F25*", "macaddress": "E84F25*",

View File

@ -3,12 +3,14 @@
from unittest.mock import patch from unittest.mock import patch
import airthings import airthings
import pytest
from homeassistant import config_entries from homeassistant import config_entries
from homeassistant.components.airthings.const import CONF_SECRET, DOMAIN from homeassistant.components.airthings.const import CONF_SECRET, DOMAIN
from homeassistant.const import CONF_ID from homeassistant.const import CONF_ID
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
@ -17,6 +19,24 @@ TEST_DATA = {
CONF_SECRET: "secret", CONF_SECRET: "secret",
} }
DHCP_SERVICE_INFO = [
DhcpServiceInfo(
hostname="airthings-view",
ip="192.168.1.100",
macaddress="00:00:00:00:00:00",
),
DhcpServiceInfo(
hostname="airthings-hub",
ip="192.168.1.101",
macaddress="D0:14:11:90:00:00",
),
DhcpServiceInfo(
hostname="airthings-hub",
ip="192.168.1.102",
macaddress="70:B3:D5:2A:00:00",
),
]
async def test_form(hass: HomeAssistant) -> None: async def test_form(hass: HomeAssistant) -> None:
"""Test we get the form.""" """Test we get the form."""
@ -37,15 +57,15 @@ async def test_form(hass: HomeAssistant) -> None:
return_value=True, return_value=True,
) as mock_setup_entry, ) as mock_setup_entry,
): ):
result2 = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(
result["flow_id"], result["flow_id"],
TEST_DATA, TEST_DATA,
) )
await hass.async_block_till_done() await hass.async_block_till_done()
assert result2["type"] is FlowResultType.CREATE_ENTRY assert result["type"] is FlowResultType.CREATE_ENTRY
assert result2["title"] == "Airthings" assert result["title"] == "Airthings"
assert result2["data"] == TEST_DATA assert result["data"] == TEST_DATA
assert len(mock_setup_entry.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1
@ -59,13 +79,13 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None:
"airthings.get_token", "airthings.get_token",
side_effect=airthings.AirthingsAuthError, side_effect=airthings.AirthingsAuthError,
): ):
result2 = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(
result["flow_id"], result["flow_id"],
TEST_DATA, TEST_DATA,
) )
assert result2["type"] is FlowResultType.FORM assert result["type"] is FlowResultType.FORM
assert result2["errors"] == {"base": "invalid_auth"} assert result["errors"] == {"base": "invalid_auth"}
async def test_form_cannot_connect(hass: HomeAssistant) -> None: async def test_form_cannot_connect(hass: HomeAssistant) -> None:
@ -78,13 +98,13 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None:
"airthings.get_token", "airthings.get_token",
side_effect=airthings.AirthingsConnectionError, side_effect=airthings.AirthingsConnectionError,
): ):
result2 = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(
result["flow_id"], result["flow_id"],
TEST_DATA, TEST_DATA,
) )
assert result2["type"] is FlowResultType.FORM assert result["type"] is FlowResultType.FORM
assert result2["errors"] == {"base": "cannot_connect"} assert result["errors"] == {"base": "cannot_connect"}
async def test_form_unknown_error(hass: HomeAssistant) -> None: async def test_form_unknown_error(hass: HomeAssistant) -> None:
@ -97,13 +117,13 @@ async def test_form_unknown_error(hass: HomeAssistant) -> None:
"airthings.get_token", "airthings.get_token",
side_effect=Exception, side_effect=Exception,
): ):
result2 = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(
result["flow_id"], result["flow_id"],
TEST_DATA, TEST_DATA,
) )
assert result2["type"] is FlowResultType.FORM assert result["type"] is FlowResultType.FORM
assert result2["errors"] == {"base": "unknown"} assert result["errors"] == {"base": "unknown"}
async def test_flow_entry_already_exists(hass: HomeAssistant) -> None: async def test_flow_entry_already_exists(hass: HomeAssistant) -> None:
@ -123,3 +143,59 @@ async def test_flow_entry_already_exists(hass: HomeAssistant) -> None:
assert result["type"] is FlowResultType.ABORT assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "already_configured" assert result["reason"] == "already_configured"
@pytest.mark.parametrize("dhcp_service_info", DHCP_SERVICE_INFO)
async def test_dhcp_flow(
hass: HomeAssistant, dhcp_service_info: DhcpServiceInfo
) -> None:
"""Test the DHCP discovery flow."""
result = await hass.config_entries.flow.async_init(
DOMAIN,
data=dhcp_service_info,
context={"source": config_entries.SOURCE_DHCP},
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "user"
with (
patch(
"homeassistant.components.airthings.async_setup_entry",
return_value=True,
) as mock_setup_entry,
patch(
"airthings.get_token",
return_value="test_token",
),
):
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
TEST_DATA,
)
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["title"] == "Airthings"
assert result["data"] == TEST_DATA
assert len(mock_setup_entry.mock_calls) == 1
async def test_dhcp_flow_hub_already_configured(hass: HomeAssistant) -> None:
"""Test that DHCP discovery fails when already configured."""
first_entry = MockConfigEntry(
domain="airthings",
data=TEST_DATA,
unique_id=TEST_DATA[CONF_ID],
)
first_entry.add_to_hass(hass)
result = await hass.config_entries.flow.async_init(
DOMAIN,
data=DHCP_SERVICE_INFO[0],
context={"source": config_entries.SOURCE_DHCP},
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "already_configured"