diff --git a/homeassistant/components/velux/binary_sensor.py b/homeassistant/components/velux/binary_sensor.py index e08d4bcf545..15d5d2c89ad 100644 --- a/homeassistant/components/velux/binary_sensor.py +++ b/homeassistant/components/velux/binary_sensor.py @@ -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 @@ -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_entity_registry_enabled_default = False _attr_device_class = BinarySensorDeviceClass.MOISTURE + _attr_translation_key = "rain_sensor" def __init__(self, node: OpeningDevice, config_entry_id: str) -> None: """Initialize VeluxRainSensor.""" super().__init__(node, config_entry_id) 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: """Fetch the latest state from the device.""" diff --git a/homeassistant/components/velux/cover.py b/homeassistant/components/velux/cover.py index d6bf8905d91..32be29c3c91 100644 --- a/homeassistant/components/velux/cover.py +++ b/homeassistant/components/velux/cover.py @@ -5,7 +5,7 @@ from __future__ import annotations from typing import Any, cast 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 ( ATTR_POSITION, @@ -44,9 +44,13 @@ class VeluxCover(VeluxEntity, CoverEntity): _is_blind = False 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: """Initialize VeluxCover.""" super().__init__(node, config_entry_id) + # Window is the default device class for covers self._attr_device_class = CoverDeviceClass.WINDOW if isinstance(node, Awning): self._attr_device_class = CoverDeviceClass.AWNING @@ -59,8 +63,6 @@ class VeluxCover(VeluxEntity, CoverEntity): self._attr_device_class = CoverDeviceClass.GATE if isinstance(node, RollerShutter): self._attr_device_class = CoverDeviceClass.SHUTTER - if isinstance(node, Window): - self._attr_device_class = CoverDeviceClass.WINDOW @property def supported_features(self) -> CoverEntityFeature: diff --git a/homeassistant/components/velux/entity.py b/homeassistant/components/velux/entity.py index 1231a98e0a8..fa06598f979 100644 --- a/homeassistant/components/velux/entity.py +++ b/homeassistant/components/velux/entity.py @@ -3,13 +3,17 @@ from pyvlx import Node from homeassistant.core import callback +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity import Entity +from .const import DOMAIN + class VeluxEntity(Entity): - """Abstraction for al Velux entities.""" + """Abstraction for all Velux entities.""" _attr_should_poll = False + _attr_has_entity_name = True def __init__(self, node: Node, config_entry_id: str) -> None: """Initialize the Velux device.""" @@ -19,7 +23,18 @@ class VeluxEntity(Entity): if node.serial_number 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 def async_register_callbacks(self): diff --git a/homeassistant/components/velux/strings.json b/homeassistant/components/velux/strings.json index 0cf578732fb..5123c59fe43 100644 --- a/homeassistant/components/velux/strings.json +++ b/homeassistant/components/velux/strings.json @@ -27,5 +27,12 @@ "name": "Reboot gateway", "description": "Reboots the KLF200 Gateway." } + }, + "entity": { + "binary_sensor": { + "rain_sensor": { + "name": "Rain sensor" + } + } } } diff --git a/tests/components/velux/test_binary_sensor.py b/tests/components/velux/test_binary_sensor.py index 8eb065a5a46..dfe994b6fa2 100644 --- a/tests/components/velux/test_binary_sensor.py +++ b/tests/components/velux/test_binary_sensor.py @@ -8,6 +8,8 @@ import pytest from homeassistant.const import STATE_OFF, STATE_ON, Platform 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 @@ -21,17 +23,15 @@ async def test_rain_sensor_state( freezer: FrozenDateTimeFactory, ) -> None: """Test the rain sensor.""" + mock_config_entry.add_to_hass(hass) - - test_entity_id = "binary_sensor.test_window_rain_sensor" - - with ( - patch("homeassistant.components.velux.PLATFORMS", [Platform.BINARY_SENSOR]), - ): + with patch("homeassistant.components.velux.PLATFORMS", [Platform.BINARY_SENSOR]): # setup config entry 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" + # simulate no rain detected freezer.tick(timedelta(minutes=5)) async_fire_time_changed(hass) @@ -48,3 +48,39 @@ async def test_rain_sensor_state( state = hass.states.get(test_entity_id) assert state is not None 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