Repository: ttu/ruuvitag-sensor (language: python)

Issue:
RSSI
Hi,

can you add RSSI to the list ?

A quick hack lto Df3Decoder like this seems to work:

    def _get_rssi(self, data):
        return data[-1]-256

                'rssi': self._get_rssi(byte_data)



{'acceleration': 1038.0934447341433, 'pressure': 1008.27, 'temperature': 24.75, 'acceleration_y': -13, 'acceleration_x': 5, 'battery': 3061, 'acceleration_z': 1038, **'rssi': -68,** 'humidity': 42.5}


Add RSSI reporting
The last byte of hcidump output is treated as a signed 8-bit int containing the RSSI value.


Discussion / hints:
That is a good addition!

There is just one thing we should add to that code. RSSI value is added by the receiver, so that should also check somehow that if payload actually contains RSSI data. Any idea how to do that?
Don't really know.. maybe inqure if the device supports and then just assume its there? :)

hciconfig -a hci0 features |grep -i rssi
                <timing accuracy> <role switch> <sniff mode> <RSSI>
                <interlaced pscan> <inquiry with RSSI> <extended SCO>

no idea how to check this with python
You can parse the report length from the data and see if there's one "extra byte" at the end (=RSSI), from my notes (a data format 3 packet in this case):

```format 3
> 04 3E 25 02 01 03 01 D5 07 82 61 3C F2 19 02 01 04 15 FF 99 04 03 2C 1A 08 C9 79 00 0B FF F5 03 EB 0A ED 00 00 00 00 CD
# PT EC LE SE NR ET PA MAC-------------- RL SL ST FL SL ST COMID DF DATA---------------------------------------------- RS

PT = Packet type (04 = event)
EC = Event code (3E = LE Meta)
LE = Packet length
SE = Sub event (02 = LE Advertising Report)
NR = Number of Reports
ET = Event Type (03 = Non-Connectable undirected advertising)
PA = Peer Address Type (01 = Random Device Address)
RL = (next) Report length

SL = AD Struct Length
ST = AD Struct Type (01 = Flags, 03 = Service UUID, FF = Manufacturer specific data)
FL = AD Struct for flags
COMID = Company Identifier (99 04 is 0x0499 = Ruuvi Innovations Ltd.)

DF = Data Format
DATA = The actual "measurement data" from the tag

RS = RSSI
```

Most importantly:
- Third byte of the entire data (LE in my notes) is the **remaining** length of the packet from hcidump, **including the RSSI if it's present.**
- The fifth byte (NR) is the number of reports, for the normal broadcast data from ruuvitags this is always (?) 1, at least with the current firmwares
- The first byte of a report is the **remaining** length of **that report** (RL in my notes), the first report of a packet starts on the 14th byte, right after the MAC address.

So the most robust solution: parse the number of reports, then parse the length of all reports, and see if there's one extra byte after the last report (or compare it to the Packet length (LE in my notes) as the LE includes the RSSI byte if present)

