Skip to content

Commit

Permalink
Palazzetti power control (home-assistant#131833)
Browse files Browse the repository at this point in the history
* Add number entity

* Catch exceptions

* Add test coverage

* Add translation

* Fix exception message

* Simplify number.py

* Remove dead code
  • Loading branch information
dotvav authored Dec 9, 2024
1 parent 4e2e661 commit bd0da03
Show file tree
Hide file tree
Showing 6 changed files with 206 additions and 1 deletion.
2 changes: 1 addition & 1 deletion homeassistant/components/palazzetti/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

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:
Expand Down
66 changes: 66 additions & 0 deletions homeassistant/components/palazzetti/number.py
Original file line number Diff line number Diff line change
@@ -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()
8 changes: 8 additions & 0 deletions homeassistant/components/palazzetti/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -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."
}
Expand All @@ -48,6 +51,11 @@
}
}
},
"number": {
"combustion_power": {
"name": "Combustion power"
}
},
"sensor": {
"pellet_quantity": {
"name": "Pellet quantity"
Expand Down
2 changes: 2 additions & 0 deletions tests/components/palazzetti/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
57 changes: 57 additions & 0 deletions tests/components/palazzetti/snapshots/test_number.ambr
Original file line number Diff line number Diff line change
@@ -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': <NumberMode.AUTO: 'auto'>,
'step': 1,
}),
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'number',
'entity_category': None,
'entity_id': 'number.stove_combustion_power',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <NumberDeviceClass.POWER_FACTOR: 'power_factor'>,
'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': <NumberMode.AUTO: 'auto'>,
'step': 1,
}),
'context': <ANY>,
'entity_id': 'number.stove_combustion_power',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '3',
})
# ---
72 changes: 72 additions & 0 deletions tests/components/palazzetti/test_number.py
Original file line number Diff line number Diff line change
@@ -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,
)

0 comments on commit bd0da03

Please sign in to comment.