Repository: XKNX/xknx
Pull request #441 (2020-09-30 11:07:40)

Add support for listening group addresses to RemoteValue
Based on #321.

The only difference is that we use a dedicated `listening_group_addresses` setting instead of mixing it with the main GA.

The integration into the devices is not yet part of this PR but will be possible once the new config schema is ready.  (#437)

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

Coverage increased (+0.03%) to 93.441% when pulling **5150c1995f1ca0a733ead5dab3da2eacea82156a on marvin-w:feature/listening_gas** into **22c87f53b55336f09101bef5bbd406a9099208e1 on XKNX:master**.


Patch:
diff --git a/home-assistant-plugin/custom_components/xknx/__init__.py b/home-assistant-plugin/custom_components/xknx/__init__.py
index 321ba7f6..241e028b 100644
--- a/home-assistant-plugin/custom_components/xknx/__init__.py
+++ b/home-assistant-plugin/custom_components/xknx/__init__.py
@@ -224,9 +224,8 @@ class KNXModule:
             return self.connection_config_tunneling()
         if CONF_XKNX_ROUTING in self.config[DOMAIN]:
             return self.connection_config_routing()
-        # return None to let xknx use config from xknx.yaml connection block if given
-        #   otherwise it will use default ConnectionConfig (Automatic)
-        return None
+        # config from xknx.yaml always has priority later on
+        return ConnectionConfig()
 
     def connection_config_routing(self):
         """Return the connection_config if routing is configured."""
diff --git a/home-assistant-plugin/custom_components/xknx/manifest.json b/home-assistant-plugin/custom_components/xknx/manifest.json
index 078c9616..5267ba02 100644
--- a/home-assistant-plugin/custom_components/xknx/manifest.json
+++ b/home-assistant-plugin/custom_components/xknx/manifest.json
@@ -3,5 +3,6 @@
   "name": "KNX",
   "documentation": "https://www.home-assistant.io/integrations/knx",
   "requirements": [],
-  "codeowners": ["@Julius2342", "@farmio", "@marvin-w"]
+  "codeowners": ["@Julius2342", "@farmio", "@marvin-w"],
+  "quality_scale": "silver"
 }
diff --git a/xknx/remote_value/remote_value.py b/xknx/remote_value/remote_value.py
index 52e299fd..f63cc03e 100644
--- a/xknx/remote_value/remote_value.py
+++ b/xknx/remote_value/remote_value.py
@@ -7,6 +7,7 @@ Remote value can be :
 - or a group of both representing the same value.
 """
 import logging
+from typing import List
 
 from xknx.exceptions import CouldNotParseTelegram
 from xknx.telegram import GroupAddress, Telegram, TelegramType
@@ -27,10 +28,14 @@ class RemoteValue:
         device_name=None,
         feature_name=None,
         after_update_cb=None,
+        passive_group_addresses: List[str] = None,
     ):
         """Initialize RemoteValue class."""
         # pylint: disable=too-many-arguments
         self.xknx = xknx
+        self.passive_group_addresses = RemoteValue.get_passive_group_addresses(
+            passive_group_addresses
+        )
         if group_address is not None:
             group_address = GroupAddress(group_address)
         if group_address_state is not None:
@@ -58,7 +63,11 @@ class RemoteValue:
     @property
     def initialized(self):
         """Evaluate if remote value is initialized with group address."""
-        return bool(self.group_address_state or self.group_address)
+        return bool(
+            self.group_address_state
+            or self.group_address
+            or self.passive_group_addresses
+        )
 
     @property
     def readable(self):
@@ -72,7 +81,14 @@ class RemoteValue:
 
     def has_group_address(self, group_address):
         """Test if device has given group address."""
-        return group_address in [self.group_address, self.group_address_state]
+
+        def _internal_addresses():
+            """Yield all group_addresses."""
+            yield self.group_address
+            yield self.group_address_state
+            yield from self.passive_group_addresses
+
+        return group_address in _internal_addresses()
 
     def payload_valid(self, payload):
         """Test if telegram payload may be parsed - to be implemented in derived class.."""
@@ -219,3 +235,10 @@ class RemoteValue:
             if key not in self.__dict__:
                 return False
         return True
+
+    @staticmethod
+    def get_passive_group_addresses(passive_group_addresses: List[str]) -> List:
+        """Obtain passive state group addresses."""
+        if passive_group_addresses is None:
+            return []
+        return [GroupAddress(ga) for ga in passive_group_addresses]
diff --git a/xknx/remote_value/remote_value_climate_mode.py b/xknx/remote_value/remote_value_climate_mode.py
index 0d7d2cae..751615b1 100644
--- a/xknx/remote_value/remote_value_climate_mode.py
+++ b/xknx/remote_value/remote_value_climate_mode.py
@@ -4,6 +4,7 @@ Module for managing an climate mode remote values.
 DPT .
 """
 from enum import Enum
