diff --git a/homeassistant/components/velux/light.py b/homeassistant/components/velux/light.py index f983f2bf5b1..d1d6b4712ce 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 +from pyvlx import Intensity, Light, OnOffLight from homeassistant.components.light import ATTR_BRIGHTNESS, ColorMode, LightEntity from homeassistant.core import HomeAssistant @@ -26,7 +26,7 @@ async def async_setup_entry( async_add_entities( VeluxLight(node, config_entry.entry_id) for node in pyvlx.nodes - if isinstance(node, Light) + if isinstance(node, (Light, OnOffLight)) ) @@ -42,7 +42,7 @@ class VeluxLight(VeluxEntity, LightEntity): @property def brightness(self): """Return the current brightness.""" - return int((100 - self.node.intensity.intensity_percent) * 255 / 100) + return int(self.node.intensity.intensity_percent * 255 / 100) @property def is_on(self): @@ -53,7 +53,7 @@ class VeluxLight(VeluxEntity, LightEntity): 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) + intensity_percent = int(kwargs[ATTR_BRIGHTNESS] / 255 * 100) await self.node.set_intensity( Intensity(intensity_percent=intensity_percent), wait_for_completion=True, diff --git a/homeassistant/components/velux/manifest.json b/homeassistant/components/velux/manifest.json index cc5089712c1..6cd3720927c 100644 --- a/homeassistant/components/velux/manifest.json +++ b/homeassistant/components/velux/manifest.json @@ -13,5 +13,5 @@ "integration_type": "hub", "iot_class": "local_polling", "loggers": ["pyvlx"], - "requirements": ["pyvlx==0.2.28"] + "requirements": ["pyvlx==0.2.29"] } diff --git a/requirements_all.txt b/requirements_all.txt index 375cc8642bb..c2a00e015ea 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2682,7 +2682,7 @@ pyvesync==3.4.1 pyvizio==0.1.61 # homeassistant.components.velux -pyvlx==0.2.28 +pyvlx==0.2.29 # homeassistant.components.volumio pyvolumio==0.1.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f6aef8be0d8..08158046f1e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2260,7 +2260,7 @@ pyvesync==3.4.1 pyvizio==0.1.61 # homeassistant.components.velux -pyvlx==0.2.28 +pyvlx==0.2.29 # homeassistant.components.volumio pyvolumio==0.1.5 diff --git a/tests/components/velux/conftest.py b/tests/components/velux/conftest.py index 954eb378ade..41a58ddd724 100644 --- a/tests/components/velux/conftest.py +++ b/tests/components/velux/conftest.py @@ -4,11 +4,9 @@ from collections.abc import Generator from unittest.mock import AsyncMock, MagicMock, patch import pytest -from pyvlx.lightening_device import Light -from pyvlx.opening_device import Blind, Window +from pyvlx import Blind, Light, OnOffLight, Scene, Window from homeassistant.components.velux import DOMAIN -from homeassistant.components.velux.scene import PyVLXScene as Scene from homeassistant.const import CONF_HOST, CONF_MAC, CONF_PASSWORD, Platform from homeassistant.core import HomeAssistant @@ -104,6 +102,18 @@ def mock_light() -> AsyncMock: return light +# a light without intensity support (e.g., a simple on/off light) +@pytest.fixture +def mock_onoff_light() -> AsyncMock: + """Create a mock Velux light.""" + light = AsyncMock(spec=OnOffLight, autospec=True) + light.name = "Test On Off Light" + light.serial_number = "0816" + light.intensity = MagicMock() + light.pyvlx = MagicMock() + return light + + # fixture to create all other cover types via parameterization @pytest.fixture def mock_cover_type(request: pytest.FixtureRequest) -> AsyncMock: @@ -122,6 +132,7 @@ def mock_cover_type(request: pytest.FixtureRequest) -> AsyncMock: def mock_pyvlx( mock_scene: AsyncMock, mock_light: AsyncMock, + mock_onoff_light: AsyncMock, mock_window: AsyncMock, mock_blind: AsyncMock, request: pytest.FixtureRequest, @@ -138,7 +149,13 @@ def mock_pyvlx( if hasattr(request, "param"): pyvlx.nodes = [request.getfixturevalue(request.param)] else: - pyvlx.nodes = [mock_light, mock_blind, mock_window, mock_cover_type] + pyvlx.nodes = [ + mock_light, + mock_onoff_light, + mock_blind, + mock_window, + mock_cover_type, + ] pyvlx.scenes = [mock_scene] diff --git a/tests/components/velux/snapshots/test_light.ambr b/tests/components/velux/snapshots/test_light.ambr new file mode 100644 index 00000000000..a4e0247b58d --- /dev/null +++ b/tests/components/velux/snapshots/test_light.ambr @@ -0,0 +1,119 @@ +# serializer version: 1 +# name: test_light_setup[mock_light][light.test_light-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'supported_color_modes': list([ + , + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.test_light', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'velux', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '0815', + 'unit_of_measurement': None, + }) +# --- +# name: test_light_setup[mock_light][light.test_light-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'brightness': None, + 'color_mode': None, + 'friendly_name': 'Test Light', + 'supported_color_modes': list([ + , + ]), + 'supported_features': , + }), + 'context': , + 'entity_id': 'light.test_light', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_light_setup[mock_onoff_light][light.test_on_off_light-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'supported_color_modes': list([ + , + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.test_on_off_light', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'velux', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '0816', + 'unit_of_measurement': None, + }) +# --- +# 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': , + }), + 'context': , + 'entity_id': 'light.test_on_off_light', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- diff --git a/tests/components/velux/test_light.py b/tests/components/velux/test_light.py index 268dc670c94..63fa15d6f18 100644 --- a/tests/components/velux/test_light.py +++ b/tests/components/velux/test_light.py @@ -16,7 +16,7 @@ from homeassistant.helpers import device_registry as dr, entity_registry as er from . import update_callback_entity -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, SnapshotAssertion, snapshot_platform # Apply setup_integration fixture to all tests in this module pytestmark = pytest.mark.usefixtures("setup_integration") @@ -28,21 +28,34 @@ def platform() -> Platform: return Platform.LIGHT +@pytest.mark.parametrize( + "mock_pyvlx", ["mock_light", "mock_onoff_light"], indirect=True +) async def test_light_setup( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, + snapshot: SnapshotAssertion, +) -> None: + """Snapshot the entity and validate registry metadata for light entities.""" + await snapshot_platform( + hass, + entity_registry, + snapshot, + mock_config_entry.entry_id, + ) + + +async def test_light_device_association( hass: HomeAssistant, entity_registry: er.EntityRegistry, device_registry: dr.DeviceRegistry, mock_light: AsyncMock, ) -> None: - """Test light entity setup and device association.""" + """Test light device association.""" test_entity_id = f"light.{mock_light.name.lower().replace(' ', '_')}" - # Check that the entity exists and its name matches the node name (the light is the main feature). - state = hass.states.get(test_entity_id) - assert state is not None - assert state.attributes.get("friendly_name") == mock_light.name - # Get entity + device entry entity_entry = entity_registry.async_get(test_entity_id) assert entity_entry is not None @@ -137,7 +150,7 @@ async def test_light_brightness_and_is_on( entity_id = f"light.{mock_light.name.lower().replace(' ', '_')}" # Set initial intensity values - mock_light.intensity.intensity_percent = 20 # 20% "intensity" -> 80% brightness + mock_light.intensity.intensity_percent = 20 # 20% "intensity" -> 20% brightness mock_light.intensity.off = False mock_light.intensity.known = True @@ -146,8 +159,8 @@ async def test_light_brightness_and_is_on( state = hass.states.get(entity_id) assert state is not None - # brightness = int((100 - 20) * 255 / 100) = int(204) - assert state.attributes.get("brightness") == 204 + # brightness = int(20 * 255 / 100) = int(51) + assert state.attributes.get("brightness") == 51 assert state.state == "on" # Mark as off @@ -161,7 +174,7 @@ async def test_light_brightness_and_is_on( async def test_light_turn_on_with_brightness_uses_set_intensity( hass: HomeAssistant, mock_light: AsyncMock ) -> None: - """Turning on with brightness calls set_intensity with inverted percent.""" + """Turning on with brightness calls set_intensity with percent.""" entity_id = f"light.{mock_light.name.lower().replace(' ', '_')}" @@ -180,8 +193,8 @@ async def test_light_turn_on_with_brightness_uses_set_intensity( # Inspect the intensity argument (first positional) args, kwargs = mock_light.set_intensity.await_args intensity_obj = args[0] - # brightness 51 -> 20% normalized -> intensity_percent = 80 - assert intensity_obj.intensity_percent == 80 + # brightness 51 -> 20% normalized -> intensity_percent = 20 + assert intensity_obj.intensity_percent == 20 assert kwargs.get("wait_for_completion") is True