mirror of
https://github.com/home-assistant/core.git
synced 2025-09-12 16:21:35 +02:00
Add DeviceInfo to Velux entities (#149575)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
"""Support for rain sensors build into some velux windows."""
|
"""Support for rain sensors built into some Velux windows."""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
@@ -44,12 +44,12 @@ class VeluxRainSensor(VeluxEntity, BinarySensorEntity):
|
|||||||
_attr_should_poll = True # the rain sensor / opening limitations needs polling unlike the rest of the Velux devices
|
_attr_should_poll = True # the rain sensor / opening limitations needs polling unlike the rest of the Velux devices
|
||||||
_attr_entity_registry_enabled_default = False
|
_attr_entity_registry_enabled_default = False
|
||||||
_attr_device_class = BinarySensorDeviceClass.MOISTURE
|
_attr_device_class = BinarySensorDeviceClass.MOISTURE
|
||||||
|
_attr_translation_key = "rain_sensor"
|
||||||
|
|
||||||
def __init__(self, node: OpeningDevice, config_entry_id: str) -> None:
|
def __init__(self, node: OpeningDevice, config_entry_id: str) -> None:
|
||||||
"""Initialize VeluxRainSensor."""
|
"""Initialize VeluxRainSensor."""
|
||||||
super().__init__(node, config_entry_id)
|
super().__init__(node, config_entry_id)
|
||||||
self._attr_unique_id = f"{self._attr_unique_id}_rain_sensor"
|
self._attr_unique_id = f"{self._attr_unique_id}_rain_sensor"
|
||||||
self._attr_name = f"{node.name} Rain sensor"
|
|
||||||
|
|
||||||
async def async_update(self) -> None:
|
async def async_update(self) -> None:
|
||||||
"""Fetch the latest state from the device."""
|
"""Fetch the latest state from the device."""
|
||||||
|
@@ -5,7 +5,7 @@ from __future__ import annotations
|
|||||||
from typing import Any, cast
|
from typing import Any, cast
|
||||||
|
|
||||||
from pyvlx import OpeningDevice, Position
|
from pyvlx import OpeningDevice, Position
|
||||||
from pyvlx.opening_device import Awning, Blind, GarageDoor, Gate, RollerShutter, Window
|
from pyvlx.opening_device import Awning, Blind, GarageDoor, Gate, RollerShutter
|
||||||
|
|
||||||
from homeassistant.components.cover import (
|
from homeassistant.components.cover import (
|
||||||
ATTR_POSITION,
|
ATTR_POSITION,
|
||||||
@@ -44,9 +44,13 @@ class VeluxCover(VeluxEntity, CoverEntity):
|
|||||||
_is_blind = False
|
_is_blind = False
|
||||||
node: OpeningDevice
|
node: OpeningDevice
|
||||||
|
|
||||||
|
# Do not name the "main" feature of the device (position control)
|
||||||
|
_attr_name = None
|
||||||
|
|
||||||
def __init__(self, node: OpeningDevice, config_entry_id: str) -> None:
|
def __init__(self, node: OpeningDevice, config_entry_id: str) -> None:
|
||||||
"""Initialize VeluxCover."""
|
"""Initialize VeluxCover."""
|
||||||
super().__init__(node, config_entry_id)
|
super().__init__(node, config_entry_id)
|
||||||
|
# Window is the default device class for covers
|
||||||
self._attr_device_class = CoverDeviceClass.WINDOW
|
self._attr_device_class = CoverDeviceClass.WINDOW
|
||||||
if isinstance(node, Awning):
|
if isinstance(node, Awning):
|
||||||
self._attr_device_class = CoverDeviceClass.AWNING
|
self._attr_device_class = CoverDeviceClass.AWNING
|
||||||
@@ -59,8 +63,6 @@ class VeluxCover(VeluxEntity, CoverEntity):
|
|||||||
self._attr_device_class = CoverDeviceClass.GATE
|
self._attr_device_class = CoverDeviceClass.GATE
|
||||||
if isinstance(node, RollerShutter):
|
if isinstance(node, RollerShutter):
|
||||||
self._attr_device_class = CoverDeviceClass.SHUTTER
|
self._attr_device_class = CoverDeviceClass.SHUTTER
|
||||||
if isinstance(node, Window):
|
|
||||||
self._attr_device_class = CoverDeviceClass.WINDOW
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def supported_features(self) -> CoverEntityFeature:
|
def supported_features(self) -> CoverEntityFeature:
|
||||||
|
@@ -3,13 +3,17 @@
|
|||||||
from pyvlx import Node
|
from pyvlx import Node
|
||||||
|
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
|
from homeassistant.helpers.device_registry import DeviceInfo
|
||||||
from homeassistant.helpers.entity import Entity
|
from homeassistant.helpers.entity import Entity
|
||||||
|
|
||||||
|
from .const import DOMAIN
|
||||||
|
|
||||||
|
|
||||||
class VeluxEntity(Entity):
|
class VeluxEntity(Entity):
|
||||||
"""Abstraction for al Velux entities."""
|
"""Abstraction for all Velux entities."""
|
||||||
|
|
||||||
_attr_should_poll = False
|
_attr_should_poll = False
|
||||||
|
_attr_has_entity_name = True
|
||||||
|
|
||||||
def __init__(self, node: Node, config_entry_id: str) -> None:
|
def __init__(self, node: Node, config_entry_id: str) -> None:
|
||||||
"""Initialize the Velux device."""
|
"""Initialize the Velux device."""
|
||||||
@@ -19,7 +23,18 @@ class VeluxEntity(Entity):
|
|||||||
if node.serial_number
|
if node.serial_number
|
||||||
else f"{config_entry_id}_{node.node_id}"
|
else f"{config_entry_id}_{node.node_id}"
|
||||||
)
|
)
|
||||||
self._attr_name = node.name if node.name else f"#{node.node_id}"
|
self._attr_device_info = DeviceInfo(
|
||||||
|
identifiers={
|
||||||
|
(
|
||||||
|
DOMAIN,
|
||||||
|
node.serial_number
|
||||||
|
if node.serial_number
|
||||||
|
else f"{config_entry_id}_{node.node_id}",
|
||||||
|
)
|
||||||
|
},
|
||||||
|
name=node.name if node.name else f"#{node.node_id}",
|
||||||
|
serial_number=node.serial_number,
|
||||||
|
)
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def async_register_callbacks(self):
|
def async_register_callbacks(self):
|
||||||
|
@@ -27,5 +27,12 @@
|
|||||||
"name": "Reboot gateway",
|
"name": "Reboot gateway",
|
||||||
"description": "Reboots the KLF200 Gateway."
|
"description": "Reboots the KLF200 Gateway."
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"entity": {
|
||||||
|
"binary_sensor": {
|
||||||
|
"rain_sensor": {
|
||||||
|
"name": "Rain sensor"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -8,6 +8,8 @@ import pytest
|
|||||||
|
|
||||||
from homeassistant.const import STATE_OFF, STATE_ON, Platform
|
from homeassistant.const import STATE_OFF, STATE_ON, Platform
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.device_registry import DeviceRegistry
|
||||||
|
from homeassistant.helpers.entity_registry import EntityRegistry
|
||||||
|
|
||||||
from tests.common import MockConfigEntry, async_fire_time_changed
|
from tests.common import MockConfigEntry, async_fire_time_changed
|
||||||
|
|
||||||
@@ -21,17 +23,15 @@ async def test_rain_sensor_state(
|
|||||||
freezer: FrozenDateTimeFactory,
|
freezer: FrozenDateTimeFactory,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test the rain sensor."""
|
"""Test the rain sensor."""
|
||||||
|
|
||||||
mock_config_entry.add_to_hass(hass)
|
mock_config_entry.add_to_hass(hass)
|
||||||
|
with patch("homeassistant.components.velux.PLATFORMS", [Platform.BINARY_SENSOR]):
|
||||||
test_entity_id = "binary_sensor.test_window_rain_sensor"
|
|
||||||
|
|
||||||
with (
|
|
||||||
patch("homeassistant.components.velux.PLATFORMS", [Platform.BINARY_SENSOR]),
|
|
||||||
):
|
|
||||||
# setup config entry
|
# setup config entry
|
||||||
assert await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
assert await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
test_entity_id = "binary_sensor.test_window_rain_sensor"
|
||||||
|
|
||||||
# simulate no rain detected
|
# simulate no rain detected
|
||||||
freezer.tick(timedelta(minutes=5))
|
freezer.tick(timedelta(minutes=5))
|
||||||
async_fire_time_changed(hass)
|
async_fire_time_changed(hass)
|
||||||
@@ -48,3 +48,39 @@ async def test_rain_sensor_state(
|
|||||||
state = hass.states.get(test_entity_id)
|
state = hass.states.get(test_entity_id)
|
||||||
assert state is not None
|
assert state is not None
|
||||||
assert state.state == STATE_ON
|
assert state.state == STATE_ON
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
||||||
|
@pytest.mark.usefixtures("mock_module")
|
||||||
|
async def test_rain_sensor_device_association(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_window: MagicMock,
|
||||||
|
mock_config_entry: MockConfigEntry,
|
||||||
|
entity_registry: EntityRegistry,
|
||||||
|
device_registry: DeviceRegistry,
|
||||||
|
) -> None:
|
||||||
|
"""Test the rain sensor is properly associated with its device."""
|
||||||
|
|
||||||
|
mock_config_entry.add_to_hass(hass)
|
||||||
|
with patch("homeassistant.components.velux.PLATFORMS", [Platform.BINARY_SENSOR]):
|
||||||
|
assert await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
test_entity_id = "binary_sensor.test_window_rain_sensor"
|
||||||
|
|
||||||
|
# Verify entity exists
|
||||||
|
state = hass.states.get(test_entity_id)
|
||||||
|
assert state is not None
|
||||||
|
|
||||||
|
# Get entity entry
|
||||||
|
entity_entry = entity_registry.async_get(test_entity_id)
|
||||||
|
assert entity_entry is not None
|
||||||
|
assert entity_entry.device_id is not None
|
||||||
|
|
||||||
|
# Get device entry
|
||||||
|
device_entry = device_registry.async_get(entity_entry.device_id)
|
||||||
|
assert device_entry is not None
|
||||||
|
|
||||||
|
# Verify device has correct identifiers
|
||||||
|
assert ("velux", mock_window.serial_number) in device_entry.identifiers
|
||||||
|
assert device_entry.name == mock_window.name
|
||||||
|
Reference in New Issue
Block a user