From c82402fee465f46aa85b4cd5b3b039393c6ad343 Mon Sep 17 00:00:00 2001 From: Wolfgang Miller-Reichling Date: Wed, 29 Oct 2025 16:40:41 +0000 Subject: [PATCH] feature(velux): add gateway as device and make it via_device --- homeassistant/components/velux/__init__.py | 38 +++++++++++++++++-- .../components/velux/binary_sensor.py | 10 ++--- homeassistant/components/velux/cover.py | 10 ++--- homeassistant/components/velux/entity.py | 14 ++++--- homeassistant/components/velux/light.py | 6 +-- homeassistant/components/velux/scene.py | 4 +- tests/components/velux/test_binary_sensor.py | 8 ++++ 7 files changed, 65 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/velux/__init__.py b/homeassistant/components/velux/__init__.py index e53514ae19fc1d..f5e9f1c5951629 100644 --- a/homeassistant/components/velux/__init__.py +++ b/homeassistant/components/velux/__init__.py @@ -1,17 +1,31 @@ """Support for VELUX KLF 200 devices.""" +from __future__ import annotations + +from dataclasses import dataclass + from pyvlx import PyVLX, PyVLXException from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_PASSWORD, EVENT_HOMEASSISTANT_STOP from homeassistant.core import HomeAssistant, ServiceCall +from homeassistant.helpers import device_registry as dr from .const import DOMAIN, LOGGER, PLATFORMS -type VeluxConfigEntry = ConfigEntry[PyVLX] + +@dataclass +class VeluxData: + """Runtime data for Velux integration.""" + + pyvlx: PyVLX + gateway_device_id: tuple[str, str] -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +type VeluxConfigEntry = ConfigEntry[VeluxData] + + +async def async_setup_entry(hass: HomeAssistant, entry: VeluxConfigEntry) -> bool: """Set up the velux component.""" host = entry.data[CONF_HOST] password = entry.data[CONF_PASSWORD] @@ -25,7 +39,23 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: LOGGER.exception("Can't connect to velux interface: %s", ex) return False - entry.runtime_data = pyvlx + device_identifier = (DOMAIN, f"gateway_{entry.entry_id}") + entry.runtime_data = VeluxData(pyvlx=pyvlx, gateway_device_id=device_identifier) + + device_registry = dr.async_get(hass) + device_registry.async_get_or_create( + config_entry_id=entry.entry_id, + identifiers={device_identifier}, + name="KLF 200 Gateway", + manufacturer="Velux", + model="KLF 200", + hw_version=str(pyvlx.klf200.version.hardwareversion) + if pyvlx.klf200.version + else None, + sw_version=str(pyvlx.klf200.version.softwareversion) + if pyvlx.klf200.version + else None, + ) async def on_hass_stop(event): """Close connection when hass stops.""" @@ -46,6 +76,6 @@ async def async_reboot_gateway(service_call: ServiceCall) -> None: return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: VeluxConfigEntry) -> bool: """Unload a config entry.""" return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/velux/binary_sensor.py b/homeassistant/components/velux/binary_sensor.py index c5dd12ad2ef9e0..003008dacfa7ee 100644 --- a/homeassistant/components/velux/binary_sensor.py +++ b/homeassistant/components/velux/binary_sensor.py @@ -24,14 +24,14 @@ async def async_setup_entry( hass: HomeAssistant, - config: VeluxConfigEntry, + config_entry: VeluxConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up rain sensor(s) for Velux platform.""" - pyvlx = config.runtime_data + pyvlx = config_entry.runtime_data.pyvlx async_add_entities( - VeluxRainSensor(node, config.entry_id) + VeluxRainSensor(node, config_entry) for node in pyvlx.nodes if isinstance(node, Window) and node.rain_sensor ) @@ -46,9 +46,9 @@ class VeluxRainSensor(VeluxEntity, BinarySensorEntity): _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: VeluxConfigEntry) -> None: """Initialize VeluxRainSensor.""" - super().__init__(node, config_entry_id) + super().__init__(node, config_entry) self._attr_unique_id = f"{self._attr_unique_id}_rain_sensor" async def async_update(self) -> None: diff --git a/homeassistant/components/velux/cover.py b/homeassistant/components/velux/cover.py index 6207611403c432..0eaa9c208557d1 100644 --- a/homeassistant/components/velux/cover.py +++ b/homeassistant/components/velux/cover.py @@ -32,13 +32,13 @@ async def async_setup_entry( hass: HomeAssistant, - config: VeluxConfigEntry, + config_entry: VeluxConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up cover(s) for Velux platform.""" - pyvlx = config.runtime_data + pyvlx = config_entry.runtime_data.pyvlx async_add_entities( - VeluxCover(node, config.entry_id) + VeluxCover(node, config_entry) for node in pyvlx.nodes if isinstance(node, OpeningDevice) ) @@ -53,9 +53,9 @@ class VeluxCover(VeluxEntity, CoverEntity): # 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: VeluxConfigEntry) -> None: """Initialize VeluxCover.""" - super().__init__(node, config_entry_id) + super().__init__(node, config_entry) # Window is the default device class for covers self._attr_device_class = CoverDeviceClass.WINDOW if isinstance(node, Awning): diff --git a/homeassistant/components/velux/entity.py b/homeassistant/components/velux/entity.py index fa06598f97958a..4971060cf14f0d 100644 --- a/homeassistant/components/velux/entity.py +++ b/homeassistant/components/velux/entity.py @@ -6,6 +6,7 @@ from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity import Entity +from . import VeluxConfigEntry from .const import DOMAIN @@ -15,25 +16,26 @@ class VeluxEntity(Entity): _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: VeluxConfigEntry) -> None: """Initialize the Velux device.""" self.node = node - self._attr_unique_id = ( + unique_id = ( node.serial_number if node.serial_number - else f"{config_entry_id}_{node.node_id}" + else f"{config_entry.entry_id}_{node.node_id}" ) + self._attr_unique_id = unique_id + self._attr_device_info = DeviceInfo( identifiers={ ( DOMAIN, - node.serial_number - if node.serial_number - else f"{config_entry_id}_{node.node_id}", + unique_id, ) }, name=node.name if node.name else f"#{node.node_id}", serial_number=node.serial_number, + via_device=config_entry.runtime_data.gateway_device_id, ) @callback diff --git a/homeassistant/components/velux/light.py b/homeassistant/components/velux/light.py index 4bd367a0d62124..a4997a504d5acb 100644 --- a/homeassistant/components/velux/light.py +++ b/homeassistant/components/velux/light.py @@ -18,13 +18,13 @@ async def async_setup_entry( hass: HomeAssistant, - config: VeluxConfigEntry, + config_entry: VeluxConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up light(s) for Velux platform.""" - pyvlx = config.runtime_data + pyvlx = config_entry.runtime_data.pyvlx async_add_entities( - VeluxLight(node, config.entry_id) + VeluxLight(node, config_entry) for node in pyvlx.nodes if isinstance(node, LighteningDevice) ) diff --git a/homeassistant/components/velux/scene.py b/homeassistant/components/velux/scene.py index f844070736be90..bd5a1b99d502ef 100644 --- a/homeassistant/components/velux/scene.py +++ b/homeassistant/components/velux/scene.py @@ -15,11 +15,11 @@ async def async_setup_entry( hass: HomeAssistant, - config: VeluxConfigEntry, + config_entry: VeluxConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up the scenes for Velux platform.""" - pyvlx = config.runtime_data + pyvlx = config_entry.runtime_data.pyvlx entities = [VeluxScene(scene) for scene in pyvlx.scenes] async_add_entities(entities) diff --git a/tests/components/velux/test_binary_sensor.py b/tests/components/velux/test_binary_sensor.py index f48a9102dfbbe3..ac39e7a3b6cccd 100644 --- a/tests/components/velux/test_binary_sensor.py +++ b/tests/components/velux/test_binary_sensor.py @@ -95,3 +95,11 @@ async def test_rain_sensor_device_association( # Verify device has correct identifiers assert ("velux", mock_window.serial_number) in device_entry.identifiers assert device_entry.name == mock_window.name + + # Verify via_device is gateway + assert device_entry.via_device_id is not None + via_device_entry = device_registry.async_get(device_entry.via_device_id) + assert via_device_entry is not None + assert via_device_entry.identifiers == { + mock_config_entry.runtime_data.gateway_device_id + }