Velux Rain Sensor

Hello,

Is there any progress in this PR (https://github.com/home-assistant/core/pull/64628)?

The Python library used by this integration is offering this value for a long time.
This value (minimum limitation) is important because it indirect indicates rain status and blocking the window for opening and would be a major value in Velux window.automation.

So, is there a plan to integrate this value/attribute to Velux integration?

No, you can see it was closed due to inactivity. No idea why the submitter gave up they were making good progress.

Not currently. You can see the other open PRs for this integration here:

https://github.com/home-assistant/core/pulls?q=is%3Aopen+is%3Apr+label%3A"integration%3A+velux"

No, you can see it was closed due to inactivity.

Yes, right. This was clear for me. This was a so called “rhetoric” question.

I just hope, that this PR could be activated again.
As I said: The used Python library for this integration is offering the value. The value is important for automation.
So, it’s just that PR-acceptance.

The Velux-API is known for that (indirect rain sensor) value since 2018. In 2023 it could be the time to integrate this value to Home Assistant official Velux integration.

Edit: And I personally don’t care if this is done via the marked PR or any other PR. At the end of the day the goal is just to integrate the min. 5 year old available value of “limitation minimum”.

did you find a solution yet ?

Yes, I replaced the HA Velux integration with the code in the PR. It works perfectly.
It’s really a pitty, that such PRs don’t get it into the official repo just because of being ignored for too long time.

In the directory /config/custom_components/velux add these files, which will replace the core integration:

__init__.py
"""Support for VELUX KLF 200 devices."""
import logging

from pyvlx import OpeningDevice, Window, PyVLX, PyVLXException
import voluptuous as vol

from homeassistant.const import (
    CONF_HOST,
    CONF_PASSWORD,
    EVENT_HOMEASSISTANT_STOP,
    Platform,
)
from homeassistant.core import HomeAssistant, ServiceCall, callback
from homeassistant.helpers import discovery
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.typing import ConfigType

DOMAIN = "velux"
DATA_VELUX = "data_velux"
PLATFORMS = [Platform.COVER, Platform.LIGHT, Platform.SCENE]
_LOGGER = logging.getLogger(__name__)

CONFIG_SCHEMA = vol.Schema(
    {
        DOMAIN: vol.Schema(
            {vol.Required(CONF_HOST): cv.string, vol.Required(CONF_PASSWORD): cv.string}
        )
    },
    extra=vol.ALLOW_EXTRA,
)


async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
    """Set up the velux component."""
    try:
        hass.data[DATA_VELUX] = VeluxModule(hass, config[DOMAIN])
        hass.data[DATA_VELUX].setup()
        await hass.data[DATA_VELUX].async_start()

    except PyVLXException as ex:
        _LOGGER.exception("Can't connect to velux interface: %s", ex)
        return False

    for platform in PLATFORMS:
        hass.async_create_task(
            discovery.async_load_platform(hass, platform, DOMAIN, {}, config)
        )
    return True


class VeluxModule:
    """Abstraction for velux component."""

    def __init__(self, hass, domain_config):
        """Initialize for velux component."""
        self.pyvlx = None
        self._hass = hass
        self._domain_config = domain_config

    def setup(self):
        """Velux component setup."""

        async def on_hass_stop(event):
            """Close connection when hass stops."""
            _LOGGER.debug("Velux interface terminated")
            await self.pyvlx.disconnect()

        async def async_reboot_gateway(service_call: ServiceCall) -> None:
            await self.pyvlx.reboot_gateway()
        

        self._hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, on_hass_stop)
        host = self._domain_config.get(CONF_HOST)
        password = self._domain_config.get(CONF_PASSWORD)
        self.pyvlx = PyVLX(host=host, password=password)

        self._hass.services.async_register(
            DOMAIN, "reboot_gateway", async_reboot_gateway
        )
        

    async def async_start(self):
        """Start velux component."""
        _LOGGER.debug("Velux interface started")
        await self.pyvlx.load_scenes()
        await self.pyvlx.load_nodes()


