"""Tests for rainbird initialization.""" from __future__ import annotations from http import HTTPStatus from typing import Any 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 device_registry as dr, 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, ) from tests.common import MockConfigEntry from tests.test_util.aiohttp import AiohttpClientMockResponse async def test_init_success( hass: HomeAssistant, config_entry: MockConfigEntry, ) -> None: """Test successful setup and unload.""" await hass.config_entries.async_setup(config_entry.entry_id) assert config_entry.state is ConfigEntryState.LOADED await hass.config_entries.async_unload(config_entry.entry_id) await hass.async_block_till_done() assert config_entry.state is ConfigEntryState.NOT_LOADED @pytest.mark.parametrize( ("config_entry_data", "responses", "config_entry_state"), [ ( CONFIG_ENTRY_DATA, [mock_response_error(HTTPStatus.SERVICE_UNAVAILABLE)], ConfigEntryState.SETUP_RETRY, ), ( CONFIG_ENTRY_DATA, [mock_response_error(HTTPStatus.INTERNAL_SERVER_ERROR)], ConfigEntryState.SETUP_RETRY, ), ( CONFIG_ENTRY_DATA, [ mock_response(MODEL_AND_VERSION_RESPONSE), mock_response_error(HTTPStatus.SERVICE_UNAVAILABLE), ], ConfigEntryState.SETUP_RETRY, ), ( CONFIG_ENTRY_DATA, [ mock_response(MODEL_AND_VERSION_RESPONSE), mock_response_error(HTTPStatus.INTERNAL_SERVER_ERROR), ], ConfigEntryState.SETUP_RETRY, ), ], ids=[ "unavailable", "server-error", "coordinator-unavailable", "coordinator-server-error", ], ) async def test_communication_failure( hass: HomeAssistant, config_entry: MockConfigEntry, config_entry_state: list[ConfigEntryState], ) -> None: """Test unable to talk to device on startup, which fails setup.""" await hass.config_entries.async_setup(config_entry.entry_id) 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 is ConfigEntryState.NOT_LOADED assert entries[0].unique_id is None assert entries[0].data.get(CONF_MAC) is None await hass.config_entries.async_setup(config_entry.entry_id) assert config_entry.state is 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 is 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 hass.config_entries.async_setup(config_entry.entry_id) # Config entry is loaded, but not updated assert config_entry.state is 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 hass.config_entries.async_setup(config_entry.entry_id) assert config_entry.state is ConfigEntryState.LOADED assert config_entry.unique_id == MAC_ADDRESS_UNIQUE_ID 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", "device_identifier", "expected_unique_id", "expected_device_identifier", ), [ ( SERIAL_NUMBER, SERIAL_NUMBER, SERIAL_NUMBER, str(SERIAL_NUMBER), MAC_ADDRESS_UNIQUE_ID, MAC_ADDRESS_UNIQUE_ID, ), ( SERIAL_NUMBER, SERIAL_NUMBER, f"{SERIAL_NUMBER}-rain-delay", f"{SERIAL_NUMBER}-1", f"{MAC_ADDRESS_UNIQUE_ID}-rain-delay", f"{MAC_ADDRESS_UNIQUE_ID}-1", ), ( SERIAL_NUMBER, SERIAL_NUMBER, SERIAL_NUMBER, SERIAL_NUMBER, MAC_ADDRESS_UNIQUE_ID, MAC_ADDRESS_UNIQUE_ID, ), ("0", 0, "0", "0", MAC_ADDRESS_UNIQUE_ID, MAC_ADDRESS_UNIQUE_ID), ( "0", 0, "0-rain-delay", "0-1", f"{MAC_ADDRESS_UNIQUE_ID}-rain-delay", f"{MAC_ADDRESS_UNIQUE_ID}-1", ), ( MAC_ADDRESS_UNIQUE_ID, SERIAL_NUMBER, MAC_ADDRESS_UNIQUE_ID, MAC_ADDRESS_UNIQUE_ID, 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}-1", f"{MAC_ADDRESS_UNIQUE_ID}-rain-delay", f"{MAC_ADDRESS_UNIQUE_ID}-1", ), ], ids=( "serial-number", "serial-number-with-suffix", "serial-number-int", "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, device_identifier: str, expected_unique_id: str, expected_device_identifier: str, entity_registry: er.EntityRegistry, device_registry: dr.DeviceRegistry, ) -> None: """Test fixing entity unique ids from old unique id formats.""" entity_entry = entity_registry.async_get_or_create( DOMAIN, "number", unique_id=entity_unique_id, config_entry=config_entry ) device_entry = device_registry.async_get_or_create( identifiers={(DOMAIN, device_identifier)}, config_entry_id=config_entry.entry_id, serial_number=config_entry.data["serial_number"], ) await hass.config_entries.async_setup(config_entry.entry_id) assert config_entry.state is ConfigEntryState.LOADED entity_entry = entity_registry.async_get(entity_entry.id) assert entity_entry assert entity_entry.unique_id == expected_unique_id device_entry = device_registry.async_get_device( {(DOMAIN, expected_device_identifier)} ) assert device_entry assert device_entry.identifiers == {(DOMAIN, expected_device_identifier)} @pytest.mark.parametrize( ( "entry1_updates", "entry2_updates", "expected_device_name", "expected_disabled_by", ), [ ({}, {}, None, None), ( { "name_by_user": "Front Sprinkler", }, {}, "Front Sprinkler", None, ), ( {}, { "name_by_user": "Front Sprinkler", }, "Front Sprinkler", None, ), ( { "name_by_user": "Sprinkler 1", }, { "name_by_user": "Sprinkler 2", }, "Sprinkler 2", None, ), ( { "disabled_by": dr.DeviceEntryDisabler.USER, }, {}, None, None, ), ( {}, { "disabled_by": dr.DeviceEntryDisabler.USER, }, None, None, ), ( { "disabled_by": dr.DeviceEntryDisabler.USER, }, { "disabled_by": dr.DeviceEntryDisabler.USER, }, None, dr.DeviceEntryDisabler.USER, ), ], ids=[ "duplicates", "prefer-old-name", "prefer-new-name", "both-names-prefers-new", "old-disabled-prefer-new", "new-disabled-prefer-old", "both-disabled", ], ) async def test_fix_duplicate_device_ids( hass: HomeAssistant, config_entry: MockConfigEntry, device_registry: dr.DeviceRegistry, entry1_updates: dict[str, Any], entry2_updates: dict[str, Any], expected_device_name: str | None, expected_disabled_by: dr.DeviceEntryDisabler | None, ) -> None: """Test fixing duplicate device ids.""" entry1 = device_registry.async_get_or_create( identifiers={(DOMAIN, str(SERIAL_NUMBER))}, config_entry_id=config_entry.entry_id, serial_number=config_entry.data["serial_number"], ) device_registry.async_update_device(entry1.id, **entry1_updates) entry2 = device_registry.async_get_or_create( identifiers={(DOMAIN, MAC_ADDRESS_UNIQUE_ID)}, config_entry_id=config_entry.entry_id, serial_number=config_entry.data["serial_number"], ) device_registry.async_update_device(entry2.id, **entry2_updates) device_entries = dr.async_entries_for_config_entry( device_registry, config_entry.entry_id ) assert len(device_entries) == 2 await hass.config_entries.async_setup(config_entry.entry_id) assert config_entry.state is ConfigEntryState.LOADED # Only the device with the new format exists device_entries = dr.async_entries_for_config_entry( device_registry, config_entry.entry_id ) assert len(device_entries) == 1 device_entry = device_registry.async_get_device({(DOMAIN, MAC_ADDRESS_UNIQUE_ID)}) assert device_entry assert device_entry.identifiers == {(DOMAIN, MAC_ADDRESS_UNIQUE_ID)} assert device_entry.name_by_user == expected_device_name assert device_entry.disabled_by == expected_disabled_by