-
-
Notifications
You must be signed in to change notification settings - Fork 35
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Initial support for Lynx Smart BMS #49
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 # ?? | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I get 5461 for io_status. If this matches that dbus/mqtt structure, IO encompasses the following things:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 16 bits is a lot of space for just these four things. Perhaps this also contains the status of attached Lynx Distributors? If it's helpful, I have one distributor attached (Distributor A) with all four fuses in the OK state. Unfortunately it's not so convenient to get to it and start pulling fuses to experiment. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm afraid I don't have any distributors to test with. |
||
assert actual.get_current() == -0.6 | ||
assert actual.get_voltage() == 26.99 | ||
assert actual.get_remaining_mins() == 14400 | ||
assert actual.get_error_flags() == 0 |
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I also get 5205, with as far as I can tell, no alarms.