Frankkkkk / python-pylontech

Python lib to talk to pylontech lithium batteries 🔋 (US2000, US3000, ...) using RS485
MIT License
64 stars 31 forks source link

Making it work into RS232 #29

Open tomascrespo opened 1 year ago

tomascrespo commented 1 year ago

Hi Frakkkkk!

I have read your code and it is great

I would like to make it work through RS232 because my Pylon are using RS485 interface to communicate with my inversor.

As far as I know Pylontech RS485 protocol and RS232 are almost the same, but I am not completly sure.

My batteries communicate with RS232 with BatteryView (Pylontech software) perfectly, using and RS232 to USB adapter connected to console port.

I think the first step is putting the serial port in 1200 bps, because this is the default for Pylontech RS232 (although they could go faster setting them up with a specific command)

Before I deep more into this I want to know if someone has tried before or your knowledge, if it is possible or not

Lots of thanks

Let's start!

Changing only the baud rate from 115200 to 1200 and adding some print for debug (same port, /dev/ttyUSB0)

----> Sending:  b'~20024642E002FFFD09\r'
7e3230303234363432453030324646464430390d
<---- Receiving:  b'~200146900000FDAA\r'
7e323030313436393030303030464441410d
Traceback (most recent call last):
  File "/home/pi/python-pylontech/tommy_test.py", line 4, in <module>
    print(p.get_values())
  File "/home/pi/python-pylontech/pylontech/pylontech.py", line 277, in get_values
    d = self.get_values_fmt.parse(f.info[1:])
  File "/usr/local/lib/python3.10/site-packages/construct-2.10.68-py3.10.egg/construct/core.py", line 288, in parse
  File "/usr/local/lib/python3.10/site-packages/construct-2.10.68-py3.10.egg/construct/core.py", line 300, in parse_stream
  File "/usr/local/lib/python3.10/site-packages/construct-2.10.68-py3.10.egg/construct/core.py", line 312, in _parsereport
  File "/usr/local/lib/python3.10/site-packages/construct-2.10.68-py3.10.egg/construct/core.py", line 2120, in _parse
  File "/usr/local/lib/python3.10/site-packages/construct-2.10.68-py3.10.egg/construct/core.py", line 312, in _parsereport
  File "/usr/local/lib/python3.10/site-packages/construct-2.10.68-py3.10.egg/construct/core.py", line 2653, in _parse
  File "/usr/local/lib/python3.10/site-packages/construct-2.10.68-py3.10.egg/construct/core.py", line 312, in _parsereport
  File "/usr/local/lib/python3.10/site-packages/construct-2.10.68-py3.10.egg/construct/core.py", line 1041, in _parse
  File "/usr/local/lib/python3.10/site-packages/construct-2.10.68-py3.10.egg/construct/core.py", line 91, in stream_read
construct.core.StreamError: Error in path (parsing) -> NumberOfModules
stream read less than specified amount, expected 1, found 0

Error parsing number of modules... 🤔 So now I have changed the address from 2 to 1 in _getvalues(): From this:

 def get_values(self):
        self.send_cmd(**2**, 0x42, b'FF')

To this

 def get_values(self):
        self.send_cmd(**1**, 0x42, b'FF')

No what I get is:

----> Sending:  b'~20014642E002FFFD0A\r'
7e3230303134363432453030324646464430410d
<---- Receiving:  b'~20014600B0D800020F0CF70C\r\n'
7e3230303134363030423044383030303230463043463730430d0a
Traceback (most recent call last):
  File "/home/pi/python-pylontech/tommy_test.py", line 4, in <module>
    print(p.get_values())
  File "/home/pi/python-pylontech/pylontech/pylontech.py", line 274, in get_values
    f = self.read_frame()
  File "/home/pi/python-pylontech/pylontech/pylontech.py", line 204, in read_frame
    f = self._decode_hw_frame(raw_frame=raw_frame)
  File "/home/pi/python-pylontech/pylontech/pylontech.py", line 183, in _decode_hw_frame
    assert got_frame_checksum == int(frame_chksum, 16)
AssertionError

🤔 I think we have not caught the full response. So I updated the timeout from 2 to 3. Now I get:

