Repository: XKNX/xknx
Pull request #675 (2021-04-21 21:08:32)

RemoteValue.value settable programmatically
<!--
  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.
-->
RemoteValue.value can now be set to a new value. It will be used to answer GroupValueRead telegrams.
RemoteValue.update_value can be awaited to set RemoteValue.value and await callbacks.

With this change we can eg. initialize the values of ExposeSesnor without sending a Telegram.
It is probably also handy in unit tests.

RemoteValue.payload was removed, so the string representations had to be changed (first commit).

Fixes # (issue)

## 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)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] 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
- [ ] The Homeassistant plugin has been adjusted in case of new config options


codecov[bot]: # [Codecov](https://codecov.io/gh/XKNX/xknx/pull/675?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX) Report
> Merging [#675](https://codecov.io/gh/XKNX/xknx/pull/675?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX) (c39d8a9) into [main](https://codecov.io/gh/XKNX/xknx/commit/4437ab6692e4b41718e047c67a26bbf0b4341c47?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX) (4437ab6) will **increase** coverage by `0.00%`.
> The diff coverage is `100.00%`.

[![Impacted file tree graph](https://codecov.io/gh/XKNX/xknx/pull/675/graphs/tree.svg?width=650&height=150&src=pr&token=irWbIygS84&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX)](https://codecov.io/gh/XKNX/xknx/pull/675?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX)

```diff
@@           Coverage Diff           @@
##             main     #675   +/-   ##
=======================================
  Coverage   94.38%   94.39%           
=======================================
  Files         101      101           
  Lines        6378     6386    +8     
=======================================
+ Hits         6020     6028    +8     
  Misses        358      358           
```


| [Impacted Files](https://codecov.io/gh/XKNX/xknx/pull/675?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX) | Coverage Δ | |
|---|---|---|
| [xknx/devices/climate.py](https://codecov.io/gh/XKNX/xknx/pull/675/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX#diff-eGtueC9kZXZpY2VzL2NsaW1hdGUucHk=) | `100.00% <ø> (ø)` | |
| [xknx/devices/climate\_mode.py](https://codecov.io/gh/XKNX/xknx/pull/675/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX#diff-eGtueC9kZXZpY2VzL2NsaW1hdGVfbW9kZS5weQ==) | `100.00% <ø> (ø)` | |
| [xknx/devices/cover.py](https://codecov.io/gh/XKNX/xknx/pull/675/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX#diff-eGtueC9kZXZpY2VzL2NvdmVyLnB5) | `100.00% <ø> (ø)` | |
| [xknx/devices/light.py](https://codecov.io/gh/XKNX/xknx/pull/675/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX#diff-eGtueC9kZXZpY2VzL2xpZ2h0LnB5) | `99.41% <ø> (ø)` | |
| [xknx/devices/weather.py](https://codecov.io/gh/XKNX/xknx/pull/675/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX#diff-eGtueC9kZXZpY2VzL3dlYXRoZXIucHk=) | `100.00% <ø> (ø)` | |
| [xknx/devices/binary\_sensor.py](https://codecov.io/gh/XKNX/xknx/pull/675/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX#diff-eGtueC9kZXZpY2VzL2JpbmFyeV9zZW5zb3IucHk=) | `97.11% <100.00%> (ø)` | |
| [xknx/devices/datetime.py](https://codecov.io/gh/XKNX/xknx/pull/675/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX#diff-eGtueC9kZXZpY2VzL2RhdGV0aW1lLnB5) | `90.69% <100.00%> (ø)` | |
| [xknx/devices/expose\_sensor.py](https://codecov.io/gh/XKNX/xknx/pull/675/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX#diff-eGtueC9kZXZpY2VzL2V4cG9zZV9zZW5zb3IucHk=) | `100.00% <100.00%> (ø)` | |
| [xknx/devices/fan.py](https://codecov.io/gh/XKNX/xknx/pull/675/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX#diff-eGtueC9kZXZpY2VzL2Zhbi5weQ==) | `100.00% <100.00%> (ø)` | |
| [xknx/devices/notification.py](https://codecov.io/gh/XKNX/xknx/pull/675/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX#diff-eGtueC9kZXZpY2VzL25vdGlmaWNhdGlvbi5weQ==) | `100.00% <100.00%> (ø)` | |
| ... and [4 more](https://codecov.io/gh/XKNX/xknx/pull/675/diff?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX) | |


Patch:
diff --git a/changelog.md b/changelog.md
index 5c01aff5..14c5d808 100644
--- a/changelog.md
+++ b/changelog.md
@@ -4,6 +4,8 @@
 
 ### Internals
 
+- RemoteValue.value changed to a settable property. It is used to create payloads for outgoing telegrams.
+- RemoteValue.update_value (async) sets a new value and awaits the callbacks without sending to the bus.
 - Round DPT 14 values to precision of 7 digits
 
 ## 0.18.0
diff --git a/home-assistant-plugin/custom_components/xknx/expose.py b/home-assistant-plugin/custom_components/xknx/expose.py
index 043807d2..9af11ef5 100644
--- a/home-assistant-plugin/custom_components/xknx/expose.py
+++ b/home-assistant-plugin/custom_components/xknx/expose.py
@@ -13,7 +13,7 @@ from homeassistant.const import (
     STATE_UNAVAILABLE,
     STATE_UNKNOWN,
 )
-from homeassistant.core import Event, HomeAssistant, callback
+from homeassistant.core import Event, HomeAssistant, State, callback
 from homeassistant.helpers.event import async_track_state_change_event
 from homeassistant.helpers.typing import ConfigType, StateType
 
@@ -74,6 +74,7 @@ class KNXExposeSensor:
         self.address = address
         self._remove_listener: Callable[[], None] | None = None
         self.device: ExposeSensor = self.async_register()
+        self._init_expose_state()
 
     @callback
     def async_register(self) -> ExposeSensor:
@@ -93,6 +94,13 @@ class KNXExposeSensor:
         )
         return device
 
+    def _init_expose_state(self) -> None:
+        """Initialize state of the exposure."""
+        init_state = self.hass.states.get(self.entity_id)
+        init_value = self._get_expose_value(init_state)
+        if init_value is not None:
+            self.device.sensor_value.value = init_value
+
     @callback
     def shutdown(self) -> None:
         """Prepare for deletion."""
@@ -101,30 +109,27 @@ class KNXExposeSensor:
             self._remove_listener = None
         self.device.shutdown()
 
+    def _get_expose_value(self, state: State) -> StateType:
+        """Extract value from state."""
+        if state is None or state.state in (STATE_UNKNOWN, STATE_UNAVAILABLE):
+            return None
+        return (
+            state.state
+            if self.expose_attribute is None
+            else state.attributes.get(self.expose_attribute)
+        )
+
     async def _async_entity_changed(self, event: Event) -> None:
         """Handle entity change."""
         new_state = event.data.get("new_state")
-        if new_state is None:
+        new_value = self._get_expose_value(new_state)
+        if new_value is None:
             return
-        if new_state.state in (STATE_UNKNOWN, STATE_UNAVAILABLE):
-            return
-
         old_state = event.data.get("old_state")
-
-        if self.expose_attribute is None:
-            if old_state is None or old_state.state != new_state.state:
-                # don't send same value sequentially
-                await self._async_set_knx_value(new_state.state)
-            return
-
-        new_attribute = new_state.attributes.get(self.expose_attribute)
-
-        if old_state is not None:
-            old_attribute = old_state.attributes.get(self.expose_attribute)
-            if old_attribute == new_attribute:
-                # don't send same value sequentially
-                return
-        await self._async_set_knx_value(new_attribute)
+        old_value = self._get_expose_value(old_state)
+        # don't send same value sequentially
+        if new_value != old_value:
+            await self._async_set_knx_value(new_value)
 
     async def _async_set_knx_value(self, value: StateType) -> None:
         """Set new value on xknx ExposeSensor."""
diff --git a/xknx/devices/binary_sensor.py b/xknx/devices/binary_sensor.py
index 3f3ed1b7..69919f9f 100644
--- a/xknx/devices/binary_sensor.py
+++ b/xknx/devices/binary_sensor.py
@@ -191,6 +191,6 @@ class BinarySensor(Device):
 
     def __str__(self) -> str:
         """Return object as readable string."""
-        return '<BinarySensor name="{}" remote_value="{}" state="{}"/>'.format(
-            self.name, self.remote_value.group_addr_str(), self.state
+        return '<BinarySensor name="{}" remote_value={} state={} />'.format(
+            self.name, self.remote_value.group_addr_str(), self.state.__repr__()
         )
diff --git a/xknx/devices/climate.py b/xknx/devices/climate.py
index 1ce31248..39fc92d0 100644
--- a/xknx/devices/climate.py
+++ b/xknx/devices/climate.py
@@ -293,13 +293,13 @@ class Climate(Device):
         """Return object as readable string."""
         return (
             '<Climate name="{}" '
-            'temperature="{}" '
-            'target_temperature="{}" '
+            "temperature={} "
+            "target_temperature={} "
             'temperature_step="{}" '
-            'setpoint_shift="{}" '
+            "setpoint_shift={} "
             'setpoint_shift_max="{}" '
             'setpoint_shift_min="{}" '
-            'group_address_on_off="{}" '
+            "group_address_on_off={} "
             "/>".format(
                 self.name,
                 self.temperature.group_addr_str(),
diff --git a/xknx/devices/climate_mode.py b/xknx/devices/climate_mode.py
index 6a86762d..5b8b097b 100644
--- a/xknx/devices/climate_mode.py
+++ b/xknx/devices/climate_mode.py
@@ -324,9 +324,9 @@ class ClimateMode(Device):
         """Return object as readable string."""
         return (
             '<ClimateMode name="{}" '
-            'operation_mode="{}" '
-            'controller_mode="{}" '
-            'controller_status="{}" '
+            "operation_mode={} "
+            "controller_mode={} "
+            "controller_status={} "
             "/>".format(
                 self.name,
                 self.remote_value_operation_mode.group_addr_str(),
diff --git a/xknx/devices/cover.py b/xknx/devices/cover.py
index ebf89a5c..96deac7a 100644
--- a/xknx/devices/cover.py
+++ b/xknx/devices/cover.py
@@ -153,13 +153,13 @@ class Cover(Device):
         """Return object as readable string."""
         return (
             '<Cover name="{}" '
-            'updown="{}" '
-            'step="{}" '
-            'stop="{}" '
-            'position_current="{}" '
-            'position_target="{}" '
-            'angle="{}" '
-            'locked="{}" '
+            "updown={} "
+            "step={} "
+            "stop_={} "
+            "position_current={} "
+            "position_target={} "
+            "angle={} "
+            "locked={} "
             'travel_time_down="{}" '
             'travel_time_up="{}" />'.format(
                 self.name,
diff --git a/xknx/devices/datetime.py b/xknx/devices/datetime.py
index 67d68289..f0d31e04 100644
--- a/xknx/devices/datetime.py
+++ b/xknx/devices/datetime.py
@@ -95,6 +95,6 @@ class DateTime(Device):
 
     def __str__(self) -> str:
         """Return object as readable string."""
-        return '<DateTime name="{}" group_address="{}" broadcast_type="{}" />'.format(
+        return '<DateTime name="{}" _remote_value={} broadcast_type="{}" />'.format(
             self.name, self._remote_value.group_addr_str(), self._broadcast_type
         )
diff --git a/xknx/devices/expose_sensor.py b/xknx/devices/expose_sensor.py
index 2d1000b6..bd239aba 100644
--- a/xknx/devices/expose_sensor.py
+++ b/xknx/devices/expose_sensor.py
@@ -88,9 +88,9 @@ class ExposeSensor(Device):
 
     def __str__(self) -> str:
         """Return object as readable string."""
-        return '<ExposeSensor name="{}" ' 'sensor="{}" value="{}" unit="{}"/>'.format(
+        return '<ExposeSensor name="{}" sensor={} value={} unit="{}"/>'.format(
             self.name,
             self.sensor_value.group_addr_str(),
-            self.resolve_state(),
+            self.resolve_state().__repr__(),
             self.unit_of_measurement(),
         )
diff --git a/xknx/devices/fan.py b/xknx/devices/fan.py
index 6c10bb10..f073bab7 100644
--- a/xknx/devices/fan.py
+++ b/xknx/devices/fan.py
@@ -102,10 +102,10 @@ class Fan(Device):
         str_oscillation = (
             ""
             if not self.supports_oscillation
-            else f' oscillation="{self.oscillation.group_addr_str()}"'
+            else f" oscillation={self.oscillation.group_addr_str()}"
         )
 
-        return '<Fan name="{}" ' 'speed="{}"{} />'.format(
+        return '<Fan name="{}" speed={}{} />'.format(
             self.name, self.speed.group_addr_str(), str_oscillation
         )
 
diff --git a/xknx/devices/light.py b/xknx/devices/light.py
index e35402dc..fc699ca5 100644
--- a/xknx/devices/light.py
+++ b/xknx/devices/light.py
@@ -304,73 +304,71 @@ class Light(Device):
         str_brightness = (
             ""
             if not self.supports_brightness
-            else f' brightness="{self.brightness.group_addr_str()}"'
+            else f" brightness={self.brightness.group_addr_str()}"
         )
 
         str_color = (
-            "" if not self.supports_color else f' color="{self.color.group_addr_str()}"'
+            "" if not self.supports_color else f" color={self.color.group_addr_str()}"
         )
 
         str_rgbw = (
-            "" if not self.supports_rgbw else f' rgbw="{self.rgbw.group_addr_str()}"'
+            "" if not self.supports_rgbw else f" rgbw={self.rgbw.group_addr_str()}"
         )
 
         str_tunable_white = (
             ""
             if not self.supports_tunable_white
-            else f' tunable white="{self.tunable_white.group_addr_str()}"'
+            else f" tunable_white={self.tunable_white.group_addr_str()}"
         )
 
         str_color_temperature = (
             ""
             if not self.supports_color_temperature
-            else ' color temperature="{}"'.format(
-                self.color_temperature.group_addr_str()
-            )
+            else f" color_temperature={self.color_temperature.group_addr_str()}"
         )
 
         str_red_state = (
             ""
             if not self.red.switch.initialized
-            else f' red_state="{self.red.switch.group_addr_str()}"'
+            else f" red_state={self.red.switch.group_addr_str()}"
         )
         str_red_brightness = (
             ""
             if not self.red.brightness.initialized
-            else f' red_brightness="{self.red.brightness.group_addr_str()}"'
+            else f" red_brightness={self.red.brightness.group_addr_str()}"
         )
 
         str_green_state = (
             ""
             if not self.green.switch.initialized
-            else f' green_state="{self.green.switch.group_addr_str()}"'
+            else f" green_state={self.green.switch.group_addr_str()}"
         )
         str_green_brightness = (
             ""
             if not self.green.brightness.initialized
-            else f' green_brightness="{self.green.brightness.group_addr_str()}"'
+            else f" green_brightness={self.green.brightness.group_addr_str()}"
         )
 
         str_blue_state = (
             ""
             if not self.blue.switch.initialized
-            else f' blue_state="{self.blue.switch.group_addr_str()}"'
+            else f" blue_state={self.blue.switch.group_addr_str()}"
         )
         str_blue_brightness = (
             ""
             if not self.blue.brightness.initialized
-            else f' blue_brightness="{self.blue.brightness.group_addr_str()}"'
+            else f" blue_brightness={self.blue.brightness.group_addr_str()}"
         )
 
         str_white_state = (
             ""
             if not self.white.switch.initialized
-            else f' white_state="{self.white.switch.group_addr_str()}"'
+            else f" white_state={self.white.switch.group_addr_str()}"
         )
         str_white_brightness = (
             ""
             if not self.white.brightness.initialized
-            else f' white_brightness="{self.white.brightness.group_addr_str()}"'
+            else f" white_brightness={self.white.brightness.group_addr_str()}"
         )
 
         return '<Light name="{}" ' 'switch="{}"{}{}{}{}{}{}{}{}{}{}{}{}{} />'.format(
diff --git a/xknx/devices/notification.py b/xknx/devices/notification.py
index 952b2283..9eb60427 100644
--- a/xknx/devices/notification.py
+++ b/xknx/devices/notification.py
@@ -58,6 +58,6 @@ class Notification(Device):
 
     def __str__(self) -> str:
         """Return object as readable string."""
-        return '<Notification name="{}" message="{}" />'.format(
+        return '<Notification name="{}" message={} />'.format(
             self.name, self._message.group_addr_str()
         )
diff --git a/xknx/devices/scene.py b/xknx/devices/scene.py
index 328f017e..d559e265 100644
--- a/xknx/devices/scene.py
+++ b/xknx/devices/scene.py
@@ -49,7 +49,7 @@ class Scene(Device):
 
     def __str__(self) -> str:
         """Return object as readable string."""
-        return '<Scene name="{}" ' 'scene_value="{}" scene_number="{}" />'.format(
+        return '<Scene name="{}" ' 'scene_value={} scene_number="{}" />'.format(
             self.name, self.scene_value.group_addr_str(), self.scene_number
         )
 
diff --git a/xknx/devices/sensor.py b/xknx/devices/sensor.py
index 1da02138..2155201f 100644
--- a/xknx/devices/sensor.py
+++ b/xknx/devices/sensor.py
@@ -104,9 +104,9 @@ class Sensor(Device):
 
     def __str__(self) -> str:
         """Return object as readable string."""
-        return '<Sensor name="{}" ' 'sensor="{}" value="{}" unit="{}"/>'.format(
+        return '<Sensor name="{}" sensor={} value={} unit="{}"/>'.format(
             self.name,
             self.sensor_value.group_addr_str(),
-            self.resolve_state(),
+            self.resolve_state().__repr__(),
             self.unit_of_measurement(),
         )
diff --git a/xknx/devices/switch.py b/xknx/devices/switch.py
index 1bd580c6..2c51658e 100644
--- a/xknx/devices/switch.py
+++ b/xknx/devices/switch.py
@@ -98,6 +98,6 @@ class Switch(Device):
 
     def __str__(self) -> str:
         """Return object as readable string."""
-        return '<Switch name="{}" switch="{}" />'.format(
+        return '<Switch name="{}" switch={} />'.format(
             self.name, self.switch.group_addr_str()
         )
diff --git a/xknx/devices/weather.py b/xknx/devices/weather.py
index d3742acb..d00ead4e 100644
--- a/xknx/devices/weather.py
+++ b/xknx/devices/weather.py
@@ -450,10 +450,10 @@ class Weather(Device):
         """Return object as readable string."""
         return (
             '<Weather name="{}" '
-            'temperature="{}" brightness_south="{}" brightness_north="{}" brightness_west="{}" '
-            'brightness_east="{}" wind_speed="{}" wind_bearing="{}" rain_alarm="{}" '
-            'wind_alarm="{}" frost_alarm="{}" day_night="{}" '
-            'air_pressure="{}" humidity="{}" />'.format(
+            "temperature={} brightness_south={} brightness_north={} brightness_west={} "
+            "brightness_east={} wind_speed={} wind_bearing={} rain_alarm={} "
+            "wind_alarm={} frost_alarm={} day_night={} "
+            "air_pressure={} humidity={} />".format(
                 self.name,
                 self._temperature.group_addr_str(),
                 self._brightness_south.group_addr_str(),
diff --git a/xknx/remote_value/remote_value.py b/xknx/remote_value/remote_value.py
index ec522798..d96dff64 100644
--- a/xknx/remote_value/remote_value.py
+++ b/xknx/remote_value/remote_value.py
@@ -72,7 +72,7 @@ class RemoteValue(ABC, Generic[DPTPayloadType, ValueType]):
 
         self.device_name: str = "Unknown" if device_name is None else device_name
         self.feature_name: str = "Unknown" if feature_name is None else feature_name
-        self.value: ValueType | None = None
+        self._value: ValueType | None = None
         self.telegram: Telegram | None = None
         self.after_update_cb: AsyncCallbackType | None = after_update_cb
 
@@ -90,6 +90,25 @@ class RemoteValue(ABC, Generic[DPTPayloadType, ValueType]):
             # AttributeError if instantiation failed (tests mostly)
             pass
 
+    @property
+    def value(self) -> ValueType | None:
+        """Get current value."""
+        return self._value
+
+    @value.setter
+    def value(self, value: Any) -> None:
+        """Set new value without creating a Telegram or calling after_update_cb. Raises ConversionError on invalid value."""
+        if value is not None:
+            # raises ConversionError on invalid value
+            self.to_knx(value)
+        self._value = value
+
+    async def update_value(self, value: Any) -> None:
+        """Set new value without creating a Telegram. Awaits after_update_cb. Raises ConversionError on invalid value."""
+        self.value = value
+        if self.after_update_cb is not None:
+            await self.after_update_cb()
+
     @property
     def initialized(self) -> bool:
         """Evaluate if remote value is initialized with group address."""
@@ -168,8 +187,8 @@ class RemoteValue(ABC, Generic[DPTPayloadType, ValueType]):
             )
         decoded_payload = self.from_knx(_new_payload)
         self.xknx.state_updater.update_received(self)
-        if self.value is None or always_callback or self.value != decoded_payload:
-            self.value = decoded_payload
+        if self._value is None or always_callback or self._value != decoded_payload:
+            self._value = decoded_payload
             self.telegram = telegram
             if self.after_update_cb is not None:
                 await self.after_update_cb()
@@ -211,12 +230,12 @@ class RemoteValue(ABC, Generic[DPTPayloadType, ValueType]):
 
         payload = self.to_knx(value)
         await self._send(payload, response)
-        # self.payload is set and after_update_cb() called when the outgoing telegram is processed.
+        # self._value is set and after_update_cb() called when the outgoing telegram is processed.
 
     async def respond(self) -> None:
         """Send current payload as GroupValueResponse telegram to KNX bus."""
-        payload = self._payload()
-        if payload is not None:
+        if self._value is not None:
+            payload = self.to_knx(self._value)
             await self._send(payload, response=True)
 
     async def read_state(self, wait_for_result: bool = False) -> None:
@@ -242,14 +261,6 @@ class RemoteValue(ABC, Generic[DPTPayloadType, ValueType]):
             else:
                 await value_reader.send_group_read()
 
-    def _payload(self) -> DPTArray | DPTBinary | None:
-        """Obtain payload from Telegram."""
-        if self.telegram is not None and isinstance(
-            self.telegram.payload, (GroupValueWrite, GroupValueResponse)
-        ):
-            return self.telegram.payload.value
-        return None
-
     @property
     def unit_of_measurement(self) -> str | None:
         """Return the unit of measurement."""
@@ -257,16 +268,16 @@ class RemoteValue(ABC, Generic[DPTPayloadType, ValueType]):
 
     def group_addr_str(self) -> str:
         """Return object as readable string."""
-        return "{}/{}/{}/{}".format(
-            self.group_address.__repr__(),
-            self.group_address_state.__repr__(),
-            self.value,
-            self._payload(),
+        return '<"{}", "{}", {}, {} />'.format(
+            self.group_address,
+            self.group_address_state,
+            self.passive_group_addresses,
+            self.value.__repr__(),
         )
 
     def __str__(self) -> str:
         """Return object as string representation."""
-        return '<{} device_name="{}" feature_name="{}" {}/>'.format(
+        return '<{} device_name="{}" feature_name="{}" {} />'.format(
             self.__class__.__name__,
             self.device_name,
             self.feature_name,


Test patch:
diff --git a/requirements/testing.txt b/requirements/testing.txt
index c333bb18..06ce73ed 100644
--- a/requirements/testing.txt
+++ b/requirements/testing.txt
@@ -8,7 +8,7 @@ astroid==2.5.2
 pylint==2.7.4
 pylint-strict-informational==0.1
 pytest==6.2.3
-pytest-asyncio==0.15.0
+pytest-asyncio==0.15.1
 pytest-cov==2.11.1
 pytest-timeout==1.4.2
 setuptools==56.0.0
diff --git a/test/devices_tests/weather_test.py b/test/devices_tests/weather_test.py
index 51f78fd7..f502942a 100644
--- a/test/devices_tests/weather_test.py
+++ b/test/devices_tests/weather_test.py
@@ -166,8 +166,8 @@ class TestWeather:
             group_address_wind_alarm="1/3/9",
         )
 
-        weather._rain_alarm.value = DPTBinary(1)
-        weather._wind_alarm.value = DPTBinary(1)
+        weather._rain_alarm.value = True
+        weather._wind_alarm.value = True
 
         assert weather.ha_current_state() == WeatherCondition.LIGHTNING_RAINY
 
@@ -181,8 +181,8 @@ class TestWeather:
             group_address_frost_alarm="1/3/10",
         )
 
-        weather._rain_alarm.value = DPTBinary(1)
-        weather._frost_alarm.value = DPTBinary(1)
+        weather._rain_alarm.value = True
+        weather._frost_alarm.value = True
 
         assert weather.ha_current_state() == WeatherCondition.SNOWY_RAINY
 
@@ -197,7 +197,7 @@ class TestWeather:
             group_address_frost_alarm="1/3/10",
         )
 
-        weather._wind_alarm.value = DPTBinary(1)
+        weather._wind_alarm.value = True
 
         assert weather.ha_current_state() == WeatherCondition.WINDY
 
@@ -212,7 +212,7 @@ class TestWeather:
             group_address_frost_alarm="1/3/10",
         )
 
-        weather._rain_alarm.value = DPTBinary(1)
+        weather._rain_alarm.value = True
 
         assert weather.ha_current_state() == WeatherCondition.RAINY
 
diff --git a/test/remote_value_tests/remote_value_test.py b/test/remote_value_tests/remote_value_test.py
index 948870e1..ba7dc5c1 100644
--- a/test/remote_value_tests/remote_value_test.py
+++ b/test/remote_value_tests/remote_value_test.py
@@ -3,8 +3,8 @@ from unittest.mock import AsyncMock, patch
 
 import pytest
 from xknx import XKNX
-from xknx.dpt import DPTArray, DPTBinary
-from xknx.exceptions import CouldNotParseTelegram
+from xknx.dpt import DPT2ByteFloat, DPTArray, DPTBinary
+from xknx.exceptions import ConversionError, CouldNotParseTelegram
 from xknx.remote_value import RemoteValue, RemoteValueSwitch
 from xknx.telegram import GroupAddress, Telegram
 from xknx.telegram.apci import GroupValueWrite
@@ -15,6 +15,45 @@ from xknx.telegram.apci import GroupValueWrite
 class TestRemoteValue:
     """Test class for RemoteValue objects."""
 
+    async def test_get_set_value(self):
+        """Test value getter and setter."""
+        xknx = XKNX()
+        remote_value = RemoteValue(xknx)
+        remote_value.to_knx = lambda value: DPTArray(DPT2ByteFloat.to_knx(value))
+        remote_value.after_update_cb = AsyncMock()
+
+        assert remote_value.value is None
+        remote_value.value = 2.2
+        assert remote_value.value == 2.2
+        # invalid value raises ConversionError
+        with pytest.raises(ConversionError):
+            remote_value.value = "a"
+        # new value is used in response Telegram
+        test_payload = remote_value.to_knx(2.2)
+        remote_value._send = AsyncMock()
+        await remote_value.respond()
+        remote_value._send.assert_called_with(test_payload, response=True)
+        # callback is not called when setting value programmatically
+        remote_value.after_update_cb.assert_not_called()
+        # no Telegram was sent to the queue
+        assert xknx.telegrams.qsize() == 0
+
+    async def test_set_value(self):
+        """Test set_value awaitable."""
+        xknx = XKNX()
+        remote_value = RemoteValue(xknx)
+        remote_value.to_knx = lambda value: DPTArray(DPT2ByteFloat.to_knx(value))
+        remote_value.after_update_cb = AsyncMock()
+
+        await remote_value.update_value(3.3)
+        assert remote_value.value == 3.3
+        remote_value.after_update_cb.assert_called_once()
+        assert xknx.telegrams.qsize() == 0
+        # invalid value raises ConversionError
+        with pytest.raises(ConversionError):
+            await remote_value.update_value("a")
+        assert remote_value.value == 3.3
+
     async def test_info_set_uninitialized(self):
         """Test for info if RemoteValue is not initialized."""
         xknx = XKNX()
diff --git a/test/str_test.py b/test/str_test.py
index 36981cb4..503c26a7 100644
--- a/test/str_test.py
+++ b/test/str_test.py
@@ -72,14 +72,14 @@ class TestStringRepresentations:
         )
         assert (
             str(remote_value)
-            == '<RemoteValue device_name="MyDevice" feature_name="Unknown" GroupAddress("1/2/3")/GroupAddress("1/2/4")/None/None/>'
+            == '<RemoteValue device_name="MyDevice" feature_name="Unknown" <"1/2/3", "1/2/4", [], None /> />'
         )
 
         remote_value.value = 34
         assert (
             str(remote_value)
             == '<RemoteValue device_name="MyDevice" feature_name="Unknown" '
-            'GroupAddress("1/2/3")/GroupAddress("1/2/4")/34/None/>'
+            '<"1/2/3", "1/2/4", [], 34 /> />'
         )
 
     def test_binary_sensor(self):
@@ -90,7 +90,7 @@ class TestStringRepresentations:
         )
         assert (
             str(binary_sensor)
-            == '<BinarySensor name="Fnord" remote_value="None/GroupAddress("1/2/3")/None/None" state="None"/>'
+            == '<BinarySensor name="Fnord" remote_value=<"None", "1/2/3", [], None /> state=None />'
         )
 
     def test_climate(self):
@@ -110,12 +110,13 @@ class TestStringRepresentations:
             group_address_on_off_state="1/2/15",
         )
         assert (
-            str(climate)
-            == '<Climate name="Wohnzimmer" temperature="None/GroupAddress("1/2/1")/None/None" '
-            'target_temperature="GroupAddress("1/2/2")/None/None/None" temperature_step="0.1" '
-            'setpoint_shift="GroupAddress("1/2/3")/GroupAddress("1/2/4")/None/None" '
+            str(climate) == '<Climate name="Wohnzimmer" '
+            'temperature=<"None", "1/2/1", [], None /> '
+            'target_temperature=<"1/2/2", "None", [], None /> '
+            'temperature_step="0.1" '
+            'setpoint_shift=<"1/2/3", "1/2/4", [], None /> '
             'setpoint_shift_max="20" setpoint_shift_min="-20" '
-            'group_address_on_off="GroupAddress("1/2/14")/GroupAddress("1/2/15")/None/None" />'
+            'group_address_on_off=<"1/2/14", "1/2/15", [], None /> />'
         )
 
     def test_climate_mode(self):
@@ -136,9 +137,9 @@ class TestStringRepresentations:
         )
         assert (
             str(climate_mode) == '<ClimateMode name="Wohnzimmer Mode" '
-            'operation_mode="GroupAddress("1/2/5")/GroupAddress("1/2/6")/None/None" '
-            'controller_mode="GroupAddress("1/2/12")/GroupAddress("1/2/13")/None/None" '
-            'controller_status="GroupAddress("1/2/10")/GroupAddress("1/2/11")/None/None" />'
+            'operation_mode=<"1/2/5", "1/2/6", [], None /> '
+            'controller_mode=<"1/2/12", "1/2/13", [], None /> '
+            'controller_status=<"1/2/10", "1/2/11", [], None /> />'
         )
 
     def test_cover(self):
@@ -159,12 +160,16 @@ class TestStringRepresentations:
             travel_time_up=10,
         )
         assert (
-            str(cover)
-            == '<Cover name="Rolladen" updown="GroupAddress("1/2/2")/None/None/None" step="GroupAddress("1/2/3")/None/None/None" '
-            'stop="GroupAddress("1/2/4")/None/None/None" '
-            'position_current="None/GroupAddress("1/2/6")/None/None" position_target="GroupAddress("1/2/5")/None/None/None" '
-            'angle="GroupAddress("1/2/7")/GroupAddress("1/2/8")/None/None" locked="None/GroupAddress("1/2/9")/None/None" '
-            'travel_time_down="8" travel_time_up="10" />'
+            str(cover) == '<Cover name="Rolladen" '
+            'updown=<"1/2/2", "None", [], None /> '
+            'step=<"1/2/3", "None", [], None /> '
+            'stop_=<"1/2/4", "None", [], None /> '
+            'position_current=<"None", "1/2/6", [], None /> '
+            'position_target=<"1/2/5", "None", [], None /> '
+            'angle=<"1/2/7", "1/2/8", [], None /> '
+            'locked=<"None", "1/2/9", [], None /> '
+            'travel_time_down="8" '
+            'travel_time_up="10" />'
         )
 
     def test_fan(self):
@@ -180,7 +185,7 @@ class TestStringRepresentations:
         )
         assert (
             str(fan)
-            == '<Fan name="Dunstabzug" speed="GroupAddress("1/2/3")/GroupAddress("1/2/4")/None/None" oscillation="GroupAddress("1/2/5")/GroupAddress("1/2/6")/None/None" />'
+            == '<Fan name="Dunstabzug" speed=<"1/2/3", "1/2/4", [], None /> oscillation=<"1/2/5", "1/2/6", [], None /> />'
         )
 
     def test_light(self):
@@ -194,7 +199,7 @@ class TestStringRepresentations:
         )
         assert (
             str(light)
-            == '<Light name="Licht" switch="GroupAddress("1/2/3")/GroupAddress("1/2/4")/None/None" />'
+            == '<Light name="Licht" switch="<"1/2/3", "1/2/4", [], None />" />'
         )
 
     def test_light_dimmable(self):
@@ -209,9 +214,9 @@ class TestStringRepresentations:
             group_address_brightness_state="1/2/6",
         )
         assert (
-            str(light)
-            == '<Light name="Licht" switch="GroupAddress("1/2/3")/GroupAddress("1/2/4")/None/None" '
-            'brightness="GroupAddress("1/2/5")/GroupAddress("1/2/6")/None/None" />'
+            str(light) == '<Light name="Licht" '
+            'switch="<"1/2/3", "1/2/4", [], None />" '
+            'brightness=<"1/2/5", "1/2/6", [], None /> />'
         )
 
     def test_light_color(self):
@@ -227,8 +232,8 @@ class TestStringRepresentations:
         )
         assert (
             str(light) == '<Light name="Licht" '
-            'switch="GroupAddress("1/2/3")/GroupAddress("1/2/4")/None/None" '
-            'color="GroupAddress("1/2/5")/GroupAddress("1/2/6")/None/None" />'
+            'switch="<"1/2/3", "1/2/4", [], None />" '
+            'color=<"1/2/5", "1/2/6", [], None /> />'
         )
 
     async def test_notification(self):
@@ -239,16 +244,13 @@ class TestStringRepresentations:
         )
         assert (
             str(notification)
-            == '<Notification name="Alarm" message="GroupAddress("1/2/3")/GroupAddress("1/2/4")/None/None" />'
+            == '<Notification name="Alarm" message=<"1/2/3", "1/2/4", [], None /> />'
         )
         await notification.set("Einbrecher im Haus")
         await notification.process(xknx.telegrams.get_nowait())
         assert (
             str(notification) == '<Notification name="Alarm" '
-            'message="GroupAddress("1/2/3")/'
-            'GroupAddress("1/2/4")/Einbrecher im /'
-            '<DPTArray value="[0x45,0x69,0x6e,0x62,0x72,0x65,0x63,0x68,0x65,0x72,0x20,0x69,0x6d,0x20]" />'
-            '" />'
+            'message=<"1/2/3", "1/2/4", [], \'Einbrecher im \' /> />'
         )
 
     def test_scene(self):
@@ -257,7 +259,7 @@ class TestStringRepresentations:
         scene = Scene(xknx, name="Romantic", group_address="1/2/3", scene_number=23)
         assert (
             str(scene)
-            == '<Scene name="Romantic" scene_value="GroupAddress("1/2/3")/None/None/None" scene_number="23" />'
+            == '<Scene name="Romantic" scene_value=<"1/2/3", "None", [], None /> scene_number="23" />'
         )
 
     async def test_sensor(self):
@@ -268,7 +270,7 @@ class TestStringRepresentations:
         )
         assert (
             str(sensor)
-            == '<Sensor name="MeinSensor" sensor="None/GroupAddress("1/2/3")/None/None" value="None" unit="%"/>'
+            == '<Sensor name="MeinSensor" sensor=<"None", "1/2/3", [], None /> value=None unit="%"/>'
         )
         # self.loop.run_until_complete(sensor.sensor_value.set(25))
         telegram = Telegram(
@@ -279,7 +281,7 @@ class TestStringRepresentations:
         await sensor.process_group_write(telegram)
         assert (
             str(sensor)
-            == '<Sensor name="MeinSensor" sensor="None/GroupAddress("1/2/3")/25/<DPTArray value="[0x40]" />" value="25" unit="%"/>'
+            == '<Sensor name="MeinSensor" sensor=<"None", "1/2/3", [], 25 /> value=25 unit="%"/>'
         )
 
     async def test_expose_sensor(self):
@@ -290,13 +292,13 @@ class TestStringRepresentations:
         )
         assert (
             str(sensor)
-            == '<ExposeSensor name="MeinSensor" sensor="GroupAddress("1/2/3")/None/None/None" value="None" unit="%"/>'
+            == '<ExposeSensor name="MeinSensor" sensor=<"1/2/3", "None", [], None /> value=None unit="%"/>'
         )
         await sensor.set(25)
         await sensor.process(xknx.telegrams.get_nowait())
         assert (
             str(sensor)
-            == '<ExposeSensor name="MeinSensor" sensor="GroupAddress("1/2/3")/None/25/<DPTArray value="[0x40]" />" value="25" unit="%"/>'
+            == '<ExposeSensor name="MeinSensor" sensor=<"1/2/3", "None", [], 25 /> value=25 unit="%"/>'
         )
 
     def test_switch(self):
@@ -307,7 +309,7 @@ class TestStringRepresentations:
         )
         assert (
             str(switch)
-            == '<Switch name="Schalter" switch="GroupAddress("1/2/3")/GroupAddress("1/2/4")/None/None" />'
+            == '<Switch name="Schalter" switch=<"1/2/3", "1/2/4", [], None /> />'
         )
 
     async def test_weather(self):
@@ -332,14 +334,14 @@ class TestStringRepresentations:
         )
         assert (
             str(weather)
-            == '<Weather name="Home" temperature="None/GroupAddress("7/0/1")/None/None" '
-            'brightness_south="None/GroupAddress("7/0/5")/None/None" brightness_north="None/None/None/None" '
-            'brightness_west="None/GroupAddress("7/0/3")/None/None" '
-            'brightness_east="None/GroupAddress("7/0/4")/None/None" wind_speed="None/GroupAddress("7/0/2")/None/None" '
-            'wind_bearing="None/GroupAddress("7/0/6")/None/None" rain_alarm="None/GroupAddress("7/0/0")/None/None" '
-            'wind_alarm="None/GroupAddress("7/0/10")/None/None" frost_alarm="None/GroupAddress("7/0/8")/None/None" '
-            'day_night="None/GroupAddress("7/0/7")/None/None" air_pressure="None/GroupAddress("7/0/9")/None/None" '
-            'humidity="None/GroupAddress("7/0/9")/None/None" />'
+            == '<Weather name="Home" temperature=<"None", "7/0/1", [], None /> '
+            'brightness_south=<"None", "7/0/5", [], None /> brightness_north=<"None", "None", [], None /> '
+            'brightness_west=<"None", "7/0/3", [], None /> '
+            'brightness_east=<"None", "7/0/4", [], None /> wind_speed=<"None", "7/0/2", [], None /> '
+            'wind_bearing=<"None", "7/0/6", [], None /> rain_alarm=<"None", "7/0/0", [], None /> '
+            'wind_alarm=<"None", "7/0/10", [], None /> frost_alarm=<"None", "7/0/8", [], None /> '
+            'day_night=<"None", "7/0/7", [], None /> air_pressure=<"None", "7/0/9", [], None /> '
+            'humidity=<"None", "7/0/9", [], None /> />'
         )
 
         telegram = Telegram(
@@ -351,14 +353,14 @@ class TestStringRepresentations:
 
         assert (
             str(weather)
-            == '<Weather name="Home" temperature="None/GroupAddress("7/0/1")/None/None" '
-            'brightness_south="None/GroupAddress("7/0/5")/None/None" brightness_north="None/None/None/None" '
-            'brightness_west="None/GroupAddress("7/0/3")/None/None" '
-            'brightness_east="None/GroupAddress("7/0/4")/None/None" wind_speed="None/GroupAddress("7/0/2")/None/None" '
-            'wind_bearing="None/GroupAddress("7/0/6")/None/None" rain_alarm="None/GroupAddress("7/0/0")/None/None" '
-            'wind_alarm="None/GroupAddress("7/0/10")/True/<DPTBinary value="1" />" '
-            'frost_alarm="None/GroupAddress("7/0/8")/None/None" day_night="None/GroupAddress("7/0/7")/None/None" '
-            'air_pressure="None/GroupAddress("7/0/9")/None/None" humidity="None/GroupAddress("7/0/9")/None/None" />'
+            == '<Weather name="Home" temperature=<"None", "7/0/1", [], None /> '
+            'brightness_south=<"None", "7/0/5", [], None /> brightness_north=<"None", "None", [], None /> '
+            'brightness_west=<"None", "7/0/3", [], None /> '
+            'brightness_east=<"None", "7/0/4", [], None /> wind_speed=<"None", "7/0/2", [], None /> '
+            'wind_bearing=<"None", "7/0/6", [], None /> rain_alarm=<"None", "7/0/0", [], None /> '
+            'wind_alarm=<"None", "7/0/10", [], True /> '
+            'frost_alarm=<"None", "7/0/8", [], None /> day_night=<"None", "7/0/7", [], None /> '
+            'air_pressure=<"None", "7/0/9", [], None /> humidity=<"None", "7/0/9", [], None /> />'
         )
 
     def test_datetime(self):
@@ -367,7 +369,7 @@ class TestStringRepresentations:
         date_time = DateTime(xknx, name="Zeit", group_address="1/2/3", localtime=False)
         assert (
             str(date_time)
-            == '<DateTime name="Zeit" group_address="GroupAddress("1/2/3")/None/None/None" broadcast_type="TIME" />'
+            == '<DateTime name="Zeit" _remote_value=<"1/2/3", "None", [], None /> broadcast_type="TIME" />'
         )
 
     def test_could_not_parse_telegramn_exception(self):