mirror of
https://github.com/home-assistant/core.git
synced 2026-08-06 21:35:13 +01:00
Fix via_device race in verisure (#177847)
This commit is contained in:
@@ -9,10 +9,10 @@ from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_EMAIL, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
from homeassistant.helpers.storage import STORAGE_DIR
|
||||
|
||||
from .const import CONF_LOCK_DEFAULT_CODE, LOGGER
|
||||
from .const import CONF_GIID, CONF_LOCK_DEFAULT_CODE, DOMAIN, LOGGER
|
||||
from .coordinator import VerisureConfigEntry, VerisureDataUpdateCoordinator
|
||||
|
||||
PLATFORMS = [
|
||||
@@ -38,7 +38,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: VerisureConfigEntry) ->
|
||||
|
||||
entry.runtime_data = coordinator
|
||||
|
||||
# Migrate lock default code from config entry to lock entity
|
||||
# Register the alarm (VBox) device so children can link to it via via_device_id
|
||||
# regardless of platform setup order.
|
||||
dr.async_get(hass).async_get_or_create(
|
||||
config_entry_id=entry.entry_id,
|
||||
identifiers={(DOMAIN, entry.data[CONF_GIID])},
|
||||
manufacturer="Verisure",
|
||||
model="VBox",
|
||||
name="Verisure Alarm",
|
||||
configuration_url="https://mypages.verisure.com",
|
||||
)
|
||||
|
||||
# Set up all platforms for this device/entry.
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
@@ -8,6 +8,7 @@ from homeassistant.components.binary_sensor import (
|
||||
)
|
||||
from homeassistant.const import ATTR_LAST_TRIP_TIME, EntityCategory
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
@@ -51,18 +52,17 @@ class VerisureDoorWindowSensor(
|
||||
super().__init__(coordinator)
|
||||
self._attr_unique_id = f"{serial_number}_door_window"
|
||||
self.serial_number = serial_number
|
||||
|
||||
@property
|
||||
@override
|
||||
def device_info(self) -> DeviceInfo:
|
||||
"""Return device information about this entity."""
|
||||
area = self.coordinator.data["door_window"][self.serial_number]["area"]
|
||||
return DeviceInfo(
|
||||
area = coordinator.data["door_window"][serial_number]["area"]
|
||||
self._attr_device_info = DeviceInfo(
|
||||
name=area,
|
||||
manufacturer="Verisure",
|
||||
model="Shock Sensor Detector",
|
||||
identifiers={(DOMAIN, self.serial_number)},
|
||||
via_device=(DOMAIN, self.coordinator.config_entry.data[CONF_GIID]),
|
||||
identifiers={(DOMAIN, serial_number)},
|
||||
via_device_id=dr.async_get_device_id_by_identifier(
|
||||
coordinator.hass,
|
||||
(DOMAIN, coordinator.config_entry.data[CONF_GIID]),
|
||||
config_entry_id=coordinator.config_entry.entry_id,
|
||||
),
|
||||
configuration_url="https://mypages.verisure.com",
|
||||
)
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ from verisure import Error as VerisureError
|
||||
from homeassistant.components.camera import Camera
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import (
|
||||
AddConfigEntryEntitiesCallback,
|
||||
@@ -63,18 +64,17 @@ class VerisureSmartcam(CoordinatorEntity[VerisureDataUpdateCoordinator], Camera)
|
||||
self._directory_path = directory_path
|
||||
self._image: str | None = None
|
||||
self._image_id: str | None = None
|
||||
|
||||
@property
|
||||
@override
|
||||
def device_info(self) -> DeviceInfo:
|
||||
"""Return device information about this entity."""
|
||||
area = self.coordinator.data["cameras"][self.serial_number]["device"]["area"]
|
||||
return DeviceInfo(
|
||||
area = coordinator.data["cameras"][serial_number]["device"]["area"]
|
||||
self._attr_device_info = DeviceInfo(
|
||||
name=area,
|
||||
manufacturer="Verisure",
|
||||
model="SmartCam",
|
||||
identifiers={(DOMAIN, self.serial_number)},
|
||||
via_device=(DOMAIN, self.coordinator.config_entry.data[CONF_GIID]),
|
||||
identifiers={(DOMAIN, serial_number)},
|
||||
via_device_id=dr.async_get_device_id_by_identifier(
|
||||
coordinator.hass,
|
||||
(DOMAIN, coordinator.config_entry.data[CONF_GIID]),
|
||||
config_entry_id=coordinator.config_entry.entry_id,
|
||||
),
|
||||
configuration_url="https://mypages.verisure.com",
|
||||
)
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ from verisure import Error as VerisureError
|
||||
from homeassistant.components.lock import LockEntity, LockState
|
||||
from homeassistant.const import ATTR_CODE
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import (
|
||||
AddConfigEntryEntitiesCallback,
|
||||
@@ -70,18 +71,17 @@ class VerisureDoorlock(CoordinatorEntity[VerisureDataUpdateCoordinator], LockEnt
|
||||
self._attr_is_locked = None
|
||||
self._attr_changed_by = None
|
||||
self._changed_method: str | None = None
|
||||
|
||||
@property
|
||||
@override
|
||||
def device_info(self) -> DeviceInfo:
|
||||
"""Return device information about this entity."""
|
||||
area = self.coordinator.data["locks"][self.serial_number]["device"]["area"]
|
||||
return DeviceInfo(
|
||||
area = coordinator.data["locks"][serial_number]["device"]["area"]
|
||||
self._attr_device_info = DeviceInfo(
|
||||
name=area,
|
||||
manufacturer="Verisure",
|
||||
model="Lockguard Smartlock",
|
||||
identifiers={(DOMAIN, self.serial_number)},
|
||||
via_device=(DOMAIN, self.coordinator.config_entry.data[CONF_GIID]),
|
||||
identifiers={(DOMAIN, serial_number)},
|
||||
via_device_id=dr.async_get_device_id_by_identifier(
|
||||
coordinator.hass,
|
||||
(DOMAIN, coordinator.config_entry.data[CONF_GIID]),
|
||||
config_entry_id=coordinator.config_entry.entry_id,
|
||||
),
|
||||
configuration_url="https://mypages.verisure.com",
|
||||
)
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ from homeassistant.components.sensor import (
|
||||
)
|
||||
from homeassistant.const import PERCENTAGE, UnitOfTemperature
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
@@ -58,21 +59,18 @@ class VerisureThermometer(
|
||||
super().__init__(coordinator)
|
||||
self._attr_unique_id = f"{serial_number}_temperature"
|
||||
self.serial_number = serial_number
|
||||
|
||||
@property
|
||||
@override
|
||||
def device_info(self) -> DeviceInfo:
|
||||
"""Return device information about this entity."""
|
||||
device_type = self.coordinator.data["climate"][self.serial_number]["device"][
|
||||
"gui"
|
||||
]["label"]
|
||||
area = self.coordinator.data["climate"][self.serial_number]["device"]["area"]
|
||||
return DeviceInfo(
|
||||
name=area,
|
||||
device = coordinator.data["climate"][serial_number]["device"]
|
||||
device_type = device["gui"]["label"]
|
||||
self._attr_device_info = DeviceInfo(
|
||||
name=device["area"],
|
||||
manufacturer="Verisure",
|
||||
model=DEVICE_TYPE_NAME.get(device_type, device_type),
|
||||
identifiers={(DOMAIN, self.serial_number)},
|
||||
via_device=(DOMAIN, self.coordinator.config_entry.data[CONF_GIID]),
|
||||
identifiers={(DOMAIN, serial_number)},
|
||||
via_device_id=dr.async_get_device_id_by_identifier(
|
||||
coordinator.hass,
|
||||
(DOMAIN, coordinator.config_entry.data[CONF_GIID]),
|
||||
config_entry_id=coordinator.config_entry.entry_id,
|
||||
),
|
||||
configuration_url="https://mypages.verisure.com",
|
||||
)
|
||||
|
||||
@@ -111,21 +109,18 @@ class VerisureHygrometer(
|
||||
super().__init__(coordinator)
|
||||
self._attr_unique_id = f"{serial_number}_humidity"
|
||||
self.serial_number = serial_number
|
||||
|
||||
@property
|
||||
@override
|
||||
def device_info(self) -> DeviceInfo:
|
||||
"""Return device information about this entity."""
|
||||
device_type = self.coordinator.data["climate"][self.serial_number]["device"][
|
||||
"gui"
|
||||
]["label"]
|
||||
area = self.coordinator.data["climate"][self.serial_number]["device"]["area"]
|
||||
return DeviceInfo(
|
||||
name=area,
|
||||
device = coordinator.data["climate"][serial_number]["device"]
|
||||
device_type = device["gui"]["label"]
|
||||
self._attr_device_info = DeviceInfo(
|
||||
name=device["area"],
|
||||
manufacturer="Verisure",
|
||||
model=DEVICE_TYPE_NAME.get(device_type, device_type),
|
||||
identifiers={(DOMAIN, self.serial_number)},
|
||||
via_device=(DOMAIN, self.coordinator.config_entry.data[CONF_GIID]),
|
||||
identifiers={(DOMAIN, serial_number)},
|
||||
via_device_id=dr.async_get_device_id_by_identifier(
|
||||
coordinator.hass,
|
||||
(DOMAIN, coordinator.config_entry.data[CONF_GIID]),
|
||||
config_entry_id=coordinator.config_entry.entry_id,
|
||||
),
|
||||
configuration_url="https://mypages.verisure.com",
|
||||
)
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ from typing import Any, override
|
||||
|
||||
from homeassistant.components.switch import SwitchEntity
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
@@ -42,20 +43,17 @@ class VerisureSmartplug(CoordinatorEntity[VerisureDataUpdateCoordinator], Switch
|
||||
self.serial_number = serial_number
|
||||
self._change_timestamp: float = 0
|
||||
self._state = False
|
||||
|
||||
@property
|
||||
@override
|
||||
def device_info(self) -> DeviceInfo:
|
||||
"""Return device information about this entity."""
|
||||
area = self.coordinator.data["smart_plugs"][self.serial_number]["device"][
|
||||
"area"
|
||||
]
|
||||
return DeviceInfo(
|
||||
area = coordinator.data["smart_plugs"][serial_number]["device"]["area"]
|
||||
self._attr_device_info = DeviceInfo(
|
||||
name=area,
|
||||
manufacturer="Verisure",
|
||||
model="SmartPlug",
|
||||
identifiers={(DOMAIN, self.serial_number)},
|
||||
via_device=(DOMAIN, self.coordinator.config_entry.data[CONF_GIID]),
|
||||
identifiers={(DOMAIN, serial_number)},
|
||||
via_device_id=dr.async_get_device_id_by_identifier(
|
||||
coordinator.hass,
|
||||
(DOMAIN, coordinator.config_entry.data[CONF_GIID]),
|
||||
config_entry_id=coordinator.config_entry.entry_id,
|
||||
),
|
||||
configuration_url="https://mypages.verisure.com",
|
||||
)
|
||||
|
||||
|
||||
@@ -28,7 +28,12 @@ OVERVIEW = [
|
||||
"smartLocks": [
|
||||
{"device": {"deviceLabel": "lock-1"}, "status": "LOCKED"}
|
||||
],
|
||||
"smartplugs": [{"device": {"deviceLabel": "plug-1"}, "status": "on"}],
|
||||
"smartplugs": [
|
||||
{
|
||||
"device": {"deviceLabel": "plug-1", "area": "Living room"},
|
||||
"currentState": "ON",
|
||||
}
|
||||
],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ from verisure import (
|
||||
)
|
||||
|
||||
from homeassistant.components.verisure.const import (
|
||||
CONF_GIID,
|
||||
COOKIE_REFRESH_INTERVAL,
|
||||
DEFAULT_SCAN_INTERVAL,
|
||||
DOMAIN,
|
||||
@@ -24,7 +25,7 @@ from homeassistant.components.verisure.const import (
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.const import STATE_UNAVAILABLE, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import update_coordinator
|
||||
from homeassistant.helpers import device_registry as dr, update_coordinator
|
||||
|
||||
from tests.common import MockConfigEntry, async_fire_time_changed
|
||||
|
||||
@@ -69,6 +70,30 @@ async def test_setup_success(
|
||||
assert hass.states.get(ALARM_ENTITY_ID).state == "disarmed"
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_verisure")
|
||||
async def test_child_device_links_to_alarm_via_device(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
) -> None:
|
||||
"""Child devices link to the alarm (VBox) device via via_device_id."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
with patch("homeassistant.components.verisure.PLATFORMS", [Platform.SWITCH]):
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
giid = mock_config_entry.data[CONF_GIID]
|
||||
alarm_device = device_registry.async_get_device_by_identifier(
|
||||
(DOMAIN, giid), mock_config_entry.entry_id
|
||||
)
|
||||
plug_device = device_registry.async_get_device_by_identifier(
|
||||
(DOMAIN, "plug-1"), mock_config_entry.entry_id
|
||||
)
|
||||
assert alarm_device is not None
|
||||
assert plug_device is not None
|
||||
assert plug_device.via_device_id == alarm_device.id
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"exc",
|
||||
[
|
||||
|
||||
Reference in New Issue
Block a user