165 lines
6.8 KiB
Python
165 lines
6.8 KiB
Python
import unittest
|
|
|
|
from bulltron_gui import DeviceState, bulltron_match_reason, decode_response, read_frame, split_frames, write_single_frame
|
|
|
|
|
|
class BullTronParserTest(unittest.TestCase):
|
|
def test_read_frame_crc(self):
|
|
self.assertEqual(read_frame(0, 62).hex().upper(), "D2030000003ED7B9")
|
|
|
|
def test_split_and_decode_live_frame(self):
|
|
words = [0] * 62
|
|
words[0:4] = [3306, 3312, 3308, 3311]
|
|
words[40] = 132
|
|
words[41] = 29925
|
|
words[42] = 652
|
|
words[43] = 3312
|
|
words[44] = 3306
|
|
words[47] = 2
|
|
words[48] = 1075
|
|
words[51] = 3
|
|
words[53] = 1
|
|
words[54] = 1
|
|
words[56] = 6
|
|
words[61] = 0x0010
|
|
payload = b"".join(word.to_bytes(2, "big") for word in words)
|
|
body = bytes([0xD2, 0x03, len(payload)]) + payload
|
|
from bulltron_gui import crc16_modbus_swapped
|
|
|
|
frame = body + crc16_modbus_swapped(body).to_bytes(2, "big")
|
|
buffer = bytearray(b"\x00" + frame + frame)
|
|
frames = split_frames(buffer)
|
|
self.assertEqual(len(frames), 2)
|
|
|
|
state = DeviceState()
|
|
decode_response(frames[0], state)
|
|
self.assertAlmostEqual(state.telemetry.voltage, 13.2)
|
|
self.assertAlmostEqual(state.telemetry.current, -7.5)
|
|
self.assertAlmostEqual(state.telemetry.discharge_current, 7.5)
|
|
self.assertAlmostEqual(state.telemetry.charge_current, 0.0)
|
|
self.assertAlmostEqual(state.telemetry.soc, 65.2)
|
|
self.assertEqual(state.telemetry.cycle_count, 3)
|
|
self.assertTrue(state.telemetry.charge_mos)
|
|
self.assertTrue(state.telemetry.discharge_mos)
|
|
self.assertEqual(state.telemetry.cell_count, 4)
|
|
self.assertEqual(state.telemetry.time_value, "14 h 20 m")
|
|
|
|
def test_charging_time_uses_soc_fraction_and_rated_capacity(self):
|
|
state = DeviceState(rated_capacity_ah=160)
|
|
words = [0] * 62
|
|
words[40] = 132
|
|
words[41] = 30152
|
|
words[42] = 450
|
|
words[47] = 1
|
|
words[48] = 720
|
|
payload = b"".join(word.to_bytes(2, "big") for word in words)
|
|
body = bytes([0xD2, 0x03, len(payload)]) + payload
|
|
from bulltron_gui import crc16_modbus_swapped
|
|
|
|
frame = body + crc16_modbus_swapped(body).to_bytes(2, "big")
|
|
decode_response(frame, state)
|
|
self.assertAlmostEqual(state.telemetry.charge_current, 15.2)
|
|
self.assertAlmostEqual(state.telemetry.charge_power_w, 200.64)
|
|
self.assertAlmostEqual(state.telemetry.discharge_current, 0.0)
|
|
self.assertEqual(state.telemetry.time_label, "Time till full")
|
|
self.assertEqual(state.telemetry.time_value, "5 h 47 m")
|
|
|
|
def test_current_direction_uses_signed_current_for_display(self):
|
|
state = DeviceState(rated_capacity_ah=160)
|
|
words = [0] * 62
|
|
words[40] = 128
|
|
words[41] = 29922
|
|
words[42] = 490
|
|
words[47] = 1
|
|
words[48] = 784
|
|
payload = b"".join(word.to_bytes(2, "big") for word in words)
|
|
body = bytes([0xD2, 0x03, len(payload)]) + payload
|
|
from bulltron_gui import crc16_modbus_swapped
|
|
|
|
frame = body + crc16_modbus_swapped(body).to_bytes(2, "big")
|
|
decode_response(frame, state)
|
|
self.assertAlmostEqual(state.telemetry.current, -7.8)
|
|
self.assertAlmostEqual(state.telemetry.discharge_current, 7.8)
|
|
self.assertAlmostEqual(state.telemetry.discharge_power_w, 99.84)
|
|
self.assertAlmostEqual(state.telemetry.charge_current, 0.0)
|
|
self.assertEqual(state.telemetry.time_label, "Time till empty")
|
|
self.assertEqual(state.telemetry.time_value, "10 h 03 m")
|
|
|
|
def test_residue_register_does_not_hide_estimated_empty_time(self):
|
|
state = DeviceState(rated_capacity_ah=160)
|
|
residue_payload = (0).to_bytes(2, "big")
|
|
residue_body = bytes([0xD2, 0x03, len(residue_payload)]) + residue_payload
|
|
from bulltron_gui import crc16_modbus_swapped
|
|
|
|
residue_frame = residue_body + crc16_modbus_swapped(residue_body).to_bytes(2, "big")
|
|
decode_response(residue_frame, state)
|
|
|
|
words = [0] * 62
|
|
words[40] = 128
|
|
words[41] = 29922
|
|
words[42] = 490
|
|
words[47] = 2
|
|
words[48] = 784
|
|
payload = b"".join(word.to_bytes(2, "big") for word in words)
|
|
body = bytes([0xD2, 0x03, len(payload)]) + payload
|
|
live_frame = body + crc16_modbus_swapped(body).to_bytes(2, "big")
|
|
decode_response(live_frame, state)
|
|
|
|
self.assertEqual(state.telemetry.time_label, "Time till empty")
|
|
self.assertEqual(state.telemetry.time_value, "10 h 03 m")
|
|
|
|
def test_settings_capacity_uses_app_word_zero(self):
|
|
state = DeviceState()
|
|
words = [1600, 3750, 42] + [0] * 34 + [1, 0, 99, 0]
|
|
payload = b"".join(word.to_bytes(2, "big") for word in words)
|
|
body = bytes([0xD2, 0x03, len(payload)]) + payload
|
|
from bulltron_gui import crc16_modbus_swapped
|
|
|
|
frame = body + crc16_modbus_swapped(body).to_bytes(2, "big")
|
|
decode_response(frame, state)
|
|
self.assertAlmostEqual(state.rated_capacity_ah, 160.0)
|
|
self.assertTrue(state.control_charge_mos)
|
|
self.assertFalse(state.control_discharge_mos)
|
|
|
|
def test_direct_mos_control_read_uses_register_context(self):
|
|
state = DeviceState()
|
|
payload = b"\x00\x00\x00\x01"
|
|
body = bytes([0xD2, 0x03, len(payload)]) + payload
|
|
from bulltron_gui import crc16_modbus_swapped
|
|
|
|
frame = body + crc16_modbus_swapped(body).to_bytes(2, "big")
|
|
decode_response(frame, state, 0x00A5)
|
|
|
|
self.assertFalse(state.control_charge_mos)
|
|
self.assertTrue(state.control_discharge_mos)
|
|
|
|
def test_four_byte_non_mos_read_still_decodes_production_date(self):
|
|
state = DeviceState()
|
|
payload = b"\x19\x01\x01\x00"
|
|
body = bytes([0xD2, 0x03, len(payload)]) + payload
|
|
from bulltron_gui import crc16_modbus_swapped
|
|
|
|
frame = body + crc16_modbus_swapped(body).to_bytes(2, "big")
|
|
decode_response(frame, state, 0x00C9)
|
|
|
|
self.assertEqual(state.production_date, "2025-01-01")
|
|
|
|
def test_system_mos_write_registers(self):
|
|
self.assertEqual(write_single_frame(0x00A5, 1).hex().upper()[:8], "D20600A5")
|
|
self.assertEqual(write_single_frame(0x00A6, 0).hex().upper()[:8], "D20600A6")
|
|
|
|
def test_bulltron_scan_filter_matches_apk_markers(self):
|
|
self.assertEqual(bulltron_match_reason("DL-1234", [], []), "name:DL")
|
|
self.assertEqual(bulltron_match_reason("B35-1234", [], []), "name:B35")
|
|
self.assertEqual(
|
|
bulltron_match_reason("unknown", ["0000fff0-0000-1000-8000-00805f9b34fb"], []),
|
|
"service:fff0",
|
|
)
|
|
payload = bytes.fromhex("00" * 14 + "4A4842" + "00")
|
|
self.assertEqual(bulltron_match_reason("unknown", [], [payload]), "adv:JHB")
|
|
self.assertIsNone(bulltron_match_reason("keyboard", [], [b"\x01\x02\x03"]))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|