From 0c48b8ec52d384f8ac2e1088b1dacfe776418250 Mon Sep 17 00:00:00 2001
From: Guillermo Ruffino <glm.net@gmail.com>
Date: Sun, 17 Nov 2019 19:14:19 -0300
Subject: [PATCH] Esphome climate features (#28786)

* add esphome climate fan

* adds back compatibility support

* fixes, add swing mode

* revert client config

* sort import

* rebase
---
 homeassistant/components/esphome/climate.py   | 96 ++++++++++++++++++-
 .../components/esphome/manifest.json          |  2 +-
 requirements_all.txt                          |  2 +-
 requirements_test_all.txt                     |  2 +-
 4 files changed, 97 insertions(+), 5 deletions(-)

diff --git a/homeassistant/components/esphome/climate.py b/homeassistant/components/esphome/climate.py
index 5fed8da76ef..49d284b6de2 100644
--- a/homeassistant/components/esphome/climate.py
+++ b/homeassistant/components/esphome/climate.py
@@ -2,7 +2,13 @@
 import logging
 from typing import List, Optional
 
-from aioesphomeapi import ClimateInfo, ClimateMode, ClimateState
+from aioesphomeapi import (
+    ClimateInfo,
+    ClimateMode,
+    ClimateState,
+    ClimateFanMode,
+    ClimateSwingMode,
+)
 
 from homeassistant.components.climate import ClimateDevice
 from homeassistant.components.climate.const import (
@@ -12,12 +18,29 @@ from homeassistant.components.climate.const import (
     HVAC_MODE_HEAT_COOL,
     HVAC_MODE_COOL,
     HVAC_MODE_HEAT,
+    HVAC_MODE_FAN_ONLY,
+    HVAC_MODE_DRY,
+    HVAC_MODE_OFF,
     SUPPORT_TARGET_TEMPERATURE,
     SUPPORT_PRESET_MODE,
     SUPPORT_TARGET_TEMPERATURE_RANGE,
+    SUPPORT_FAN_MODE,
+    SUPPORT_SWING_MODE,
     PRESET_AWAY,
-    HVAC_MODE_OFF,
     PRESET_HOME,
+    FAN_ON,
+    FAN_OFF,
+    FAN_AUTO,
+    FAN_LOW,
+    FAN_MEDIUM,
+    FAN_HIGH,
+    FAN_MIDDLE,
+    FAN_FOCUS,
+    FAN_DIFFUSE,
+    SWING_OFF,
+    SWING_BOTH,
+    SWING_VERTICAL,
+    SWING_HORIZONTAL,
 )
 from homeassistant.const import (
     ATTR_TEMPERATURE,
@@ -57,6 +80,33 @@ def _climate_modes():
         ClimateMode.AUTO: HVAC_MODE_HEAT_COOL,
         ClimateMode.COOL: HVAC_MODE_COOL,
         ClimateMode.HEAT: HVAC_MODE_HEAT,
+        ClimateMode.FAN_ONLY: HVAC_MODE_FAN_ONLY,
+        ClimateMode.DRY: HVAC_MODE_DRY,
+    }
+
+
+@esphome_map_enum
+def _fan_modes():
+    return {
+        ClimateFanMode.ON: FAN_ON,
+        ClimateFanMode.OFF: FAN_OFF,
+        ClimateFanMode.AUTO: FAN_AUTO,
+        ClimateFanMode.LOW: FAN_LOW,
+        ClimateFanMode.MEDIUM: FAN_MEDIUM,
+        ClimateFanMode.HIGH: FAN_HIGH,
+        ClimateFanMode.MIDDLE: FAN_MIDDLE,
+        ClimateFanMode.FOCUS: FAN_FOCUS,
+        ClimateFanMode.DIFFUSE: FAN_DIFFUSE,
+    }
+
+
+@esphome_map_enum
+def _swing_modes():
+    return {
+        ClimateSwingMode.OFF: SWING_OFF,
+        ClimateSwingMode.BOTH: SWING_BOTH,
+        ClimateSwingMode.VERTICAL: SWING_VERTICAL,
+        ClimateSwingMode.HORIZONTAL: SWING_HORIZONTAL,
     }
 
 
@@ -94,11 +144,27 @@ class EsphomeClimateDevice(EsphomeEntity, ClimateDevice):
             for mode in self._static_info.supported_modes
         ]
 
+    @property
+    def fan_modes(self):
+        """Return the list of available fan modes."""
+        return [
+            _fan_modes.from_esphome(mode)
+            for mode in self._static_info.supported_fan_modes
+        ]
+
     @property
     def preset_modes(self):
         """Return preset modes."""
         return [PRESET_AWAY, PRESET_HOME] if self._static_info.supports_away else []
 
+    @property
+    def swing_modes(self):
+        """Return the list of available swing modes."""
+        return [
+            _swing_modes.from_esphome(mode)
+            for mode in self._static_info.supported_swing_modes
+        ]
+
     @property
     def target_temperature_step(self) -> float:
         """Return the supported step of target temperature."""
@@ -125,6 +191,10 @@ class EsphomeClimateDevice(EsphomeEntity, ClimateDevice):
             features |= SUPPORT_TARGET_TEMPERATURE
         if self._static_info.supports_away:
             features |= SUPPORT_PRESET_MODE
+        if self._static_info.supported_fan_modes:
+            features |= SUPPORT_FAN_MODE
+        if self._static_info.supported_swing_modes:
+            features |= SUPPORT_SWING_MODE
         return features
 
     # https://github.com/PyCQA/pylint/issues/3150 for all @esphome_state_property
@@ -135,11 +205,21 @@ class EsphomeClimateDevice(EsphomeEntity, ClimateDevice):
         """Return current operation ie. heat, cool, idle."""
         return _climate_modes.from_esphome(self._state.mode)
 
+    @esphome_state_property
+    def fan_mode(self):
+        """Return current fan setting."""
+        return _fan_modes.from_esphome(self._state.fan_mode)
+
     @esphome_state_property
     def preset_mode(self):
         """Return current preset mode."""
         return PRESET_AWAY if self._state.away else PRESET_HOME
 
+    @esphome_state_property
+    def swing_mode(self):
+        """Return current swing mode."""
+        return _swing_modes.from_esphome(self._state.swing_mode)
+
     @esphome_state_property
     def current_temperature(self) -> Optional[float]:
         """Return the current temperature."""
@@ -183,3 +263,15 @@ class EsphomeClimateDevice(EsphomeEntity, ClimateDevice):
         """Set preset mode."""
         away = preset_mode == PRESET_AWAY
         await self._client.climate_command(key=self._static_info.key, away=away)
+
+    async def async_set_fan_mode(self, fan_mode: str) -> None:
+        """Set new fan mode."""
+        await self._client.climate_command(
+            key=self._static_info.key, fan_mode=_fan_modes.from_hass(fan_mode)
+        )
+
+    async def async_set_swing_mode(self, swing_mode: str) -> None:
+        """Set new swing mode."""
+        await self._client.climate_command(
+            key=self._static_info.key, swing_mode=_swing_modes.from_hass(swing_mode)
+        )
diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json
index 724946e6984..425ecc6ce32 100644
--- a/homeassistant/components/esphome/manifest.json
+++ b/homeassistant/components/esphome/manifest.json
@@ -4,7 +4,7 @@
   "config_flow": true,
   "documentation": "https://www.home-assistant.io/integrations/esphome",
   "requirements": [
-    "aioesphomeapi==2.5.0"
+    "aioesphomeapi==2.6.0"
   ],
   "dependencies": [],
   "zeroconf": ["_esphomelib._tcp.local."],
diff --git a/requirements_all.txt b/requirements_all.txt
index e4fa1aa04d8..d80cfc27930 100644
--- a/requirements_all.txt
+++ b/requirements_all.txt
@@ -142,7 +142,7 @@ aiobotocore==0.10.4
 aiodns==2.0.0
 
 # homeassistant.components.esphome
-aioesphomeapi==2.5.0
+aioesphomeapi==2.6.0
 
 # homeassistant.components.freebox
 aiofreepybox==0.0.8
diff --git a/requirements_test_all.txt b/requirements_test_all.txt
index bfd97c58fd8..3870f200c2d 100644
--- a/requirements_test_all.txt
+++ b/requirements_test_all.txt
@@ -50,7 +50,7 @@ aioautomatic==0.6.5
 aiobotocore==0.10.4
 
 # homeassistant.components.esphome
-aioesphomeapi==2.5.0
+aioesphomeapi==2.6.0
 
 # homeassistant.components.emulated_hue
 # homeassistant.components.http
-- 
GitLab