From 88c6cb3877fd9a212206327ea95954252d136bd7 Mon Sep 17 00:00:00 2001 From: wollew Date: Fri, 13 Feb 2026 19:42:44 +0100 Subject: [PATCH] add OnOffLight without brightness control to velux integration (#162835) --- homeassistant/components/velux/light.py | 57 ++++++++++++------- .../velux/snapshots/test_light.ambr | 5 +- tests/components/velux/test_light.py | 45 +++++++++------ 3 files changed, 65 insertions(+), 42 deletions(-) diff --git a/homeassistant/components/velux/light.py b/homeassistant/components/velux/light.py index d1d6b4712ce..163403ddf9d 100644 --- a/homeassistant/components/velux/light.py +++ b/homeassistant/components/velux/light.py @@ -4,7 +4,7 @@ from __future__ import annotations from typing import Any -from pyvlx import Intensity, Light, OnOffLight +from pyvlx import DimmableDevice, Intensity, Light, OnOffLight from homeassistant.components.light import ATTR_BRIGHTNESS, ColorMode, LightEntity from homeassistant.core import HomeAssistant @@ -23,32 +23,52 @@ async def async_setup_entry( ) -> None: """Set up light(s) for Velux platform.""" pyvlx = config_entry.runtime_data - async_add_entities( - VeluxLight(node, config_entry.entry_id) - for node in pyvlx.nodes - if isinstance(node, (Light, OnOffLight)) - ) + entities: list[VeluxOnOffLight] = [] + for node in pyvlx.nodes: + if isinstance(node, Light): + entities.append(VeluxDimmableLight(node, config_entry.entry_id)) + elif isinstance(node, OnOffLight): + entities.append(VeluxOnOffLight(node, config_entry.entry_id)) + async_add_entities(entities) -class VeluxLight(VeluxEntity, LightEntity): - """Representation of a Velux light.""" +class VeluxOnOffLight(VeluxEntity, LightEntity): + """Representation of a Velux light without brightness control.""" + + _attr_supported_color_modes = {ColorMode.ONOFF} + _attr_color_mode = ColorMode.ONOFF + _attr_name = None + + node: DimmableDevice + + @property + def is_on(self) -> bool: + """Return true if light is on.""" + return not self.node.intensity.off and self.node.intensity.known + + @wrap_pyvlx_call_exceptions + async def async_turn_on(self, **kwargs: Any) -> None: + """Instruct the light to turn on.""" + await self.node.turn_on(wait_for_completion=True) + + @wrap_pyvlx_call_exceptions + async def async_turn_off(self, **kwargs: Any) -> None: + """Instruct the light to turn off.""" + await self.node.turn_off(wait_for_completion=True) + + +class VeluxDimmableLight(VeluxOnOffLight): + """Representation of a Velux light with brightness control.""" _attr_supported_color_modes = {ColorMode.BRIGHTNESS} _attr_color_mode = ColorMode.BRIGHTNESS _attr_name = None - node: Light - @property - def brightness(self): + def brightness(self) -> int: """Return the current brightness.""" return int(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 - @wrap_pyvlx_call_exceptions async def async_turn_on(self, **kwargs: Any) -> None: """Instruct the light to turn on.""" @@ -60,8 +80,3 @@ class VeluxLight(VeluxEntity, LightEntity): ) else: await self.node.turn_on(wait_for_completion=True) - - @wrap_pyvlx_call_exceptions - async def async_turn_off(self, **kwargs: Any) -> None: - """Instruct the light to turn off.""" - await self.node.turn_off(wait_for_completion=True) diff --git a/tests/components/velux/snapshots/test_light.ambr b/tests/components/velux/snapshots/test_light.ambr index a4e0247b58d..1f6931fc169 100644 --- a/tests/components/velux/snapshots/test_light.ambr +++ b/tests/components/velux/snapshots/test_light.ambr @@ -65,7 +65,7 @@ 'area_id': None, 'capabilities': dict({ 'supported_color_modes': list([ - , + , ]), }), 'config_entry_id': , @@ -101,11 +101,10 @@ # name: test_light_setup[mock_onoff_light][light.test_on_off_light-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'brightness': None, 'color_mode': None, 'friendly_name': 'Test On Off Light', 'supported_color_modes': list([ - , + , ]), 'supported_features': , }), diff --git a/tests/components/velux/test_light.py b/tests/components/velux/test_light.py index 63fa15d6f18..b7ef427f60d 100644 --- a/tests/components/velux/test_light.py +++ b/tests/components/velux/test_light.py @@ -1,6 +1,6 @@ """Test Velux light entities.""" -from unittest.mock import AsyncMock +from unittest.mock import AsyncMock, MagicMock import pytest @@ -10,7 +10,7 @@ from homeassistant.components.light import ( SERVICE_TURN_OFF, SERVICE_TURN_ON, ) -from homeassistant.const import Platform +from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er @@ -107,14 +107,14 @@ async def test_entity_availability( await update_callback_entity(hass, mock_light) state = hass.states.get(entity_id) assert state is not None - assert state.state != "unavailable" + assert state.state != STATE_UNAVAILABLE # Simulate disconnection mock_light.pyvlx.get_connected.return_value = False await update_callback_entity(hass, mock_light) state = hass.states.get(entity_id) assert state is not None - assert state.state == "unavailable" + assert state.state == STATE_UNAVAILABLE assert caplog.text.count(f"Entity {entity_id} is unavailable") == 1 # Simulate disconnection, check we don't log again @@ -122,7 +122,7 @@ async def test_entity_availability( await update_callback_entity(hass, mock_light) state = hass.states.get(entity_id) assert state is not None - assert state.state == "unavailable" + assert state.state == STATE_UNAVAILABLE assert caplog.text.count(f"Entity {entity_id} is unavailable") == 1 # Simulate reconnection @@ -130,7 +130,7 @@ async def test_entity_availability( await update_callback_entity(hass, mock_light) state = hass.states.get(entity_id) assert state is not None - assert state.state != "unavailable" + assert state.state != STATE_UNAVAILABLE assert caplog.text.count(f"Entity {entity_id} is back online") == 1 # Simulate reconnection, check we don't log again @@ -138,7 +138,7 @@ async def test_entity_availability( await update_callback_entity(hass, mock_light) state = hass.states.get(entity_id) assert state is not None - assert state.state != "unavailable" + assert state.state != STATE_UNAVAILABLE assert caplog.text.count(f"Entity {entity_id} is back online") == 1 @@ -160,15 +160,15 @@ async def test_light_brightness_and_is_on( state = hass.states.get(entity_id) assert state is not None # brightness = int(20 * 255 / 100) = int(51) - assert state.attributes.get("brightness") == 51 - assert state.state == "on" + assert state.attributes.get(ATTR_BRIGHTNESS) == 51 + assert state.state == STATE_ON # Mark as off mock_light.intensity.off = True await update_callback_entity(hass, mock_light) state = hass.states.get(entity_id) assert state is not None - assert state.state == "off" + assert state.state == STATE_OFF async def test_light_turn_on_with_brightness_uses_set_intensity( @@ -198,12 +198,16 @@ async def test_light_turn_on_with_brightness_uses_set_intensity( assert kwargs.get("wait_for_completion") is True +@pytest.mark.parametrize( + "mock_pyvlx", ["mock_light", "mock_onoff_light"], indirect=True +) async def test_light_turn_on_without_brightness_calls_turn_on( - hass: HomeAssistant, mock_light: AsyncMock + hass: HomeAssistant, mock_pyvlx: MagicMock ) -> None: - """Turning on without brightness uses device.turn_on.""" + """Turning on without brightness uses node.turn_on.""" - entity_id = f"light.{mock_light.name.lower().replace(' ', '_')}" + node = mock_pyvlx.nodes[0] + entity_id = f"light.{node.name.lower().replace(' ', '_')}" await hass.services.async_call( LIGHT_DOMAIN, @@ -212,16 +216,20 @@ async def test_light_turn_on_without_brightness_calls_turn_on( blocking=True, ) - mock_light.turn_on.assert_awaited_once_with(wait_for_completion=True) - assert mock_light.set_intensity.await_count == 0 + node.turn_on.assert_awaited_once_with(wait_for_completion=True) + assert node.set_intensity.await_count == 0 +@pytest.mark.parametrize( + "mock_pyvlx", ["mock_light", "mock_onoff_light"], indirect=True +) async def test_light_turn_off_calls_turn_off( - hass: HomeAssistant, mock_light: AsyncMock + hass: HomeAssistant, mock_pyvlx: MagicMock ) -> None: """Turning off calls device.turn_off with wait_for_completion.""" - entity_id = f"light.{mock_light.name.lower().replace(' ', '_')}" + node = mock_pyvlx.nodes[0] + entity_id = f"light.{node.name.lower().replace(' ', '_')}" await hass.services.async_call( LIGHT_DOMAIN, @@ -230,4 +238,5 @@ async def test_light_turn_off_calls_turn_off( blocking=True, ) - mock_light.turn_off.assert_awaited_once_with(wait_for_completion=True) + node.turn_off.assert_awaited_once_with(wait_for_completion=True) + assert node.set_intensity.await_count == 0