+from typing import List
 
 from xknx.dpt import (
     DPTArray,
@@ -38,6 +39,7 @@ class RemoteValueClimateMode(RemoteValue):
         feature_name="Climate Mode",
         climate_mode_type=None,
         after_update_cb=None,
+        passive_group_addresses: List[str] = None,
     ):
         """Initialize remote value of KNX climate mode."""
         # pylint: disable=too-many-arguments
@@ -49,6 +51,7 @@ class RemoteValueClimateMode(RemoteValue):
             device_name=device_name,
             feature_name=feature_name,
             after_update_cb=after_update_cb,
+            passive_group_addresses=passive_group_addresses,
         )
         if not isinstance(climate_mode_type, self.ClimateModeType):
             raise ConversionError(
diff --git a/xknx/remote_value/remote_value_color_rgb.py b/xknx/remote_value/remote_value_color_rgb.py
index 796b63cc..9fa8f47c 100644
--- a/xknx/remote_value/remote_value_color_rgb.py
+++ b/xknx/remote_value/remote_value_color_rgb.py
@@ -3,6 +3,8 @@ Module for managing an RGB remote value.
 
 DPT 232.600.
 """
+from typing import List
+
 from xknx.dpt import DPTArray
 from xknx.exceptions import ConversionError
 
@@ -20,6 +22,7 @@ class RemoteValueColorRGB(RemoteValue):
         device_name=None,
         feature_name="Color RGB",
         after_update_cb=None,
+        passive_group_addresses: List[str] = None,
     ):
         """Initialize remote value of KNX DPT 232.600 (DPT_Color_RGB)."""
         # pylint: disable=too-many-arguments
@@ -30,6 +33,7 @@ class RemoteValueColorRGB(RemoteValue):
             device_name=device_name,
             feature_name=feature_name,
             after_update_cb=after_update_cb,
+            passive_group_addresses=passive_group_addresses,
         )
 
     def payload_valid(self, payload):
diff --git a/xknx/remote_value/remote_value_color_rgbw.py b/xknx/remote_value/remote_value_color_rgbw.py
index 7c541822..28bd041e 100644
--- a/xknx/remote_value/remote_value_color_rgbw.py
+++ b/xknx/remote_value/remote_value_color_rgbw.py
@@ -3,6 +3,8 @@ Module for managing an RGBW remote value.
 
 DPT 251.600.
 """
+from typing import List
+
 from xknx.dpt import DPTArray
 from xknx.exceptions import ConversionError
 
@@ -20,6 +22,7 @@ class RemoteValueColorRGBW(RemoteValue):
         device_name=None,
         feature_name="Color RGBW",
         after_update_cb=None,
+        passive_group_addresses: List[str] = None,
     ):
         """Initialize remote value of KNX DPT 251.600 (DPT_Color_RGBW)."""
         # pylint: disable=too-many-arguments
@@ -30,6 +33,7 @@ class RemoteValueColorRGBW(RemoteValue):
             device_name=device_name,
             feature_name=feature_name,
             after_update_cb=after_update_cb,
+            passive_group_addresses=passive_group_addresses,
         )
         self.previous_value = (0, 0, 0, 0)
 
diff --git a/xknx/remote_value/remote_value_datetime.py b/xknx/remote_value/remote_value_datetime.py
index 2ce02672..0b2ada73 100644
--- a/xknx/remote_value/remote_value_datetime.py
+++ b/xknx/remote_value/remote_value_datetime.py
@@ -5,6 +5,7 @@ DPT 10.001, 11.001 and 19.001
 """
 from enum import Enum
 import time
+from typing import List
 
 from xknx.dpt import DPTArray, DPTDate, DPTDateTime, DPTTime
 from xknx.exceptions import ConversionError
@@ -33,6 +34,7 @@ class RemoteValueDateTime(RemoteValue):
         device_name=None,
         feature_name="DateTime",
         after_update_cb=None,
+        passive_group_addresses: List[str] = None,
     ):
         """Initialize RemoteValueSensor class."""
         # pylint: disable=too-many-arguments
@@ -53,6 +55,7 @@ class RemoteValueDateTime(RemoteValue):
             device_name=device_name,
             feature_name=feature_name,
             after_update_cb=after_update_cb,
+            passive_group_addresses=passive_group_addresses,
         )
 
     def payload_valid(self, payload):
diff --git a/xknx/remote_value/remote_value_dpt_2_byte_unsigned.py b/xknx/remote_value/remote_value_dpt_2_byte_unsigned.py
index f6b79315..7e536075 100644
--- a/xknx/remote_value/remote_value_dpt_2_byte_unsigned.py
+++ b/xknx/remote_value/remote_value_dpt_2_byte_unsigned.py
@@ -3,6 +3,8 @@ Module for managing a DTP 7001 remote value.
 
 DPT 7.001.
 """
