Palazzetti integration: Add support for additional fans (#135377)

* Add support for second and third fans

* Update test mock and snapshot

* Test coverage and error message

* Rename fans left and right instead of 2 and 3
pull/135530/head
dotvav 2025-01-13 17:17:46 +01:00 committed by GitHub
parent 1fa3d90d73
commit 153496b5f4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 238 additions and 9 deletions

View File

@ -122,7 +122,7 @@ class PalazzettiClimateEntity(PalazzettiEntity, ClimateEntity):
@property
def fan_mode(self) -> str | None:
"""Return the fan mode."""
api_state = self.coordinator.client.fan_speed
api_state = self.coordinator.client.current_fan_speed()
return FAN_MODES[api_state]
async def async_set_fan_mode(self, fan_mode: str) -> None:

View File

@ -3,6 +3,7 @@
from __future__ import annotations
from pypalazzetti.exceptions import CommunicationError, ValidationError
from pypalazzetti.fan import FanType
from homeassistant.components.number import NumberDeviceClass, NumberEntity
from homeassistant.core import HomeAssistant
@ -21,7 +22,18 @@ async def async_setup_entry(
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up Palazzetti number platform."""
async_add_entities([PalazzettiCombustionPowerEntity(config_entry.runtime_data)])
entities: list[PalazzettiEntity] = [
PalazzettiCombustionPowerEntity(config_entry.runtime_data)
]
if config_entry.runtime_data.client.has_fan(FanType.LEFT):
entities.append(PalazzettiFanEntity(config_entry.runtime_data, FanType.LEFT))
if config_entry.runtime_data.client.has_fan(FanType.RIGHT):
entities.append(PalazzettiFanEntity(config_entry.runtime_data, FanType.RIGHT))
async_add_entities(entities)
class PalazzettiCombustionPowerEntity(PalazzettiEntity, NumberEntity):
@ -64,3 +76,49 @@ class PalazzettiCombustionPowerEntity(PalazzettiEntity, NumberEntity):
) from err
await self.coordinator.async_request_refresh()
class PalazzettiFanEntity(PalazzettiEntity, NumberEntity):
"""Representation of Palazzetti number entity for Combustion power."""
_attr_device_class = NumberDeviceClass.WIND_SPEED
_attr_native_step = 1
def __init__(
self, coordinator: PalazzettiDataUpdateCoordinator, fan: FanType
) -> None:
"""Initialize the Palazzetti number entity."""
super().__init__(coordinator)
self.fan = fan
self._attr_translation_key = f"fan_{str.lower(fan.name)}_speed"
self._attr_native_min_value = coordinator.client.min_fan_speed(fan)
self._attr_native_max_value = coordinator.client.max_fan_speed(fan)
self._attr_unique_id = (
f"{coordinator.config_entry.unique_id}-fan_{str.lower(fan.name)}_speed"
)
@property
def native_value(self) -> float:
"""Return the state of the setting entity."""
return self.coordinator.client.current_fan_speed(self.fan)
async def async_set_native_value(self, value: float) -> None:
"""Update the setting."""
try:
await self.coordinator.client.set_fan_speed(int(value), self.fan)
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_fan_speed",
translation_placeholders={
"name": str.lower(self.fan.name),
"value": str(value),
},
) from err
await self.coordinator.async_request_refresh()

View File

@ -27,6 +27,9 @@
"invalid_fan_mode": {
"message": "Fan mode {value} is invalid."
},
"invalid_fan_speed": {
"message": "Fan {name} speed {value} is invalid."
},
"invalid_target_temperature": {
"message": "Target temperature {value} is invalid."
},
@ -59,6 +62,12 @@
"number": {
"combustion_power": {
"name": "Combustion power"
},
"fan_left_speed": {
"name": "Left fan speed"
},
"fan_right_speed": {
"name": "Right fan speed"
}
},
"sensor": {

View File

@ -79,7 +79,12 @@ def mock_palazzetti_client() -> Generator[AsyncMock]:
mock_client.target_temperature_max = 50
mock_client.pellet_quantity = 1248
mock_client.pellet_level = 0
mock_client.has_second_fan = True
mock_client.has_second_fan = False
mock_client.fan_speed = 3
mock_client.current_fan_speed.return_value = 3
mock_client.min_fan_speed.return_value = 0
mock_client.max_fan_speed.return_value = 5
mock_client.connect.return_value = True
mock_client.update_state.return_value = True
mock_client.set_on.return_value = True

View File

@ -55,3 +55,115 @@
'state': '3',
})
# ---
# name: test_all_entities[number.stove_left_fan_speed-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'max': 5,
'min': 0,
'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_left_fan_speed',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <NumberDeviceClass.WIND_SPEED: 'wind_speed'>,
'original_icon': None,
'original_name': 'Left fan speed',
'platform': 'palazzetti',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'fan_left_speed',
'unique_id': '11:22:33:44:55:66-fan_left_speed',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[number.stove_left_fan_speed-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'wind_speed',
'friendly_name': 'Stove Left fan speed',
'max': 5,
'min': 0,
'mode': <NumberMode.AUTO: 'auto'>,
'step': 1,
}),
'context': <ANY>,
'entity_id': 'number.stove_left_fan_speed',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '3',
})
# ---
# name: test_all_entities[number.stove_right_fan_speed-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'max': 5,
'min': 0,
'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_right_fan_speed',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <NumberDeviceClass.WIND_SPEED: 'wind_speed'>,
'original_icon': None,
'original_name': 'Right fan speed',
'platform': 'palazzetti',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'fan_right_speed',
'unique_id': '11:22:33:44:55:66-fan_right_speed',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[number.stove_right_fan_speed-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'wind_speed',
'friendly_name': 'Stove Right fan speed',
'max': 5,
'min': 0,
'mode': <NumberMode.AUTO: 'auto'>,
'step': 1,
}),
'context': <ANY>,
'entity_id': 'number.stove_right_fan_speed',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '3',
})
# ---

View File

@ -3,6 +3,7 @@
from unittest.mock import AsyncMock, patch
from pypalazzetti.exceptions import CommunicationError, ValidationError
from pypalazzetti.fan import FanType
import pytest
from syrupy import SnapshotAssertion
@ -16,7 +17,8 @@ from . import setup_integration
from tests.common import MockConfigEntry, snapshot_platform
ENTITY_ID = "number.stove_combustion_power"
POWER_ENTITY_ID = "number.stove_combustion_power"
FAN_ENTITY_ID = "number.stove_left_fan_speed"
async def test_all_entities(
@ -33,7 +35,7 @@ async def test_all_entities(
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
async def test_async_set_data(
async def test_async_set_data_power(
hass: HomeAssistant,
mock_palazzetti_client: AsyncMock,
mock_config_entry: MockConfigEntry,
@ -45,7 +47,7 @@ async def test_async_set_data(
await hass.services.async_call(
NUMBER_DOMAIN,
SERVICE_SET_VALUE,
{ATTR_ENTITY_ID: ENTITY_ID, "value": 1},
{ATTR_ENTITY_ID: POWER_ENTITY_ID, "value": 1},
blocking=True,
)
mock_palazzetti_client.set_power_mode.assert_called_once_with(1)
@ -53,20 +55,63 @@ async def test_async_set_data(
# Set value: Error
mock_palazzetti_client.set_power_mode.side_effect = CommunicationError()
with pytest.raises(HomeAssistantError):
message = "Could not connect to the device"
with pytest.raises(HomeAssistantError, match=message):
await hass.services.async_call(
NUMBER_DOMAIN,
SERVICE_SET_VALUE,
{ATTR_ENTITY_ID: ENTITY_ID, "value": 1},
{ATTR_ENTITY_ID: POWER_ENTITY_ID, "value": 1},
blocking=True,
)
mock_palazzetti_client.set_power_mode.reset_mock()
mock_palazzetti_client.set_power_mode.side_effect = ValidationError()
with pytest.raises(ServiceValidationError):
message = "Combustion power 1.0 is invalid"
with pytest.raises(ServiceValidationError, match=message):
await hass.services.async_call(
NUMBER_DOMAIN,
SERVICE_SET_VALUE,
{ATTR_ENTITY_ID: ENTITY_ID, "value": 1},
{ATTR_ENTITY_ID: POWER_ENTITY_ID, "value": 1},
blocking=True,
)
async def test_async_set_data_fan(
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: FAN_ENTITY_ID, "value": 1},
blocking=True,
)
mock_palazzetti_client.set_fan_speed.assert_called_once_with(1, FanType.LEFT)
mock_palazzetti_client.set_on.reset_mock()
# Set value: Error
mock_palazzetti_client.set_fan_speed.side_effect = CommunicationError()
message = "Could not connect to the device"
with pytest.raises(HomeAssistantError, match=message):
await hass.services.async_call(
NUMBER_DOMAIN,
SERVICE_SET_VALUE,
{ATTR_ENTITY_ID: FAN_ENTITY_ID, "value": 1},
blocking=True,
)
mock_palazzetti_client.set_on.reset_mock()
mock_palazzetti_client.set_fan_speed.side_effect = ValidationError()
message = "Fan left speed 1.0 is invalid"
with pytest.raises(ServiceValidationError, match=message):
await hass.services.async_call(
NUMBER_DOMAIN,
SERVICE_SET_VALUE,
{ATTR_ENTITY_ID: FAN_ENTITY_ID, "value": 1},
blocking=True,
)