class VeluxEntity(Entity):
    """Abstraction for al Velux entities."""

    _attr_should_poll = False

    def __init__(self, node: OpeningDevice) -> None:
        """Initialize the Velux device."""
        self.node = node
        self._attr_unique_id = node.serial_number
        self._attr_name = node.name if node.name else f"#{node.node_id}"
        
    async def async_init(self):
        """Initialize the entity async."""

    @callback
    def async_register_callbacks(self):
        """Register callbacks to update hass after device was changed."""

        async def after_update_callback(device):
            """Call after device was updated."""
            self.async_write_ha_state()

        self.node.register_device_updated_cb(after_update_callback)

    async def async_added_to_hass(self):
        """Store register state change callback."""
        self.async_register_callbacks()
cover.py
"""Support for Velux covers."""
from __future__ import annotations

from datetime import timedelta
import logging

from typing import Any, TYPE_CHECKING

from pyvlx import OpeningDevice, Position
from pyvlx.exception import PyVLXException
from pyvlx.opening_device import Awning, Blind, GarageDoor, Gate, RollerShutter, Window

from homeassistant.components.cover import (
    ATTR_POSITION,
    ATTR_TILT_POSITION,
    CoverDeviceClass,
    CoverEntity,
    CoverEntityFeature,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator

from . import DATA_VELUX, VeluxEntity

PARALLEL_UPDATES = 1

if TYPE_CHECKING:
    from pyvlx.node import Node

DEFAULT_SCAN_INTERVAL = timedelta(minutes=2)

_LOGGER = logging.getLogger(__name__)


async def async_setup_platform(
    hass: HomeAssistant,
    config: ConfigType,
    async_add_entities: AddEntitiesCallback,
    discovery_info: DiscoveryInfoType | None = None,
) -> None:
    """Set up cover(s) for Velux platform."""
    entities: list[VeluxWindow | VeluxCover] = []

    for node in hass.data[DATA_VELUX].pyvlx.nodes:
        if isinstance(node, OpeningDevice):
            if isinstance(node, Window):
                entities.append(VeluxWindow(hass, node))
            else:
                entities.append(VeluxCover(node))

    for entity in entities:
        await entity.async_init()

    async_add_entities(entities)


class VeluxCover(VeluxEntity, CoverEntity):
    """Representation of a Velux cover."""

    _is_blind = False

    def __init__(self, node: OpeningDevice) -> None:
        """Initialize VeluxCover."""
        super().__init__(node)
        self._attr_device_class = CoverDeviceClass.WINDOW
        if isinstance(node, Awning):
            self._attr_device_class = CoverDeviceClass.AWNING
        if isinstance(node, Blind):
            self._attr_device_class = CoverDeviceClass.BLIND
            self._is_blind = True
        if isinstance(node, GarageDoor):
            self._attr_device_class = CoverDeviceClass.GARAGE
        if isinstance(node, Gate):
            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:
        """Flag supported features."""
        supported_features = (
            CoverEntityFeature.OPEN
            | CoverEntityFeature.CLOSE
            | CoverEntityFeature.SET_POSITION
            | CoverEntityFeature.STOP
        )
        if self.current_cover_tilt_position is not None:
            supported_features |= (
                CoverEntityFeature.OPEN_TILT
                | CoverEntityFeature.CLOSE_TILT
                | CoverEntityFeature.SET_TILT_POSITION
                | CoverEntityFeature.STOP_TILT
            )
        return supported_features

    @property
    def current_cover_position(self) -> int:
        """Return the current position of the cover."""
        return 100 - self.node.position.position_percent

    @property
    def current_cover_tilt_position(self) -> int | None:
        """Return the current position of the cover."""
        if self._is_blind:
            return 100 - self.node.orientation.position_percent
        return None

    @property
    def is_closed(self) -> bool:
        """Return if the cover is closed."""
        return self.node.position.closed

    async def async_close_cover(self, **kwargs: Any) -> None:
        """Close the cover."""
        await self.node.close(wait_for_completion=False)

    async def async_open_cover(self, **kwargs: Any) -> None:
        """Open the cover."""
        await self.node.open(wait_for_completion=False)

    async def async_set_cover_position(self, **kwargs: Any) -> None:
        """Move the cover to a specific position."""
        position_percent = 100 - kwargs[ATTR_POSITION]

        await self.node.set_position(
            Position(position_percent=position_percent), wait_for_completion=False
        )

    async def async_stop_cover(self, **kwargs: Any) -> None:
        """Stop the cover."""
        await self.node.stop(wait_for_completion=False)

    async def async_close_cover_tilt(self, **kwargs: Any) -> None:
        """Close cover tilt."""
        await self.node.close_orientation(wait_for_completion=False)

    async def async_open_cover_tilt(self, **kwargs: Any) -> None:
        """Open cover tilt."""
        await self.node.open_orientation(wait_for_completion=False)

    async def async_stop_cover_tilt(self, **kwargs: Any) -> None:
        """Stop cover tilt."""
        await self.node.stop_orientation(wait_for_completion=False)

    async def async_set_cover_tilt_position(self, **kwargs: Any) -> None:
        """Move cover tilt to a specific position."""
        position_percent = 100 - kwargs[ATTR_TILT_POSITION]
        orientation = Position(position_percent=position_percent)
        await self.node.set_orientation(
            orientation=orientation, wait_for_completion=False
        )


class VeluxWindow(VeluxCover):
    """Representation of a Velux window."""

    def __init__(self, hass: HomeAssistant, node: Node) -> None:
        """Initialize Velux window."""
        super().__init__(node)
        self._hass = hass
        self._extra_attr_limitation_min: int | None = None
        self._extra_attr_limitation_max: int | None = None

        self.coordinator = DataUpdateCoordinator(
            self._hass,
            _LOGGER,
            name=self.unique_id,
            update_method=self.async_update_limitation,
            update_interval=DEFAULT_SCAN_INTERVAL,
        )

    async def async_init(self):
        """Async initialize."""
        return await self.coordinator.async_config_entry_first_refresh()

    async def async_update_limitation(self):
        """Get the updated status of the cover (limitations only)."""
        try:
            limitation = await self.node.get_limitation()
            self._extra_attr_limitation_min = limitation.min_value
            self._extra_attr_limitation_max = limitation.max_value
        except PyVLXException:
            _LOGGER.error("Error fetch limitation data for cover %s", self.name)

    @property
    def extra_state_attributes(self) -> dict[str, int | None]:
        """Return the state attributes."""
        return {
            "limitation_min": self._extra_attr_limitation_min,
            "limitation_max": self._extra_attr_limitation_max,
        }

    async def async_added_to_hass(self) -> None:
        """When entity is added to hass."""
        await super().async_added_to_hass()
        self.async_on_remove(
            self.coordinator.async_add_listener(self._handle_coordinator_update)
        )

    @callback
    def _handle_coordinator_update(self) -> None:
        """Handle updated data from the coordinator."""
        self.async_write_ha_state()

    async def async_update(self) -> None:
        """Update the entity.
        Only used by the generic entity update service.
        """
        await self.coordinator.async_request_refresh()
        
light.py
"""Support for Velux lights."""
from __future__ import annotations

from typing import Any

from pyvlx import Intensity, LighteningDevice

from homeassistant.components.light import ATTR_BRIGHTNESS, ColorMode, LightEntity
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType

from . import DATA_VELUX, VeluxEntity

PARALLEL_UPDATES = 1


async def async_setup_platform(
    hass: HomeAssistant,
    config: ConfigType,
    async_add_entities: AddEntitiesCallback,
    discovery_info: DiscoveryInfoType | None = None,
) -> None:
    """Set up light(s) for Velux platform."""
    async_add_entities(
        VeluxLight(node)
        for node in hass.data[DATA_VELUX].pyvlx.nodes
        if isinstance(node, LighteningDevice)
    )


class VeluxLight(VeluxEntity, LightEntity):
    """Representation of a Velux light."""

    _attr_supported_color_modes = {ColorMode.BRIGHTNESS}
    _attr_color_mode = ColorMode.BRIGHTNESS

    @property
    def brightness(self):
        """Return the current brightness."""
        return int((100 - self.node.intensity.intensity_percent) * 255 / 100)

    @property
    def is_on(self):
        """Return true if light is on."""
        return not self.node.intensity.off and self.node.intensity.known

    async def async_turn_on(self, **kwargs: Any) -> None:
        """Instruct the light to turn on."""
        if ATTR_BRIGHTNESS in kwargs:
            intensity_percent = int(100 - kwargs[ATTR_BRIGHTNESS] / 255 * 100)
            await self.node.set_intensity(
                Intensity(intensity_percent=intensity_percent),
                wait_for_completion=True,
            )
        else:
            await self.node.turn_on(wait_for_completion=True)

    async def async_turn_off(self, **kwargs: Any) -> None:
        """Instruct the light to turn off."""
        await self.node.turn_off(wait_for_completion=True)
manifest.json
{
    "domain": "velux",
    "name": "Velux",
    "documentation": "https://www.home-assistant.io/integrations/velux",
    "requirements": ["pyvlx==0.2.20"],
    "codeowners": ["@Julius2342"],
    "iot_class": "local_polling",
    "loggers": ["pyvlx"],
    "version": "2022.1.28"
}
scene.py
"""Support for VELUX scenes."""
from __future__ import annotations

from typing import Any

from homeassistant.components.scene import Scene
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType

from . import _LOGGER, DATA_VELUX

PARALLEL_UPDATES = 1


async def async_setup_platform(
    hass: HomeAssistant,
    config: ConfigType,
    async_add_entities: AddEntitiesCallback,
    discovery_info: DiscoveryInfoType | None = None,
) -> None:
    """Set up the scenes for Velux platform."""
    entities = [VeluxScene(scene) for scene in hass.data[DATA_VELUX].pyvlx.scenes]
    async_add_entities(entities)


class VeluxScene(Scene):
    """Representation of a Velux scene."""

    def __init__(self, scene):
        """Init velux scene."""
        _LOGGER.info("Adding Velux scene: %s", scene)
        self.scene = scene

    @property
    def name(self):
        """Return the name of the scene."""
        return self.scene.name

    async def async_activate(self, **kwargs: Any) -> None:
        """Activate the scene."""
        await self.scene.run(wait_for_completion=False)
services.yaml
# Velux Integration services

reboot_gateway:
strings.json
{
    "services": {
    "reboot_gateway": {
        "name": "Reboot gateway",
        "description": "Reboots the KLF200 Gateway."
    }
    }
}

The relevant different part is in cover.py.at line 180ff.

2 Likes

did you have a rain sensor you can use ?

No, there is not a specific rain sensor. The changes add the attributes “Limitation min” and “Limitation max”
You can create a binary template sensor which checks the “Limitation min” attribute value of the window.
The normal value is 0. If the value is 93 then it is raining.

{% if state_attr('cover.WINDOW_ENTITY_ID', 'limitation_min') == 93 %}
    'on'
{% else %}
    'off'
{% endif %}

Edit: There is a hardware Velux rain sensor connected to the window, if you meant that. This rain sensor is regularly distributed with every Velux Integra window. This hardware rain sensor is just not directly accessible through KLF 200 gateway.
The Velux rain sensor status is only accessible through the “limitation” value of the window.

1 Like

To be fair here is that the submitter was proposing a change, did incorporate feedback, and then was suddenly asked to provide a “rain_sensor” instead in a different PR. I guess motivation started to be lacking.

Arguably, the rain_sensor would be derived from a value which has no clear direct relationship with a binary value of yes/no. In addition the limitation will - what’s in a name - limit until how far a window can be opened. This can be a different value and would be nice to have reflected in HA so that the user does not try to go beyond or has the expectancy to go beyond. Finally, it is also possible to clear and set limitations (not in the lib yet) and a “rain_sensor” does not cover that.

In other words I think the PR should be reconsidered.

2 Likes

Just an additional info:
Why is the value of “93” of the attribute “limitation_min” the indicator for binary rain status?
Velux windows have a rain proof ventilation function. This ventilation function works with rain proof and closed window with open handle bar.

So the value 93 means closed window and open handle bar (open ventilation). If you close the window to 100% also the handle bar/ventilation will be closed. A value above 93 like i.e. 94 or 96 will always result like the value of 100: complete closed handle bar.

You can open the window while raining for ca. 30% (or so) for 15 minutes with pressing on the original Velux control the up or down button and the center button at the same time. This function is not (known) possible with software via KLF 200.

I have Velux dome windows for flat roofs. There is no handle and there is no ventilation. If its raining, they just won´t open, neither with the remote nor via KLF200.

Roof dome windows are different, so they don’t have a handle (and so no rain proof ventilation).

With KLF200 you will never be able to open a window while it’s raining/the raining sensor recognizes water on it.
With the Velux remote you can press “center”+“up” at the same time. Then it will open for 15 minutes and then close again.

Edit: It can be that the “15 Minutes Option” is not possible for roof dome windows. That’s a Velux decision. Here we were talking about standard flat Velux Integra roof windows for ca. 30° angle roofs.

With Home Assistant 2025.8, this feature has finally been implemented. :tada:

New binary_sensor entities are now available for supported windows, with the suffix _rain_sensor. These entities need to be manually enabled before they become accessible.

One limitation I’ve noticed is that polling currently runs only every 5 minutes. Hopefully, it will be possible to trigger more frequent updates via automation using the homeassistant.update_entity service.

I also wanted this for a long time so I decided to open a new PR. As for the fixed polling interval: While implementing the sensor I also thought about having this configurable, but the fixed interval is basically mandated by HA, an integration following standards cannot make this configurable. OTOH, using homeassistant.update_entity should work, let me know if it doesn’t. More on this here: WTH is there no good way to adjust an integration's polling interval?. BTW, Not updating the sensor toooo often makes a lot of sense especially for solar powered windows because obviously this will have an impact on your battery. How much though I have no idea.

Thank you for this great contribution! :pray: Using homeassistant.update_entity works perfectly. I’m aware that polling too frequently isn’t ideal for the KLF-200, as some users reported it can become unstable. In my case the sensor is mains-powered, so I don’t expect major issues (though I realize battery-powered setups might be different).

One thing I’m unsure about: does calling homeassistant.update_entity on just two rain sensors trigger polling only for those entities, or does it poll all connected devices at once? Right now, I’ve set the update interval to 1 minute, but I might try even more frequent updates if it doesn’t cause problems.

I am not 100% sure which code is triggered within an integration when homeassistant.update_entity is called for an entity. The integration is definitely capable of updating only a single entity, I would expect that you need to poll all devices instead of only a single one.

Great news!

Will it be possible to send a manual override via the KLF200 API (like the genuine VELUX remote) to open the window for 15mins in the future? That will be a great addition!

Yes it would be a great addition, but unfortunately Velux does not provide any documentation on how to do that via the API. So unless someone comes up with a documented way how to do that the answer unfortunately is no, this will not be implemented.

As long as I know and experienced, if Velux is not updating the KLF200 firmware this wont be possible. That’s not a question of 3rd-party developing.

You could take a Velux control PCB and an arduino and simulate the button press. But actual KLF200 firmware is not able to “override” the rain sensor limitation.

Hi, I can’t figure out my issue. I have the latest version of HA with Velux Integration. My window sensor is not reporting rain even though velux API does. I have not been able to trigger debug logs from Velux integration instead I have executed this script – sorry in french –

which reports this
Nom : VeluxCuisine
ID : 9
Classe : Window
Limitation min : 89
Limitation max : 124

While when there is no rain it reports
Nom : VeluxCuisine
ID : 9
Classe : Window
Limitation min : 0
Limitation max : 124

So it seems that limitation.min_value changes between 0 and 89 for my hardware.

I can’t correlate 89 with 13% or 7% I read in this chat so does it mean I have a unsupported hardware that need to be included ?
– note: exposing value_min and value_max directly as attributes as proposed here Velux Rain Sensor - #5 by MaasOne by would allow me to create my own template I wish this would be implemented.

Can you create an issue for the Velux integration in the HA core github repo and attach debug logs for both a situation with and without rain detected (if you can reach your rain sensor you can easily trigger this by using a wet cloth touching the sensor)? I’ll have a look at it then and possibly create a PR with a fix.

If debug logging for some reason does not work, you can also run your script with debug logging enabled and attach those logs, that should do it as well. Although if debug logging does not work then that is probably worth another issue :smiley:

On the wish to have the limitation as an attribute: I can see that, but the core developers of HA want to move away from attributes and would rather represent things in separate entities, so a PR to simply expose the limitation as an attribute will most likely not be accepted.

1 Like