Repository: XKNX/xknx

Issue:
Feature request: add the "ignore_internal_state" property to "Sensors" the same way it works already for "Binary Sensors"
Hi,

In the last release the property "ignore_internal_state" was added to the knx "binary sensor", and it would be great to have this property for knx "sensors" as well.

For example I am using a sensor with type "scene_number" together with an automation in order to execute some actions within HA when a knx scene number is called, but if the same scene is called two times without any other scene number in between, the value of the HA sensor state is not changed and the automation is not called again.

Note : the KNX scene entity is no help in this case as its state in HA does not change when triggered (it stays on "scening") and is only used the other way around (make HA to trigger a KNX scene).

Thank you very much in advance !

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

Coverage increased (+0.004%) to 93.719% when pulling **22be1b7aad8648d7667184901325e82e2b25ed1c on marvin-w:always_callback_sensor** into **715856fdb69deceba4d54afe4ccf3f7ad304c5ec on XKNX:master**.


Patch:
diff --git a/changelog.md b/changelog.md
index 97c850a5..7fdd1b95 100644
--- a/changelog.md
+++ b/changelog.md
@@ -4,6 +4,7 @@
 
 ### Devices
 
+- Sensor: add `always_callback` option
 - ClimateMode: Refactor climate modes in operation_mode and controller_mode, also fixes a bug for binary operation modes where the mode would be set to AWAY no matter what value was sent to the bus.
 
 ### Internals
diff --git a/docs/sensor.md b/docs/sensor.md
index c741e111..22ae2581 100644
--- a/docs/sensor.md
+++ b/docs/sensor.md
@@ -15,6 +15,7 @@ Sensors are monitoring temperature, air humidity, pressure etc. from KNX bus.
     sensor = Sensor(
         xknx=xknx,
         name='DiningRoom.Temperature.Sensor',
+        always_callback=False,
         group_address_state='6/2/1',
         sync_state=True,
         value_type='temperature'
@@ -25,6 +26,7 @@ Sensors are monitoring temperature, air humidity, pressure etc. from KNX bus.
 
 * `xknx` is the XKNX object.
 * `name` is the name of the object.
+* `always_callback` defines if a callback/update should always be triggered no matter if the previous and the new state are identical.
 * `group_address_state` is the KNX group address of the sensor device.
 * `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`
 * `value_type` controls how the value should be rendered in a human readable representation. The attribut may have may have the values `percent`, `temperature`, `illuminance`, `speed_ms` or `current`.
diff --git a/home-assistant-plugin/custom_components/xknx/factory.py b/home-assistant-plugin/custom_components/xknx/factory.py
index de552ff2..005d24d5 100644
--- a/home-assistant-plugin/custom_components/xknx/factory.py
+++ b/home-assistant-plugin/custom_components/xknx/factory.py
@@ -214,6 +214,7 @@ def _create_sensor(knx_module: XKNX, config: ConfigType) -> XknxSensor:
         name=config[CONF_NAME],
         group_address_state=config[SensorSchema.CONF_STATE_ADDRESS],
         sync_state=config[SensorSchema.CONF_SYNC_STATE],
+        always_callback=config[SensorSchema.CONF_ALWAYS_CALLBACK],
         value_type=config[CONF_TYPE],
     )
 
diff --git a/home-assistant-plugin/custom_components/xknx/schema.py b/home-assistant-plugin/custom_components/xknx/schema.py
index 9c628bed..d97a532a 100644
--- a/home-assistant-plugin/custom_components/xknx/schema.py
+++ b/home-assistant-plugin/custom_components/xknx/schema.py
@@ -306,6 +306,7 @@ class NotifySchema:
 class SensorSchema:
     """Voluptuous schema for KNX sensors."""
 
+    CONF_ALWAYS_CALLBACK = "always_callback"
     CONF_STATE_ADDRESS = CONF_STATE_ADDRESS
     CONF_SYNC_STATE = CONF_SYNC_STATE
     DEFAULT_NAME = "KNX Sensor"
