Skip to content

Commit

Permalink
Replace climate fan speed 'silent' with a button (home-assistant#135075)
Browse files Browse the repository at this point in the history
  • Loading branch information
dotvav authored Jan 13, 2025
1 parent fc6695b commit e1ffd93
Show file tree
Hide file tree
Showing 12 changed files with 195 additions and 25 deletions.
7 changes: 6 additions & 1 deletion homeassistant/components/palazzetti/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,12 @@

from .coordinator import PalazzettiConfigEntry, PalazzettiDataUpdateCoordinator

PLATFORMS: list[Platform] = [Platform.CLIMATE, Platform.NUMBER, Platform.SENSOR]
PLATFORMS: list[Platform] = [
Platform.BUTTON,
Platform.CLIMATE,
Platform.NUMBER,
Platform.SENSOR,
]


async def async_setup_entry(hass: HomeAssistant, entry: PalazzettiConfigEntry) -> bool:
Expand Down
52 changes: 52 additions & 0 deletions homeassistant/components/palazzetti/button.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
"""Support for Palazzetti buttons."""

from __future__ import annotations

from pypalazzetti.exceptions import CommunicationError

from homeassistant.components.button import ButtonEntity
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
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 button platform."""

coordinator = config_entry.runtime_data
if coordinator.client.has_fan_silent:
async_add_entities([PalazzettiSilentButtonEntity(coordinator)])


class PalazzettiSilentButtonEntity(PalazzettiEntity, ButtonEntity):
"""Representation of a Palazzetti Silent button."""

_attr_translation_key = "silent"

def __init__(
self,
coordinator: PalazzettiDataUpdateCoordinator,
) -> None:
"""Initialize a Palazzetti Silent button."""
super().__init__(coordinator)
self._attr_unique_id = f"{coordinator.config_entry.unique_id}-silent"

async def async_press(self) -> None:
"""Press the button."""
try:
await self.coordinator.client.set_fan_silent()
except CommunicationError as err:
raise HomeAssistantError(
translation_domain=DOMAIN, translation_key="cannot_connect"
) from err

await self.coordinator.async_request_refresh()
8 changes: 2 additions & 6 deletions homeassistant/components/palazzetti/climate.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
from homeassistant.helpers.entity_platform import AddEntitiesCallback

from . import PalazzettiConfigEntry
from .const import DOMAIN, FAN_AUTO, FAN_HIGH, FAN_MODES, FAN_SILENT
from .const import DOMAIN, FAN_AUTO, FAN_HIGH, FAN_MODES
from .coordinator import PalazzettiDataUpdateCoordinator
from .entity import PalazzettiEntity

Expand Down Expand Up @@ -57,8 +57,6 @@ def __init__(self, coordinator: PalazzettiDataUpdateCoordinator) -> None:
self._attr_fan_modes = list(
map(str, range(client.fan_speed_min, client.fan_speed_max + 1))
)
if client.has_fan_silent:
self._attr_fan_modes.insert(0, FAN_SILENT)
if client.has_fan_high:
self._attr_fan_modes.append(FAN_HIGH)
if client.has_fan_auto:
Expand Down Expand Up @@ -130,9 +128,7 @@ def fan_mode(self) -> str | None:
async def async_set_fan_mode(self, fan_mode: str) -> None:
"""Set new fan mode."""
try:
if fan_mode == FAN_SILENT:
await self.coordinator.client.set_fan_silent()
elif fan_mode == FAN_HIGH:
if fan_mode == FAN_HIGH:
await self.coordinator.client.set_fan_high()
elif fan_mode == FAN_AUTO:
await self.coordinator.client.set_fan_auto()
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/palazzetti/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
FAN_SILENT: Final = "silent"
FAN_HIGH: Final = "high"
FAN_AUTO: Final = "auto"
FAN_MODES: Final = [FAN_SILENT, "1", "2", "3", "4", "5", FAN_HIGH, FAN_AUTO]
FAN_MODES: Final = ["0", "1", "2", "3", "4", "5", FAN_HIGH, FAN_AUTO]

STATUS_TO_HA: Final[dict[StateType, str]] = {
0: "off",
Expand Down
9 changes: 9 additions & 0 deletions homeassistant/components/palazzetti/icons.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"entity": {
"button": {
"silent": {
"default": "mdi:volume-mute"
}
}
}
}
4 changes: 1 addition & 3 deletions homeassistant/components/palazzetti/quality_scale.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,7 @@ rules:
entity-translations: done
exception-translations: done
icon-translations:
status: exempt
comment: |
This integration does not have custom icons.
status: done
reconfiguration-flow: todo
repair-issues:
status: exempt
Expand Down
5 changes: 5 additions & 0 deletions homeassistant/components/palazzetti/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@
}
},
"entity": {
"button": {
"silent": {
"name": "Silent"
}
},
"climate": {
"palazzetti": {
"state_attributes": {
Expand Down
47 changes: 47 additions & 0 deletions tests/components/palazzetti/snapshots/test_button.ambr
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# serializer version: 1
# name: test_all_entities[button.stove_silent-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'button',
'entity_category': None,
'entity_id': 'button.stove_silent',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Silent',
'platform': 'palazzetti',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'silent',
'unique_id': '11:22:33:44:55:66-silent',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[button.stove_silent-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Stove Silent',
}),
'context': <ANY>,
'entity_id': 'button.stove_silent',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
2 changes: 0 additions & 2 deletions tests/components/palazzetti/snapshots/test_climate.ambr
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
'area_id': None,
'capabilities': dict({
'fan_modes': list([
'silent',
'1',
'2',
'3',
Expand Down Expand Up @@ -56,7 +55,6 @@
'current_temperature': 18,
'fan_mode': '3',
'fan_modes': list([
'silent',
'1',
'2',
'3',
Expand Down
69 changes: 69 additions & 0 deletions tests/components/palazzetti/test_button.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
"""Tests for the Palazzetti button platform."""

from unittest.mock import AsyncMock, patch

from pypalazzetti.exceptions import CommunicationError
import pytest
from syrupy 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

ENTITY_ID = "button.stove_silent"


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.BUTTON]):
await setup_integration(hass, mock_config_entry)

await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)


async def test_async_press(
hass: HomeAssistant,
mock_palazzetti_client: AsyncMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test pressing via service call."""
await setup_integration(hass, mock_config_entry)

await hass.services.async_call(
BUTTON_DOMAIN,
SERVICE_PRESS,
{ATTR_ENTITY_ID: ENTITY_ID},
blocking=True,
)
mock_palazzetti_client.set_fan_silent.assert_called_once()


async def test_async_press_error(
hass: HomeAssistant,
mock_palazzetti_client: AsyncMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test pressing with error via service call."""
await setup_integration(hass, mock_config_entry)

mock_palazzetti_client.set_fan_silent.side_effect = CommunicationError()
error_message = "Could not connect to the device"
with pytest.raises(HomeAssistantError, match=error_message):
await hass.services.async_call(
BUTTON_DOMAIN,
SERVICE_PRESS,
{ATTR_ENTITY_ID: ENTITY_ID},
blocking=True,
)
11 changes: 1 addition & 10 deletions tests/components/palazzetti/test_climate.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
SERVICE_SET_TEMPERATURE,
HVACMode,
)
from homeassistant.components.palazzetti.const import FAN_AUTO, FAN_HIGH, FAN_SILENT
from homeassistant.components.palazzetti.const import FAN_AUTO, FAN_HIGH
from homeassistant.const import ATTR_ENTITY_ID, ATTR_TEMPERATURE, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
Expand Down Expand Up @@ -118,15 +118,6 @@ async def test_async_set_data(
)

# Set Fan Mode: Success
await hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_SET_FAN_MODE,
{ATTR_ENTITY_ID: ENTITY_ID, ATTR_FAN_MODE: FAN_SILENT},
blocking=True,
)
mock_palazzetti_client.set_fan_silent.assert_called_once()
mock_palazzetti_client.set_fan_silent.reset_mock()

await hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_SET_FAN_MODE,
Expand Down
4 changes: 2 additions & 2 deletions tests/components/palazzetti/test_number.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ async def test_async_set_data(
blocking=True,
)
mock_palazzetti_client.set_power_mode.assert_called_once_with(1)
mock_palazzetti_client.set_on.reset_mock()
mock_palazzetti_client.set_power_mode.reset_mock()

# Set value: Error
mock_palazzetti_client.set_power_mode.side_effect = CommunicationError()
Expand All @@ -60,7 +60,7 @@ async def test_async_set_data(
{ATTR_ENTITY_ID: ENTITY_ID, "value": 1},
blocking=True,
)
mock_palazzetti_client.set_on.reset_mock()
mock_palazzetti_client.set_power_mode.reset_mock()

mock_palazzetti_client.set_power_mode.side_effect = ValidationError()
with pytest.raises(ServiceValidationError):
Expand Down

0 comments on commit e1ffd93

Please sign in to comment.