diff --git a/homeassistant/components/vallox/__init__.py b/homeassistant/components/vallox/__init__.py index 6a2234af9e6..1a1788aeeed 100644 --- a/homeassistant/components/vallox/__init__.py +++ b/homeassistant/components/vallox/__init__.py @@ -63,6 +63,7 @@ PLATFORMS: list[str] = [ Platform.SENSOR, Platform.FAN, Platform.BINARY_SENSOR, + Platform.SWITCH, ] ATTR_PROFILE_FAN_SPEED = "fan_speed" diff --git a/homeassistant/components/vallox/switch.py b/homeassistant/components/vallox/switch.py new file mode 100644 index 00000000000..4c8116ebd27 --- /dev/null +++ b/homeassistant/components/vallox/switch.py @@ -0,0 +1,105 @@ +"""Support for Vallox ventilation unit switches.""" +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + +from vallox_websocket_api import Vallox + +from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import ValloxDataUpdateCoordinator, ValloxEntity +from .const import DOMAIN + + +class ValloxSwitchEntity(ValloxEntity, SwitchEntity): + """Representation of a Vallox switch.""" + + entity_description: ValloxSwitchEntityDescription + _attr_entity_category = EntityCategory.CONFIG + _attr_has_entity_name = True + + def __init__( + self, + name: str, + coordinator: ValloxDataUpdateCoordinator, + description: ValloxSwitchEntityDescription, + client: Vallox, + ) -> None: + """Initialize the Vallox switch.""" + super().__init__(name, coordinator) + + self.entity_description = description + + self._attr_unique_id = f"{self._device_uuid}-{description.key}" + self._client = client + + @property + def is_on(self) -> bool | None: + """Return true if the switch is on.""" + if ( + value := self.coordinator.data.get_metric( + self.entity_description.metric_key + ) + ) is None: + return None + return value == 1 + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn on.""" + await self._set_value(True) + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn off.""" + await self._set_value(False) + + async def _set_value(self, value: bool) -> None: + """Update the current value.""" + metric_key = self.entity_description.metric_key + await self._client.set_values({metric_key: 1 if value else 0}) + await self.coordinator.async_request_refresh() + + +@dataclass +class ValloxMetricKeyMixin: + """Dataclass to allow defining metric_key without a default value.""" + + metric_key: str + + +@dataclass +class ValloxSwitchEntityDescription(SwitchEntityDescription, ValloxMetricKeyMixin): + """Describes Vallox switch entity.""" + + +SWITCH_ENTITIES: tuple[ValloxSwitchEntityDescription, ...] = ( + ValloxSwitchEntityDescription( + key="bypass_locked", + name="Bypass locked", + icon="mdi:arrow-horizontal-lock", + metric_key="A_CYC_BYPASS_LOCKED", + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the switches.""" + + data = hass.data[DOMAIN][entry.entry_id] + client = data["client"] + client.set_settable_address("A_CYC_BYPASS_LOCKED", int) + + async_add_entities( + [ + ValloxSwitchEntity(data["name"], data["coordinator"], description, client) + for description in SWITCH_ENTITIES + ] + ) diff --git a/tests/components/vallox/conftest.py b/tests/components/vallox/conftest.py index ef9fd2a0e4b..0c14f359b5f 100644 --- a/tests/components/vallox/conftest.py +++ b/tests/components/vallox/conftest.py @@ -39,6 +39,11 @@ def patch_metrics(metrics: dict[str, Any]): ) +def patch_metrics_set(): + """Patch the Vallox metrics set values.""" + return patch("homeassistant.components.vallox.Vallox.set_values") + + @pytest.fixture(autouse=True) def patch_profile_home(): """Patch the Vallox profile response.""" diff --git a/tests/components/vallox/test_switch.py b/tests/components/vallox/test_switch.py new file mode 100644 index 00000000000..5a3f0ebd648 --- /dev/null +++ b/tests/components/vallox/test_switch.py @@ -0,0 +1,68 @@ +"""Tests for Vallox switch platform.""" +import pytest + +from homeassistant.components.switch.const import DOMAIN as SWITCH_DOMAIN +from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON +from homeassistant.core import HomeAssistant + +from .conftest import patch_metrics, patch_metrics_set + +from tests.common import MockConfigEntry + + +@pytest.mark.parametrize( + "entity_id, metric_key, value, expected_state", + [ + ("switch.vallox_bypass_locked", "A_CYC_BYPASS_LOCKED", 1, "on"), + ("switch.vallox_bypass_locked", "A_CYC_BYPASS_LOCKED", 0, "off"), + ], +) +async def test_switch_entities( + entity_id: str, + metric_key: str, + value: int, + expected_state: str, + mock_entry: MockConfigEntry, + hass: HomeAssistant, +) -> None: + """Test switch entities.""" + # Arrange + metrics = {metric_key: value} + + # Act + with patch_metrics(metrics=metrics): + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + + # Assert + sensor = hass.states.get(entity_id) + assert sensor + assert sensor.state == expected_state + + +@pytest.mark.parametrize( + "service, metric_key, value", + [ + (SERVICE_TURN_ON, "A_CYC_BYPASS_LOCKED", 1), + (SERVICE_TURN_OFF, "A_CYC_BYPASS_LOCKED", 0), + ], +) +async def test_bypass_lock_switch_entitity_set( + service: str, + metric_key: str, + value: int, + mock_entry: MockConfigEntry, + hass: HomeAssistant, +) -> None: + """Test bypass lock switch set.""" + # Act + with patch_metrics(metrics={}), patch_metrics_set() as metrics_set: + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + await hass.services.async_call( + SWITCH_DOMAIN, + service, + service_data={ATTR_ENTITY_ID: "switch.vallox_bypass_locked"}, + ) + await hass.async_block_till_done() + metrics_set.assert_called_once_with({metric_key: value})