@@ -318,6 +319,7 @@ class SensorSchema:
                 cv.boolean,
                 cv.string,
             ),
+            vol.Optional(CONF_ALWAYS_CALLBACK, default=False): cv.boolean,
             vol.Required(CONF_STATE_ADDRESS): cv.string,
             vol.Required(CONF_TYPE): vol.Any(int, float, str),
         }
diff --git a/xknx/devices/sensor.py b/xknx/devices/sensor.py
index 1ac1471e..de62ec95 100644
--- a/xknx/devices/sensor.py
+++ b/xknx/devices/sensor.py
@@ -19,14 +19,15 @@ class Sensor(Device):
         xknx,
         name,
         group_address_state=None,
-        sync_state=True,
-        value_type=None,
+        sync_state: bool = True,
+        always_callback: bool = False,
+        value_type: str = None,
         device_updated_cb=None,
     ):
         """Initialize Sensor class."""
         # pylint: disable=too-many-arguments
         super().__init__(xknx, name, device_updated_cb)
-
+        self.always_callback = always_callback
         self.sensor_value = RemoteValueSensor(
             xknx,
             group_address_state=group_address_state,
@@ -45,6 +46,7 @@ class Sensor(Device):
         """Initialize object from configuration structure."""
         group_address_state = config.get("group_address_state")
         sync_state = config.get("sync_state", True)
+        always_callback = config.get("always_callback", False)
         value_type = config.get("value_type")
 
         return cls(
@@ -52,11 +54,16 @@ class Sensor(Device):
             name,
             group_address_state=group_address_state,
             sync_state=sync_state,
+            always_callback=always_callback,
             value_type=value_type,
         )
 
     async def process_group_write(self, telegram):
         """Process incoming and outgoing GROUP WRITE telegram."""
+        await self.sensor_value.process(telegram, always_callback=self.always_callback)
+
+    async def process_group_response(self, telegram):
+        """Process incoming GroupValueResponse telegrams."""
         await self.sensor_value.process(telegram)
 
     def unit_of_measurement(self):


Test patch:
diff --git a/test/devices_tests/sensor_test.py b/test/devices_tests/sensor_test.py
index 2043eb3c..5875ea4e 100644
--- a/test/devices_tests/sensor_test.py
+++ b/test/devices_tests/sensor_test.py
@@ -47,6 +47,55 @@ class TestSensor(unittest.TestCase):
         self.assertEqual(sensor.unit_of_measurement(), "K")
         self.assertEqual(sensor.ha_device_class(), None)
 
+    def test_always_callback_sensor(self):
+        """Test always callback sensor."""
+        xknx = XKNX()
+        sensor = Sensor(
+            xknx,
+            "TestSensor",
+            group_address_state="1/2/3",
+            always_callback=False,
+            value_type="volume_liquid_litre",
+        )
+
+        after_update_callback = Mock()
+
+        async def async_after_update_callback(device):
+            """Async callback."""
+            after_update_callback(device)
+
+        sensor.register_device_updated_cb(async_after_update_callback)
+
+        payload = DPTArray(
+            (
+                0x00,
+                0x00,
+                0x01,
+                0x00,
+            )
+        )
+
+        #  set initial payload of sensor
+        sensor.sensor_value.payload = payload
+
+        telegram = Telegram(group_address=GroupAddress("1/2/3"), payload=payload)
+
+        # verify not called when always_callback is False
+        self.loop.run_until_complete(sensor.process(telegram))
+        after_update_callback.assert_not_called()
+        after_update_callback.reset_mock()
+
+        sensor.always_callback = True
+
+        # verify called when always_callback is True
+        self.loop.run_until_complete(sensor.process(telegram))
+        after_update_callback.assert_called_once()
+        after_update_callback.reset_mock()
+
+        # verify not called when processing read responses
+        self.loop.run_until_complete(sensor.process_group_response(telegram))
+        after_update_callback.assert_not_called()
+
     def test_str_acceleration(self):
         """Test resolve state with acceleration sensor."""
         xknx = XKNX()