From 3b28bf07d1f920d6997dea196f1b55dca4b1e7a9 Mon Sep 17 00:00:00 2001 From: Marco <46717884+marcodutto@users.noreply.github.com> Date: Fri, 1 Nov 2024 06:08:55 -0400 Subject: [PATCH] Add boost switch to Smarty (#129466) --- homeassistant/components/smarty/__init__.py | 2 +- homeassistant/components/smarty/strings.json | 5 ++ homeassistant/components/smarty/switch.py | 90 +++++++++++++++++++ tests/components/smarty/conftest.py | 2 + .../smarty/snapshots/test_switch.ambr | 47 ++++++++++ tests/components/smarty/test_switch.py | 58 ++++++++++++ 6 files changed, 203 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/smarty/switch.py create mode 100644 tests/components/smarty/snapshots/test_switch.ambr create mode 100644 tests/components/smarty/test_switch.py diff --git a/homeassistant/components/smarty/__init__.py b/homeassistant/components/smarty/__init__.py index cc7215349a6..0e5ca216621 100644 --- a/homeassistant/components/smarty/__init__.py +++ b/homeassistant/components/smarty/__init__.py @@ -30,7 +30,7 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) -PLATFORMS = [Platform.BINARY_SENSOR, Platform.FAN, Platform.SENSOR] +PLATFORMS = [Platform.BINARY_SENSOR, Platform.FAN, Platform.SENSOR, Platform.SWITCH] async def async_setup(hass: HomeAssistant, hass_config: ConfigType) -> bool: diff --git a/homeassistant/components/smarty/strings.json b/homeassistant/components/smarty/strings.json index 367a3a34625..5553a1c0135 100644 --- a/homeassistant/components/smarty/strings.json +++ b/homeassistant/components/smarty/strings.json @@ -61,6 +61,11 @@ "filter_days_left": { "name": "Filter days left" } + }, + "switch": { + "boost": { + "name": "Boost" + } } } } diff --git a/homeassistant/components/smarty/switch.py b/homeassistant/components/smarty/switch.py new file mode 100644 index 00000000000..bf5fe80db44 --- /dev/null +++ b/homeassistant/components/smarty/switch.py @@ -0,0 +1,90 @@ +"""Platform to control a Salda Smarty XP/XV ventilation unit.""" + +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass +import logging +from typing import Any + +from pysmarty2 import Smarty + +from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .coordinator import SmartyConfigEntry, SmartyCoordinator +from .entity import SmartyEntity + +_LOGGER = logging.getLogger(__name__) + + +@dataclass(frozen=True, kw_only=True) +class SmartySwitchDescription(SwitchEntityDescription): + """Class describing Smarty switch.""" + + is_on_fn: Callable[[Smarty], bool] + turn_on_fn: Callable[[Smarty], bool | None] + turn_off_fn: Callable[[Smarty], bool | None] + + +ENTITIES: tuple[SmartySwitchDescription, ...] = ( + SmartySwitchDescription( + key="boost", + translation_key="boost", + is_on_fn=lambda smarty: smarty.boost, + turn_on_fn=lambda smarty: smarty.enable_boost(), + turn_off_fn=lambda smarty: smarty.disable_boost(), + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: SmartyConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Smarty Switch Platform.""" + + coordinator = entry.runtime_data + + async_add_entities( + SmartySwitch(coordinator, description) for description in ENTITIES + ) + + +class SmartySwitch(SmartyEntity, SwitchEntity): + """Representation of a Smarty Switch.""" + + entity_description: SmartySwitchDescription + + def __init__( + self, + coordinator: SmartyCoordinator, + entity_description: SmartySwitchDescription, + ) -> None: + """Initialize the entity.""" + super().__init__(coordinator) + self.entity_description = entity_description + self._attr_unique_id = ( + f"{coordinator.config_entry.entry_id}_{entity_description.key}" + ) + + @property + def is_on(self) -> bool: + """Return the state of the switch.""" + return self.entity_description.is_on_fn(self.coordinator.client) + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn the switch on.""" + await self.hass.async_add_executor_job( + self.entity_description.turn_on_fn, self.coordinator.client + ) + await self.coordinator.async_refresh() + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the switch off.""" + await self.hass.async_add_executor_job( + self.entity_description.turn_off_fn, self.coordinator.client + ) + await self.coordinator.async_refresh() diff --git a/tests/components/smarty/conftest.py b/tests/components/smarty/conftest.py index c62097f0516..c61ec4b1022 100644 --- a/tests/components/smarty/conftest.py +++ b/tests/components/smarty/conftest.py @@ -40,6 +40,8 @@ def mock_smarty() -> Generator[AsyncMock]: client.warning = False client.alarm = False client.boost = False + client.enable_boost.return_value = True + client.disable_boost.return_value = True client.supply_air_temperature = 20 client.extract_air_temperature = 23 client.outdoor_air_temperature = 24 diff --git a/tests/components/smarty/snapshots/test_switch.ambr b/tests/components/smarty/snapshots/test_switch.ambr new file mode 100644 index 00000000000..be1da7c6961 --- /dev/null +++ b/tests/components/smarty/snapshots/test_switch.ambr @@ -0,0 +1,47 @@ +# serializer version: 1 +# name: test_all_entities[switch.mock_title_boost-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.mock_title_boost', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Boost', + 'platform': 'smarty', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'boost', + 'unique_id': '01JAZ5DPW8C62D620DGYNG2R8H_boost', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[switch.mock_title_boost-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Mock Title Boost', + }), + 'context': , + 'entity_id': 'switch.mock_title_boost', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- diff --git a/tests/components/smarty/test_switch.py b/tests/components/smarty/test_switch.py new file mode 100644 index 00000000000..1a6748e2d23 --- /dev/null +++ b/tests/components/smarty/test_switch.py @@ -0,0 +1,58 @@ +"""Tests for the Smarty switch platform.""" + +from unittest.mock import AsyncMock, patch + +from syrupy import SnapshotAssertion + +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN +from homeassistant.const import ( + ATTR_ENTITY_ID, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + Platform, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import setup_integration + +from tests.common import MockConfigEntry, snapshot_platform + + +async def test_all_entities( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + mock_smarty: AsyncMock, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, +) -> None: + """Test all entities.""" + with patch("homeassistant.components.smarty.PLATFORMS", [Platform.SWITCH]): + await setup_integration(hass, mock_config_entry) + + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + + +async def test_setting_value( + hass: HomeAssistant, + mock_smarty: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test setting value.""" + await setup_integration(hass, mock_config_entry) + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + target={ATTR_ENTITY_ID: "switch.mock_title_boost"}, + blocking=True, + ) + mock_smarty.enable_boost.assert_called_once_with() + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + target={ATTR_ENTITY_ID: "switch.mock_title_boost"}, + blocking=True, + ) + mock_smarty.disable_boost.assert_called_once_with()