Repository: XKNX/xknx
Pull request #494 (2020-11-16 19:07:40)

Cover, Light, Switch: allow unknown state
<!--
  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.
-->
- Light and Switch are initialized with state `None` in xknx
- Cover, Light and Switch in HA use `assumed_state = True` if the devices state is None. This allows eg. for switching "on" or "off" instead of just "on" because state was shown as "off".

See #491 

## 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)
- [x] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] This change requires a documentation update

## Checklist:

- [x] I have performed a self-review of my own code
- [ ] 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
- [x] The Homeassistant plugin has been adjusted in case of new config options



Patch:
diff --git a/changelog.md b/changelog.md
index cec7d858..b53da94b 100644
--- a/changelog.md
+++ b/changelog.md
@@ -1,5 +1,11 @@
 # Changelog
 
+## Unreleased changes
+
+### Devices
+
+- Light, Switch: initialize state with `None` instead of `False` to account for unknown state.
+
 ## 0.15.3 Opposite day! 2020-10-29
 
 ### Devices
diff --git a/home-assistant-plugin/custom_components/xknx/cover.py b/home-assistant-plugin/custom_components/xknx/cover.py
index b88b1cfe..f1d046a8 100644
--- a/home-assistant-plugin/custom_components/xknx/cover.py
+++ b/home-assistant-plugin/custom_components/xknx/cover.py
@@ -91,6 +91,11 @@ class KNXCover(KnxEntity, CoverEntity):
         """Return if the cover is closing or not."""
         return self._device.is_closing()
 
+    @property
+    def assumed_state(self) -> bool:
+        """Return True if unable to access real state of the entity."""
+        return self._device.current_position() is None
+
     async def async_close_cover(self, **kwargs):
         """Close the cover."""
         await self._device.set_down()
diff --git a/home-assistant-plugin/custom_components/xknx/light.py b/home-assistant-plugin/custom_components/xknx/light.py
index d9f0f9c0..cb65d147 100644
--- a/home-assistant-plugin/custom_components/xknx/light.py
+++ b/home-assistant-plugin/custom_components/xknx/light.py
@@ -202,3 +202,8 @@ class KNXLight(KnxEntity, LightEntity):
     async def async_turn_off(self, **kwargs):
         """Turn the light off."""
         await self._device.set_off()
+
+    @property
+    def assumed_state(self) -> bool:
+        """Return True if unable to access real state of the entity."""
+        return self._device.state is None
diff --git a/home-assistant-plugin/custom_components/xknx/switch.py b/home-assistant-plugin/custom_components/xknx/switch.py
index ae3048e2..048ec443 100644
--- a/home-assistant-plugin/custom_components/xknx/switch.py
+++ b/home-assistant-plugin/custom_components/xknx/switch.py
@@ -35,3 +35,8 @@ class KNXSwitch(KnxEntity, SwitchEntity):
     async def async_turn_off(self, **kwargs):
         """Turn the device off."""
         await self._device.set_off()