The quickest solution is to check the 14th byte (remaining length of the first report) and see if the remaining length of the data is the same or 1 more (this will work only if there's one report per packet)
Hi, my team and I are working on a smart basket and we're using your library.

For protocols 3 and 5 it was possible to extract the RSSI value in the last byte of the packet, and in our tests this proved to be useful to measure the distance later.

This is a quick solution, as we only take the last byte.


Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4a94649..3b65854 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,7 @@
 ## Changelog
 
 ### [Unreleased]
+* CHANGE: Add RSSI reporting
 
 ## [2.0.0] - 2022-08-03
 * CHANGE: Support only Python 3.7 and above
diff --git a/README.md b/README.md
index 363727d..c21c93b 100644
--- a/README.md
+++ b/README.md
@@ -61,7 +61,7 @@ if __name__ == '__main__':
 
 The line `if __name__ == '__main__':` is required on Windows and macOS due to the way the `multiprocessing` library works. It is not required on Linux, but it is recommended. It is omitted from the rest of the examples below.
 
-Optional list of macs and run flag can be passed to the `get_data` function. Callback is called only for macs in the list and setting run flag to false will stop execution. If run flag is not passed, function will execute forever.
+Optional list of MACs and run flag can be passed to the `get_data` function. Callback is called only for MACs in the list and setting run flag to false will stop execution. If run flag is not passed, function will execute forever.
 
 ```python
 from ruuvitag_sensor.ruuvi import RuuviTagSensor, RunFlag
@@ -79,7 +79,7 @@ def handle_data(found_data):
     if counter < 0:
         run_flag.running = False
 
-# List of macs of sensors which will execute callback function
+# List of MACs of sensors which will execute callback function
 macs = ['AA:2C:6A:1E:59:3D', 'CC:2C:6A:1E:59:3D']
 
 RuuviTagSensor.get_data(handle_data, macs, run_flag)
@@ -92,7 +92,7 @@ RuuviTagSensor.get_data(handle_data, macs, run_flag)
 ```python
 from ruuvitag_sensor.ruuvi import RuuviTagSensor
 
-# List of macs of sensors which data will be collected
+# List of MACs of sensors which data will be collected
 # If list is empty, data will be collected for all found sensors
 macs = ['AA:2C:6A:1E:59:3D', 'CC:2C:6A:1E:59:3D']
 # get_data_for_sensors will look data for the duration of timeout_in_sec
diff --git a/developer_notes.md b/developer_notes.md
index 1f5e559..2165be6 100644
--- a/developer_notes.md
+++ b/developer_notes.md
@@ -135,7 +135,7 @@ Kickstarter FW
 
 2.4.2 RAW v2-mode
 ```python
-{'data_format': 5, 'humidity': 44.88, 'temperature': 25.2, 'pressure': 1008.69, 'acceleration': 1034.3616388865164, 'acceleration_x': -64, 'acceleration_y': 28, 'acceleration_z': 1032, 'tx_power': 4, 'battery': 3199, 'movement_counter': 209, 'measurement_sequence_number': 30638, 'mac': 'e62eb92e73e5'}
+{'data_format': 5, 'humidity': 44.88, 'temperature': 25.2, 'pressure': 1008.69, 'acceleration': 1034.3616388865164, 'acceleration_x': -64, 'acceleration_y': 28, 'acceleration_z': 1032, 'tx_power': 4, 'battery': 3199, 'movement_counter': 209, 'measurement_sequence_number': 30638, 'mac': 'e62eb92e73e5', 'rssi': -55}
 ```
 
 ## BLE Broadcast data from RuuviTags
diff --git a/ruuvitag_sensor/adapters/nix_hci.py b/ruuvitag_sensor/adapters/nix_hci.py
index 517adb7..e1a93fa 100644
--- a/ruuvitag_sensor/adapters/nix_hci.py
+++ b/ruuvitag_sensor/adapters/nix_hci.py
@@ -127,7 +127,7 @@ def get_data(blacklist=[], bt_device=''):
 
                 # The third byte is the parameter length, this should cover
                 # the length of the entire packet, minus the first three bytes.
-                # Note that the data is in hex format, so uses two chars per
+                # Note that the data is in hex format, so use two chars per
                 # byte
                 plen = int(line[4:6], 16)
                 if plen != (len(line) / 2) - 3:
diff --git a/ruuvitag_sensor/data_formats.py b/ruuvitag_sensor/data_formats.py
index aa79b7d..83605c4 100644
--- a/ruuvitag_sensor/data_formats.py
+++ b/ruuvitag_sensor/data_formats.py
@@ -52,8 +52,8 @@ def convert_data(raw):
         try:
             # The data starts with a length byte, covering the data
             # length, minus the length byte itself. There might be additional
-            # data at the end (an RSSI value) which we're ignoring
-            data, _ = _dechunk(raw)
+            # data at the end: the RSSI value
+            data, rssi = _dechunk(raw)
 
             # The remaining data is a list of length:type:data chunks.
             # We look for a chunk with vendor specific data (type 0xff),
@@ -96,7 +96,7 @@ def convert_data(raw):
             return (3, candidate[6:])
 
         if candidate.startswith("FF990405"):
-            return (5, candidate[6:])
+            return (5, (candidate[6:] + rssi) if rssi else candidate[6:])
 
         if candidate.startswith("16AAFE"):
             # TODO: Check from raw data correct data format
diff --git a/ruuvitag_sensor/decoder.py b/ruuvitag_sensor/decoder.py
index 422ee1c..d0f1295 100644
--- a/ruuvitag_sensor/decoder.py
+++ b/ruuvitag_sensor/decoder.py
@@ -239,6 +239,13 @@ def _get_measurementsequencenumber(self, data):
     def _get_mac(self, data):
         return ''.join(f'{x:02x}' for x in data[10:])
 
+    def _get_rssi(self, rssi_byte: str) -> int:
+        """Return RSSI value in dBm."""
+        rssi = int(rssi_byte, 16)
+        if rssi > 127:
+            rssi = (256 - rssi) * -1
+        return rssi
+
     def decode_data(self, data):
         """
         Decode sensor data.
@@ -248,6 +255,7 @@ def decode_data(self, data):
         """
         try:
             byte_data = struct.unpack('>BhHHhhhHBH6B', bytearray.fromhex(data[:48]))
+            rssi = data[48:]
 
             acc_x, acc_y, acc_z = self._get_acceleration(byte_data)
             return {
@@ -263,7 +271,8 @@ def decode_data(self, data):
                 'battery': self._get_battery(byte_data),
                 'movement_counter': self._get_movementcounter(byte_data),
                 'measurement_sequence_number': self._get_measurementsequencenumber(byte_data),
-                'mac': self._get_mac(byte_data)
+                'mac': self._get_mac(byte_data),
+                'rssi': self._get_rssi(rssi) if rssi else None
             }
         except Exception:
             log.exception('Value: %s not valid', data)


Test patch:
diff --git a/tests/test_decoder.py b/tests/test_decoder.py
index 879fb15..d8ea59d 100644
--- a/tests/test_decoder.py
+++ b/tests/test_decoder.py
@@ -132,9 +132,10 @@ def test_df5decode_is_valid(self):
         movement_counter = '42'
         measurement_sequence = '00CD'
         mac = 'CBB8334C884F'
+        rssi = 'C6'
         data = decoder.decode_data(
             f'{data_format}{temp}{humidity}{pressure}{accX}{accY}{accZ}{power_info}'
-            f'{movement_counter}{measurement_sequence}{mac}')
+            f'{movement_counter}{measurement_sequence}{mac}{rssi}')
 
         assert data['temperature'] == 24.30
         assert data['humidity'] == 53.49
