mirror of
https://github.com/home-assistant/core.git
synced 2026-11-06 05:32:45 -04:00
Fix Rainbird unique to use a more reliable source (mac address) (#101603)
* Fix rainbird unique id to use a mac address for new entries * Fix typo * Normalize mac address before using as unique id * Apply suggestions from code review Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Update test check and remove dead code * Update all config entries to the new format * Update config entry tests for migration * Fix rainbird entity unique ids * Add test coverage for repair failure * Apply suggestions from code review Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Apply suggestions from code review Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Remove unnecessary migration failure checks * Remove invalid config entries * Update entry when entering a different hostname for an existing host. --------- Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
co-authored by
Martin Hjelmare
parent
31ac03fe50
commit
2a26dea587
@@ -3,6 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from http import HTTPStatus
|
||||
import json
|
||||
from typing import Any
|
||||
from unittest.mock import patch
|
||||
|
||||
@@ -24,6 +25,8 @@ HOST = "example.com"
|
||||
URL = "http://example.com/stick"
|
||||
PASSWORD = "password"
|
||||
SERIAL_NUMBER = 0x12635436566
|
||||
MAC_ADDRESS = "4C:A1:61:00:11:22"
|
||||
MAC_ADDRESS_UNIQUE_ID = "4c:a1:61:00:11:22"
|
||||
|
||||
#
|
||||
# Response payloads below come from pyrainbird test cases.
|
||||
@@ -50,6 +53,20 @@ RAIN_DELAY = "B60010" # 0x10 is 16
|
||||
RAIN_DELAY_OFF = "B60000"
|
||||
# ACK command 0x10, Echo 0x06
|
||||
ACK_ECHO = "0106"
|
||||
WIFI_PARAMS_RESPONSE = {
|
||||
"macAddress": MAC_ADDRESS,
|
||||
"localIpAddress": "1.1.1.38",
|
||||
"localNetmask": "255.255.255.0",
|
||||
"localGateway": "1.1.1.1",
|
||||
"rssi": -61,
|
||||
"wifiSsid": "wifi-ssid-name",
|
||||
"wifiPassword": "wifi-password-name",
|
||||
"wifiSecurity": "wpa2-aes",
|
||||
"apTimeoutNoLan": 20,
|
||||
"apTimeoutIdle": 20,
|
||||
"apSecurity": "unknown",
|
||||
"stickVersion": "Rain Bird Stick Rev C/1.63",
|
||||
}
|
||||
|
||||
|
||||
CONFIG = {
|
||||
@@ -62,10 +79,16 @@ CONFIG = {
|
||||
}
|
||||
}
|
||||
|
||||
CONFIG_ENTRY_DATA_OLD_FORMAT = {
|
||||
"host": HOST,
|
||||
"password": PASSWORD,
|
||||
"serial_number": SERIAL_NUMBER,
|
||||
}
|
||||
CONFIG_ENTRY_DATA = {
|
||||
"host": HOST,
|
||||
"password": PASSWORD,
|
||||
"serial_number": SERIAL_NUMBER,
|
||||
"mac": MAC_ADDRESS,
|
||||
}
|
||||
|
||||
|
||||
@@ -77,14 +100,23 @@ def platforms() -> list[Platform]:
|
||||
|
||||
@pytest.fixture
|
||||
async def config_entry_unique_id() -> str:
|
||||
"""Fixture for serial number used in the config entry."""
|
||||
"""Fixture for config entry unique id."""
|
||||
return MAC_ADDRESS_UNIQUE_ID
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def serial_number() -> int:
|
||||
"""Fixture for serial number used in the config entry data."""
|
||||
return SERIAL_NUMBER
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def config_entry_data() -> dict[str, Any]:
|
||||
async def config_entry_data(serial_number: int) -> dict[str, Any]:
|
||||
"""Fixture for MockConfigEntry data."""
|
||||
return CONFIG_ENTRY_DATA
|
||||
return {
|
||||
**CONFIG_ENTRY_DATA,
|
||||
"serial_number": serial_number,
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -123,17 +155,24 @@ def setup_platforms(
|
||||
yield
|
||||
|
||||
|
||||
def rainbird_response(data: str) -> bytes:
|
||||
def rainbird_json_response(result: dict[str, str]) -> bytes:
|
||||
"""Create a fake API response."""
|
||||
return encryption.encrypt(
|
||||
'{"jsonrpc": "2.0", "result": {"data":"%s"}, "id": 1} ' % data,
|
||||
'{"jsonrpc": "2.0", "result": %s, "id": 1} ' % json.dumps(result),
|
||||
PASSWORD,
|
||||
)
|
||||
|
||||
|
||||
def mock_json_response(result: dict[str, str]) -> AiohttpClientMockResponse:
|
||||
"""Create a fake AiohttpClientMockResponse."""
|
||||
return AiohttpClientMockResponse(
|
||||
"POST", URL, response=rainbird_json_response(result)
|
||||
)
|
||||
|
||||
|
||||
def mock_response(data: str) -> AiohttpClientMockResponse:
|
||||
"""Create a fake AiohttpClientMockResponse."""
|
||||
return AiohttpClientMockResponse("POST", URL, response=rainbird_response(data))
|
||||
return mock_json_response({"data": data})
|
||||
|
||||
|
||||
def mock_response_error(
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
"""Tests for rainbird sensor platform."""
|
||||
|
||||
|
||||
from http import HTTPStatus
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
@@ -8,7 +10,12 @@ from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from .conftest import RAIN_SENSOR_OFF, RAIN_SENSOR_ON, SERIAL_NUMBER
|
||||
from .conftest import (
|
||||
CONFIG_ENTRY_DATA_OLD_FORMAT,
|
||||
RAIN_SENSOR_OFF,
|
||||
RAIN_SENSOR_ON,
|
||||
mock_response_error,
|
||||
)
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
from tests.test_util.aiohttp import AiohttpClientMockResponse
|
||||
@@ -51,47 +58,25 @@ async def test_rainsensor(
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("config_entry_unique_id", "entity_unique_id"),
|
||||
("config_entry_data", "config_entry_unique_id", "setup_config_entry"),
|
||||
[
|
||||
(SERIAL_NUMBER, "1263613994342-rainsensor"),
|
||||
# Some existing config entries may have a "0" serial number but preserve
|
||||
# their unique id
|
||||
(0, "0-rainsensor"),
|
||||
],
|
||||
)
|
||||
async def test_unique_id(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
entity_unique_id: str,
|
||||
) -> None:
|
||||
"""Test rainsensor binary sensor."""
|
||||
rainsensor = hass.states.get("binary_sensor.rain_bird_controller_rainsensor")
|
||||
assert rainsensor is not None
|
||||
assert rainsensor.attributes == {
|
||||
"friendly_name": "Rain Bird Controller Rainsensor",
|
||||
"icon": "mdi:water",
|
||||
}
|
||||
|
||||
entity_entry = entity_registry.async_get(
|
||||
"binary_sensor.rain_bird_controller_rainsensor"
|
||||
)
|
||||
assert entity_entry
|
||||
assert entity_entry.unique_id == entity_unique_id
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("config_entry_unique_id"),
|
||||
[
|
||||
(None),
|
||||
(CONFIG_ENTRY_DATA_OLD_FORMAT, None, None),
|
||||
],
|
||||
)
|
||||
async def test_no_unique_id(
|
||||
hass: HomeAssistant,
|
||||
responses: list[AiohttpClientMockResponse],
|
||||
entity_registry: er.EntityRegistry,
|
||||
config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test rainsensor binary sensor with no unique id."""
|
||||
|
||||
# Failure to migrate config entry to a unique id
|
||||
responses.insert(0, mock_response_error(HTTPStatus.SERVICE_UNAVAILABLE))
|
||||
|
||||
await config_entry.async_setup(hass)
|
||||
assert config_entry.state == ConfigEntryState.LOADED
|
||||
|
||||
rainsensor = hass.states.get("binary_sensor.rain_bird_controller_rainsensor")
|
||||
assert rainsensor is not None
|
||||
assert (
|
||||
|
||||
@@ -17,7 +17,7 @@ from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from .conftest import mock_response, mock_response_error
|
||||
from .conftest import CONFIG_ENTRY_DATA_OLD_FORMAT, mock_response, mock_response_error
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
from tests.test_util.aiohttp import AiohttpClientMockResponse
|
||||
@@ -210,7 +210,7 @@ async def test_event_state(
|
||||
|
||||
entity = entity_registry.async_get(TEST_ENTITY)
|
||||
assert entity
|
||||
assert entity.unique_id == 1263613994342
|
||||
assert entity.unique_id == "4c:a1:61:00:11:22"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -280,18 +280,26 @@ async def test_program_schedule_disabled(
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("config_entry_unique_id"),
|
||||
("config_entry_data", "config_entry_unique_id", "setup_config_entry"),
|
||||
[
|
||||
(None),
|
||||
(CONFIG_ENTRY_DATA_OLD_FORMAT, None, None),
|
||||
],
|
||||
)
|
||||
async def test_no_unique_id(
|
||||
hass: HomeAssistant,
|
||||
get_events: GetEventsFn,
|
||||
responses: list[AiohttpClientMockResponse],
|
||||
entity_registry: er.EntityRegistry,
|
||||
config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test calendar entity with no unique id."""
|
||||
|
||||
# Failure to migrate config entry to a unique id
|
||||
responses.insert(0, mock_response_error(HTTPStatus.SERVICE_UNAVAILABLE))
|
||||
|
||||
await config_entry.async_setup(hass)
|
||||
assert config_entry.state == ConfigEntryState.LOADED
|
||||
|
||||
state = hass.states.get(TEST_ENTITY)
|
||||
assert state is not None
|
||||
assert state.attributes.get("friendly_name") == "Rain Bird Controller"
|
||||
|
||||
@@ -19,11 +19,14 @@ from homeassistant.data_entry_flow import FlowResult, FlowResultType
|
||||
from .conftest import (
|
||||
CONFIG_ENTRY_DATA,
|
||||
HOST,
|
||||
MAC_ADDRESS_UNIQUE_ID,
|
||||
PASSWORD,
|
||||
SERIAL_NUMBER,
|
||||
SERIAL_RESPONSE,
|
||||
URL,
|
||||
WIFI_PARAMS_RESPONSE,
|
||||
ZERO_SERIAL_RESPONSE,
|
||||
mock_json_response,
|
||||
mock_response,
|
||||
)
|
||||
|
||||
@@ -34,7 +37,7 @@ from tests.test_util.aiohttp import AiohttpClientMocker, AiohttpClientMockRespon
|
||||
@pytest.fixture(name="responses")
|
||||
def mock_responses() -> list[AiohttpClientMockResponse]:
|
||||
"""Set up fake serial number response when testing the connection."""
|
||||
return [mock_response(SERIAL_RESPONSE)]
|
||||
return [mock_response(SERIAL_RESPONSE), mock_json_response(WIFI_PARAMS_RESPONSE)]
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
@@ -74,14 +77,20 @@ async def complete_flow(hass: HomeAssistant) -> FlowResult:
|
||||
("responses", "expected_config_entry", "expected_unique_id"),
|
||||
[
|
||||
(
|
||||
[mock_response(SERIAL_RESPONSE)],
|
||||
[
|
||||
mock_response(SERIAL_RESPONSE),
|
||||
mock_json_response(WIFI_PARAMS_RESPONSE),
|
||||
],
|
||||
CONFIG_ENTRY_DATA,
|
||||
SERIAL_NUMBER,
|
||||
MAC_ADDRESS_UNIQUE_ID,
|
||||
),
|
||||
(
|
||||
[mock_response(ZERO_SERIAL_RESPONSE)],
|
||||
[
|
||||
mock_response(ZERO_SERIAL_RESPONSE),
|
||||
mock_json_response(WIFI_PARAMS_RESPONSE),
|
||||
],
|
||||
{**CONFIG_ENTRY_DATA, "serial_number": 0},
|
||||
None,
|
||||
MAC_ADDRESS_UNIQUE_ID,
|
||||
),
|
||||
],
|
||||
)
|
||||
@@ -115,17 +124,32 @@ async def test_controller_flow(
|
||||
(
|
||||
"other-serial-number",
|
||||
{**CONFIG_ENTRY_DATA, "host": "other-host"},
|
||||
[mock_response(SERIAL_RESPONSE)],
|
||||
[mock_response(SERIAL_RESPONSE), mock_json_response(WIFI_PARAMS_RESPONSE)],
|
||||
CONFIG_ENTRY_DATA,
|
||||
),
|
||||
(
|
||||
"11:22:33:44:55:66",
|
||||
{
|
||||
**CONFIG_ENTRY_DATA,
|
||||
"host": "other-host",
|
||||
},
|
||||
[
|
||||
mock_response(SERIAL_RESPONSE),
|
||||
mock_json_response(WIFI_PARAMS_RESPONSE),
|
||||
],
|
||||
CONFIG_ENTRY_DATA,
|
||||
),
|
||||
(
|
||||
None,
|
||||
{**CONFIG_ENTRY_DATA, "serial_number": 0, "host": "other-host"},
|
||||
[mock_response(ZERO_SERIAL_RESPONSE)],
|
||||
[
|
||||
mock_response(ZERO_SERIAL_RESPONSE),
|
||||
mock_json_response(WIFI_PARAMS_RESPONSE),
|
||||
],
|
||||
{**CONFIG_ENTRY_DATA, "serial_number": 0},
|
||||
),
|
||||
],
|
||||
ids=["with-serial", "zero-serial"],
|
||||
ids=["with-serial", "with-mac-address", "zero-serial"],
|
||||
)
|
||||
async def test_multiple_config_entries(
|
||||
hass: HomeAssistant,
|
||||
@@ -154,22 +178,52 @@ async def test_multiple_config_entries(
|
||||
"config_entry_unique_id",
|
||||
"config_entry_data",
|
||||
"config_flow_responses",
|
||||
"expected_config_entry_data",
|
||||
),
|
||||
[
|
||||
# Config entry is a pure duplicate with the same mac address unique id
|
||||
(
|
||||
MAC_ADDRESS_UNIQUE_ID,
|
||||
CONFIG_ENTRY_DATA,
|
||||
[
|
||||
mock_response(SERIAL_RESPONSE),
|
||||
mock_json_response(WIFI_PARAMS_RESPONSE),
|
||||
],
|
||||
CONFIG_ENTRY_DATA,
|
||||
),
|
||||
# Old unique id with serial, but same host
|
||||
(
|
||||
SERIAL_NUMBER,
|
||||
CONFIG_ENTRY_DATA,
|
||||
[mock_response(SERIAL_RESPONSE)],
|
||||
[mock_response(SERIAL_RESPONSE), mock_json_response(WIFI_PARAMS_RESPONSE)],
|
||||
CONFIG_ENTRY_DATA,
|
||||
),
|
||||
# Old unique id with no serial, but same host
|
||||
(
|
||||
None,
|
||||
{**CONFIG_ENTRY_DATA, "serial_number": 0},
|
||||
[mock_response(ZERO_SERIAL_RESPONSE)],
|
||||
[
|
||||
mock_response(ZERO_SERIAL_RESPONSE),
|
||||
mock_json_response(WIFI_PARAMS_RESPONSE),
|
||||
],
|
||||
{**CONFIG_ENTRY_DATA, "serial_number": 0},
|
||||
),
|
||||
# Enters a different hostname that points to the same mac address
|
||||
(
|
||||
MAC_ADDRESS_UNIQUE_ID,
|
||||
{
|
||||
**CONFIG_ENTRY_DATA,
|
||||
"host": f"other-{HOST}",
|
||||
},
|
||||
[mock_response(SERIAL_RESPONSE), mock_json_response(WIFI_PARAMS_RESPONSE)],
|
||||
CONFIG_ENTRY_DATA, # Updated the host
|
||||
),
|
||||
],
|
||||
ids=[
|
||||
"duplicate-serial-number",
|
||||
"duplicate-mac-unique-id",
|
||||
"duplicate-host-legacy-serial-number",
|
||||
"duplicate-host-port-no-serial",
|
||||
"duplicate-duplicate-hostname",
|
||||
],
|
||||
)
|
||||
async def test_duplicate_config_entries(
|
||||
@@ -177,6 +231,7 @@ async def test_duplicate_config_entries(
|
||||
config_entry: MockConfigEntry,
|
||||
responses: list[AiohttpClientMockResponse],
|
||||
config_flow_responses: list[AiohttpClientMockResponse],
|
||||
expected_config_entry_data: dict[str, Any],
|
||||
) -> None:
|
||||
"""Test that a device can not be registered twice."""
|
||||
await config_entry.async_setup(hass)
|
||||
@@ -186,8 +241,10 @@ async def test_duplicate_config_entries(
|
||||
responses.extend(config_flow_responses)
|
||||
|
||||
result = await complete_flow(hass)
|
||||
assert len(hass.config_entries.async_entries(DOMAIN)) == 1
|
||||
assert result.get("type") == FlowResultType.ABORT
|
||||
assert result.get("reason") == "already_configured"
|
||||
assert dict(config_entry.data) == expected_config_entry_data
|
||||
|
||||
|
||||
async def test_controller_cannot_connect(
|
||||
|
||||
@@ -6,12 +6,21 @@ from http import HTTPStatus
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.rainbird.const import DOMAIN
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.const import CONF_MAC
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from .conftest import (
|
||||
CONFIG_ENTRY_DATA,
|
||||
CONFIG_ENTRY_DATA_OLD_FORMAT,
|
||||
MAC_ADDRESS,
|
||||
MAC_ADDRESS_UNIQUE_ID,
|
||||
MODEL_AND_VERSION_RESPONSE,
|
||||
SERIAL_NUMBER,
|
||||
WIFI_PARAMS_RESPONSE,
|
||||
mock_json_response,
|
||||
mock_response,
|
||||
mock_response_error,
|
||||
)
|
||||
@@ -20,22 +29,11 @@ from tests.common import MockConfigEntry
|
||||
from tests.test_util.aiohttp import AiohttpClientMockResponse
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("config_entry_data", "initial_response"),
|
||||
[
|
||||
(CONFIG_ENTRY_DATA, None),
|
||||
],
|
||||
ids=["config_entry"],
|
||||
)
|
||||
async def test_init_success(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
responses: list[AiohttpClientMockResponse],
|
||||
initial_response: AiohttpClientMockResponse | None,
|
||||
) -> None:
|
||||
"""Test successful setup and unload."""
|
||||
if initial_response:
|
||||
responses.insert(0, initial_response)
|
||||
|
||||
await config_entry.async_setup(hass)
|
||||
assert config_entry.state == ConfigEntryState.LOADED
|
||||
@@ -88,6 +86,196 @@ async def test_communication_failure(
|
||||
config_entry_state: list[ConfigEntryState],
|
||||
) -> None:
|
||||
"""Test unable to talk to device on startup, which fails setup."""
|
||||
|
||||
await config_entry.async_setup(hass)
|
||||
assert config_entry.state == config_entry_state
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("config_entry_unique_id", "config_entry_data"),
|
||||
[
|
||||
(
|
||||
None,
|
||||
{**CONFIG_ENTRY_DATA, "mac": None},
|
||||
),
|
||||
],
|
||||
ids=["config_entry"],
|
||||
)
|
||||
async def test_fix_unique_id(
|
||||
hass: HomeAssistant,
|
||||
responses: list[AiohttpClientMockResponse],
|
||||
config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test fix of a config entry with no unique id."""
|
||||
|
||||
responses.insert(0, mock_json_response(WIFI_PARAMS_RESPONSE))
|
||||
|
||||
entries = hass.config_entries.async_entries(DOMAIN)
|
||||
assert len(entries) == 1
|
||||
assert entries[0].state == ConfigEntryState.NOT_LOADED
|
||||
assert entries[0].unique_id is None
|
||||
assert entries[0].data.get(CONF_MAC) is None
|
||||
|
||||
await config_entry.async_setup(hass)
|
||||
assert config_entry.state == ConfigEntryState.LOADED
|
||||
|
||||
# Verify config entry now has a unique id
|
||||
entries = hass.config_entries.async_entries(DOMAIN)
|
||||
assert len(entries) == 1
|
||||
assert entries[0].state == ConfigEntryState.LOADED
|
||||
assert entries[0].unique_id == MAC_ADDRESS_UNIQUE_ID
|
||||
assert entries[0].data.get(CONF_MAC) == MAC_ADDRESS
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
(
|
||||
"config_entry_unique_id",
|
||||
"config_entry_data",
|
||||
"initial_response",
|
||||
"expected_warning",
|
||||
),
|
||||
[
|
||||
(
|
||||
None,
|
||||
CONFIG_ENTRY_DATA_OLD_FORMAT,
|
||||
mock_response_error(HTTPStatus.SERVICE_UNAVAILABLE),
|
||||
"Unable to fix missing unique id:",
|
||||
),
|
||||
(
|
||||
None,
|
||||
CONFIG_ENTRY_DATA_OLD_FORMAT,
|
||||
mock_response_error(HTTPStatus.NOT_FOUND),
|
||||
"Unable to fix missing unique id:",
|
||||
),
|
||||
(
|
||||
None,
|
||||
CONFIG_ENTRY_DATA_OLD_FORMAT,
|
||||
mock_response("bogus"),
|
||||
"Unable to fix missing unique id (mac address was None)",
|
||||
),
|
||||
],
|
||||
ids=["service_unavailable", "not_found", "unexpected_response_format"],
|
||||
)
|
||||
async def test_fix_unique_id_failure(
|
||||
hass: HomeAssistant,
|
||||
initial_response: AiohttpClientMockResponse,
|
||||
responses: list[AiohttpClientMockResponse],
|
||||
expected_warning: str,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test a failure during fix of a config entry with no unique id."""
|
||||
|
||||
responses.insert(0, initial_response)
|
||||
|
||||
await config_entry.async_setup(hass)
|
||||
# Config entry is loaded, but not updated
|
||||
assert config_entry.state == ConfigEntryState.LOADED
|
||||
assert config_entry.unique_id is None
|
||||
|
||||
assert expected_warning in caplog.text
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("config_entry_unique_id"),
|
||||
[(MAC_ADDRESS_UNIQUE_ID)],
|
||||
)
|
||||
async def test_fix_unique_id_duplicate(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
responses: list[AiohttpClientMockResponse],
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test that a config entry unique id already exists during fix."""
|
||||
# Add a second config entry that has no unique id, but has the same
|
||||
# mac address. When fixing the unique id, it can't use the mac address
|
||||
# since it already exists.
|
||||
other_entry = MockConfigEntry(
|
||||
unique_id=None,
|
||||
domain=DOMAIN,
|
||||
data=CONFIG_ENTRY_DATA_OLD_FORMAT,
|
||||
)
|
||||
other_entry.add_to_hass(hass)
|
||||
|
||||
# Responses for the second config entry. This first fetches wifi params
|
||||
# to repair the unique id.
|
||||
responses_copy = [*responses]
|
||||
responses.append(mock_json_response(WIFI_PARAMS_RESPONSE))
|
||||
responses.extend(responses_copy)
|
||||
|
||||
await config_entry.async_setup(hass)
|
||||
assert config_entry.state == ConfigEntryState.LOADED
|
||||
assert config_entry.unique_id == MAC_ADDRESS_UNIQUE_ID
|
||||
|
||||
await other_entry.async_setup(hass)
|
||||
# Config entry unique id could not be updated since it already exists
|
||||
assert other_entry.state == ConfigEntryState.SETUP_ERROR
|
||||
|
||||
assert "Unable to fix missing unique id (already exists)" in caplog.text
|
||||
|
||||
await hass.async_block_till_done()
|
||||
assert len(hass.config_entries.async_entries(DOMAIN)) == 1
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
(
|
||||
"config_entry_unique_id",
|
||||
"serial_number",
|
||||
"entity_unique_id",
|
||||
"expected_unique_id",
|
||||
),
|
||||
[
|
||||
(SERIAL_NUMBER, SERIAL_NUMBER, SERIAL_NUMBER, MAC_ADDRESS_UNIQUE_ID),
|
||||
(
|
||||
SERIAL_NUMBER,
|
||||
SERIAL_NUMBER,
|
||||
f"{SERIAL_NUMBER}-rain-delay",
|
||||
f"{MAC_ADDRESS_UNIQUE_ID}-rain-delay",
|
||||
),
|
||||
("0", 0, "0", MAC_ADDRESS_UNIQUE_ID),
|
||||
(
|
||||
"0",
|
||||
0,
|
||||
"0-rain-delay",
|
||||
f"{MAC_ADDRESS_UNIQUE_ID}-rain-delay",
|
||||
),
|
||||
(
|
||||
MAC_ADDRESS_UNIQUE_ID,
|
||||
SERIAL_NUMBER,
|
||||
MAC_ADDRESS_UNIQUE_ID,
|
||||
MAC_ADDRESS_UNIQUE_ID,
|
||||
),
|
||||
(
|
||||
MAC_ADDRESS_UNIQUE_ID,
|
||||
SERIAL_NUMBER,
|
||||
f"{MAC_ADDRESS_UNIQUE_ID}-rain-delay",
|
||||
f"{MAC_ADDRESS_UNIQUE_ID}-rain-delay",
|
||||
),
|
||||
],
|
||||
ids=(
|
||||
"serial-number",
|
||||
"serial-number-with-suffix",
|
||||
"zero-serial",
|
||||
"zero-serial-suffix",
|
||||
"new-format",
|
||||
"new-format-suffx",
|
||||
),
|
||||
)
|
||||
async def test_fix_entity_unique_ids(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
entity_unique_id: str,
|
||||
expected_unique_id: str,
|
||||
) -> None:
|
||||
"""Test fixing entity unique ids from old unique id formats."""
|
||||
|
||||
entity_registry = er.async_get(hass)
|
||||
entity_entry = entity_registry.async_get_or_create(
|
||||
DOMAIN, "number", unique_id=entity_unique_id, config_entry=config_entry
|
||||
)
|
||||
|
||||
await config_entry.async_setup(hass)
|
||||
assert config_entry.state == ConfigEntryState.LOADED
|
||||
|
||||
entity_entry = entity_registry.async_get(entity_entry.id)
|
||||
assert entity_entry
|
||||
assert entity_entry.unique_id == expected_unique_id
|
||||
|
||||
@@ -6,7 +6,7 @@ import pytest
|
||||
|
||||
from homeassistant.components import number
|
||||
from homeassistant.components.rainbird import DOMAIN
|
||||
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.const import ATTR_ENTITY_ID, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
@@ -14,15 +14,16 @@ from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
|
||||
from .conftest import (
|
||||
ACK_ECHO,
|
||||
CONFIG_ENTRY_DATA_OLD_FORMAT,
|
||||
MAC_ADDRESS,
|
||||
RAIN_DELAY,
|
||||
RAIN_DELAY_OFF,
|
||||
SERIAL_NUMBER,
|
||||
mock_response,
|
||||
mock_response_error,
|
||||
)
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
from tests.test_util.aiohttp import AiohttpClientMocker
|
||||
from tests.test_util.aiohttp import AiohttpClientMocker, AiohttpClientMockResponse
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -66,46 +67,23 @@ async def test_number_values(
|
||||
|
||||
entity_entry = entity_registry.async_get("number.rain_bird_controller_rain_delay")
|
||||
assert entity_entry
|
||||
assert entity_entry.unique_id == "1263613994342-rain-delay"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("config_entry_unique_id", "entity_unique_id"),
|
||||
[
|
||||
(SERIAL_NUMBER, "1263613994342-rain-delay"),
|
||||
# Some existing config entries may have a "0" serial number but preserve
|
||||
# their unique id
|
||||
(0, "0-rain-delay"),
|
||||
],
|
||||
)
|
||||
async def test_unique_id(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
entity_unique_id: str,
|
||||
) -> None:
|
||||
"""Test number platform."""
|
||||
|
||||
raindelay = hass.states.get("number.rain_bird_controller_rain_delay")
|
||||
assert raindelay is not None
|
||||
assert (
|
||||
raindelay.attributes.get("friendly_name") == "Rain Bird Controller Rain delay"
|
||||
)
|
||||
|
||||
entity_entry = entity_registry.async_get("number.rain_bird_controller_rain_delay")
|
||||
assert entity_entry
|
||||
assert entity_entry.unique_id == entity_unique_id
|
||||
assert entity_entry.unique_id == "4c:a1:61:00:11:22-rain-delay"
|
||||
|
||||
|
||||
async def test_set_value(
|
||||
hass: HomeAssistant,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
responses: list[str],
|
||||
config_entry: ConfigEntry,
|
||||
) -> None:
|
||||
"""Test setting the rain delay number."""
|
||||
|
||||
raindelay = hass.states.get("number.rain_bird_controller_rain_delay")
|
||||
assert raindelay is not None
|
||||
|
||||
device_registry = dr.async_get(hass)
|
||||
device = device_registry.async_get_device(identifiers={(DOMAIN, SERIAL_NUMBER)})
|
||||
device = device_registry.async_get_device(
|
||||
identifiers={(DOMAIN, MAC_ADDRESS.lower())}
|
||||
)
|
||||
assert device
|
||||
assert device.name == "Rain Bird Controller"
|
||||
assert device.model == "ESP-TM2"
|
||||
@@ -138,7 +116,6 @@ async def test_set_value_error(
|
||||
hass: HomeAssistant,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
responses: list[str],
|
||||
config_entry: ConfigEntry,
|
||||
status: HTTPStatus,
|
||||
expected_msg: str,
|
||||
) -> None:
|
||||
@@ -162,17 +139,25 @@ async def test_set_value_error(
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("config_entry_unique_id"),
|
||||
("config_entry_data", "config_entry_unique_id", "setup_config_entry"),
|
||||
[
|
||||
(None),
|
||||
(CONFIG_ENTRY_DATA_OLD_FORMAT, None, None),
|
||||
],
|
||||
)
|
||||
async def test_no_unique_id(
|
||||
hass: HomeAssistant,
|
||||
responses: list[AiohttpClientMockResponse],
|
||||
entity_registry: er.EntityRegistry,
|
||||
config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test number platform with no unique id."""
|
||||
|
||||
# Failure to migrate config entry to a unique id
|
||||
responses.insert(0, mock_response_error(HTTPStatus.SERVICE_UNAVAILABLE))
|
||||
|
||||
await config_entry.async_setup(hass)
|
||||
assert config_entry.state == ConfigEntryState.LOADED
|
||||
|
||||
raindelay = hass.states.get("number.rain_bird_controller_rain_delay")
|
||||
assert raindelay is not None
|
||||
assert (
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""Tests for rainbird sensor platform."""
|
||||
|
||||
from http import HTTPStatus
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -8,9 +9,15 @@ from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from .conftest import CONFIG_ENTRY_DATA, RAIN_DELAY, RAIN_DELAY_OFF
|
||||
from .conftest import (
|
||||
CONFIG_ENTRY_DATA_OLD_FORMAT,
|
||||
RAIN_DELAY,
|
||||
RAIN_DELAY_OFF,
|
||||
mock_response_error,
|
||||
)
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
from tests.test_util.aiohttp import AiohttpClientMockResponse
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -49,37 +56,38 @@ async def test_sensors(
|
||||
|
||||
entity_entry = entity_registry.async_get("sensor.rain_bird_controller_raindelay")
|
||||
assert entity_entry
|
||||
assert entity_entry.unique_id == "1263613994342-raindelay"
|
||||
assert entity_entry.unique_id == "4c:a1:61:00:11:22-raindelay"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("config_entry_unique_id", "config_entry_data"),
|
||||
("config_entry_unique_id", "config_entry_data", "setup_config_entry"),
|
||||
[
|
||||
# Config entry setup without a unique id since it had no serial number
|
||||
(
|
||||
None,
|
||||
{
|
||||
**CONFIG_ENTRY_DATA,
|
||||
"serial_number": 0,
|
||||
},
|
||||
),
|
||||
# Legacy case for old config entries with serial number 0 preserves old behavior
|
||||
(
|
||||
"0",
|
||||
{
|
||||
**CONFIG_ENTRY_DATA,
|
||||
**CONFIG_ENTRY_DATA_OLD_FORMAT,
|
||||
"serial_number": 0,
|
||||
},
|
||||
None,
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_sensor_no_unique_id(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
responses: list[AiohttpClientMockResponse],
|
||||
config_entry_unique_id: str | None,
|
||||
config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test sensor platform with no unique id."""
|
||||
|
||||
# Failure to migrate config entry to a unique id
|
||||
responses.insert(0, mock_response_error(HTTPStatus.SERVICE_UNAVAILABLE))
|
||||
|
||||
await config_entry.async_setup(hass)
|
||||
assert config_entry.state == ConfigEntryState.LOADED
|
||||
|
||||
raindelay = hass.states.get("sensor.rain_bird_controller_raindelay")
|
||||
assert raindelay is not None
|
||||
assert raindelay.attributes.get("friendly_name") == "Rain Bird Controller Raindelay"
|
||||
|
||||
@@ -13,12 +13,13 @@ from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from .conftest import (
|
||||
ACK_ECHO,
|
||||
CONFIG_ENTRY_DATA_OLD_FORMAT,
|
||||
EMPTY_STATIONS_RESPONSE,
|
||||
HOST,
|
||||
MAC_ADDRESS,
|
||||
PASSWORD,
|
||||
RAIN_DELAY_OFF,
|
||||
RAIN_SENSOR_OFF,
|
||||
SERIAL_NUMBER,
|
||||
ZONE_3_ON_RESPONSE,
|
||||
ZONE_5_ON_RESPONSE,
|
||||
ZONE_OFF_RESPONSE,
|
||||
@@ -109,7 +110,7 @@ async def test_zones(
|
||||
|
||||
# Verify unique id for one of the switches
|
||||
entity_entry = entity_registry.async_get("switch.rain_bird_sprinkler_3")
|
||||
assert entity_entry.unique_id == "1263613994342-3"
|
||||
assert entity_entry.unique_id == "4c:a1:61:00:11:22-3"
|
||||
|
||||
|
||||
async def test_switch_on(
|
||||
@@ -226,6 +227,7 @@ async def test_irrigation_service(
|
||||
"1": "Garden Sprinkler",
|
||||
"2": "Back Yard",
|
||||
},
|
||||
"mac": MAC_ADDRESS,
|
||||
}
|
||||
)
|
||||
],
|
||||
@@ -274,9 +276,9 @@ async def test_switch_error(
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("config_entry_unique_id"),
|
||||
("config_entry_data", "config_entry_unique_id", "setup_config_entry"),
|
||||
[
|
||||
(None),
|
||||
(CONFIG_ENTRY_DATA_OLD_FORMAT, None, None),
|
||||
],
|
||||
)
|
||||
async def test_no_unique_id(
|
||||
@@ -284,8 +286,15 @@ async def test_no_unique_id(
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
responses: list[AiohttpClientMockResponse],
|
||||
entity_registry: er.EntityRegistry,
|
||||
config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test an irrigation switch with no unique id."""
|
||||
"""Test an irrigation switch with no unique id due to migration failure."""
|
||||
|
||||
# Failure to migrate config entry to a unique id
|
||||
responses.insert(0, mock_response_error(HTTPStatus.SERVICE_UNAVAILABLE))
|
||||
|
||||
await config_entry.async_setup(hass)
|
||||
assert config_entry.state == ConfigEntryState.LOADED
|
||||
|
||||
zone = hass.states.get("switch.rain_bird_sprinkler_3")
|
||||
assert zone is not None
|
||||
@@ -294,31 +303,3 @@ async def test_no_unique_id(
|
||||
|
||||
entity_entry = entity_registry.async_get("switch.rain_bird_sprinkler_3")
|
||||
assert entity_entry is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("config_entry_unique_id", "entity_unique_id"),
|
||||
[
|
||||
(SERIAL_NUMBER, "1263613994342-3"),
|
||||
# Some existing config entries may have a "0" serial number but preserve
|
||||
# their unique id
|
||||
(0, "0-3"),
|
||||
],
|
||||
)
|
||||
async def test_has_unique_id(
|
||||
hass: HomeAssistant,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
responses: list[AiohttpClientMockResponse],
|
||||
entity_registry: er.EntityRegistry,
|
||||
entity_unique_id: str,
|
||||
) -> None:
|
||||
"""Test an irrigation switch with no unique id."""
|
||||
|
||||
zone = hass.states.get("switch.rain_bird_sprinkler_3")
|
||||
assert zone is not None
|
||||
assert zone.attributes.get("friendly_name") == "Rain Bird Sprinkler 3"
|
||||
assert zone.state == "off"
|
||||
|
||||
entity_entry = entity_registry.async_get("switch.rain_bird_sprinkler_3")
|
||||
assert entity_entry
|
||||
assert entity_entry.unique_id == entity_unique_id
|
||||
|
||||
Reference in New Issue
Block a user