+from typing import List
+
 from xknx.dpt import DPT2ByteUnsigned, DPTArray
 
 from .remote_value import RemoteValue
@@ -19,6 +21,7 @@ class RemoteValueDpt2ByteUnsigned(RemoteValue):
         device_name=None,
         feature_name="Value",
         after_update_cb=None,
+        passive_group_addresses: List[str] = None,
     ):
         """Initialize remote value of KNX DPT 7.001."""
         # pylint: disable=too-many-arguments
@@ -29,6 +32,7 @@ class RemoteValueDpt2ByteUnsigned(RemoteValue):
             device_name=device_name,
             feature_name=feature_name,
             after_update_cb=after_update_cb,
+            passive_group_addresses=passive_group_addresses,
         )
 
     def payload_valid(self, payload):
diff --git a/xknx/remote_value/remote_value_scaling.py b/xknx/remote_value/remote_value_scaling.py
index 91af884c..ed8b52e4 100644
--- a/xknx/remote_value/remote_value_scaling.py
+++ b/xknx/remote_value/remote_value_scaling.py
@@ -3,6 +3,8 @@ Module for managing a Scaling remote value.
 
 DPT 5.001.
 """
+from typing import List
+
 from xknx.dpt import DPTArray
 
 from .remote_value import RemoteValue
@@ -21,6 +23,7 @@ class RemoteValueScaling(RemoteValue):
         after_update_cb=None,
         range_from=0,
         range_to=100,
+        passive_group_addresses: List[str] = None,
     ):
         """Initialize remote value of KNX DPT 5.001 (DPT_Scaling)."""
         # pylint: disable=too-many-arguments
@@ -31,6 +34,7 @@ class RemoteValueScaling(RemoteValue):
             device_name=device_name,
             feature_name=feature_name,
             after_update_cb=after_update_cb,
+            passive_group_addresses=passive_group_addresses,
         )
         self.range_from = range_from
         self.range_to = range_to
diff --git a/xknx/remote_value/remote_value_sensor.py b/xknx/remote_value/remote_value_sensor.py
index 7cf96af6..06a75397 100644
--- a/xknx/remote_value/remote_value_sensor.py
+++ b/xknx/remote_value/remote_value_sensor.py
@@ -4,6 +4,8 @@ Module for managing a remote value typically used within a sensor.
 The module maps a given value_type to a DPT class and uses this class
 for serialization and deserialization of the KNX value.
 """
+from typing import List
+
 from xknx.dpt import DPTArray, DPTBase
 from xknx.exceptions import ConversionError
 
@@ -23,6 +25,7 @@ class RemoteValueSensor(RemoteValue):
         device_name=None,
         feature_name="Value",
         after_update_cb=None,
+        passive_group_addresses: List[str] = None,
     ):
         """Initialize RemoteValueSensor class."""
         # pylint: disable=too-many-arguments
@@ -40,6 +43,7 @@ class RemoteValueSensor(RemoteValue):
             device_name=device_name,
             feature_name=feature_name,
             after_update_cb=after_update_cb,
+            passive_group_addresses=passive_group_addresses,
         )
 
     def payload_valid(self, payload):
diff --git a/xknx/remote_value/remote_value_setpoint_shift.py b/xknx/remote_value/remote_value_setpoint_shift.py
index d6c54be4..d0cae883 100644
--- a/xknx/remote_value/remote_value_setpoint_shift.py
+++ b/xknx/remote_value/remote_value_setpoint_shift.py
@@ -3,6 +3,8 @@ Module for managing setpoint shifting.
 
 DPT 6.010.
 """
+from typing import List
+
 from xknx.remote_value import RemoteValue1Count
 
 
@@ -17,6 +19,7 @@ class RemoteValueSetpointShift(RemoteValue1Count):
         device_name=None,
         after_update_cb=None,
         setpoint_shift_step=0.1,
+        passive_group_addresses: List[str] = None,
     ):
         """Initialize RemoteValueSetpointShift class."""
         # pylint: disable=too-many-arguments
@@ -27,6 +30,7 @@ class RemoteValueSetpointShift(RemoteValue1Count):
             device_name=device_name,
             feature_name="Setpoint shift value",
             after_update_cb=after_update_cb,
+            passive_group_addresses=passive_group_addresses,
         )
 
         self.setpoint_shift_step = setpoint_shift_step
diff --git a/xknx/remote_value/remote_value_step.py b/xknx/remote_value/remote_value_step.py
index a9ac00d8..e2243043 100644
--- a/xknx/remote_value/remote_value_step.py
+++ b/xknx/remote_value/remote_value_step.py
@@ -4,6 +4,7 @@ Module for managing an DPT Step remote value.
 DPT 1.007.
 """
 from enum import Enum
