mirror of
https://github.com/home-assistant/core.git
synced 2025-04-23 16:57:53 +00:00
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:
parent
b3b5ca9b95
commit
2d58ab0e1c
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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,
|
||||
)
|
||||
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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):
|
||||
|
@ -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},
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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",
|
||||
),
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user