From b8f6fdeb2b3b643022fb4c426ad3640dce8d3e27 Mon Sep 17 00:00:00 2001
From: Joshua Shaffer <xeio@live.com>
Date: Thu, 24 Oct 2024 08:25:40 +0000
Subject: [PATCH] Use fan mode when heat/cool is idle in homekit_controller
 (#128618)

---
 .../components/homekit_controller/climate.py     | 16 +++++++++++++++-
 .../components/homekit_controller/manifest.json  |  2 +-
 requirements_all.txt                             |  2 +-
 requirements_test_all.txt                        |  2 +-
 .../homekit_controller/test_climate.py           | 16 ++++++++++++++++
 5 files changed, 34 insertions(+), 4 deletions(-)

diff --git a/homeassistant/components/homekit_controller/climate.py b/homeassistant/components/homekit_controller/climate.py
index 3be0af17dbd..4e55c8212be 100644
--- a/homeassistant/components/homekit_controller/climate.py
+++ b/homeassistant/components/homekit_controller/climate.py
@@ -8,6 +8,7 @@ from typing import Any, Final
 from aiohomekit.model.characteristics import (
     ActivationStateValues,
     CharacteristicsTypes,
+    CurrentFanStateValues,
     CurrentHeaterCoolerStateValues,
     HeatingCoolingCurrentValues,
     HeatingCoolingTargetValues,
@@ -484,6 +485,7 @@ class HomeKitClimateEntity(HomeKitBaseClimateEntity):
             CharacteristicsTypes.TEMPERATURE_TARGET,
             CharacteristicsTypes.RELATIVE_HUMIDITY_CURRENT,
             CharacteristicsTypes.RELATIVE_HUMIDITY_TARGET,
+            CharacteristicsTypes.FAN_STATE_CURRENT,
         ]
 
     async def async_set_temperature(self, **kwargs: Any) -> None:
@@ -666,7 +668,19 @@ class HomeKitClimateEntity(HomeKitBaseClimateEntity):
             return HVACAction.IDLE
 
         value = self.service.value(CharacteristicsTypes.HEATING_COOLING_CURRENT)
-        return CURRENT_MODE_HOMEKIT_TO_HASS.get(value)
+        current_hass_value = CURRENT_MODE_HOMEKIT_TO_HASS.get(value)
+
+        # If a device has a fan state (such as an Ecobee thermostat)
+        # show the Fan state when the device is otherwise idle.
+        if (
+            current_hass_value == HVACAction.IDLE
+            and self.service.has(CharacteristicsTypes.FAN_STATE_CURRENT)
+            and self.service.value(CharacteristicsTypes.FAN_STATE_CURRENT)
+            == CurrentFanStateValues.ACTIVE
+        ):
+            return HVACAction.FAN
+
+        return current_hass_value
 
     @property
     def hvac_mode(self) -> HVACMode:
diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json
index b2b215a98b9..598e8078a2c 100644
--- a/homeassistant/components/homekit_controller/manifest.json
+++ b/homeassistant/components/homekit_controller/manifest.json
@@ -14,6 +14,6 @@
   "documentation": "https://www.home-assistant.io/integrations/homekit_controller",
   "iot_class": "local_push",
   "loggers": ["aiohomekit", "commentjson"],
-  "requirements": ["aiohomekit==3.2.3"],
+  "requirements": ["aiohomekit==3.2.5"],
   "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."]
 }
diff --git a/requirements_all.txt b/requirements_all.txt
index 4f4d9689333..3065fd7c71d 100644
--- a/requirements_all.txt
+++ b/requirements_all.txt
@@ -262,7 +262,7 @@ aioharmony==0.2.10
 aiohasupervisor==0.2.0b0
 
 # homeassistant.components.homekit_controller
-aiohomekit==3.2.3
+aiohomekit==3.2.5
 
 # homeassistant.components.hue
 aiohue==4.7.3
diff --git a/requirements_test_all.txt b/requirements_test_all.txt
index f44c222af83..f9589fec773 100644
--- a/requirements_test_all.txt
+++ b/requirements_test_all.txt
@@ -247,7 +247,7 @@ aioharmony==0.2.10
 aiohasupervisor==0.2.0b0
 
 # homeassistant.components.homekit_controller
-aiohomekit==3.2.3
+aiohomekit==3.2.5
 
 # homeassistant.components.hue
 aiohue==4.7.3
diff --git a/tests/components/homekit_controller/test_climate.py b/tests/components/homekit_controller/test_climate.py
index 76935d314a5..62c73af9977 100644
--- a/tests/components/homekit_controller/test_climate.py
+++ b/tests/components/homekit_controller/test_climate.py
@@ -6,6 +6,7 @@ from aiohomekit.model import Accessory
 from aiohomekit.model.characteristics import (
     ActivationStateValues,
     CharacteristicsTypes,
+    CurrentFanStateValues,
     CurrentHeaterCoolerStateValues,
     SwingModeValues,
     TargetHeaterCoolerStateValues,
@@ -66,6 +67,9 @@ def create_thermostat_service(accessory: Accessory) -> None:
     char = service.add_char(CharacteristicsTypes.RELATIVE_HUMIDITY_CURRENT)
     char.value = 0
 
+    char = service.add_char(CharacteristicsTypes.FAN_STATE_CURRENT)
+    char.value = 0
+
 
 def create_thermostat_service_min_max(accessory: Accessory) -> None:
     """Define thermostat characteristics."""
@@ -648,6 +652,18 @@ async def test_hvac_mode_vs_hvac_action(
     assert state.state == "heat"
     assert state.attributes["hvac_action"] == "idle"
 
+    # Simulate the fan running while the heat/cool is idle
+    await helper.async_update(
+        ServicesTypes.THERMOSTAT,
+        {
+            CharacteristicsTypes.FAN_STATE_CURRENT: CurrentFanStateValues.ACTIVE,
+        },
+    )
+
+    state = await helper.poll_and_get_state()
+    assert state.state == "heat"
+    assert state.attributes["hvac_action"] == "fan"
+
     # Simulate that current temperature is below target temp
     # Heating might be on and hvac_action currently 'heat'
     await helper.async_update(
-- 
GitLab