----> Sending:  b'~20014642E002FFFD0A\r'
7e3230303134363432453030324646464430410d
<---- Receiving:  b'~20014600B0D811020F0CF70CF60CF70CF70CF70CF80CF70CF70CF70CF70CF80CF50CF40CF60CF7050BA50B870B870B870B87FFEEC274B5A402C35000010F0CF50CF60CF60CF50CF70CF60CF70CF70CF60CF~20024692E00202FD2E\r6050B9B0B870B870B870B87FFEDC268BF6802C350027FCC3A\r\r\r\n'
7e323030313436303042304438313130323046304346373043463630434637304346373043463730434638304346373043463730434637304346373043463830434635304346343043463630434637303530424135304238373042383730423837304238374646454543323734423541343032433335303030303130463043463530434636304346363043463530434637304346363043463730434637304346363043467e3230303234363932453030323032464432450d363035304239423042383730423837304238373042383746464544433236384246363830324333353030323746434333410d0d0d0a
Traceback (most recent call last):
  File "/home/pi/python-pylontech/tommy_test.py", line 4, in <module>
    print(p.get_values())
  File "/home/pi/python-pylontech/pylontech/pylontech.py", line 274, in get_values
    f = self.read_frame()
  File "/home/pi/python-pylontech/pylontech/pylontech.py", line 204, in read_frame
    f = self._decode_hw_frame(raw_frame=raw_frame)
  File "/home/pi/python-pylontech/pylontech/pylontech.py", line 183, in _decode_hw_frame
    assert got_frame_checksum == int(frame_chksum, 16)
AssertionError

This response looks quite longer, but same error. I am thinking in changing the self.s.read() for something like _readuntil('\r') or something similar, I mean reading until certain byte value, to detect the end of the frame and avoid receiving garbage. What do you think? Have you seen something I can not see?

I would like learning to break down the response. I have read the protocol PDF but I will need some detailed example

tomascrespo commented 1 year ago

Finally I get it working. At least most of the times.

p.get_values_single(1) works always p.get_system_parameters() works always

However p.get_values() only works sometimes. Other times it fails with a weird response, I do not why 😕

What I did: Change address from 2 to 1 into get_values() and into get_system_parameters() Modify constructor to allow communication at 4800 speed rate:

    def __init__(self, serial_port='/dev/ttyUSB0', baudrate=1200):
        self.s = serial.Serial(serial_port, baudrate, bytesize=8, parity=serial.PARITY_NONE, stopbits=1, timeout=6)
        self.s.write(bytes([0x7E, 0x32, 0x30, 0x30, 0x31, 0x34, 0x36, 0x39, 0x31, 0x45, 0x30, 0x30, 0x32, 0x30, 0x33, 0x46, 0x44, 0x32, 0x46, 0x0D])) # Sets speed to 4800
        time.sleep(1)
        self.s.close()
        self.s = serial.Serial(serial_port, 4800, bytesize=8, parity=serial.PARITY_NONE, stopbits=1, timeout=2)
        self.s.isOpen()
        self.s.flush()

Change read_frame() to read until '\n' (end of frame)

    def read_frame(self):
        #raw_frame = self.s.readline()
        raw_frame = self.s.read_until(b'\r')
        f = self._decode_hw_frame(raw_frame=raw_frame)
        parsed = self._decode_frame(f)
        return parsed

As mentioned, p.get_system_parameters() does never fail.

----> Sending:  b'~200146470000FDA8\r'
7e323030313436343730303030464441380d
<---- Receiving:  b'~20014600B032000E420BEA0B540D0D0A3D03FCD2F0B3B0ADD40D0D0A3DFC18F240\r'
7e3230303134363030423033323030304534323042454130423534304430443041334430334643443246304233423041444434304430443041334446433138463234300d
Container:
    CellHighVoltageLimit = 3.65
    CellLowVoltageLimit = 3.05
    CellUnderVoltageLimit = 2.9
    ChargeHighTemperatureLimit = 61.0
    ChargeLowTemperatureLimit = -11.0
    ChargeCurrentLimit = 10.2
    ModuleHighVoltageLimit = 54.0
    ModuleLowVoltageLimit = 46.0
    ModuleUnderVoltageLimit = 44.5
    DischargeHighTemperatureLimit = 61.0
    DischargeLowTemperatureLimit = -11.0
    DischargeCurrentLimit = -10.0