@@ -147,6 +148,14 @@ def test_df5decode_is_valid(self):
         assert data['movement_counter'] == 66
         assert data['measurement_sequence_number'] == 205
         assert data['mac'] == 'cbb8334c884f'
+        assert data['rssi'] == -58
+
+        # No RSSI case
+        data = decoder.decode_data(
+            f'{data_format}{temp}{humidity}{pressure}{accX}{accY}{accZ}{power_info}'
+            f'{movement_counter}{measurement_sequence}{mac}')
+
+        assert data['rssi'] is None
 
     def test_parse_df5_mac(self):
         mac_payload = 'e62eb92e73e5'
diff --git a/tests/test_ruuvitag_sensor.py b/tests/test_ruuvitag_sensor.py
index bfadff2..b5af2d7 100644
--- a/tests/test_ruuvitag_sensor.py
+++ b/tests/test_ruuvitag_sensor.py
@@ -59,6 +59,8 @@ def get_data(self, blacklist=[], bt_device=''):
             # The following is a ruuvitag with firmware 3.x, sending a
             # measurement advertisement again
             ('CE:D6:05:F5:17:AA', '1F0201061BFF99040511F83B83CC5DFFFCFFFC03DCA7161427D2CED605F517AACB'),  # noqa: E501
+            # RuuviTag with firmware 3.3x sending RSSI
+            ('D5:57:97:65:88:14', '1F0201061BFF99040517B24633FFFFFFFCFFD403E4AF56388D51D55797658814B8'),  # noqa: E501
         ]
 
         for data in tag_data:
@@ -70,21 +72,24 @@ def get_data(self, blacklist=[], bt_device=''):
            get_data)
     def test_find_tags(self):
         tags = RuuviTagSensor.find_ruuvitags()
-        assert len(tags) == 8
+        assert len(tags) == 9
 
     @patch('ruuvitag_sensor.adapters.dummy.BleCommunicationDummy.get_data',
            get_data)
     @patch('ruuvitag_sensor.adapters.nix_hci.BleCommunicationNix.get_data',
            get_data)
     def test_get_data_for_sensors(self):
-        macs = ['CC:2C:6A:1E:59:3D', 'DD:2C:6A:1E:59:3D', 'EE:2C:6A:1E:59:3D']
+        macs = ['CC:2C:6A:1E:59:3D', 'DD:2C:6A:1E:59:3D', 'EE:2C:6A:1E:59:3D',
+                'D5:57:97:65:88:14']
         data = RuuviTagSensor.get_data_for_sensors(macs, 4)
-        assert len(data) == 3
+        assert len(data) == 4
         assert 'CC:2C:6A:1E:59:3D' in data
         assert 'DD:2C:6A:1E:59:3D' in data
+        assert 'D5:57:97:65:88:14' in data
         assert data['CC:2C:6A:1E:59:3D']['temperature'] == 24.0
         assert data['EE:2C:6A:1E:59:3D']['temperature'] == 25.12
         assert data['EE:2C:6A:1E:59:3D']['identifier'] == '0'
+        assert data['D5:57:97:65:88:14']['rssi'] == -72
 
     @patch('ruuvitag_sensor.adapters.dummy.BleCommunicationDummy.get_data',
            get_data)
@@ -93,7 +98,7 @@ def test_get_data_for_sensors(self):
     def test_get_data(self):
         data = []
         RuuviTagSensor.get_data(data.append)
-        assert len(data) == 8
+        assert len(data) == 9
 
     @patch('ruuvitag_sensor.adapters.dummy.BleCommunicationDummy.get_data',
            get_data)