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, "9 h 21 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] = 72 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_settings_capacity_uses_app_word_zero(self): state = DeviceState() words = [1600, 3750, 42] + [0] * 38 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) 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()