+from typing import List
 
 from xknx.dpt import DPTBinary
 from xknx.exceptions import ConversionError, CouldNotParseTelegram
@@ -29,6 +30,7 @@ class RemoteValueStep(RemoteValue):
         feature_name="Step",
         after_update_cb=None,
         invert=False,
+        passive_group_addresses: List[str] = None,
     ):
         """Initialize remote value of KNX DPT 1.007."""
         # pylint: disable=too-many-arguments
@@ -39,6 +41,7 @@ class RemoteValueStep(RemoteValue):
             device_name=device_name,
             feature_name=feature_name,
             after_update_cb=after_update_cb,
+            passive_group_addresses=passive_group_addresses,
         )
         self.invert = invert
 
diff --git a/xknx/remote_value/remote_value_switch.py b/xknx/remote_value/remote_value_switch.py
index a583516b..66cfac28 100644
--- a/xknx/remote_value/remote_value_switch.py
+++ b/xknx/remote_value/remote_value_switch.py
@@ -3,6 +3,8 @@ Module for managing an DPT Switch remote value.
 
 DPT 1.001.
 """
+from typing import List
+
 from xknx.dpt import DPTBinary
 from xknx.exceptions import ConversionError, CouldNotParseTelegram
 
@@ -22,6 +24,7 @@ class RemoteValueSwitch(RemoteValue):
         feature_name="State",
         after_update_cb=None,
         invert=False,
+        passive_group_addresses: List[str] = None,
     ):
         """Initialize remote value of KNX DPT 1.001."""
         # pylint: disable=too-many-arguments
@@ -33,6 +36,7 @@ class RemoteValueSwitch(RemoteValue):
             device_name=device_name,
             feature_name=feature_name,
             after_update_cb=after_update_cb,
+            passive_group_addresses=passive_group_addresses,
         )
         self.invert = invert
 
diff --git a/xknx/remote_value/remote_value_updown.py b/xknx/remote_value/remote_value_updown.py
index 3910617c..9c4502d3 100644
--- a/xknx/remote_value/remote_value_updown.py
+++ b/xknx/remote_value/remote_value_updown.py
@@ -4,6 +4,7 @@ Module for managing an DPT Up/Down remote value.
 DPT 1.008.
 """
 from enum import Enum
+from typing import List
 
 from xknx.dpt import DPTBinary
 from xknx.exceptions import ConversionError, CouldNotParseTelegram
@@ -30,6 +31,7 @@ class RemoteValueUpDown(RemoteValue):
         feature_name="Up/Down",
         after_update_cb=None,
         invert=False,
+        passive_group_addresses: List[str] = None,
     ):
         """Initialize remote value of KNX DPT 1.008."""
         # pylint: disable=too-many-arguments
@@ -40,6 +42,7 @@ class RemoteValueUpDown(RemoteValue):
             device_name=device_name,
             feature_name=feature_name,
             after_update_cb=after_update_cb,
+            passive_group_addresses=passive_group_addresses,
         )
         self.invert = invert
 


Test patch:
diff --git a/test/remote_value_tests/remote_value_test.py b/test/remote_value_tests/remote_value_test.py
index d04153cc..cd87890a 100644
--- a/test/remote_value_tests/remote_value_test.py
+++ b/test/remote_value_tests/remote_value_test.py
@@ -139,6 +139,27 @@ class TestRemoteValue(unittest.TestCase):
                 "State",
             )
 
+    def test_process_listening_address(self):
+        """Test if listening group addresses are processed."""
+        xknx = XKNX()
+        remote_value = RemoteValue(
+            xknx, group_address="1/2/3", passive_group_addresses=["1/1/1"]
+        )
+        self.assertTrue(remote_value.writable)
+        self.assertFalse(remote_value.readable)
+        # RemoteValue is initialized with only passive group address
+        self.assertTrue(remote_value.initialized)
+        with patch("xknx.remote_value.RemoteValue.payload_valid") as patch_valid:
+            patch_valid.return_value = True
+            test_payload = DPTArray((0x01, 0x02))
+            telegram = Telegram(GroupAddress("1/1/1"), payload=test_payload)
+            self.assertTrue(
+                self.loop.run_until_complete(
+                    asyncio.Task(remote_value.process(telegram))
+                )
+            )
+            self.assertEqual(remote_value.payload, test_payload)
+
     def test_eq(self):
         """Test __eq__ operator."""
         xknx = XKNX()