+
+    @property
+    def assumed_state(self) -> bool:
+        """Return True if unable to access real state of the entity."""
+        return self._device.state is None
diff --git a/xknx/devices/light.py b/xknx/devices/light.py
index 49129567..34480547 100644
--- a/xknx/devices/light.py
+++ b/xknx/devices/light.py
@@ -12,6 +12,7 @@ It provides functionality for
 """
 from enum import Enum
 import logging
+from typing import Optional
 
 from xknx.remote_value import (
     RemoteValueColorRGB,
@@ -242,10 +243,9 @@ class Light(Device):
         )
 
     @property
-    def state(self):
+    def state(self) -> Optional[bool]:
         """Return the current switch state of the device."""
-        # None will return False
-        return bool(self.switch.value)
+        return self.switch.value
 
     async def set_on(self):
         """Switch light on."""
diff --git a/xknx/devices/switch.py b/xknx/devices/switch.py
index eb46180b..e37c111b 100644
--- a/xknx/devices/switch.py
+++ b/xknx/devices/switch.py
@@ -36,7 +36,7 @@ class Switch(Device):
 
         self.reset_after = reset_after
         self._reset_task = None
-        self.state = False
+        self.state = None
 
         self.switch = RemoteValueSwitch(
             xknx,
diff --git a/xknx/remote_value/remote_value_switch.py b/xknx/remote_value/remote_value_switch.py
index 18fbfcee..4351eff9 100644
--- a/xknx/remote_value/remote_value_switch.py
+++ b/xknx/remote_value/remote_value_switch.py
@@ -44,7 +44,7 @@ class RemoteValueSwitch(RemoteValue):
         """Test if telegram payload may be parsed."""
         return isinstance(payload, DPTBinary)
 
-    def to_knx(self, value):
+    def to_knx(self, value: bool):
         """Convert value to payload."""
         if isinstance(value, bool):
             return DPTBinary(value ^ self.invert)


Test patch:
diff --git a/test/devices_tests/binary_sensor_test.py b/test/devices_tests/binary_sensor_test.py
index 471be8fe..59e48332 100644
--- a/test/devices_tests/binary_sensor_test.py
+++ b/test/devices_tests/binary_sensor_test.py
@@ -128,7 +128,7 @@ class TestBinarySensor(unittest.TestCase):
         binary_sensor.actions.append(action_off)
 
         self.assertEqual(binary_sensor.state, None)
-        self.assertEqual(switch.state, False)
+        self.assertEqual(switch.state, None)
 
         telegram_on = Telegram(
             group_address=GroupAddress("1/2/3"), payload=DPTBinary(1)
@@ -161,7 +161,7 @@ class TestBinarySensor(unittest.TestCase):
         binary_sensor.actions.append(action_on)
 
         self.assertEqual(binary_sensor.state, None)
-        self.assertEqual(switch.state, False)
+        self.assertEqual(switch.state, None)
 
         telegram_on = Telegram(
             group_address=GroupAddress("1/2/3"), payload=DPTBinary(1)
diff --git a/test/devices_tests/light_test.py b/test/devices_tests/light_test.py
index 239414ed..3666e01e 100644
--- a/test/devices_tests/light_test.py
+++ b/test/devices_tests/light_test.py
@@ -434,7 +434,7 @@ class TestLight(unittest.TestCase):
             group_address_switch="1/2/3",
             group_address_brightness="1/2/5",
         )
-        self.assertEqual(light.state, False)
+        self.assertEqual(light.state, None)
 
         telegram = Telegram(GroupAddress("1/2/3"), payload=DPTBinary(1))
         self.loop.run_until_complete(light.process(telegram))
diff --git a/test/devices_tests/switch_test.py b/test/devices_tests/switch_test.py
index 55fe603d..124565ae 100644
--- a/test/devices_tests/switch_test.py
+++ b/test/devices_tests/switch_test.py
@@ -58,37 +58,44 @@ class TestSwitch(unittest.TestCase):
     def test_process(self):
         """Test process / reading telegrams from telegram queue. Test if device was updated."""
         xknx = XKNX()
-        switch = Switch(xknx, "TestOutlet", group_address="1/2/3")
-        self.assertEqual(switch.state, False)
+        switch1 = Switch(xknx, "TestOutlet", group_address="1/2/3")
+        switch2 = Switch(xknx, "TestOutlet", group_address="1/2/3")
+        self.assertEqual(switch1.state, None)
+        self.assertEqual(switch2.state, None)
 
         telegram_on = Telegram(
             group_address=GroupAddress("1/2/3"), payload=DPTBinary(1)
         )
-        self.loop.run_until_complete(switch.process(telegram_on))
-        self.assertEqual(switch.state, True)
-
         telegram_off = Telegram(
             group_address=GroupAddress("1/2/3"), payload=DPTBinary(0)
         )
-        self.loop.run_until_complete(switch.process(telegram_off))
-        self.assertEqual(switch.state, False)
+
+        self.loop.run_until_complete(switch1.process(telegram_on))
+        self.assertEqual(switch1.state, True)
+        self.loop.run_until_complete(switch1.process(telegram_off))
+        self.assertEqual(switch1.state, False)
+        # test setting switch2 to False with first telegram
+        self.loop.run_until_complete(switch2.process(telegram_off))
+        self.assertEqual(switch2.state, False)
+        self.loop.run_until_complete(switch2.process(telegram_on))
+        self.assertEqual(switch2.state, True)
 
     def test_process_invert(self):
         """Test process / reading telegrams from telegram queue with inverted switch."""
         xknx = XKNX()
         switch = Switch(xknx, "TestOutlet", group_address="1/2/3", invert=True)
-        self.assertEqual(switch.state, False)
+        self.assertEqual(switch.state, None)
 
-        telegram_on = Telegram(
+        telegram_inv_on = Telegram(
             group_address=GroupAddress("1/2/3"), payload=DPTBinary(0)
         )
-        self.loop.run_until_complete(switch.process(telegram_on))
-        self.assertEqual(switch.state, True)
-
-        telegram_off = Telegram(
+        telegram_inv_off = Telegram(
             group_address=GroupAddress("1/2/3"), payload=DPTBinary(1)
         )
-        self.loop.run_until_complete(switch.process(telegram_off))
+
+        self.loop.run_until_complete(switch.process(telegram_inv_on))
+        self.assertEqual(switch.state, True)
+        self.loop.run_until_complete(switch.process(telegram_inv_off))
         self.assertEqual(switch.state, False)
 
     def test_process_reset_after(self):