Repository: XKNX/xknx
Pull request #573 (2021-01-25 06:30:53)

Bump setuptools from 51.3.3 to 52.0.0
Bumps [setuptools](https://github.com/pypa/setuptools) from 51.3.3 to 52.0.0.
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a href="https://github.com/pypa/setuptools/blob/main/CHANGES.rst">setuptools's changelog</a>.</em></p>
<blockquote>
<h1>v52.0.0</h1>
<h2>Breaking Changes</h2>
<ul>
<li><a href="https://github-redirect.dependabot.com/pypa/setuptools/issues/2537">#2537</a>: Remove fallback support for fetch_build_eggs using easy_install. Now pip is required for setup_requires to succeed.</li>
<li><a href="https://github-redirect.dependabot.com/pypa/setuptools/issues/2544">#2544</a>: Removed 'easy_install' top-level model (runpy entry point) and 'easy_install' console script.</li>
<li><a href="https://github-redirect.dependabot.com/pypa/setuptools/issues/2545">#2545</a>: Removed support for eggsecutables.</li>
</ul>
<h2>Changes</h2>
<ul>
<li><a href="https://github-redirect.dependabot.com/pypa/setuptools/issues/2459">#2459</a>: Tests now run in parallel via pytest-xdist, completing in about half the time. Special thanks to webknjaz for hard work implementing test isolation. To run without parallelization, disable the plugin with <code>tox -- -p no:xdist</code>.</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/pypa/setuptools/commit/e1ffc2abbae4f2aa78dd09ee9827d754b7702b7b"><code>e1ffc2a</code></a> Bump version: 51.3.3 → 52.0.0</li>
<li><a href="https://github.com/pypa/setuptools/commit/4a4ef0bad70904fad479e803cbe528e756087b93"><code>4a4ef0b</code></a> Merge pull request <a href="https://github-redirect.dependabot.com/pypa/setuptools/issues/2544">#2544</a> from pypa/feature/remove-easy-install</li>
<li><a href="https://github.com/pypa/setuptools/commit/4fb7735edd1cb7e746b7fb25fdba2aca145bde10"><code>4fb7735</code></a> Update changelog</li>
<li><a href="https://github.com/pypa/setuptools/commit/2885ca26494e6f555fae85f8f9983c2361c93829"><code>2885ca2</code></a> Remove 'main' function from 'easy_install'.</li>
<li><a href="https://github.com/pypa/setuptools/commit/4b0408a18dcda286af6668b7ef6934e53d1f247c"><code>4b0408a</code></a> Remove easy_install script and module.</li>
<li><a href="https://github.com/pypa/setuptools/commit/ea22005e27871431a29781632a52fee2a0a7cf17"><code>ea22005</code></a> Merge pull request <a href="https://github-redirect.dependabot.com/pypa/setuptools/issues/2545">#2545</a> from pypa/feature/remove-eggsecutable</li>
<li><a href="https://github.com/pypa/setuptools/commit/c0660de32f80ad71b3940abcb26b237ca2103876"><code>c0660de</code></a> Update changelog.</li>
<li><a href="https://github.com/pypa/setuptools/commit/aaf0661edbcbdb2f77d39026b66ab9eda4fe75b5"><code>aaf0661</code></a> Merge pull request <a href="https://github-redirect.dependabot.com/pypa/setuptools/issues/2537">#2537</a> from pypa/feature/drop-fetch-build-eggs-easy-install...</li>
<li><a href="https://github.com/pypa/setuptools/commit/0afc4ef094eabbb0e7161acc3f4f8cba3bae7022"><code>0afc4ef</code></a> Merge branch 'main' into feature/drop-fetch-build-eggs-easy-install-fallback</li>
<li><a href="https://github.com/pypa/setuptools/commit/fa81391fe65fa727f79f1be73f058e83ffe97193"><code>fa81391</code></a> Remove eggsecutable</li>
<li>Additional commits viewable in <a href="https://github.com/pypa/setuptools/compare/v51.3.3...v52.0.0">compare view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility score](https://api.dependabot.com/badges/compatibility_score?dependency-name=setuptools&package-manager=pip&previous-version=51.3.3&new-version=52.0.0)](https://dependabot.com/compatibility-score/?dependency-name=setuptools&package-manager=pip&previous-version=51.3.3&new-version=52.0.0)

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
- `@dependabot use these labels` will set the current labels as the default for future PRs for this repo and language
- `@dependabot use these reviewers` will set the current reviewers as the default for future PRs for this repo and language
- `@dependabot use these assignees` will set the current assignees as the default for future PRs for this repo and language
- `@dependabot use this milestone` will set the current milestone as the default for future PRs for this repo and language
- `@dependabot badge me` will comment on this PR with code to add a "Dependabot enabled" badge to your readme

Additionally, you can set the following in your Dependabot [dashboard](https://app.dependabot.com):
- Update frequency (including time of day and day of week)
- Pull request limits (per update run and/or open at any time)
- Out-of-range updates (receive only lockfile updates, if desired)
- Security updates (receive only security updates, if desired)



</details>

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

Coverage remained the same at 94.851% when pulling **79a1b9d83499a650678fca278d5c0dbea44685db on dependabot/pip/setuptools-52.0.0** into **21b63f5915c6f815f3c0281c8f5cfa8c6057dc5a on master**.


Patch:
diff --git a/changelog.md b/changelog.md
index 4656935f..e68058d9 100644
--- a/changelog.md
+++ b/changelog.md
@@ -1,5 +1,11 @@
 # Changelog
 
+## 0.xx
+
+### Devices
+
+- Fan: Add "mode" parameter to control fans by steps instead of percentage.
+
 ## 0.16.2 Bugfix for yaml loader 2021-01-24
 
 ### Internals
diff --git a/docs/fan.md b/docs/fan.md
new file mode 100644
index 00000000..9a0591b0
--- /dev/null
+++ b/docs/fan.md
@@ -0,0 +1,49 @@
+---
+layout: default
+title: Fan
+parent: Devices
+nav_order: 4
+---
+
+# [](#header-1)Fans
+
+## [](#header-2)Overview
+
+Fans are simple representations of KNX controlled fans. They support setting the speed.
+
+## [](#header-2)Interface
+
+- `xknx` XKNX object.
+- `name` name of the device.
+- `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
+
+## [](#header-2)Example
+
+```python
+fan = Fan(xknx,
+              'TestFan',
+              group_address='1/2/1',
+              group_address_state='1/2/2')
+
+# Set the fan speed
+await fan.set_speed(50)
+
+# Accessing speed
+print(fan.current_speed)
+
+# Requesting state via KNX GroupValueRead
+await fan.sync()
+```
+
+## [](#header-2)Configuration via **xknx.yaml**
+
+Fans are usually configured via [`xknx.yaml`](/configuration):
+
+```yaml
+groups:
+    fan:
+        Livingroom.Fan_1: {group_address: '1/4/1', group_address_state: '1/4/2' }
+```
diff --git a/docs/light.md b/docs/light.md
index 549d4495..d4ed63e0 100644
--- a/docs/light.md
+++ b/docs/light.md
@@ -2,7 +2,7 @@
 layout: default
 title: Lights / Dimmer
 parent: Devices
-nav_order: 4
+nav_order: 5
 ---
 
 # [](#header-1)Light & Dimmer
diff --git a/docs/sensor.md b/docs/sensor.md
index 4fa91ba4..01454ffd 100644
--- a/docs/sensor.md
+++ b/docs/sensor.md
@@ -2,7 +2,7 @@
 layout: default
 title: Sensor
 parent: Devices
-nav_order: 5
+nav_order: 6
 ---
 
 # [](#header-1)Sensor - Monitor values of KNX
diff --git a/docs/switch.md b/docs/switch.md
index 46275311..40d212ad 100644
--- a/docs/switch.md
+++ b/docs/switch.md
@@ -2,7 +2,7 @@
 layout: default
 title: Switches
 parent: Devices
-nav_order: 6
+nav_order: 7
 ---
 
 # [](#header-1)Switches
diff --git a/docs/time.md b/docs/time.md
index b47605a5..b81ff4aa 100644
--- a/docs/time.md
+++ b/docs/time.md
@@ -2,7 +2,7 @@
 layout: default
 title: Time
 parent: Devices
-nav_order: 7
+nav_order: 8
 ---
 
 # [](#header-1)Time
diff --git a/docs/weather.md b/docs/weather.md
index b81f8be4..11eec415 100644
--- a/docs/weather.md
+++ b/docs/weather.md
@@ -2,7 +2,7 @@
 layout: default
 title: Weather
 parent: Devices
-nav_order: 8
+nav_order: 9
 ---
 
 # [](#header-1)Weather device
diff --git a/xknx/devices/fan.py b/xknx/devices/fan.py
index e986c53b..a00ca055 100644
--- a/xknx/devices/fan.py
+++ b/xknx/devices/fan.py
@@ -3,17 +3,19 @@ Module for managing a fan via KNX.
 
 It provides functionality for
 
-* setting fan to specific speed
+* setting fan to specific speed / step
 * reading the current speed from KNX bus.
 """
+from enum import Enum
 import logging
-from typing import TYPE_CHECKING, Any, Iterator, Optional
+from typing import TYPE_CHECKING, Any, Iterator, Optional, Union
 
-from xknx.remote_value import RemoteValueScaling
+from xknx.remote_value import RemoteValueDptValue1Ucount, RemoteValueScaling
 
 from .device import Device, DeviceCallbackType
 
 if TYPE_CHECKING:
+    from xknx.remote_value import RemoteValue
     from xknx.telegram import Telegram
     from xknx.telegram.address import GroupAddressableType
     from xknx.xknx import XKNX
@@ -21,6 +23,16 @@ if TYPE_CHECKING:
 logger = logging.getLogger("xknx.log")
 
 
+class FanSpeedMode(Enum):
+    """Enum for setting the fan speed mode."""
+
+    Percent = 1
+    Step = 2
+
+
+DEFAULT_MODE = FanSpeedMode.Percent
+
+
 class Fan(Device):
     """Class for managing a fan."""
 
@@ -34,23 +46,35 @@ 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,
     ):
         """Initialize fan class."""
         # pylint: disable=too-many-arguments
         super().__init__(xknx, name, device_updated_cb)
 
-        self.speed = RemoteValueScaling(
-            xknx,
-            group_address_speed,
-            group_address_speed_state,
-            device_name=self.name,
-            feature_name="Speed",
-            after_update_cb=self.after_update,
-            range_from=0,
-            range_to=100,
-        )
+        self.speed: Union[RemoteValueDptValue1Ucount, RemoteValueScaling]
+        if mode == FanSpeedMode.Step:
+            self.speed = RemoteValueDptValue1Ucount(
+                xknx,
+                group_address_speed,
+                group_address_speed_state,
+                device_name=self.name,
+                feature_name="Speed",
+                after_update_cb=self.after_update,
+            )
+        else:
+            self.speed = RemoteValueScaling(
+                xknx,
+                group_address_speed,
+                group_address_speed_state,
+                device_name=self.name,
+                feature_name="Speed",
+                after_update_cb=self.after_update,
+                range_from=0,
+                range_to=100,
+            )
 
-    def _iter_remote_values(self) -> Iterator[RemoteValueScaling]:
+    def _iter_remote_values(self) -> Iterator["RemoteValue"]:
         """Iterate the devices RemoteValue classes."""
         yield self.speed
 
@@ -59,12 +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)
 
         return cls(
             xknx,
             name,
             group_address_speed=group_address_speed,
             group_address_speed_state=group_address_speed_state,
+            mode=mode,
         )
 
     def __str__(self) -> str:


Test patch:
diff --git a/requirements/testing.txt b/requirements/testing.txt
index 26774b2c..1c48a71a 100644
--- a/requirements/testing.txt
+++ b/requirements/testing.txt
@@ -10,6 +10,6 @@ pylint-strict-informational==0.1
 pytest==6.2.1
 pytest-cov==2.11.1
 pytest-timeout==1.4.2
-setuptools==51.3.3
+setuptools==52.0.0
 tox==3.21.2
 mypy==0.790
diff --git a/test/devices_tests/fan_test.py b/test/devices_tests/fan_test.py
index 0e7a7767..0038af2f 100644
--- a/test/devices_tests/fan_test.py
+++ b/test/devices_tests/fan_test.py
@@ -5,6 +5,7 @@ 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
@@ -44,6 +45,27 @@ class TestFan(unittest.TestCase):
             ),
         )
 
+    def test_sync_step(self):
+        """Test sync function / sending group reads to KNX bus."""
+        xknx = XKNX()
+        fan = Fan(
+            xknx,
+            name="TestFan",
+            group_address_speed_state="1/2/3",
+            mode=FanSpeedMode.Step,
+        )
+        self.loop.run_until_complete(fan.sync())
+
+        self.assertEqual(xknx.telegrams.qsize(), 1)
+
+        telegram1 = xknx.telegrams.get_nowait()
+        self.assertEqual(
+            telegram1,
+            Telegram(
+                destination_address=GroupAddress("1/2/3"), payload=GroupValueRead()
+            ),
+        )
+
     #
     # SYNC WITH STATE ADDRESS
     #
@@ -88,6 +110,27 @@ class TestFan(unittest.TestCase):
             ),
         )
 
+    #
+    #
+    # TEST SET SPEED STEP
+    #
+    def test_set_speed_step(self):
+        """Test setting the speed of a Fan."""
+        xknx = XKNX()
+        fan = Fan(
+            xknx, name="TestFan", group_address_speed="1/2/3", mode=FanSpeedMode.Step
+        )
+        self.loop.run_until_complete(fan.set_speed(2))
+        self.assertEqual(xknx.telegrams.qsize(), 1)
+        telegram = xknx.telegrams.get_nowait()
+        self.assertEqual(
+            telegram,
+            Telegram(
+                destination_address=GroupAddress("1/2/3"),
+                payload=GroupValueWrite(DPTArray(2)),
+            ),
+        )
+
     #
     # TEST PROCESS
     #
@@ -128,6 +171,24 @@ class TestFan(unittest.TestCase):
         with self.assertRaises(CouldNotParseTelegram):
             self.loop.run_until_complete(fan.process(telegram))
 
+    #
+    # TEST PROCESS STEP MODE
+    #
+    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
+        )
+        self.assertEqual(fan.current_speed, None)
+
+        telegram = Telegram(
+            destination_address=GroupAddress("1/2/3"),
+            payload=GroupValueWrite(DPTArray(2)),
+        )
+        self.loop.run_until_complete(fan.process(telegram))
+        self.assertEqual(fan.current_speed, 2)
+
     #
     # TEST DO
     #