Repository: keshavdv/victron-ble

Issue:
Official details from Victron are now available
Finally Victron released the documentation of all the data:

https://community.victronenergy.com/storage/attachments/48745-extra-manufacturer-data-2022-12-14.pdf

who wants to work on the implementation?

Discussion:
@keshavdv: I've been working on adding a few of the other device implementations based on the spec, but I've been focusing on the ones I own and can validate with.@keshavdv: I've asked for some clarification on byte ordering from Victron since it isn't immediately apparent when consuming the data in Python@mpvader: Hey @keshavdv was that answered?@keshavdv: Unfortunately, I haven't heard back yet.@ThiemoVanEngelen: Hi @keshavdv ,

The specification follows the following convention:
- All signed values are two's complement, meaning that when all bits set, the value is -1 before scaling.
- The numbering of bits and bytes start at 0.
- The data ordering for all values is least significant bit first.

So when a field starts at bit 0 and is 16 bits long, bit 0 of byte 0 of the instant readout data is the least significant bit of the value and bit 7 of byte 1 is the most significant bit of the value.
Or when a field starts at bit 140 and is 10 bits long, its value it is built up in the following way:
bit 4 of byte 17 => bit 0
...
bit 7 of byte 17 => bit 3
bit 0 of byte 18 => bit 4
...
bit 5 of byte 18 => bit 9

Perhaps  this answers your question? 
Otherwise, where did you ask the question or how did you try to reach us?

Kind regards,
Thiemo@keshavdv: With the migration to a custom BitReader thanks to @j9brown, the published Victron docs can now easily be translated into device parser implementations. Python's poor support for handling bit-level manipulation made this hard to grok for me originally.

Solution patch:
diff --git a/victron_ble/devices/base.py b/victron_ble/devices/base.py
index f1f5a07..779acae 100644
--- a/victron_ble/devices/base.py
+++ b/victron_ble/devices/base.py
@@ -7,6 +7,8 @@ from Crypto.Cipher import AES
 from Crypto.Util import Counter
 from Crypto.Util.Padding import pad
 
+from victron_ble.exceptions import AdvertisementKeyMismatchError
+
 
 # Sourced from VE.Direct docs
 class OperationMode(Enum):
@@ -388,11 +390,16 @@ class Device(abc.ABC):
     def decrypt(self, data: bytes) -> bytes:
         container = self.PARSER.parse(data)
 
-        # The first byte of advertised data seems to match the first byte of the advertisement key
+        advertisement_key = bytes.fromhex(self.advertisement_key)
+
+        # The first data byte is a key check byte
+        if container.encrypted_data[0] != advertisement_key[0]:
+            raise AdvertisementKeyMismatchError("Incorrect advertisement key")
+
         ctr = Counter.new(128, initial_value=container.iv, little_endian=True)
 
         cipher = AES.new(
-            bytes.fromhex(self.advertisement_key),
+            advertisement_key,
             AES.MODE_CTR,
             counter=ctr,
         )
diff --git a/victron_ble/exceptions.py b/victron_ble/exceptions.py
index 313805f..fc9297e 100644
--- a/victron_ble/exceptions.py
+++ b/victron_ble/exceptions.py
@@ -4,3 +4,7 @@ class UnknownDeviceError(Exception):
 
 class AdvertisementKeyMissingError(Exception):
     pass
+
+
+class AdvertisementKeyMismatchError(Exception):
+    pass


Test patch:
diff --git a/tests/test_battery_monitor.py b/tests/test_battery_monitor.py
index b2824bd..af1711b 100644
--- a/tests/test_battery_monitor.py
+++ b/tests/test_battery_monitor.py
@@ -1,4 +1,7 @@
+import pytest
+
 from victron_ble.devices.battery_monitor import BatteryMonitor, AuxMode
+from victron_ble.exceptions import AdvertisementKeyMismatchError
 
 
 class TestBatteryMonitor:
@@ -39,3 +42,10 @@ class TestBatteryMonitor:
             bytes.fromhex(data)
         )
         assert actual.get_temperature() == 382.2
+
+    def test_key_mismatch(self) -> None:
+        data = "100289a302bb01af129087600b9b97bc2c32867c8238da"
+        with pytest.raises(AdvertisementKeyMismatchError):
+            BatteryMonitor("ffffffffffffffffffffffffffffffff").parse(
+                bytes.fromhex(data)
+            )