p.get_values_single(1) does not fail:

----> Sending:  b'~20014642E00201FD35\r'
7e3230303134363432453030323031464433350d
<---- Receiving:  b'~20014600C06E00010F0CF80CF80CFA0CF90CF70CFA0CF90CF70CF90CFA0CF90CF90CF80CF90CF9050BA50B870B870B870B87FFF6C293A60402C3500001E47C\r'
7e3230303134363030433036453030303130463043463830434638304346413043463930434637304346413043463930434637304346393043464130434639304346393043463830434639304346393035304241353042383730423837304238373042383746464636433239334136303430324333353030303031453437430d
Container:
    NumberOfModule = 1
    NumberOfCells = 15
    CellVoltages = ListContainer:
        3.32
        3.32
        3.322
        3.321
        3.319
        3.322
        3.321
        3.319
        3.321
        3.322
        3.321
        3.321
        3.32
        3.321
        3.321
    NumberOfTemperatures = 5
    AverageBMSTemperature = 25.0
    GroupedCellsTemperatures = ListContainer:
        22.0
        22.0
        22.0
        22.0
    Current = -1.0
    Voltage = 49.811
    Power = -49.811
    CycleNumber = 1
    RemainingCapacity = 42.5
    TotalCapacity = 50.0
    TotalPower = -49.811
    StateOfCharge = 0.85

But p.get_values() fails most of the times (one good each a lot of bads):

----> Sending:  b'~20014642E002FFFD0A\r'
7e3230303134363432453030324646464430410d
<---- Receiving:  b'~20014600B0D800020F0CF80CF80CF70CF90CF80CF80CF80CF80CF80CF80CF90CF80CF70CF90CF7050BA50B870B870B870B87FFF5C288A60402C35000010F0CF80CF80CF60CF70CF80CF80CF70CF80CF80CF90CF90CF90CF80CF80CF8050B9B0B870B870B870B87FFF4C287A7F802C3500280CC47\r'
7e323030313436303042304438303030323046304346383043463830434637304346393043463830434638304346383043463830434638304346383043463930434638304346373043463930434637303530424135304238373042383730423837304238374646463543323838413630343032433335303030303130463043463830434638304346363043463730434638304346383043463730434638304346383043463930434639304346393043463830434638304346383035304239423042383730423837304238373042383746464634433238374137463830324333353030323830434334370d
Container:
    NumberOfModules = 2
    Module = ListContainer:
        Container:
            NumberOfCells = 15
            CellVoltages = ListContainer:
                3.32
                3.32
                3.319
                3.321
                3.32
                3.32
                3.32
                3.32
                3.32
                3.32
                3.321
                3.32
                3.319
                3.321
                3.319
            NumberOfTemperatures = 5
            AverageBMSTemperature = 25.0
            GroupedCellsTemperatures = ListContainer:
                22.0
                22.0
                22.0
                22.0
            Current = -1.1
            Voltage = 49.8
            Power = -54.78
            CycleNumber = 1
            RemainingCapacity = 42.5
            TotalCapacity = 50.0
        Container:
            NumberOfCells = 15
            CellVoltages = ListContainer:
                3.32
                3.32
                3.318
                3.319
                3.32
                3.32
                3.319
                3.32
                3.32
                3.321
                3.321
                3.321
                3.32
                3.32
                3.32
            NumberOfTemperatures = 5
            AverageBMSTemperature = 24.0
            GroupedCellsTemperatures = ListContainer:
                22.0
                22.0
                22.0
                22.0
            Current = -1.2
            Voltage = 49.799
            Power = -59.758799999999994
            CycleNumber = 640
            RemainingCapacity = 43.0
            TotalCapacity = 50.0
    TotalPower = -114.5388
    StateOfCharge = 0.855

Bad:

