1
0
mirror of https://github.com/home-assistant/core.git synced 2026-02-15 07:36:16 +00:00

add switch platform for Velux on/off switches (#163002)

This commit is contained in:
wollew
2026-02-14 15:36:51 +01:00
committed by GitHub
parent 675884ad78
commit 89e900dca1
5 changed files with 253 additions and 1 deletions

View File

@@ -11,5 +11,6 @@ PLATFORMS = [
Platform.COVER,
Platform.LIGHT,
Platform.SCENE,
Platform.SWITCH,
]
LOGGER = getLogger(__package__)

View File

@@ -0,0 +1,53 @@
"""Support for Velux switches."""
from __future__ import annotations
from typing import Any
from pyvlx import OnOffSwitch
from homeassistant.components.switch import SwitchEntity
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import VeluxConfigEntry
from .entity import VeluxEntity, wrap_pyvlx_call_exceptions
PARALLEL_UPDATES = 1
async def async_setup_entry(
hass: HomeAssistant,
config_entry: VeluxConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up switch(es) for Velux platform."""
pyvlx = config_entry.runtime_data
async_add_entities(
VeluxOnOffSwitch(node, config_entry.entry_id)
for node in pyvlx.nodes
if isinstance(node, OnOffSwitch)
)
class VeluxOnOffSwitch(VeluxEntity, SwitchEntity):
"""Representation of a Velux on/off switch."""
_attr_name = None
node: OnOffSwitch
@property
def is_on(self) -> bool:
"""Return true if switch is on."""
return self.node.is_on()
@wrap_pyvlx_call_exceptions
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the switch on."""
await self.node.set_on()
@wrap_pyvlx_call_exceptions
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the switch off."""
await self.node.set_off()

View File

@@ -4,7 +4,7 @@ from collections.abc import Generator
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from pyvlx import Blind, Light, OnOffLight, Scene, Window
from pyvlx import Blind, Light, OnOffLight, OnOffSwitch, Scene, Window
from homeassistant.components.velux import DOMAIN
from homeassistant.const import CONF_HOST, CONF_MAC, CONF_PASSWORD, Platform
@@ -114,6 +114,19 @@ def mock_onoff_light() -> AsyncMock:
return light
# an on/off switch
@pytest.fixture
def mock_onoff_switch() -> AsyncMock:
"""Create a mock Velux on/off switch."""
switch = AsyncMock(spec=OnOffSwitch, autospec=True)
switch.name = "Test On Off Switch"
switch.serial_number = "0817"
switch.is_on.return_value = False
switch.is_off.return_value = True
switch.pyvlx = MagicMock()
return switch
# fixture to create all other cover types via parameterization
@pytest.fixture
def mock_cover_type(request: pytest.FixtureRequest) -> AsyncMock:
@@ -133,6 +146,7 @@ def mock_pyvlx(
mock_scene: AsyncMock,
mock_light: AsyncMock,
mock_onoff_light: AsyncMock,
mock_onoff_switch: AsyncMock,
mock_window: AsyncMock,
mock_blind: AsyncMock,
request: pytest.FixtureRequest,
@@ -152,6 +166,7 @@ def mock_pyvlx(
pyvlx.nodes = [
mock_light,
mock_onoff_light,
mock_onoff_switch,
mock_blind,
mock_window,
mock_cover_type,

View File

@@ -0,0 +1,50 @@
# serializer version: 1
# name: test_switch_setup[mock_onoff_switch][switch.test_on_off_switch-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'switch',
'entity_category': None,
'entity_id': 'switch.test_on_off_switch',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'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': '0817',
'unit_of_measurement': None,
})
# ---
# name: test_switch_setup[mock_onoff_switch][switch.test_on_off_switch-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Test On Off Switch',
}),
'context': <ANY>,
'entity_id': 'switch.test_on_off_switch',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---

View File

@@ -0,0 +1,133 @@
"""Test Velux switch entities."""
from unittest.mock import AsyncMock
import pytest
from pyvlx import PyVLXException
from homeassistant.components.switch import (
DOMAIN as SWITCH_DOMAIN,
SERVICE_TURN_OFF,
SERVICE_TURN_ON,
)
from homeassistant.const import STATE_OFF, STATE_ON, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import device_registry as dr, entity_registry as er
from . import update_callback_entity
from tests.common import MockConfigEntry, SnapshotAssertion, snapshot_platform
# Apply setup_integration fixture to all tests in this module
pytestmark = pytest.mark.usefixtures("setup_integration")
@pytest.fixture
def platform() -> Platform:
"""Fixture to specify platform to test."""
return Platform.SWITCH
@pytest.mark.parametrize("mock_pyvlx", ["mock_onoff_switch"], indirect=True)
async def test_switch_setup(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
entity_registry: er.EntityRegistry,
mock_pyvlx: AsyncMock,
snapshot: SnapshotAssertion,
) -> None:
"""Snapshot the entity and validate registry metadata for switch entities."""
await snapshot_platform(
hass,
entity_registry,
snapshot,
mock_config_entry.entry_id,
)
async def test_switch_device_association(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
device_registry: dr.DeviceRegistry,
mock_onoff_switch: AsyncMock,
) -> None:
"""Test switch device association."""
test_entity_id = f"switch.{mock_onoff_switch.name.lower().replace(' ', '_')}"
entity_entry = entity_registry.async_get(test_entity_id)
assert entity_entry is not None
assert entity_entry.device_id is not None
device_entry = device_registry.async_get(entity_entry.device_id)
assert device_entry is not None
assert ("velux", mock_onoff_switch.serial_number) in device_entry.identifiers
assert device_entry.name == mock_onoff_switch.name
async def test_switch_is_on(hass: HomeAssistant, mock_onoff_switch: AsyncMock) -> None:
"""Test switch on state."""
entity_id = f"switch.{mock_onoff_switch.name.lower().replace(' ', '_')}"
# Initial state is off
state = hass.states.get(entity_id)
assert state is not None
assert state.state == STATE_OFF
# Simulate switching on
mock_onoff_switch.is_on.return_value = True
mock_onoff_switch.is_off.return_value = False
await update_callback_entity(hass, mock_onoff_switch)
state = hass.states.get(entity_id)
assert state is not None
assert state.state == STATE_ON
async def test_switch_turn_on_off(
hass: HomeAssistant, mock_onoff_switch: AsyncMock
) -> None:
"""Test turning switch on."""
entity_id = f"switch.{mock_onoff_switch.name.lower().replace(' ', '_')}"
await hass.services.async_call(
SWITCH_DOMAIN,
SERVICE_TURN_ON,
{"entity_id": entity_id},
blocking=True,
)
mock_onoff_switch.set_on.assert_awaited_once()
await hass.services.async_call(
SWITCH_DOMAIN,
SERVICE_TURN_OFF,
{"entity_id": entity_id},
blocking=True,
)
mock_onoff_switch.set_off.assert_awaited_once()
@pytest.mark.parametrize("mock_pyvlx", ["mock_onoff_switch"], indirect=True)
async def test_switch_error_handling(
hass: HomeAssistant, mock_onoff_switch: AsyncMock
) -> None:
"""Test error handling when turning switching fails."""
entity_id = f"switch.{mock_onoff_switch.name.lower().replace(' ', '_')}"
mock_onoff_switch.set_on.side_effect = PyVLXException("Connection lost")
mock_onoff_switch.set_off.side_effect = PyVLXException("Connection lost")
with pytest.raises(HomeAssistantError):
await hass.services.async_call(
SWITCH_DOMAIN,
SERVICE_TURN_ON,
{"entity_id": entity_id},
blocking=True,
)
with pytest.raises(HomeAssistantError):
await hass.services.async_call(
SWITCH_DOMAIN,
SERVICE_TURN_OFF,
{"entity_id": entity_id},
blocking=True,
)