From ff21c02cb6373fe662c086579ffd659dff9bc94d Mon Sep 17 00:00:00 2001
From: Tucker Kern <mill1000@users.noreply.github.com>
Date: Wed, 6 Dec 2023 09:53:52 -0700
Subject: [PATCH] Add preset modes to ESPHome fan entities (#103781)

Co-authored-by: J. Nick Koston <nick@koston.org>
---
 homeassistant/components/esphome/fan.py | 17 +++++++++++++++++
 tests/components/esphome/test_fan.py    | 14 ++++++++++++++
 2 files changed, 31 insertions(+)

diff --git a/homeassistant/components/esphome/fan.py b/homeassistant/components/esphome/fan.py
index 9942498e12d..08135e1a702 100644
--- a/homeassistant/components/esphome/fan.py
+++ b/homeassistant/components/esphome/fan.py
@@ -105,6 +105,10 @@ class EsphomeFan(EsphomeEntity[FanInfo, FanState], FanEntity):
             key=self._key, direction=_FAN_DIRECTIONS.from_hass(direction)
         )
 
+    async def async_set_preset_mode(self, preset_mode: str) -> None:
+        """Set the preset mode of the fan."""
+        await self._client.fan_command(key=self._key, preset_mode=preset_mode)
+
     @property
     @esphome_state_property
     def is_on(self) -> bool | None:
@@ -144,6 +148,17 @@ class EsphomeFan(EsphomeEntity[FanInfo, FanState], FanEntity):
         """Return the current fan direction."""
         return _FAN_DIRECTIONS.from_esphome(self._state.direction)
 
+    @property
+    @esphome_state_property
+    def preset_mode(self) -> str | None:
+        """Return the current fan preset mode."""
+        return self._state.preset_mode
+
+    @property
+    def preset_modes(self) -> list[str] | None:
+        """Return the supported fan preset modes."""
+        return self._static_info.supported_preset_modes
+
     @callback
     def _on_static_info_update(self, static_info: EntityInfo) -> None:
         """Set attrs from static info."""
@@ -156,4 +171,6 @@ class EsphomeFan(EsphomeEntity[FanInfo, FanState], FanEntity):
             flags |= FanEntityFeature.SET_SPEED
         if static_info.supports_direction:
             flags |= FanEntityFeature.DIRECTION
+        if static_info.supported_preset_modes:
+            flags |= FanEntityFeature.PRESET_MODE
         self._attr_supported_features = flags
diff --git a/tests/components/esphome/test_fan.py b/tests/components/esphome/test_fan.py
index 99f4bbc86a9..6f383dcb6ba 100644
--- a/tests/components/esphome/test_fan.py
+++ b/tests/components/esphome/test_fan.py
@@ -16,12 +16,14 @@ from homeassistant.components.fan import (
     ATTR_DIRECTION,
     ATTR_OSCILLATING,
     ATTR_PERCENTAGE,
+    ATTR_PRESET_MODE,
     DOMAIN as FAN_DOMAIN,
     SERVICE_DECREASE_SPEED,
     SERVICE_INCREASE_SPEED,
     SERVICE_OSCILLATE,
     SERVICE_SET_DIRECTION,
     SERVICE_SET_PERCENTAGE,
+    SERVICE_SET_PRESET_MODE,
     SERVICE_TURN_OFF,
     SERVICE_TURN_ON,
     STATE_ON,
@@ -145,6 +147,7 @@ async def test_fan_entity_with_all_features_new_api(
             supports_direction=True,
             supports_speed=True,
             supports_oscillation=True,
+            supported_preset_modes=["Preset1", "Preset2"],
         )
     ]
     states = [
@@ -154,6 +157,7 @@ async def test_fan_entity_with_all_features_new_api(
             oscillating=True,
             speed_level=3,
             direction=FanDirection.REVERSE,
+            preset_mode=None,
         )
     ]
     user_service = []
@@ -270,6 +274,15 @@ async def test_fan_entity_with_all_features_new_api(
     )
     mock_client.fan_command.reset_mock()
 
+    await hass.services.async_call(
+        FAN_DOMAIN,
+        SERVICE_SET_PRESET_MODE,
+        {ATTR_ENTITY_ID: "fan.test_myfan", ATTR_PRESET_MODE: "Preset1"},
+        blocking=True,
+    )
+    mock_client.fan_command.assert_has_calls([call(key=1, preset_mode="Preset1")])
+    mock_client.fan_command.reset_mock()
+
 
 async def test_fan_entity_with_no_features_new_api(
     hass: HomeAssistant, mock_client: APIClient, mock_generic_device_entry
@@ -285,6 +298,7 @@ async def test_fan_entity_with_no_features_new_api(
             supports_direction=False,
             supports_speed=False,
             supports_oscillation=False,
+            supported_preset_modes=[],
         )
     ]
     states = [FanState(key=1, state=True)]
-- 
GitLab