From bd0da03eb9d77848262cb40e8e34f7002281c3af Mon Sep 17 00:00:00 2001 From: dotvav Date: Mon, 9 Dec 2024 14:02:17 +0100 Subject: [PATCH] Palazzetti power control (#131833) * Add number entity * Catch exceptions * Add test coverage * Add translation * Fix exception message * Simplify number.py * Remove dead code --- .../components/palazzetti/__init__.py | 2 +- homeassistant/components/palazzetti/number.py | 66 +++++++++++++++++ .../components/palazzetti/strings.json | 8 +++ tests/components/palazzetti/conftest.py | 2 + .../palazzetti/snapshots/test_number.ambr | 57 +++++++++++++++ tests/components/palazzetti/test_number.py | 72 +++++++++++++++++++ 6 files changed, 206 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/palazzetti/number.py create mode 100644 tests/components/palazzetti/snapshots/test_number.ambr create mode 100644 tests/components/palazzetti/test_number.py diff --git a/homeassistant/components/palazzetti/__init__.py b/homeassistant/components/palazzetti/__init__.py index 4bea4434496..f20b3d11261 100644 --- a/homeassistant/components/palazzetti/__init__.py +++ b/homeassistant/components/palazzetti/__init__.py @@ -7,7 +7,7 @@ from homeassistant.core import HomeAssistant from .coordinator import PalazzettiConfigEntry, PalazzettiDataUpdateCoordinator -PLATFORMS: list[Platform] = [Platform.CLIMATE, Platform.SENSOR] +PLATFORMS: list[Platform] = [Platform.CLIMATE, Platform.NUMBER, Platform.SENSOR] async def async_setup_entry(hass: HomeAssistant, entry: PalazzettiConfigEntry) -> bool: diff --git a/homeassistant/components/palazzetti/number.py b/homeassistant/components/palazzetti/number.py new file mode 100644 index 00000000000..06114bfef54 --- /dev/null +++ b/homeassistant/components/palazzetti/number.py @@ -0,0 +1,66 @@ +"""Number platform for Palazzetti settings.""" + +from __future__ import annotations + +from pypalazzetti.exceptions import CommunicationError, ValidationError + +from homeassistant.components.number import NumberDeviceClass, NumberEntity +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError, ServiceValidationError +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import PalazzettiConfigEntry +from .const import DOMAIN +from .coordinator import PalazzettiDataUpdateCoordinator +from .entity import PalazzettiEntity + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: PalazzettiConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up Palazzetti number platform.""" + async_add_entities([PalazzettiCombustionPowerEntity(config_entry.runtime_data)]) + + +class PalazzettiCombustionPowerEntity(PalazzettiEntity, NumberEntity): + """Representation of Palazzetti number entity for Combustion power.""" + + _attr_translation_key = "combustion_power" + _attr_device_class = NumberDeviceClass.POWER_FACTOR + _attr_native_min_value = 1 + _attr_native_max_value = 5 + _attr_native_step = 1 + + def __init__( + self, + coordinator: PalazzettiDataUpdateCoordinator, + ) -> None: + """Initialize the Palazzetti number entity.""" + super().__init__(coordinator) + self._attr_unique_id = f"{coordinator.config_entry.unique_id}-combustion_power" + + @property + def native_value(self) -> float: + """Return the state of the setting entity.""" + return self.coordinator.client.power_mode + + async def async_set_native_value(self, value: float) -> None: + """Update the setting.""" + try: + await self.coordinator.client.set_power_mode(int(value)) + except CommunicationError as err: + raise HomeAssistantError( + translation_domain=DOMAIN, translation_key="cannot_connect" + ) from err + except ValidationError as err: + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="invalid_combustion_power", + translation_placeholders={ + "value": str(value), + }, + ) from err + + await self.coordinator.async_request_refresh() diff --git a/homeassistant/components/palazzetti/strings.json b/homeassistant/components/palazzetti/strings.json index 435ec0aab85..60c6e20c402 100644 --- a/homeassistant/components/palazzetti/strings.json +++ b/homeassistant/components/palazzetti/strings.json @@ -30,6 +30,9 @@ "invalid_target_temperature": { "message": "Target temperature {value} is invalid." }, + "invalid_combustion_power": { + "message": "Combustion power {value} is invalid." + }, "cannot_connect": { "message": "Could not connect to the device." } @@ -48,6 +51,11 @@ } } }, + "number": { + "combustion_power": { + "name": "Combustion power" + } + }, "sensor": { "pellet_quantity": { "name": "Pellet quantity" diff --git a/tests/components/palazzetti/conftest.py b/tests/components/palazzetti/conftest.py index ec58afc324a..a9f76b259c3 100644 --- a/tests/components/palazzetti/conftest.py +++ b/tests/components/palazzetti/conftest.py @@ -87,6 +87,8 @@ def mock_palazzetti_client() -> Generator[AsyncMock]: mock_client.set_fan_silent.return_value = True mock_client.set_fan_high.return_value = True mock_client.set_fan_auto.return_value = True + mock_client.set_power_mode.return_value = True + mock_client.power_mode = 3 mock_client.list_temperatures.return_value = [ TemperatureDefinition( description_key=TemperatureDescriptionKey.ROOM_TEMP, diff --git a/tests/components/palazzetti/snapshots/test_number.ambr b/tests/components/palazzetti/snapshots/test_number.ambr new file mode 100644 index 00000000000..0a25a1cfa8b --- /dev/null +++ b/tests/components/palazzetti/snapshots/test_number.ambr @@ -0,0 +1,57 @@ +# serializer version: 1 +# name: test_all_entities[number.stove_combustion_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 5, + 'min': 1, + 'mode': , + 'step': 1, + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': None, + 'entity_id': 'number.stove_combustion_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Combustion power', + 'platform': 'palazzetti', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'combustion_power', + 'unique_id': '11:22:33:44:55:66-combustion_power', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[number.stove_combustion_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power_factor', + 'friendly_name': 'Stove Combustion power', + 'max': 5, + 'min': 1, + 'mode': , + 'step': 1, + }), + 'context': , + 'entity_id': 'number.stove_combustion_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '3', + }) +# --- diff --git a/tests/components/palazzetti/test_number.py b/tests/components/palazzetti/test_number.py new file mode 100644 index 00000000000..939c7c72c19 --- /dev/null +++ b/tests/components/palazzetti/test_number.py @@ -0,0 +1,72 @@ +"""Tests for the Palazzetti sensor platform.""" + +from unittest.mock import AsyncMock, patch + +from pypalazzetti.exceptions import CommunicationError, ValidationError +import pytest +from syrupy import SnapshotAssertion + +from homeassistant.components.number import DOMAIN as NUMBER_DOMAIN, SERVICE_SET_VALUE +from homeassistant.const import ATTR_ENTITY_ID, Platform +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError, ServiceValidationError +from homeassistant.helpers import entity_registry as er + +from . import setup_integration + +from tests.common import MockConfigEntry, snapshot_platform + +ENTITY_ID = "number.stove_combustion_power" + + +async def test_all_entities( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + mock_palazzetti_client: AsyncMock, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, +) -> None: + """Test all entities.""" + with patch("homeassistant.components.palazzetti.PLATFORMS", [Platform.NUMBER]): + await setup_integration(hass, mock_config_entry) + + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + + +async def test_async_set_data( + hass: HomeAssistant, + mock_palazzetti_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test setting number data via service call.""" + await setup_integration(hass, mock_config_entry) + + # Set value: Success + await hass.services.async_call( + NUMBER_DOMAIN, + SERVICE_SET_VALUE, + {ATTR_ENTITY_ID: ENTITY_ID, "value": 1}, + blocking=True, + ) + mock_palazzetti_client.set_power_mode.assert_called_once_with(1) + mock_palazzetti_client.set_on.reset_mock() + + # Set value: Error + mock_palazzetti_client.set_power_mode.side_effect = CommunicationError() + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + NUMBER_DOMAIN, + SERVICE_SET_VALUE, + {ATTR_ENTITY_ID: ENTITY_ID, "value": 1}, + blocking=True, + ) + mock_palazzetti_client.set_on.reset_mock() + + mock_palazzetti_client.set_power_mode.side_effect = ValidationError() + with pytest.raises(ServiceValidationError): + await hass.services.async_call( + NUMBER_DOMAIN, + SERVICE_SET_VALUE, + {ATTR_ENTITY_ID: ENTITY_ID, "value": 1}, + blocking=True, + )