Fix rainbird entity unique ids (#101168)

* Fix unique ids for rainbird entities

* Update entity unique id use based on config entry entity id

* Update tests/components/rainbird/test_binary_sensor.py

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* Rename all entity_registry variables

* Shorten long comment under line length limits

---------

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
Allen Porter 2023-10-01 08:12:44 -07:00 committed by GitHub
parent b3b5ca9b95
commit 2d58ab0e1c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 237 additions and 38 deletions

View File

@ -48,8 +48,11 @@ class RainBirdSensor(CoordinatorEntity[RainbirdUpdateCoordinator], BinarySensorE
"""Initialize the Rain Bird sensor."""
super().__init__(coordinator)
self.entity_description = description
self._attr_unique_id = f"{coordinator.serial_number}-{description.key}"
self._attr_device_info = coordinator.device_info
if coordinator.unique_id:
self._attr_unique_id = f"{coordinator.unique_id}-{description.key}"
self._attr_device_info = coordinator.device_info
else:
self._attr_name = f"{coordinator.device_name} Rainsensor"
@property
def is_on(self) -> bool | None:

View File

@ -34,8 +34,9 @@ async def async_setup_entry(
[
RainBirdCalendarEntity(
data.schedule_coordinator,
data.coordinator.serial_number,
data.coordinator.unique_id,
data.coordinator.device_info,
data.coordinator.device_name,
)
]
)
@ -47,20 +48,24 @@ class RainBirdCalendarEntity(
"""A calendar event entity."""
_attr_has_entity_name = True
_attr_name = None
_attr_name: str | None = None
_attr_icon = "mdi:sprinkler"
def __init__(
self,
coordinator: RainbirdScheduleUpdateCoordinator,
serial_number: str,
device_info: DeviceInfo,
unique_id: str | None,
device_info: DeviceInfo | None,
device_name: str,
) -> None:
"""Create the Calendar event device."""
super().__init__(coordinator)
self._event: CalendarEvent | None = None
self._attr_unique_id = serial_number
self._attr_device_info = device_info
if unique_id:
self._attr_unique_id = unique_id
self._attr_device_info = device_info
else:
self._attr_name = device_name
@property
def event(self) -> CalendarEvent | None:

View File

@ -21,7 +21,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import CONF_SERIAL_NUMBER, DOMAIN, MANUFACTURER, TIMEOUT_SECONDS
from .const import DOMAIN, MANUFACTURER, TIMEOUT_SECONDS
UPDATE_INTERVAL = datetime.timedelta(minutes=1)
# The calendar data requires RPCs for each program/zone, and the data rarely
@ -51,7 +51,7 @@ class RainbirdUpdateCoordinator(DataUpdateCoordinator[RainbirdDeviceState]):
hass: HomeAssistant,
name: str,
controller: AsyncRainbirdController,
serial_number: str,
unique_id: str | None,
model_info: ModelAndVersion,
) -> None:
"""Initialize RainbirdUpdateCoordinator."""
@ -62,7 +62,7 @@ class RainbirdUpdateCoordinator(DataUpdateCoordinator[RainbirdDeviceState]):
update_interval=UPDATE_INTERVAL,
)
self._controller = controller
self._serial_number = serial_number
self._unique_id = unique_id
self._zones: set[int] | None = None
self._model_info = model_info
@ -72,16 +72,23 @@ class RainbirdUpdateCoordinator(DataUpdateCoordinator[RainbirdDeviceState]):
return self._controller
@property
def serial_number(self) -> str:
"""Return the device serial number."""
return self._serial_number
def unique_id(self) -> str | None:
"""Return the config entry unique id."""
return self._unique_id
@property
def device_info(self) -> DeviceInfo:
def device_name(self) -> str:
"""Device name for the rainbird controller."""
return f"{MANUFACTURER} Controller"
@property
def device_info(self) -> DeviceInfo | None:
"""Return information about the device."""
if not self._unique_id:
return None
return DeviceInfo(
name=f"{MANUFACTURER} Controller",
identifiers={(DOMAIN, self._serial_number)},
name=self.device_name,
identifiers={(DOMAIN, self._unique_id)},
manufacturer=MANUFACTURER,
model=self._model_info.model_name,
sw_version=f"{self._model_info.major}.{self._model_info.minor}",
@ -164,7 +171,7 @@ class RainbirdData:
self.hass,
name=self.entry.title,
controller=self.controller,
serial_number=self.entry.data[CONF_SERIAL_NUMBER],
unique_id=self.entry.unique_id,
model_info=self.model_info,
)

View File

@ -51,8 +51,11 @@ class RainDelayNumber(CoordinatorEntity[RainbirdUpdateCoordinator], NumberEntity
) -> None:
"""Initialize the Rain Bird sensor."""
super().__init__(coordinator)
self._attr_unique_id = f"{coordinator.serial_number}-rain-delay"
self._attr_device_info = coordinator.device_info
if coordinator.unique_id:
self._attr_unique_id = f"{coordinator.unique_id}-rain-delay"
self._attr_device_info = coordinator.device_info
else:
self._attr_name = f"{coordinator.device_name} Rain delay"
@property
def native_value(self) -> float | None:

View File

@ -52,8 +52,13 @@ class RainBirdSensor(CoordinatorEntity[RainbirdUpdateCoordinator], SensorEntity)
"""Initialize the Rain Bird sensor."""
super().__init__(coordinator)
self.entity_description = description
self._attr_unique_id = f"{coordinator.serial_number}-{description.key}"
self._attr_device_info = coordinator.device_info
if coordinator.unique_id:
self._attr_unique_id = f"{coordinator.unique_id}-{description.key}"
self._attr_device_info = coordinator.device_info
else:
self._attr_name = (
f"{coordinator.device_name} {description.key.capitalize()}"
)
@property
def native_value(self) -> StateType:

View File

@ -65,20 +65,23 @@ class RainBirdSwitch(CoordinatorEntity[RainbirdUpdateCoordinator], SwitchEntity)
"""Initialize a Rain Bird Switch Device."""
super().__init__(coordinator)
self._zone = zone
if coordinator.unique_id:
self._attr_unique_id = f"{coordinator.unique_id}-{zone}"
device_name = f"{MANUFACTURER} Sprinkler {zone}"
if imported_name:
self._attr_name = imported_name
self._attr_has_entity_name = False
else:
self._attr_name = None
self._attr_name = None if coordinator.unique_id else device_name
self._attr_has_entity_name = True
self._duration_minutes = duration_minutes
self._attr_unique_id = f"{coordinator.serial_number}-{zone}"
self._attr_device_info = DeviceInfo(
name=f"{MANUFACTURER} Sprinkler {zone}",
identifiers={(DOMAIN, self._attr_unique_id)},
manufacturer=MANUFACTURER,
via_device=(DOMAIN, coordinator.serial_number),
)
if coordinator.unique_id and self._attr_unique_id:
self._attr_device_info = DeviceInfo(
name=device_name,
identifiers={(DOMAIN, self._attr_unique_id)},
manufacturer=MANUFACTURER,
via_device=(DOMAIN, coordinator.unique_id),
)
@property
def extra_state_attributes(self):

View File

@ -86,7 +86,7 @@ def yaml_config() -> dict[str, Any]:
@pytest.fixture
async def unique_id() -> str:
async def config_entry_unique_id() -> str:
"""Fixture for serial number used in the config entry."""
return SERIAL_NUMBER
@ -100,13 +100,13 @@ async def config_entry_data() -> dict[str, Any]:
@pytest.fixture
async def config_entry(
config_entry_data: dict[str, Any] | None,
unique_id: str,
config_entry_unique_id: str | None,
) -> MockConfigEntry | None:
"""Fixture for MockConfigEntry."""
if config_entry_data is None:
return None
return MockConfigEntry(
unique_id=unique_id,
unique_id=config_entry_unique_id,
domain=DOMAIN,
data=config_entry_data,
options={ATTR_DURATION: DEFAULT_TRIGGER_TIME_MINUTES},

View File

@ -5,6 +5,7 @@ import pytest
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, ComponentSetup
@ -25,6 +26,7 @@ async def test_rainsensor(
hass: HomeAssistant,
setup_integration: ComponentSetup,
responses: list[AiohttpClientMockResponse],
entity_registry: er.EntityRegistry,
expected_state: bool,
) -> None:
"""Test rainsensor binary sensor."""
@ -38,3 +40,37 @@ async def test_rainsensor(
"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 == "1263613994342-rainsensor"
@pytest.mark.parametrize(
("config_entry_unique_id"),
[
(None),
],
)
async def test_no_unique_id(
hass: HomeAssistant,
setup_integration: ComponentSetup,
responses: list[AiohttpClientMockResponse],
entity_registry: er.EntityRegistry,
) -> None:
"""Test rainsensor binary sensor with no unique id."""
assert await setup_integration()
rainsensor = hass.states.get("binary_sensor.rain_bird_controller_rainsensor")
assert rainsensor is not None
assert (
rainsensor.attributes.get("friendly_name") == "Rain Bird Controller Rainsensor"
)
entity_entry = entity_registry.async_get(
"binary_sensor.rain_bird_controller_rainsensor"
)
assert not entity_entry

View File

@ -14,6 +14,7 @@ import pytest
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from .conftest import ComponentSetup, mock_response, mock_response_error
@ -176,6 +177,7 @@ async def test_event_state(
freezer: FrozenDateTimeFactory,
freeze_time: datetime.datetime,
expected_state: str,
entity_registry: er.EntityRegistry,
) -> None:
"""Test calendar upcoming event state."""
freezer.move_to(freeze_time)
@ -196,6 +198,10 @@ async def test_event_state(
}
assert state.state == expected_state
entity = entity_registry.async_get(TEST_ENTITY)
assert entity
assert entity.unique_id == 1263613994342
@pytest.mark.parametrize(
("model_and_version_response", "has_entity"),
@ -270,3 +276,27 @@ async def test_program_schedule_disabled(
"friendly_name": "Rain Bird Controller",
"icon": "mdi:sprinkler",
}
@pytest.mark.parametrize(
("config_entry_unique_id"),
[
(None),
],
)
async def test_no_unique_id(
hass: HomeAssistant,
setup_integration: ComponentSetup,
get_events: GetEventsFn,
entity_registry: er.EntityRegistry,
) -> None:
"""Test calendar entity with no unique id."""
assert await setup_integration()
state = hass.states.get(TEST_ENTITY)
assert state is not None
assert state.attributes.get("friendly_name") == "Rain Bird Controller"
entity_entry = entity_registry.async_get(TEST_ENTITY)
assert not entity_entry

View File

@ -106,7 +106,7 @@ async def test_controller_flow(
@pytest.mark.parametrize(
(
"unique_id",
"config_entry_unique_id",
"config_entry_data",
"config_flow_responses",
"expected_config_entry",
@ -154,7 +154,7 @@ async def test_multiple_config_entries(
@pytest.mark.parametrize(
(
"unique_id",
"config_entry_unique_id",
"config_entry_data",
"config_flow_responses",
),

View File

@ -10,7 +10,7 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_ENTITY_ID, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers import device_registry as dr, entity_registry as er
from .conftest import (
ACK_ECHO,
@ -39,8 +39,9 @@ async def test_number_values(
hass: HomeAssistant,
setup_integration: ComponentSetup,
expected_state: str,
entity_registry: er.EntityRegistry,
) -> None:
"""Test sensor platform."""
"""Test number platform."""
assert await setup_integration()
@ -57,6 +58,10 @@ async def test_number_values(
"unit_of_measurement": "d",
}
entity_entry = entity_registry.async_get("number.rain_bird_controller_rain_delay")
assert entity_entry
assert entity_entry.unique_id == "1263613994342-rain-delay"
async def test_set_value(
hass: HomeAssistant,
@ -127,3 +132,28 @@ async def test_set_value_error(
)
assert len(aioclient_mock.mock_calls) == 1
@pytest.mark.parametrize(
("config_entry_unique_id"),
[
(None),
],
)
async def test_no_unique_id(
hass: HomeAssistant,
setup_integration: ComponentSetup,
entity_registry: er.EntityRegistry,
) -> None:
"""Test number platform with no unique id."""
assert await setup_integration()
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 not entity_entry

View File

@ -5,8 +5,9 @@ import pytest
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from .conftest import RAIN_DELAY, RAIN_DELAY_OFF, ComponentSetup
from .conftest import CONFIG_ENTRY_DATA, RAIN_DELAY, RAIN_DELAY_OFF, ComponentSetup
@pytest.fixture
@ -22,6 +23,7 @@ def platforms() -> list[str]:
async def test_sensors(
hass: HomeAssistant,
setup_integration: ComponentSetup,
entity_registry: er.EntityRegistry,
expected_state: str,
) -> None:
"""Test sensor platform."""
@ -35,3 +37,46 @@ async def test_sensors(
"friendly_name": "Rain Bird Controller Raindelay",
"icon": "mdi:water-off",
}
entity_entry = entity_registry.async_get("sensor.rain_bird_controller_raindelay")
assert entity_entry
assert entity_entry.unique_id == "1263613994342-raindelay"
@pytest.mark.parametrize(
("config_entry_unique_id", "config_entry_data"),
[
# 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,
"serial_number": 0,
},
),
],
)
async def test_sensor_no_unique_id(
hass: HomeAssistant,
setup_integration: ComponentSetup,
entity_registry: er.EntityRegistry,
config_entry_unique_id: str | None,
) -> None:
"""Test sensor platform with no unique id."""
assert await setup_integration()
raindelay = hass.states.get("sensor.rain_bird_controller_raindelay")
assert raindelay is not None
assert raindelay.attributes.get("friendly_name") == "Rain Bird Controller Raindelay"
entity_entry = entity_registry.async_get("sensor.rain_bird_controller_raindelay")
assert (entity_entry is None) == (config_entry_unique_id is None)

View File

@ -8,6 +8,7 @@ from homeassistant.components.rainbird import DOMAIN
from homeassistant.const import ATTR_ENTITY_ID, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import entity_registry as er
from .conftest import (
ACK_ECHO,
@ -57,6 +58,7 @@ async def test_no_zones(
async def test_zones(
hass: HomeAssistant,
setup_integration: ComponentSetup,
entity_registry: er.EntityRegistry,
) -> None:
"""Test switch platform with fake data that creates 7 zones with one enabled."""
@ -100,6 +102,10 @@ async def test_zones(
assert not hass.states.get("switch.rain_bird_sprinkler_8")
# 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"
async def test_switch_on(
hass: HomeAssistant,
@ -275,3 +281,29 @@ async def test_switch_error(
with pytest.raises(HomeAssistantError, match=expected_msg):
await switch_common.async_turn_off(hass, "switch.rain_bird_sprinkler_3")
await hass.async_block_till_done()
@pytest.mark.parametrize(
("config_entry_unique_id"),
[
None,
],
)
async def test_no_unique_id(
hass: HomeAssistant,
setup_integration: ComponentSetup,
aioclient_mock: AiohttpClientMocker,
responses: list[AiohttpClientMockResponse],
entity_registry: er.EntityRegistry,
) -> None:
"""Test an irrigation switch with no unique id."""
assert await setup_integration()
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 is None