diff --git a/homeassistant/components/bond/fan.py b/homeassistant/components/bond/fan.py index b5d7059b67e..32edfb206a6 100644 --- a/homeassistant/components/bond/fan.py +++ b/homeassistant/components/bond/fan.py @@ -35,6 +35,8 @@ from .utils import BondDevice, BondHub _LOGGER = logging.getLogger(__name__) +PRESET_MODE_BREEZE = "Breeze" + async def async_setup_entry( hass: HomeAssistant, @@ -74,11 +76,15 @@ class BondFan(BondEntity, FanEntity): self._power: bool | None = None self._speed: int | None = None self._direction: int | None = None + if self._device.has_action(Action.BREEZE_ON): + self._attr_preset_modes = [PRESET_MODE_BREEZE] def _apply_state(self, state: dict) -> None: self._power = state.get("power") self._speed = state.get("speed") self._direction = state.get("direction") + breeze = state.get("breeze", [0, 0, 0]) + self._attr_preset_mode = PRESET_MODE_BREEZE if breeze[0] else None @property def supported_features(self) -> int: @@ -185,13 +191,27 @@ class BondFan(BondEntity, FanEntity): """Turn on the fan.""" _LOGGER.debug("Fan async_turn_on called with percentage %s", percentage) - if percentage is not None: + if preset_mode is not None: + await self.async_set_preset_mode(preset_mode) + elif percentage is not None: await self.async_set_percentage(percentage) else: await self._hub.bond.action(self._device.device_id, Action.turn_on()) + async def async_set_preset_mode(self, preset_mode: str) -> None: + """Set the preset mode of the fan.""" + if preset_mode != PRESET_MODE_BREEZE or not self._device.has_action( + Action.BREEZE_ON + ): + raise ValueError(f"Invalid preset mode: {preset_mode}") + await self._hub.bond.action(self._device.device_id, Action(Action.BREEZE_ON)) + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the fan off.""" + if self.preset_mode == PRESET_MODE_BREEZE: + await self._hub.bond.action( + self._device.device_id, Action(Action.BREEZE_OFF) + ) await self._hub.bond.action(self._device.device_id, Action.turn_off()) async def async_set_direction(self, direction: str) -> None: diff --git a/tests/components/bond/test_fan.py b/tests/components/bond/test_fan.py index 0c58596bb7f..4168cbd35d2 100644 --- a/tests/components/bond/test_fan.py +++ b/tests/components/bond/test_fan.py @@ -2,6 +2,7 @@ from __future__ import annotations from datetime import timedelta +from unittest.mock import call from bond_api import Action, DeviceType, Direction import pytest @@ -12,14 +13,18 @@ from homeassistant.components.bond.const import ( DOMAIN as BOND_DOMAIN, SERVICE_SET_FAN_SPEED_TRACKED_STATE, ) +from homeassistant.components.bond.fan import PRESET_MODE_BREEZE from homeassistant.components.fan import ( ATTR_DIRECTION, + ATTR_PRESET_MODE, + ATTR_PRESET_MODES, ATTR_SPEED, ATTR_SPEED_LIST, DIRECTION_FORWARD, DIRECTION_REVERSE, DOMAIN as FAN_DOMAIN, SERVICE_SET_DIRECTION, + SERVICE_SET_PRESET_MODE, SERVICE_SET_SPEED, SPEED_OFF, ) @@ -49,14 +54,26 @@ def ceiling_fan(name: str): } +def ceiling_fan_with_breeze(name: str): + """Create a ceiling fan with given name with breeze support.""" + return { + "name": name, + "type": DeviceType.CEILING_FAN, + "actions": ["SetSpeed", "SetDirection", "BreezeOn"], + } + + async def turn_fan_on( hass: core.HomeAssistant, fan_id: str, speed: str | None = None, percentage: int | None = None, + preset_mode: str | None = None, ) -> None: """Turn the fan on at the specified speed.""" service_data = {ATTR_ENTITY_ID: fan_id} + if preset_mode: + service_data[fan.ATTR_PRESET_MODE] = preset_mode if speed: service_data[fan.ATTR_SPEED] = speed if percentage: @@ -205,6 +222,88 @@ async def test_turn_on_fan_with_percentage_6_speeds(hass: core.HomeAssistant): mock_set_speed.assert_called_with("test-device-id", Action.set_speed(6)) +async def test_turn_on_fan_preset_mode(hass: core.HomeAssistant): + """Tests that turn on command delegates to breeze on API.""" + await setup_platform( + hass, + FAN_DOMAIN, + ceiling_fan_with_breeze("name-1"), + bond_device_id="test-device-id", + props={"max_speed": 6}, + ) + assert hass.states.get("fan.name_1").attributes[ATTR_PRESET_MODES] == [ + PRESET_MODE_BREEZE + ] + + with patch_bond_action() as mock_set_preset_mode, patch_bond_device_state(): + await turn_fan_on(hass, "fan.name_1", preset_mode=PRESET_MODE_BREEZE) + + mock_set_preset_mode.assert_called_with("test-device-id", Action(Action.BREEZE_ON)) + + with patch_bond_action() as mock_set_preset_mode, patch_bond_device_state(): + await hass.services.async_call( + FAN_DOMAIN, + SERVICE_SET_PRESET_MODE, + service_data={ + ATTR_PRESET_MODE: PRESET_MODE_BREEZE, + ATTR_ENTITY_ID: "fan.name_1", + }, + blocking=True, + ) + + mock_set_preset_mode.assert_called_with("test-device-id", Action(Action.BREEZE_ON)) + + +async def test_turn_on_fan_preset_mode_not_supported(hass: core.HomeAssistant): + """Tests calling breeze mode on a fan that does not support it raises.""" + await setup_platform( + hass, + FAN_DOMAIN, + ceiling_fan("name-1"), + bond_device_id="test-device-id", + props={"max_speed": 6}, + ) + + with patch_bond_action(), patch_bond_device_state(), pytest.raises( + fan.NotValidPresetModeError + ): + await turn_fan_on(hass, "fan.name_1", preset_mode=PRESET_MODE_BREEZE) + + with patch_bond_action(), patch_bond_device_state(), pytest.raises(ValueError): + await hass.services.async_call( + FAN_DOMAIN, + SERVICE_SET_PRESET_MODE, + service_data={ + ATTR_PRESET_MODE: PRESET_MODE_BREEZE, + ATTR_ENTITY_ID: "fan.name_1", + }, + blocking=True, + ) + + +async def test_turn_on_fan_with_off_with_breeze(hass: core.HomeAssistant): + """Tests that turn off command delegates to turn off API.""" + await setup_platform( + hass, + FAN_DOMAIN, + ceiling_fan_with_breeze("name-1"), + bond_device_id="test-device-id", + state={"breeze": [1, 0, 0]}, + ) + + assert ( + hass.states.get("fan.name_1").attributes[ATTR_PRESET_MODE] == PRESET_MODE_BREEZE + ) + + with patch_bond_action() as mock_actions, patch_bond_device_state(): + await turn_fan_on(hass, "fan.name_1", fan.SPEED_OFF) + + assert mock_actions.mock_calls == [ + call("test-device-id", Action(Action.BREEZE_OFF)), + call("test-device-id", Action.turn_off()), + ] + + async def test_turn_on_fan_without_speed(hass: core.HomeAssistant): """Tests that turn on command delegates to turn on API.""" await setup_platform( @@ -218,7 +317,7 @@ async def test_turn_on_fan_without_speed(hass: core.HomeAssistant): async def test_turn_on_fan_with_off_speed(hass: core.HomeAssistant): - """Tests that turn on command delegates to turn off API.""" + """Tests that turn off command delegates to turn off API.""" await setup_platform( hass, FAN_DOMAIN, ceiling_fan("name-1"), bond_device_id="test-device-id" )