Repository: XKNX/xknx (python)

Issue:
Switch: Add invert function
Sometimes it makes sense to invert a switch. So it would make sense to add an `invert` flag to Switch and pass it to the RemoteValue (that already supports this).

Pull request description:
Invertable Switch and BinarySensor
<!--
  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.
-->
- BinarySensor: added option to invert payloads
- Switch: added option to invert payloads

Bugfix: 
- HA Switch entity: keep state without state_address. Previously `after_update` was called before the internal state of a Switch() was changed. So HA would read the old state, not the updated one. Moving the call to `after_update` from the RemoteValue to Switch.process_group_write() should fix this.

closes #442 

## 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.
-->

- [x] 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
- [x] The Homeassistant plugin has been adjusted in case of new config options


Patch:
diff --git a/changelog.md b/changelog.md
index 3c14d7f5..2d972af4 100644
--- a/changelog.md
+++ b/changelog.md
@@ -1,5 +1,16 @@
 # Changelog
 
+## Unreleased Changes
+
+### Devices
+
+- BinarySensor: added option to invert payloads
+- Switch: added option to invert payloads
+
+### Bugfixes
+
+- HA Switch entity: keep state without state_address
+
 ## 0.15.2 Winter is coming
 
 ### Devices
diff --git a/docs/binary_sensor.md b/docs/binary_sensor.md
index 9c9041d3..ed523784 100644
--- a/docs/binary_sensor.md
+++ b/docs/binary_sensor.md
@@ -22,6 +22,7 @@ binarysensor = BinarySensor(xknx, 'TestInput', group_address_state='1/2/3', devi
 - `xknx` is the XKNX object.
 - `name` is the name of the object.
 - `group_address_state` is the KNX group address of the sensor device.
+- `invert` inverts the payload so state "on" is represented by 0 on bus and "off" by 1. Defaults to `False`
 - `sync_state` defines if the value should be actively read from the bus. If `False` no GroupValueRead telegrams will be sent to its group address. Defaults to `True`
 - `ignore_internal_state` allows callback call regardless of the current binary sensor state. Defaults to `True`
 - `context_timeout` time in seconds telegrams should be counted towards the current context to increment the counter. To be used with `ignore_internal_state=True`. Defaults to `None`
diff --git a/docs/switch.md b/docs/switch.md
index df918a3a..6f28ec8c 100644
--- a/docs/switch.md
+++ b/docs/switch.md
@@ -11,6 +11,13 @@ nav_order: 6
 
 Switches are simple representations of binary actors. They mainly support switching on and off.
 
+- `xknx` is the XKNX object.
+- `name` is the name of the object.
+- `group_address` is the KNX group address of the switch device. Used for sending.
+- `group_address_state` is the KNX group address of the switch state. Used for updating and reading state.
+- `invert` inverts the payload so state "on" is represented by 0 on bus and "off" by 1. Defaults to `False`
+- `reset_after` may be used to reset the switch to `OFF` again after given time in sec. Defaults to `None`
+
 ## [](#header-2)Example
 
 ```python
diff --git a/home-assistant-plugin/custom_components/xknx/const.py b/home-assistant-plugin/custom_components/xknx/const.py
index 457694be..55f74cc8 100644
--- a/home-assistant-plugin/custom_components/xknx/const.py
+++ b/home-assistant-plugin/custom_components/xknx/const.py
@@ -17,6 +17,7 @@ from homeassistant.components.climate.const import (
 
 DOMAIN = "xknx"
 
+CONF_INVERT = "invert"
 CONF_STATE_ADDRESS = "state_address"
 CONF_SYNC_STATE = "sync_state"
 CONF_RESET_AFTER = "reset_after"
diff --git a/home-assistant-plugin/custom_components/xknx/factory.py b/home-assistant-plugin/custom_components/xknx/factory.py
index 005d24d5..83c48d2f 100644
--- a/home-assistant-plugin/custom_components/xknx/factory.py
+++ b/home-assistant-plugin/custom_components/xknx/factory.py
@@ -203,6 +203,7 @@ def _create_switch(knx_module: XKNX, config: ConfigType) -> XknxSwitch:
         name=config[CONF_NAME],
         group_address=config[CONF_ADDRESS],
         group_address_state=config.get(SwitchSchema.CONF_STATE_ADDRESS),
+        invert=config.get(SwitchSchema.CONF_INVERT),
         reset_after=config.get(SwitchSchema.CONF_RESET_AFTER),
     )
 
@@ -246,6 +247,7 @@ def _create_binary_sensor(knx_module: XKNX, config: ConfigType) -> XknxBinarySen
         knx_module,
         name=device_name,
         group_address_state=config[BinarySensorSchema.CONF_STATE_ADDRESS],
+        invert=config.get(BinarySensorSchema.CONF_INVERT),
         sync_state=config[BinarySensorSchema.CONF_SYNC_STATE],
         device_class=config.get(CONF_DEVICE_CLASS),
         ignore_internal_state=config[BinarySensorSchema.CONF_IGNORE_INTERNAL_STATE],
diff --git a/home-assistant-plugin/custom_components/xknx/schema.py b/home-assistant-plugin/custom_components/xknx/schema.py
index d97a532a..cf3177bf 100644
--- a/home-assistant-plugin/custom_components/xknx/schema.py
+++ b/home-assistant-plugin/custom_components/xknx/schema.py
@@ -15,6 +15,7 @@ from homeassistant.const import (
 import homeassistant.helpers.config_validation as cv
 
 from .const import (
+    CONF_INVERT,
     CONF_RESET_AFTER,
     CONF_STATE_ADDRESS,
     CONF_SYNC_STATE,
@@ -85,6 +86,7 @@ class BinarySensorSchema:
 
     CONF_STATE_ADDRESS = CONF_STATE_ADDRESS
     CONF_SYNC_STATE = CONF_SYNC_STATE
+    CONF_INVERT = CONF_INVERT
     CONF_IGNORE_INTERNAL_STATE = "ignore_internal_state"
     CONF_CONTEXT_TIMEOUT = "context_timeout"
     CONF_RESET_AFTER = CONF_RESET_AFTER
@@ -108,6 +110,7 @@ class BinarySensorSchema:
                     vol.Coerce(float), vol.Range(min=0, max=10)
                 ),
                 vol.Optional(CONF_DEVICE_CLASS): cv.string,
+                vol.Optional(CONF_INVERT): cv.boolean,
                 vol.Optional(CONF_RESET_AFTER): cv.positive_int,
             }
         ),
@@ -257,6 +260,7 @@ class ClimateSchema:
 class SwitchSchema:
     """Voluptuous schema for KNX switches."""
 
+    CONF_INVERT = CONF_INVERT
     CONF_STATE_ADDRESS = CONF_STATE_ADDRESS
     CONF_RESET_AFTER = CONF_RESET_AFTER
 
@@ -266,6 +270,7 @@ class SwitchSchema:
             vol.Required(CONF_ADDRESS): cv.string,
             vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
             vol.Optional(CONF_STATE_ADDRESS): cv.string,
+            vol.Optional(CONF_INVERT): cv.boolean,
             vol.Optional(CONF_RESET_AFTER): vol.All(vol.Coerce(int), vol.Range(min=0)),
         }
     )
diff --git a/xknx/devices/binary_sensor.py b/xknx/devices/binary_sensor.py
index f1909de0..548d6224 100644
--- a/xknx/devices/binary_sensor.py
+++ b/xknx/devices/binary_sensor.py
@@ -25,8 +25,9 @@ class BinarySensor(Device):
     def __init__(
         self,
         xknx,
-        name,
+        name: str,
         group_address_state=None,
+        invert: Optional[bool] = False,
         sync_state: bool = True,
         ignore_internal_state: bool = True,
         device_class: str = None,
@@ -57,6 +58,7 @@ class BinarySensor(Device):
         self.remote_value = RemoteValueSwitch(
             xknx,
             group_address_state=group_address_state,
+            invert=invert,
             sync_state=sync_state,
             device_name=self.name,
             # after_update called internally
@@ -79,6 +81,7 @@ class BinarySensor(Device):
     def from_config(cls, xknx, name, config):
         """Initialize object from configuration structure."""
         group_address_state = config.get("group_address_state")
+        invert = config.get("invert")
         context_timeout = config.get("context_timeout")
         reset_after = config.get("reset_after")
         sync_state = config.get("sync_state", True)
@@ -94,6 +97,7 @@ class BinarySensor(Device):
             xknx,
             name,
             group_address_state=group_address_state,
+            invert=invert,
             sync_state=sync_state,
             ignore_internal_state=ignore_internal_state,
             context_timeout=context_timeout,
diff --git a/xknx/devices/switch.py b/xknx/devices/switch.py
index 0a9dab20..eb46180b 100644
--- a/xknx/devices/switch.py
+++ b/xknx/devices/switch.py
@@ -8,6 +8,7 @@ It provides functionality for
 """
 import asyncio
 import logging
+from typing import Optional
 
 from xknx.remote_value import RemoteValueSwitch
 
@@ -22,10 +23,11 @@ class Switch(Device):
     def __init__(
         self,
         xknx,
-        name,
-        reset_after=None,
+        name: str,
         group_address=None,
         group_address_state=None,
+        invert: Optional[bool] = False,
+        reset_after: Optional[float] = None,
         device_updated_cb=None,
     ):
         """Initialize Switch class."""
@@ -40,8 +42,8 @@ class Switch(Device):
             xknx,
             group_address,
             group_address_state,
+            invert=invert,
             device_name=self.name,
-            after_update_cb=self.after_update,
         )
 
     def _iter_remote_values(self):
@@ -58,12 +60,16 @@ class Switch(Device):
         """Initialize object from configuration structure."""
         group_address = config.get("group_address")
         group_address_state = config.get("group_address_state")
+        invert = config.get("invert")
+        reset_after = config.get("reset_after")
 
         return cls(
             xknx,
             name,
             group_address=group_address,
             group_address_state=group_address_state,
+            invert=invert,
+            reset_after=reset_after,
         )
 
     async def set_on(self):
@@ -95,6 +101,7 @@ class Switch(Device):
                 self._reset_task = asyncio.create_task(
                     self._reset_state(self.reset_after)
                 )
+            await self.after_update()
 
     async def _reset_state(self, wait_seconds: float):
         await asyncio.sleep(wait_seconds)
diff --git a/xknx/remote_value/remote_value_switch.py b/xknx/remote_value/remote_value_switch.py
index 66cfac28..18fbfcee 100644
--- a/xknx/remote_value/remote_value_switch.py
+++ b/xknx/remote_value/remote_value_switch.py
@@ -3,7 +3,7 @@ Module for managing an DPT Switch remote value.
 
 DPT 1.001.
 """
-from typing import List
+from typing import List, Optional
 
 from xknx.dpt import DPTBinary
 from xknx.exceptions import ConversionError, CouldNotParseTelegram
@@ -19,11 +19,11 @@ class RemoteValueSwitch(RemoteValue):
         xknx,
         group_address=None,
         group_address_state=None,
-        sync_state=True,
-        device_name=None,
-        feature_name="State",
+        sync_state: bool = True,
+        device_name: str = None,
+        feature_name: str = "State",
         after_update_cb=None,
-        invert=False,
+        invert: Optional[bool] = False,
         passive_group_addresses: List[str] = None,
     ):
         """Initialize remote value of KNX DPT 1.001."""
@@ -38,7 +38,7 @@ class RemoteValueSwitch(RemoteValue):
             after_update_cb=after_update_cb,
             passive_group_addresses=passive_group_addresses,
         )
-        self.invert = invert
+        self.invert = bool(invert)
 
     def payload_valid(self, payload):
         """Test if telegram payload may be parsed."""


Test patch:
diff --git a/test/devices_tests/binary_sensor_test.py b/test/devices_tests/binary_sensor_test.py
index c7bad9c8..853428cc 100644
--- a/test/devices_tests/binary_sensor_test.py
+++ b/test/devices_tests/binary_sensor_test.py
@@ -59,6 +59,26 @@ class TestBinarySensor(unittest.TestCase):
         self.loop.run_until_complete(binaryinput2.process(telegram_off2))
         self.assertEqual(binaryinput2.state, False)
 
+    def test_process_invert(self):
+        """Test process / reading telegrams from telegram queue."""
+        xknx = XKNX()
+        bs_invert = BinarySensor(xknx, "TestInput", "1/2/3", invert=True)
+
+        self.assertEqual(bs_invert.state, None)
+
+        telegram_on = Telegram(
+            group_address=GroupAddress("1/2/3"), payload=DPTBinary(0)
+        )
+        self.loop.run_until_complete(bs_invert.process(telegram_on))
+
+        self.assertEqual(bs_invert.state, True)
+
+        telegram_off = Telegram(
+            group_address=GroupAddress("1/2/3"), payload=DPTBinary(1)
+        )
+        self.loop.run_until_complete(bs_invert.process(telegram_off))
+        self.assertEqual(bs_invert.state, False)
+
     def test_process_reset_after(self):
         """Test process / reading telegrams from telegram queue."""
         xknx = XKNX()
diff --git a/test/devices_tests/switch_test.py b/test/devices_tests/switch_test.py
index a9520431..55fe603d 100644
--- a/test/devices_tests/switch_test.py
+++ b/test/devices_tests/switch_test.py
@@ -59,21 +59,36 @@ class TestSwitch(unittest.TestCase):
         """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)
 
         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)
 
+    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)
+
+        telegram_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(
+            group_address=GroupAddress("1/2/3"), payload=DPTBinary(1)
+        )
+        self.loop.run_until_complete(switch.process(telegram_off))
         self.assertEqual(switch.state, False)
 
     def test_process_reset_after(self):
@@ -165,6 +180,28 @@ class TestSwitch(unittest.TestCase):
             telegram, Telegram(GroupAddress("1/2/3"), payload=DPTBinary(0))
         )
 
+    #
+    # TEST SET INVERT
+    #
+    def test_set_invert(self):
+        """Test switching on/off inverted switch."""
+        xknx = XKNX()
+        switch = Switch(xknx, "TestOutlet", group_address="1/2/3", invert=True)
+
+        self.loop.run_until_complete(switch.set_on())
+        self.assertEqual(xknx.telegrams.qsize(), 1)
+        telegram = xknx.telegrams.get_nowait()
+        self.assertEqual(
+            telegram, Telegram(GroupAddress("1/2/3"), payload=DPTBinary(0))
+        )
+
+        self.loop.run_until_complete(switch.set_off())
+        self.assertEqual(xknx.telegrams.qsize(), 1)
+        telegram = xknx.telegrams.get_nowait()
+        self.assertEqual(
+            telegram, Telegram(GroupAddress("1/2/3"), payload=DPTBinary(1))
+        )
+
     #
     # TEST DO
     #