Repository: XKNX/xknx
Pull request #582 (2021-01-29 15:49:36)

Change fan step mode to support new HA fan entity
<!--
  You are awesome! Thanks for contributing to our project!
  Please, DO NOT DELETE ANY TEXT from this template!.
-->
## Description
<!--
Please include a summary of the change and which issue is fixed.
Please also include relevant motivation and context.
List any dependencies that are required for this change.
-->
HA now provides only the percentage for fans. To support step fans, the percentage has to be converted to steps.

## Type of change
<!--
Please tick the applicable options.
NOTE: Ticking multiple options most likely indicates
that your change is to big and it is suggested to split it into several smaller PRs.
-->

- [ ] Bug fix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [x] This change requires a documentation update

## Checklist:

- [x] I have performed a self-review of my own code
- [x] The documentation has been adjusted accordingly
- [x] The changes generate no new warnings
- [x] Tests have been added that prove the fix is effective or that the feature works
- [x] The changes are documented in the changelog
- [ ] The Homeassistant plugin has been adjusted in case of new config options


coveralls: 
[![Coverage Status](https://coveralls.io/builds/36702920/badge)](https://coveralls.io/builds/36702920)

Coverage increased (+0.01%) to 94.868% when pulling **94a157a23d4232dcdcf98e36f1808e6f9007bd3c on crazyfx1:feat/add_fan_ha_support** into **dad8d48fcf0faa91e30fd22d9dd3256519a6407f on XKNX:master**.


Patch:
diff --git a/changelog.md b/changelog.md
index e68058d9..7aff5797 100644
--- a/changelog.md
+++ b/changelog.md
@@ -4,7 +4,7 @@
 
 ### Devices
 
-- Fan: Add "mode" parameter to control fans by steps instead of percentage.
+- Fan: Add `max_step` attribute which defines the maximum amount of steps. If set, the fan is controlled by steps instead of percentage.
 
 ## 0.16.2 Bugfix for yaml loader 2021-01-24
 
diff --git a/docs/fan.md b/docs/fan.md
index 9a0591b0..402ffe2f 100644
--- a/docs/fan.md
+++ b/docs/fan.md
@@ -18,7 +18,7 @@ Fans are simple representations of KNX controlled fans. They support setting the
 - `group_address` is the KNX group address of the fan device. Used for sending.
 - `group_address_state` is the KNX group address of the fan state. Used for updating and reading state.
 - `device_updated_cb` awaitable callback for each update.
-- `mode` Enum for the type of the fan control. Can be *Percent* or *Step*. Default: FanSpeedMode.Percent
+- `max_step` Maximum step amount for fans which are controlled with steps and not percentage.
 
 ## [](#header-2)Example
 
diff --git a/home-assistant-plugin/custom_components/xknx/knx_entity.py b/home-assistant-plugin/custom_components/xknx/knx_entity.py
index 296bcb2f..f4597ad2 100644
--- a/home-assistant-plugin/custom_components/xknx/knx_entity.py
+++ b/home-assistant-plugin/custom_components/xknx/knx_entity.py
@@ -40,12 +40,12 @@ class KnxEntity(Entity):
         """Store register state change callback."""
         self._device.register_device_updated_cb(self.after_update_callback)
 
-        if isinstance(self._device, XknxClimate):
+        if isinstance(self._device, XknxClimate) and self._device.mode is not None:
             self._device.mode.register_device_updated_cb(self.after_update_callback)
 
     async def async_will_remove_from_hass(self) -> None:
         """Disconnect device object when removed."""
         self._device.unregister_device_updated_cb(self.after_update_callback)
 
-        if isinstance(self._device, XknxClimate):
+        if isinstance(self._device, XknxClimate) and self._device.mode is not None:
             self._device.mode.unregister_device_updated_cb(self.after_update_callback)
diff --git a/xknx/devices/fan.py b/xknx/devices/fan.py
index a00ca055..6655865c 100644
--- a/xknx/devices/fan.py
+++ b/xknx/devices/fan.py
@@ -30,9 +30,6 @@ class FanSpeedMode(Enum):
     Step = 2
 
 
-DEFAULT_MODE = FanSpeedMode.Percent
-
-
 class Fan(Device):
     """Class for managing a fan."""
 
@@ -46,14 +43,17 @@ class Fan(Device):
         group_address_speed: Optional["GroupAddressableType"] = None,
         group_address_speed_state: Optional["GroupAddressableType"] = None,
         device_updated_cb: Optional[DeviceCallbackType] = None,
-        mode: FanSpeedMode = DEFAULT_MODE,
+        max_step: Optional[int] = None,
     ):
         """Initialize fan class."""
         # pylint: disable=too-many-arguments
         super().__init__(xknx, name, device_updated_cb)
 
         self.speed: Union[RemoteValueDptValue1Ucount, RemoteValueScaling]
-        if mode == FanSpeedMode.Step:
+        self.mode = FanSpeedMode.Step if max_step is not None else FanSpeedMode.Percent
+        self.max_step = max_step
+
+        if self.mode == FanSpeedMode.Step:
             self.speed = RemoteValueDptValue1Ucount(
                 xknx,
                 group_address_speed,
@@ -83,14 +83,14 @@ class Fan(Device):
         """Initialize object from configuration structure."""
         group_address_speed = config.get("group_address_speed")
         group_address_speed_state = config.get("group_address_speed_state")
-        mode = config.get("mode", DEFAULT_MODE)
+        max_step = config.get("max_step")
 
         return cls(
             xknx,
             name,
             group_address_speed=group_address_speed,
             group_address_speed_state=group_address_speed_state,
-            mode=mode,
+            max_step=max_step,
         )
 
     def __str__(self) -> str:


Test patch:
diff --git a/test/devices_tests/fan_test.py b/test/devices_tests/fan_test.py
index 0038af2f..5bfef7de 100644
--- a/test/devices_tests/fan_test.py
+++ b/test/devices_tests/fan_test.py
@@ -5,7 +5,6 @@ from unittest.mock import patch
 
 from xknx import XKNX
 from xknx.devices import Fan
-from xknx.devices.fan import FanSpeedMode
 from xknx.dpt import DPTArray, DPTBinary
 from xknx.exceptions import CouldNotParseTelegram
 from xknx.telegram import GroupAddress, Telegram
@@ -52,7 +51,6 @@ class TestFan(unittest.TestCase):
             xknx,
             name="TestFan",
             group_address_speed_state="1/2/3",
-            mode=FanSpeedMode.Step,
         )
         self.loop.run_until_complete(fan.sync())
 
@@ -115,10 +113,14 @@ class TestFan(unittest.TestCase):
     # TEST SET SPEED STEP
     #
     def test_set_speed_step(self):
-        """Test setting the speed of a Fan."""
+        """Test setting the speed of a Fan in step mode."""
+
         xknx = XKNX()
         fan = Fan(
-            xknx, name="TestFan", group_address_speed="1/2/3", mode=FanSpeedMode.Step
+            xknx,
+            name="TestFan",
+            group_address_speed="1/2/3",
+            max_step=3,
         )
         self.loop.run_until_complete(fan.set_speed(2))
         self.assertEqual(xknx.telegrams.qsize(), 1)
@@ -176,9 +178,13 @@ class TestFan(unittest.TestCase):
     #
     def test_process_speed_step(self):
         """Test process / reading telegrams from telegram queue. Test if speed is processed."""
+
         xknx = XKNX()
         fan = Fan(
-            xknx, name="TestFan", group_address_speed="1/2/3", mode=FanSpeedMode.Step
+            xknx,
+            name="TestFan",
+            group_address_speed="1/2/3",
+            max_step=3,
         )
         self.assertEqual(fan.current_speed, None)