----> Sending:  b'~20014642E002FFFD0A\r'
7e3230303134363432453030324646464430410d
<---- Receiving:  b'~20014600B0D800020F0CF60CF70CF80CFA0CF\r'
7e323030313436303042304438303030323046304346363043463730434638304346413043460d
Traceback (most recent call last):
  File "/home/pi/python-pylontech/tommy_test.py", line 4, in <module>
    print(p.get_values())
  File "/home/pi/python-pylontech/pylontech/pylontech.py", line 284, in get_values
    f = self.read_frame()
  File "/home/pi/python-pylontech/pylontech/pylontech.py", line 214, in read_frame
    f = self._decode_hw_frame(raw_frame=raw_frame)
  File "/home/pi/python-pylontech/pylontech/pylontech.py", line 192, in _decode_hw_frame
    assert got_frame_checksum == int(frame_chksum, 16)
AssertionError

Bad:

----> Sending:  b'~20014642E002FFFD0A\r'
7e3230303134363432453030324646464430410d
<---- Receiving:  b'~20014600B0D800020F0CF80CF60CF90CF80CF70CF80CF90CF80CF90CFA0CFB0CFB0CF90CFA0CFA050BA50B870B870B870B87FFF5C295A60402C35000010F0CF80CF80CF70CF80CF80CF70CF80CF70CF80CF~20024692E00202FD2E\r'
7e323030313436303042304438303030323046304346383043463630434639304346383043463730434638304346393043463830434639304346413043464230434642304346393043464130434641303530424135304238373042383730423837304238374646463543323935413630343032433335303030303130463043463830434638304346373043463830434638304346373043463830434637304346383043467e3230303234363932453030323032464432450d
Traceback (most recent call last):
  File "/home/pi/python-pylontech/tommy_test.py", line 4, in <module>
    print(p.get_values())
  File "/home/pi/python-pylontech/pylontech/pylontech.py", line 284, in get_values
    f = self.read_frame()
  File "/home/pi/python-pylontech/pylontech/pylontech.py", line 214, in read_frame
    f = self._decode_hw_frame(raw_frame=raw_frame)
  File "/home/pi/python-pylontech/pylontech/pylontech.py", line 192, in _decode_hw_frame
    assert got_frame_checksum == int(frame_chksum, 16)
AssertionError

Bad:

----> Sending:  b'~20014642E002FFFD0A\r'
7e3230303134363432453030324646464430410d
<---- Receiving:  b''

Traceback (most recent call last):
  File "/home/pi/python-pylontech/tommy_test.py", line 4, in <module>
    print(p.get_values())
  File "/home/pi/python-pylontech/pylontech/pylontech.py", line 284, in get_values
    f = self.read_frame()
  File "/home/pi/python-pylontech/pylontech/pylontech.py", line 214, in read_frame
    f = self._decode_hw_frame(raw_frame=raw_frame)
  File "/home/pi/python-pylontech/pylontech/pylontech.py", line 192, in _decode_hw_frame
    assert got_frame_checksum == int(frame_chksum, 16)
ValueError: invalid literal for int() with base 16: b''

Some clue? Perhaps is something normal and the packets in this protocol get corrupted sometimes, more probabbly if they are greater? Does this happen with RS485?

One of the most received error frame is this: b'~200146060000FDAD\r' could someone break it down for me? It has to specify some type of error in some field

MC68030 commented 8 months ago

Hi tomascrespo, i had the same idea as you described/ask, let's use the serial console. Can you please do a fork/share your code ,so we can work together on it?

I have two UP5000 which i plan to monitor .. BTW, beside the RS485 communication document, there also exists document from pylontech regarding the RS232 communication.

KR

Frankkkkk commented 8 months ago

Hi, I completely forgot about this issue :(. I didn't know that the rs232 proto was documented.; maybe I should find + upload the PDF here?

Indeed, merging both protocols would be awesome!

Happy new year :-)

On December 29, 2023 5:38:08 PM GMT+01:00, MC68030 @.***> wrote:

Hi tomascrespo, i had the same idea as you described/ask, let's use the serial console. Can you please do a fork/share your code ,so we can work together on it?

I have two UP5000 which i plan to monitor .. BTW, beside the RS485 communication document, there also exists document from pylontech regarding the RS232 communication.

KR

-- Reply to this email directly or view it on GitHub: https://github.com/Frankkkkk/python-pylontech/issues/29#issuecomment-1872208412 You are receiving this because you are subscribed to this thread.

Message ID: @.***>

MC68030 commented 8 months ago

Just search for "PYLON LFP Battery communication protocol - RS232" :-) And happy new year too!