Repository: astarte-platform/astarte-device-sdk-python

Issue:
Sending empty payloads unsets properties
Sending an empty payload through the `send` function for interfaces of type property sends an unset command.
The bug originates here:
https://github.com/astarte-platform/astarte-device-sdk-python/blob/60a3f88e7ca5089aedc91e29f87a2cc463929a10/astarte/device/device.py#L500-L504
A simple fix can be to change `if payload:` to `if payload is not None:`.

Comments:

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d2d9f3a..5aae20f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file.
 The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
 and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 
+## [0.12.0] - Unreleased
+### Fixed
+- Sending zero payloads for endpoints in property interfaces was unsetting the property.
+
 ## [0.11.0] - 2023-03-16
 ### Added
 - Initial Astarte Device SDK release
diff --git a/astarte/device/device.py b/astarte/device/device.py
index 01ccb7b..868d6f5 100644
--- a/astarte/device/device.py
+++ b/astarte/device/device.py
@@ -498,7 +498,7 @@ class Device:  # pylint: disable=too-many-instance-attributes
         """
 
         bson_payload = b""
-        if payload:
+        if payload is not None:
             validation_result = self.__validate_data(
                 interface_name, interface_path, payload, timestamp
             )


Test patch:
diff --git a/tests/test_device.py b/tests/test_device.py
index 07c7713..471e317 100644
--- a/tests/test_device.py
+++ b/tests/test_device.py
@@ -500,6 +500,45 @@ class UnitTests(unittest.TestCase):
             qos="reliability value",
         )
 
+    @mock.patch.object(Client, "publish")
+    @mock.patch("astarte.device.device.bson.dumps")
+    @mock.patch.object(Introspection, "get_interface")
+    def test_send_zero(self, mock_get_interface, mock_bson_dumps, mock_mqtt_publish):
+        device = self.helper_initialize_device(loop=None)
+
+        mock_mapping = mock.MagicMock()
+        mock_mapping.reliability = "reliability value"
+        mock_interface = mock.MagicMock()
+        mock_interface.validate.return_value = None
+        mock_interface.get_mapping.return_value = mock_mapping
+        mock_interface.is_aggregation_object.return_value = False
+        mock_get_interface.return_value = mock_interface
+
+        mock_bson_dumps.return_value = bytes("bson content", "utf-8")
+
+        interface_name = "interface name"
+        interface_path = "interface path"
+        payload = 0
+        timestamp = datetime.now()
+        device.send(interface_name, interface_path, payload, timestamp)
+
+        calls = [
+            mock.call(interface_name),
+            mock.call(interface_name),
+            mock.call(interface_name),
+        ]
+        mock_get_interface.assert_has_calls(calls, any_order=True)
+        self.assertEqual(mock_get_interface.call_count, 3)
+        mock_interface.is_aggregation_object.assert_called_once()
+        mock_interface.validate.assert_called_once_with(interface_path, payload, timestamp)
+        mock_interface.get_mapping.assert_called_once_with(interface_path)
+        mock_bson_dumps.assert_called_once_with({"v": payload, "t": timestamp})
+        mock_mqtt_publish.assert_called_once_with(
+            "realm_name/device_id/" + interface_name + interface_path,
+            bytes("bson content", "utf-8"),
+            qos="reliability value",
+        )
+
     @mock.patch.object(Client, "publish")
     @mock.patch("astarte.device.device.bson.dumps")
     @mock.patch.object(Introspection, "get_interface")