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:
wollew
2025-08-27 10:15:52 +02:00
committed by GitHub
parent 6e45713d3a
commit 10bf1cb999
5 changed files with 73 additions and 13 deletions

View File

@@ -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."""

View File

@@ -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:

View File

@@ -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):

View File

@@ -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"
}
}
} }
} }

View File

@@ -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