From bde3b6e59fdfe940faebb0e6df5d97227e18d0ba Mon Sep 17 00:00:00 2001 From: Ermanno Baschiera Date: Thu, 4 Jun 2026 13:15:31 +0200 Subject: [PATCH] Add filter reset button to Helty Flow (#172866) Co-authored-by: Claude Opus 4.8 --- homeassistant/components/helty/__init__.py | 2 +- homeassistant/components/helty/button.py | 47 +++++++++++++ homeassistant/components/helty/fan.py | 13 +++- .../components/helty/quality_scale.yaml | 4 +- homeassistant/components/helty/strings.json | 13 ++++ .../helty/snapshots/test_button.ambr | 51 ++++++++++++++ tests/components/helty/test_button.py | 69 +++++++++++++++++++ tests/components/helty/test_fan.py | 22 +++++- 8 files changed, 213 insertions(+), 8 deletions(-) create mode 100644 homeassistant/components/helty/button.py create mode 100644 tests/components/helty/snapshots/test_button.ambr create mode 100644 tests/components/helty/test_button.py diff --git a/homeassistant/components/helty/__init__.py b/homeassistant/components/helty/__init__.py index 5e70696bd00c..ce6edf055ccc 100644 --- a/homeassistant/components/helty/__init__.py +++ b/homeassistant/components/helty/__init__.py @@ -7,7 +7,7 @@ from homeassistant.core import HomeAssistant from .coordinator import HeltyConfigEntry, HeltyDataUpdateCoordinator -PLATFORMS: list[Platform] = [Platform.FAN, Platform.SENSOR] +PLATFORMS: list[Platform] = [Platform.BUTTON, Platform.FAN, Platform.SENSOR] async def async_setup_entry(hass: HomeAssistant, entry: HeltyConfigEntry) -> bool: diff --git a/homeassistant/components/helty/button.py b/homeassistant/components/helty/button.py new file mode 100644 index 000000000000..abe5b2a9338c --- /dev/null +++ b/homeassistant/components/helty/button.py @@ -0,0 +1,47 @@ +"""Button platform for the Helty Flow integration.""" + +from pyhelty import HeltyError + +from homeassistant.components.button import ButtonEntity +from homeassistant.const import EntityCategory +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback + +from .const import DOMAIN +from .coordinator import HeltyConfigEntry, HeltyDataUpdateCoordinator +from .entity import HeltyEntity + +PARALLEL_UPDATES = 0 + + +async def async_setup_entry( + hass: HomeAssistant, + entry: HeltyConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Set up the Helty buttons.""" + async_add_entities([HeltyResetFilterButton(entry.runtime_data)]) + + +class HeltyResetFilterButton(HeltyEntity, ButtonEntity): + """Resets the filter-life counter after the filter has been replaced.""" + + _attr_entity_category = EntityCategory.CONFIG + _attr_translation_key = "reset_filter" + + def __init__(self, coordinator: HeltyDataUpdateCoordinator) -> None: + """Initialize the button.""" + super().__init__(coordinator) + self._attr_unique_id = f"{self._device_id}_reset_filter" + + async def async_press(self) -> None: + """Reset the filter-life counter.""" + try: + await self.coordinator.client.async_reset_filter() + except HeltyError as err: + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="reset_filter_failed", + ) from err + await self.coordinator.async_request_refresh() diff --git a/homeassistant/components/helty/fan.py b/homeassistant/components/helty/fan.py index 1c22bddf9954..a3aab27212c5 100644 --- a/homeassistant/components/helty/fan.py +++ b/homeassistant/components/helty/fan.py @@ -2,17 +2,18 @@ from typing import Any -from pyhelty import FanMode +from pyhelty import FanMode, HeltyError from homeassistant.components.fan import FanEntity, FanEntityFeature from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.util.percentage import ( ordered_list_item_to_percentage, percentage_to_ordered_list_item, ) -from .const import PRESET_BOOST, PRESET_FREE_COOLING, PRESET_NIGHT +from .const import DOMAIN, PRESET_BOOST, PRESET_FREE_COOLING, PRESET_NIGHT from .coordinator import HeltyConfigEntry, HeltyDataUpdateCoordinator from .entity import HeltyEntity @@ -116,5 +117,11 @@ class HeltyFan(HeltyEntity, FanEntity): await self._async_set_mode(FanMode.OFF) async def _async_set_mode(self, mode: FanMode) -> None: - await self.coordinator.client.async_set_fan_mode(mode) + try: + await self.coordinator.client.async_set_fan_mode(mode) + except HeltyError as err: + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="set_fan_mode_failed", + ) from err await self.coordinator.async_request_refresh() diff --git a/homeassistant/components/helty/quality_scale.yaml b/homeassistant/components/helty/quality_scale.yaml index 62540e227764..b650a2fd1e58 100644 --- a/homeassistant/components/helty/quality_scale.yaml +++ b/homeassistant/components/helty/quality_scale.yaml @@ -26,9 +26,7 @@ rules: unique-config-entry: done # Silver - action-exceptions: - status: exempt - comment: This integration does not provide additional actions. + action-exceptions: done config-entry-unloading: done docs-configuration-parameters: status: exempt diff --git a/homeassistant/components/helty/strings.json b/homeassistant/components/helty/strings.json index 8d8c1cd17b1d..0c1d13f5f422 100644 --- a/homeassistant/components/helty/strings.json +++ b/homeassistant/components/helty/strings.json @@ -20,6 +20,11 @@ } }, "entity": { + "button": { + "reset_filter": { + "name": "Reset filter" + } + }, "sensor": { "indoor_humidity": { "name": "Indoor humidity" @@ -31,5 +36,13 @@ "name": "Outdoor temperature" } } + }, + "exceptions": { + "reset_filter_failed": { + "message": "Failed to reset the filter." + }, + "set_fan_mode_failed": { + "message": "Failed to set the ventilation mode." + } } } diff --git a/tests/components/helty/snapshots/test_button.ambr b/tests/components/helty/snapshots/test_button.ambr new file mode 100644 index 000000000000..51abecbf67bd --- /dev/null +++ b/tests/components/helty/snapshots/test_button.ambr @@ -0,0 +1,51 @@ +# serializer version: 1 +# name: test_all_entities[button.vmc_soggiorno_reset_filter-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.vmc_soggiorno_reset_filter', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Reset filter', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Reset filter', + 'platform': 'helty', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'reset_filter', + 'unique_id': '01HHHHHHHHHHHHHHHHHHHHHHHH_reset_filter', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[button.vmc_soggiorno_reset_filter-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'VMC Soggiorno Reset filter', + }), + 'context': , + 'entity_id': 'button.vmc_soggiorno_reset_filter', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- diff --git a/tests/components/helty/test_button.py b/tests/components/helty/test_button.py new file mode 100644 index 000000000000..9f7d8a81c94d --- /dev/null +++ b/tests/components/helty/test_button.py @@ -0,0 +1,69 @@ +"""Test the Helty Flow button platform.""" + +from unittest.mock import AsyncMock, patch + +from pyhelty import HeltyError +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS +from homeassistant.const import ATTR_ENTITY_ID, Platform +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import entity_registry as er + +from . import setup_integration + +from tests.common import MockConfigEntry, snapshot_platform + +RESET_FILTER_ENTITY = "button.vmc_soggiorno_reset_filter" + + +async def test_all_entities( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + mock_helty_client: AsyncMock, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, +) -> None: + """Test all entities.""" + with patch("homeassistant.components.helty.PLATFORMS", [Platform.BUTTON]): + await setup_integration(hass, mock_config_entry) + + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + + +async def test_reset_filter_press( + hass: HomeAssistant, + mock_helty_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test pressing the reset filter button calls the client.""" + await setup_integration(hass, mock_config_entry) + + await hass.services.async_call( + BUTTON_DOMAIN, + SERVICE_PRESS, + {ATTR_ENTITY_ID: RESET_FILTER_ENTITY}, + blocking=True, + ) + mock_helty_client.async_reset_filter.assert_awaited_once() + + +async def test_reset_filter_press_error( + hass: HomeAssistant, + mock_helty_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test a device failure during reset raises a HomeAssistantError.""" + await setup_integration(hass, mock_config_entry) + + mock_helty_client.async_reset_filter.side_effect = HeltyError + + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + BUTTON_DOMAIN, + SERVICE_PRESS, + {ATTR_ENTITY_ID: RESET_FILTER_ENTITY}, + blocking=True, + ) diff --git a/tests/components/helty/test_fan.py b/tests/components/helty/test_fan.py index 0cb3350d8424..807bf634d596 100644 --- a/tests/components/helty/test_fan.py +++ b/tests/components/helty/test_fan.py @@ -2,7 +2,7 @@ from unittest.mock import AsyncMock, patch -from pyhelty import FanMode +from pyhelty import FanMode, HeltyError import pytest from syrupy.assertion import SnapshotAssertion @@ -20,6 +20,7 @@ from homeassistant.const import ( Platform, ) from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er from . import setup_integration @@ -166,3 +167,22 @@ async def test_fan_turn_on_with_preset( blocking=True, ) mock_helty_client.async_set_fan_mode.assert_awaited_with(FanMode.FREE_COOLING) + + +async def test_fan_set_mode_error( + hass: HomeAssistant, + mock_helty_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test a device failure while setting the mode raises a HomeAssistantError.""" + await setup_integration(hass, mock_config_entry) + + mock_helty_client.async_set_fan_mode.side_effect = HeltyError + + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + FAN_DOMAIN, + SERVICE_SET_PRESET_MODE, + {ATTR_ENTITY_ID: FAN_ENTITY, ATTR_PRESET_MODE: "boost"}, + blocking=True, + )