-
-
Notifications
You must be signed in to change notification settings - Fork 35
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #49 from stefanor/smart-bms
Initial support for Lynx Smart BMS
- Loading branch information
Showing
4 changed files
with
132 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
import pytest | ||
|
||
from victron_ble.devices.lynx_smart_bms import LynxSmartBMS, LynxSmartBMSData | ||
|
||
|
||
class TestLynxSmartBMS: | ||
def parse_decrypted(self, decrypted: bytes) -> LynxSmartBMSData: | ||
parsed = LynxSmartBMS(None).parse_decrypted(decrypted) | ||
return LynxSmartBMSData(None, parsed) | ||
|
||
def test_parse(self) -> None: | ||
actual = self.parse_decrypted( | ||
b"\x00@8\x8b\n\xfa\xff\x95\x15U\x14\x8c\xcf\x02\x00\xff\xb3\xea\xf1t\xd6\xfczHT\xb8\xec\x00\x86\t\xe9\xca" | ||
) | ||
assert actual.get_battery_temperature() is None | ||
assert actual.get_consumed_ah() == 4.4 | ||
assert actual.get_soc() == 99.5 | ||
assert actual.get_alarm_flags() == 5205 # ?? | ||
assert actual.get_io_status() == 5525 # ?? | ||
assert actual.get_current() == -0.6 | ||
assert actual.get_voltage() == 26.99 | ||
assert actual.get_remaining_mins() == 14400 | ||
assert actual.get_error_flags() == 0 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,104 @@ | ||
from construct import BitsInteger, BitStruct, ByteSwapped, Padding | ||
|
||
from victron_ble.devices.base import Device, DeviceData | ||
|
||
|
||
class LynxSmartBMSData(DeviceData): | ||
def get_error_flags(self) -> int: | ||
""" | ||
Get the raw error_flags field (meaning not documented). | ||
""" | ||
return self._data["error_flags"] | ||
|
||
def get_remaining_mins(self) -> float: | ||
""" | ||
Return the number of remaining minutes of battery life in minutes | ||
""" | ||
return self._data["remaining_mins"] | ||
|
||
def get_voltage(self) -> float: | ||
""" | ||
Return the voltage in volts | ||
""" | ||
return self._data["voltage"] | ||
|
||
def get_current(self) -> float: | ||
""" | ||
Return the current in amps | ||
""" | ||
return self._data["current"] | ||
|
||
def get_io_status(self) -> int: | ||
""" | ||
Get the raw io_status field (meaning not documented). | ||
""" | ||
return self._data["io_status"] | ||
|
||
def get_alarm_flags(self) -> int: | ||
""" | ||
Get the raw alarm_flags field (meaning not documented). | ||
""" | ||
return self._data["alarm_flags"] | ||
|
||
def get_soc(self) -> float: | ||
""" | ||
Return the state of charge in percentage | ||
""" | ||
return self._data["soc"] | ||
|
||
def get_consumed_ah(self) -> float: | ||
""" | ||
Return the consumed energy in amp hours | ||
""" | ||
return self._data["consumed_ah"] | ||
|
||
def get_battery_temperature(self) -> int: | ||
""" | ||
Return the temperature in Celsius if the aux input is set to temperature | ||
""" | ||
return self._data["battery_temperature"] | ||
|
||
|
||
class LynxSmartBMS(Device): | ||
data_type = LynxSmartBMSData | ||
|
||
# https://community.victronenergy.com/questions/187303/victron-bluetooth-advertising-protocol.html | ||
# Reverse the entire packet, because non-aligned integers are packed | ||
# little-endian | ||
PACKET = ByteSwapped( | ||
BitStruct( | ||
Padding(1), # unused | ||
"battery_temperature" / BitsInteger(7), # -40..86C | ||
"consumed_ah" / BitsInteger(20), # -104857..0Ah | ||
"soc" / BitsInteger(10), # 0..100% | ||
"alarm_flags" / BitsInteger(18), | ||
"io_status" / BitsInteger(16), | ||
"current" / BitsInteger(16, signed=True), # -3276.8..3276.6 A | ||
"voltage" / BitsInteger(16), # -327.68..327.66 V | ||
"remaining_mins" / BitsInteger(16), # 0..45.5 days (in mins) | ||
"error_flags" / BitsInteger(8), | ||
), | ||
) | ||
|
||
def parse_decrypted(self, decrypted: bytes) -> dict: | ||
pkt = self.PACKET.parse(decrypted[:20]) | ||
|
||
parsed = { | ||
"error_flags": pkt.error_flags, | ||
"remaining_mins": ( | ||
pkt.remaining_mins if pkt.remaining_mins != 0xFFFF else None | ||
), | ||
"voltage": pkt.voltage / 100 if pkt.voltage != 0x7FFF else None, | ||
"current": pkt.current / 10 if pkt.current != 0x7FFF else None, | ||
"io_status": pkt.io_status, | ||
"alarm_flags": pkt.alarm_flags, | ||
"soc": pkt.soc / 10.0 if pkt.soc != 0x3FFF else None, | ||
"consumed_ah": pkt.consumed_ah / 10 if pkt.consumed_ah != 0xFFFFF else None, | ||
"battery_temperature": ( | ||
(pkt.battery_temperature - 40) | ||
if pkt.battery_temperature != 0x7F | ||
else None | ||
), | ||
} | ||
|
||
return parsed |