Fix rainbird unique id (#99704)

* Don't set a unique id for devices with no serial

* Add additional check for the same config entry host/port when there is no serial

* Update homeassistant/components/rainbird/config_flow.py

Co-authored-by: Robert Resch <robert@resch.dev>

* Update tests/components/rainbird/test_config_flow.py

Co-authored-by: Robert Resch <robert@resch.dev>

* Update tests/components/rainbird/test_config_flow.py

Co-authored-by: Robert Resch <robert@resch.dev>

---------

Co-authored-by: Robert Resch <robert@resch.dev>
This commit is contained in:
Allen Porter 2023-09-23 14:14:57 -07:00 committed by GitHub
parent 1f66fc013c
commit 8d8c7187d3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 136 additions and 6 deletions

View File

@ -125,8 +125,13 @@ class RainbirdConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
options: dict[str, Any], options: dict[str, Any],
) -> FlowResult: ) -> FlowResult:
"""Create the config entry.""" """Create the config entry."""
await self.async_set_unique_id(serial_number) # Prevent devices with the same serial number. If the device does not have a serial number
self._abort_if_unique_id_configured() # then we can at least prevent configuring the same host twice.
if serial_number:
await self.async_set_unique_id(serial_number)
self._abort_if_unique_id_configured()
else:
self._async_abort_entries_match(data)
return self.async_create_entry( return self.async_create_entry(
title=data[CONF_HOST], title=data[CONF_HOST],
data=data, data=data,

View File

@ -35,6 +35,7 @@ SERIAL_NUMBER = 0x12635436566
# Get serial number Command 0x85. Serial is 0x12635436566 # Get serial number Command 0x85. Serial is 0x12635436566
SERIAL_RESPONSE = "850000012635436566" SERIAL_RESPONSE = "850000012635436566"
ZERO_SERIAL_RESPONSE = "850000000000000000"
# Model and version command 0x82 # Model and version command 0x82
MODEL_AND_VERSION_RESPONSE = "820006090C" MODEL_AND_VERSION_RESPONSE = "820006090C"
# Get available stations command 0x83 # Get available stations command 0x83
@ -84,6 +85,12 @@ def yaml_config() -> dict[str, Any]:
return {} return {}
@pytest.fixture
async def unique_id() -> str:
"""Fixture for serial number used in the config entry."""
return SERIAL_NUMBER
@pytest.fixture @pytest.fixture
async def config_entry_data() -> dict[str, Any]: async def config_entry_data() -> dict[str, Any]:
"""Fixture for MockConfigEntry data.""" """Fixture for MockConfigEntry data."""
@ -92,13 +99,14 @@ async def config_entry_data() -> dict[str, Any]:
@pytest.fixture @pytest.fixture
async def config_entry( async def config_entry(
config_entry_data: dict[str, Any] | None config_entry_data: dict[str, Any] | None,
unique_id: str,
) -> MockConfigEntry | None: ) -> MockConfigEntry | None:
"""Fixture for MockConfigEntry.""" """Fixture for MockConfigEntry."""
if config_entry_data is None: if config_entry_data is None:
return None return None
return MockConfigEntry( return MockConfigEntry(
unique_id=SERIAL_NUMBER, unique_id=unique_id,
domain=DOMAIN, domain=DOMAIN,
data=config_entry_data, data=config_entry_data,
options={ATTR_DURATION: DEFAULT_TRIGGER_TIME_MINUTES}, options={ATTR_DURATION: DEFAULT_TRIGGER_TIME_MINUTES},

View File

@ -3,6 +3,7 @@
import asyncio import asyncio
from collections.abc import Generator from collections.abc import Generator
from http import HTTPStatus from http import HTTPStatus
from typing import Any
from unittest.mock import Mock, patch from unittest.mock import Mock, patch
import pytest import pytest
@ -19,8 +20,11 @@ from .conftest import (
CONFIG_ENTRY_DATA, CONFIG_ENTRY_DATA,
HOST, HOST,
PASSWORD, PASSWORD,
SERIAL_NUMBER,
SERIAL_RESPONSE, SERIAL_RESPONSE,
URL, URL,
ZERO_SERIAL_RESPONSE,
ComponentSetup,
mock_response, mock_response,
) )
@ -66,19 +70,132 @@ async def complete_flow(hass: HomeAssistant) -> FlowResult:
) )
async def test_controller_flow(hass: HomeAssistant, mock_setup: Mock) -> None: @pytest.mark.parametrize(
("responses", "expected_config_entry", "expected_unique_id"),
[
(
[mock_response(SERIAL_RESPONSE)],
CONFIG_ENTRY_DATA,
SERIAL_NUMBER,
),
(
[mock_response(ZERO_SERIAL_RESPONSE)],
{**CONFIG_ENTRY_DATA, "serial_number": 0},
None,
),
],
)
async def test_controller_flow(
hass: HomeAssistant,
mock_setup: Mock,
expected_config_entry: dict[str, str],
expected_unique_id: int | None,
) -> None:
"""Test the controller is setup correctly.""" """Test the controller is setup correctly."""
result = await complete_flow(hass) result = await complete_flow(hass)
assert result.get("type") == "create_entry" assert result.get("type") == "create_entry"
assert result.get("title") == HOST assert result.get("title") == HOST
assert "result" in result assert "result" in result
assert result["result"].data == CONFIG_ENTRY_DATA assert dict(result["result"].data) == expected_config_entry
assert result["result"].options == {ATTR_DURATION: 6} assert result["result"].options == {ATTR_DURATION: 6}
assert result["result"].unique_id == expected_unique_id
assert len(mock_setup.mock_calls) == 1 assert len(mock_setup.mock_calls) == 1
@pytest.mark.parametrize(
(
"unique_id",
"config_entry_data",
"config_flow_responses",
"expected_config_entry",
),
[
(
"other-serial-number",
{**CONFIG_ENTRY_DATA, "host": "other-host"},
[mock_response(SERIAL_RESPONSE)],
CONFIG_ENTRY_DATA,
),
(
None,
{**CONFIG_ENTRY_DATA, "serial_number": 0, "host": "other-host"},
[mock_response(ZERO_SERIAL_RESPONSE)],
{**CONFIG_ENTRY_DATA, "serial_number": 0},
),
],
ids=["with-serial", "zero-serial"],
)
async def test_multiple_config_entries(
hass: HomeAssistant,
setup_integration: ComponentSetup,
responses: list[AiohttpClientMockResponse],
config_flow_responses: list[AiohttpClientMockResponse],
expected_config_entry: dict[str, Any] | None,
) -> None:
"""Test setting up multiple config entries that refer to different devices."""
assert await setup_integration()
entries = hass.config_entries.async_entries(DOMAIN)
assert len(entries) == 1
assert entries[0].state == ConfigEntryState.LOADED
responses.clear()
responses.extend(config_flow_responses)
result = await complete_flow(hass)
assert result.get("type") == FlowResultType.CREATE_ENTRY
assert dict(result.get("result").data) == expected_config_entry
entries = hass.config_entries.async_entries(DOMAIN)
assert len(entries) == 2
@pytest.mark.parametrize(
(
"unique_id",
"config_entry_data",
"config_flow_responses",
),
[
(
SERIAL_NUMBER,
CONFIG_ENTRY_DATA,
[mock_response(SERIAL_RESPONSE)],
),
(
None,
{**CONFIG_ENTRY_DATA, "serial_number": 0},
[mock_response(ZERO_SERIAL_RESPONSE)],
),
],
ids=[
"duplicate-serial-number",
"duplicate-host-port-no-serial",
],
)
async def test_duplicate_config_entries(
hass: HomeAssistant,
setup_integration: ComponentSetup,
responses: list[AiohttpClientMockResponse],
config_flow_responses: list[AiohttpClientMockResponse],
) -> None:
"""Test that a device can not be registered twice."""
assert await setup_integration()
entries = hass.config_entries.async_entries(DOMAIN)
assert len(entries) == 1
assert entries[0].state == ConfigEntryState.LOADED
responses.clear()
responses.extend(config_flow_responses)
result = await complete_flow(hass)
assert result.get("type") == FlowResultType.ABORT
assert result.get("reason") == "already_configured"
async def test_controller_cannot_connect( async def test_controller_cannot_connect(
hass: HomeAssistant, hass: HomeAssistant,
mock_setup: Mock, mock_setup: Mock,