Frankkkkk / python-pylontech

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

Which USB to RS485 converter to use #2

Open stuartornum opened 3 years ago

stuartornum commented 3 years ago

Hi @Frankkkkk

Would you mind letting us know what hardware you are using to communicate with the Pylontech batteries. For example, which USB to RS485 cable you are using, which OS/Hardware etc.

I'm struggling with a Raspberry Pi 3B+ and USB to RS485 adapter (https://www.amazon.com/gp/product/B08RDZVP49)

Cheers!

Frankkkkk commented 3 years ago

Hi @stuartornum , I'm using a really cheap one like the one below: image (search terms: USB RS485).

The dip switches must all be down (the first dip switch sets the speed: 115200 vs 9600 Bd).

Cheers and don't hesitate if you need more info !

Frankkkkk commented 3 years ago

PS: I don't know the pinout on the one you showed, but I'm pretty sure that it doesn't match the Pylontech RJ45 pinout ! You'd need a custom RJ45 patch cable (easy to crimp).

It's easy to do with the one I showed above because you can just take a standard RJ45/ethernet cable, cut it in half and take the two strands you're interested in

stuartornum commented 3 years ago

Awesome, thanks @Frankkkkk - I've ordered exactly the same from Amazon. I also asked the question to the seller regarding the pinout for the converter I have... no response yet. I'll keep you posted... thanks again!

stuartornum commented 3 years ago

Hi @Frankkkkk ,

I managed to get my hands on the USB RS485 adapter you referenced above. Also, made a new cable from some CAT6 on to a RJ45 (568b). As per the Pylontech manual it says pin 7 and 8 are recommended for RS485, so converting that to 568b we get (Pin 7: brown/white, Pin 8: brown).

I get the following when trying to run the library: `

import pylontech p = pylontech.Pylontech() print(p.get_values()) Traceback (most recent call last): File "", line 1, in File "/home/pi/python-pylontech/pylontech/pylontech.py", line 211, in get_values d = self.get_values_fmt.parse(f.info[1:]) File "/home/pi/.local/lib/python3.7/site-packages/construct/core.py", line 288, in parse return self.parse_stream(io.BytesIO(data), **contextkw) File "/home/pi/.local/lib/python3.7/site-packages/construct/core.py", line 300, in parse_stream return self._parsereport(stream, context, "(parsing)") File "/home/pi/.local/lib/python3.7/site-packages/construct/core.py", line 312, in _parsereport obj = self._parse(stream, context, path) File "/home/pi/.local/lib/python3.7/site-packages/construct/core.py", line 2120, in _parse subobj = sc._parsereport(stream, context, path) File "/home/pi/.local/lib/python3.7/site-packages/construct/core.py", line 312, in _parsereport obj = self._parse(stream, context, path) File "/home/pi/.local/lib/python3.7/site-packages/construct/core.py", line 2653, in _parse return self.subcon._parsereport(stream, context, path) File "/home/pi/.local/lib/python3.7/site-packages/construct/core.py", line 312, in _parsereport obj = self._parse(stream, context, path) File "/home/pi/.local/lib/python3.7/site-packages/construct/core.py", line 2413, in _parse e = self.subcon._parsereport(stream, context, path) File "/home/pi/.local/lib/python3.7/site-packages/construct/core.py", line 312, in _parsereport obj = self._parse(stream, context, path) File "/home/pi/.local/lib/python3.7/site-packages/construct/core.py", line 2120, in _parse subobj = sc._parsereport(stream, context, path) File "/home/pi/.local/lib/python3.7/site-packages/construct/core.py", line 312, in _parsereport obj = self._parse(stream, context, path) File "/home/pi/.local/lib/python3.7/site-packages/construct/core.py", line 2653, in _parse return self.subcon._parsereport(stream, context, path) File "/home/pi/.local/lib/python3.7/site-packages/construct/core.py", line 312, in _parsereport obj = self._parse(stream, context, path) File "/home/pi/.local/lib/python3.7/site-packages/construct/core.py", line 2413, in _parse e = self.subcon._parsereport(stream, context, path) File "/home/pi/.local/lib/python3.7/site-packages/construct/core.py", line 312, in _parsereport obj = self._parse(stream, context, path) File "/home/pi/.local/lib/python3.7/site-packages/construct/core.py", line 703, in _parse obj = self.subcon._parsereport(stream, context, path) File "/home/pi/.local/lib/python3.7/site-packages/construct/core.py", line 312, in _parsereport obj = self._parse(stream, context, path) File "/home/pi/.local/lib/python3.7/site-packages/construct/core.py", line 1041, in _parse data = stream_read(stream, self.length, path) File "/home/pi/.local/lib/python3.7/site-packages/construct/core.py", line 91, in stream_read raise StreamError("stream read less than specified amount, expected %d, found %d" % (length, len(data)), path=path) construct.core.StreamError: Error in path (parsing) -> Module -> GroupedCellsTemperatures stream read less than specified amount, expected 2, found 1`

I also switched the brown/brown-white wires around on the RS485-to-USB adapter to see if I got a different output, I did...:

`

p = pylontech.Pylontech() print(p.get_values()) Traceback (most recent call last): File "", line 1, in File "/home/pi/python-pylontech/pylontech/pylontech.py", line 208, in get_values f = self.read_frame() File "/home/pi/python-pylontech/pylontech/pylontech.py", line 167, in read_frame f = self._decode_hw_frame(raw_frame=raw_frame) File "/home/pi/python-pylontech/pylontech/pylontech.py", line 148, in _decode_hw_frame assert got_frame_checksum == int(frame_chksum, 16) ValueError: invalid literal for int() with base 16: b''`

Any thoughts?

Thanks again for your help! Really appreciate it.

(I'm using a Raspberry Pi 3b+)

Frankkkkk commented 3 years ago

Hi,

How are your dip switches set ? They should be all four off (down). Which pylontech modules have you got (model) ?

It would be great to show the received raw frame by either adding a print(raw_frame) after here or by launching wireshark and capturing the serial port communications.

Cheers !

Frankkkkk commented 3 years ago

I suppose that your first wiring must be correct as the frame passed the checksum validation.

Maybe your setup is a bit different than mine and we must change the frame protocol. If you can manage to dump the raw frame I can try to check the differences and patch the lib ;-)

stuartornum commented 3 years ago

Hi @Frankkkkk , thanks for getting back to me.

Debug out from printing L169: b'~2002460010F011020F0CCD0CCE0CCC0CCE0CCB0CCC0CCD0CCC0CCD0CCB0CCC0CCD0CCD0CCE0CCC050BE10BCD0BCD0BD70BCDFFC3BFFDFFFF04FFFF0234007F300121100F0CCA0CCA0CCB0CCC0CCA0CCC0CCB0CCB0CCB0CCB0CCB0CCA0CCC0CCC0CCB050BEB0BCD0BCD0BCD0BC3FFD1BFE5FFFF04FFFF0292005FB400C350C4A7\r'

Cheers

Frankkkkk commented 3 years ago

Well, I managed to decode part of the frame:

Container: 
    NumberOfModules = 2
    Module = ListContainer: 
        Container: 
            NumberOfCells = 15
            CellVoltages = ListContainer: 
                3.277
                3.278
                3.276
                3.278
                3.275
                3.276
                3.277
                3.276
                3.277
                3.275
                3.276
                3.277
                3.277
                3.278
                3.276
            NumberOfTemperatures = 5
            AverageBMSTemperature = 30.41
            GroupedCellsTemperatures = ListContainer: 
                30.21
                30.21
                30.31
                30.21
            Current = -6.1
            Voltage = 49.149
            Power = -299.8089
            RemainingCapacity = 65.535
            TotalCapacity = 65.535
            CycleNumber = 564
    foobar = 16
    Module2 = ListContainer: 
        Container: 
            NumberOfCells = 15
            CellVoltages = ListContainer: 
                3.274
                3.274
                3.275
                3.276
                3.274
                3.276
                3.275
                3.275
                3.275
                3.275
                3.275
                3.274
                3.276
                3.276
                3.275
            NumberOfTemperatures = 5
            AverageBMSTemperature = 30.51
            GroupedCellsTemperatures = ListContainer: 
                30.21
                30.21
                30.21
                30.11
            Current = -4.7
            Voltage = 49.125
            Power = -230.88750000000002
            RemainingCapacity = 65.535
            TotalCapacity = 65.535
            CycleNumber = 658
    greedy = ListContainer: 
        0
        95
        180
        0
        195
        80
    TotalPower = -299.8089
    StateOfCharge = 1.0

But I specified the protocol manually:

    get_values_fmt = construct.Struct(
        "NumberOfModules" / construct.Byte,
        "Module" / construct.Array(1, construct.Struct(
            "NumberOfCells" / construct.Int8ub,
            "CellVoltages" / construct.Array(construct.this.NumberOfCells, ToVolt(construct.Int16sb)),
            "NumberOfTemperatures" / construct.Int8ub,
            "AverageBMSTemperature" / ToCelsius(construct.Int16sb),
            "GroupedCellsTemperatures" / construct.Array(construct.this.NumberOfTemperatures - 1, ToCelsius(construct.Int16sb)),
            "Current" / ToAmp(construct.Int16sb),
            "Voltage" / ToVolt(construct.Int16ub),
            "Power" / construct.Computed(construct.this.Current * construct.this.Voltage),
            "RemainingCapacity" / DivideBy1000(construct.Int16ub),
            "_undef1" / construct.Int8ub,
            "TotalCapacity" / DivideBy1000(construct.Int16ub),
            "CycleNumber" / construct.Int16ub,
        )),
        "foobar" / construct.Int8ub,
        "foobar" / construct.Int8ub,
        "foobar" / construct.Int8ub,
        "foobar" / construct.Int8ub,
        "foobar" / construct.Int8ub,
        "foobar" / construct.Int8ub,
        "Module2" / construct.Array(1, construct.Struct(
            "NumberOfCells" / construct.Int8ub,
            "CellVoltages" / construct.Array(construct.this.NumberOfCells, ToVolt(construct.Int16sb)),
            "NumberOfTemperatures" / construct.Int8ub,
            "AverageBMSTemperature" / ToCelsius(construct.Int16sb),
            "GroupedCellsTemperatures" / construct.Array(construct.this.NumberOfTemperatures - 1, ToCelsius(construct.Int16sb)),
            "Current" / ToAmp(construct.Int16sb),
            "Voltage" / ToVolt(construct.Int16ub),
            "Power" / construct.Computed(construct.this.Current * construct.this.Voltage),
            "RemainingCapacity" / DivideBy1000(construct.Int16ub),
            "_undef1" / construct.Int8ub,
            "TotalCapacity" / DivideBy1000(construct.Int16ub),
            "CycleNumber" / construct.Int16ub,
        )),
        "greedy" / construct.GreedyRange(construct.Byte),
        "TotalPower" / construct.Computed(lambda this: sum([x.Power for x in this.Module])),
        "StateOfCharge" / construct.Computed(lambda this: sum([x.RemainingCapacity for x in this.Module]) / sum([x.TotalCapacity for x in this.Module])),

    )

There seems to be some extra bytes sent after the first module (written as foobar in the construct proto). So, my questions are:

Interesting though

Cheers !

Frankkkkk commented 3 years ago

And the code if you want to try manually:

    def get_values(self):
        #self.send_cmd(2, 0x42, b'FF')
        #f = self.read_frame()
        rf =  b'~2002460010F011020F0CCD0CCE0CCC0CCE0CCB0CCC0CCD0CCC0CCD0CCB0CCC0CCD0CCD0CCE0CCC050BE10BCD0BCD0BD70BCDFFC3BFFDFFFF04FFFF0234007F300121100F0CCA0CCA0CCB0CCC0CCA0CCC0CCB0CCB0CCB0CCB0CCB0CCA0CCC0CCC0CCB050BEB0BCD0BCD0BCD0BC3FFD1BFE5FFFF04FFFF0292005FB400C350C4A7\r'
        ff = self._decode_hw_frame(raw_frame=rf)
        f = self._decode_frame(ff)
        print(f)
        print(f.info[1:])

        # infoflag = f.info[0]
        d = self.get_values_fmt.parse(f.info[1:])
        return d
stuartornum commented 3 years ago

Hi @Frankkkkk ,

Apologies for the delay in getting back to you:

Thanks for your help on this, it's really is appreciated

stuartornum commented 3 years ago

I believe I've switched the US2000 to become the master, however, the documentation says to always use the US3000 as the primary when you have a mixture of US2000's/US3000's.

Here is the output:

    NumberOfModules = 2
    Module = ListContainer: 
        Container: 
            NumberOfCells = 15
            CellVoltages = ListContainer: 
                3.287
                3.289
                3.288
                3.287
                3.287
                3.289
                3.289
                3.288
                3.286
                3.287
                3.287
                3.288
                3.288
                3.287
                3.288
            NumberOfTemperatures = 5
            AverageBMSTemperature = 30.11
            GroupedCellsTemperatures = ListContainer: 
                30.11
                30.11
                30.21
                30.11
            Current = -1.8
            Voltage = 49.315
            Power = -88.767
            RemainingCapacity = 29.0
            TotalCapacity = 50.0
            CycleNumber = 659
        Container: 
            NumberOfCells = 15
            CellVoltages = ListContainer: 
                3.288
                3.289
                3.289
                3.288
                3.289
                3.288
                3.288
                3.29
                3.289
                3.289
                3.289
                3.289
                3.289
                3.29
                3.289
            NumberOfTemperatures = 5
            AverageBMSTemperature = 30.21
            GroupedCellsTemperatures = ListContainer: 
                30.21
                30.21
                30.31
                30.21
            Current = -1.7
            Voltage = 49.333
            Power = -83.86609999999999
            RemainingCapacity = 43.66
            TotalCapacity = 8.464
            CycleNumber = 565
    TotalPower = -172.63309999999998
    StateOfCharge = 1.242816091954023

I'm not sure how much I believe some of the numbers, "StateOfCharge" for example...?

Frankkkkk commented 3 years ago

Hi, No problem for the delays ! :-) Interesting results :thinking:

I suppose we could patch the decoding to handle US3000 as primary modules.. It's strange though. Sadly I don't have one so I can't really test this edge case. Maybe we could add a warning in the readme as a workaround.

As for the StateOfCharge, it's meant to be a percent (0-1: sum(all remaining capacities)/sum(total capacities)). However in your case the US3000 states a RemainingCapacity of 43 but a TotalCapacity of... 8.464 :frowning: which thus skews the calculation

If that's okay with you, I'll just add a warning for this edge case in the readme, and maybe in the future we can fix the US3000-US2000 bug ?

Cheers

petero-dk commented 2 years ago

I would like to assist on this, what would be the preferred next steps. I have both US2000 and US3000 with the 3000 as the primary right now

michaelhutter commented 2 years ago

Hello, using a cable like mentioned above I get the following error message:

Traceback (most recent call last):
  File "/usr/lib/python3/dist-packages/thonny/backend.py", line 1171, in wrapper
    result = method(self, *args, **kwargs)
  File "/usr/lib/python3/dist-packages/thonny/backend.py", line 1158, in wrapper
    return method(self, *args, **kwargs)
  File "/usr/lib/python3/dist-packages/thonny/backend.py", line 1232, in _execute_prepared_user_code
    exec(statements, global_vars)
  File "/home/pi/syncthing/Lixy/Python/Pylontech/pylontech/Pylontech-test.py", line 6, in <module>
    print(p.get_values())
  File "/home/pi/syncthing/Lixy/Python/Pylontech/pylontech/pylontech.py", line 273, in get_values
    f = self.read_frame()
  File "/home/pi/syncthing/Lixy/Python/Pylontech/pylontech/pylontech.py", line 203, in read_frame
    f = self._decode_hw_frame(raw_frame=raw_frame)
  File "/home/pi/syncthing/Lixy/Python/Pylontech/pylontech/pylontech.py", line 184, in _decode_hw_frame
    assert got_frame_checksum == int(frame_chksum, 16)
ValueError: invalid literal for int() with base 16: b'\xbf\xff\xff\xff'

If I insert a print(raw_frame) in _decode_hw_frame() I get the result b'\xf0\xff\xff\xff\xff\xbf\xff\xff\xff\xff\xff\xbf\xff\xff\xff\x1f'

Do you have any idea what could be wrong? I have a single US3000C connected on port "B/RS485" and I only wired pins 7 and 8.

michaelhutter commented 2 years ago

Without any change now I get the following result as raw_data: b'\xf0\xff\xff\xff\xff\xbf\xff\xff\xff\xff\xff\xff\xff\xbf\xf7\xf7\x1f~20024600F07A11020F0CF80CF80CF80CF80CF90CF80CF80CF80CF80CF80CF90CF90CF90CF90CF9050B9D0B7A0B770B770B8D0000C28EFFFF04FFFF000000DBB0012110E1B6\r\x00'

What does the header mean or how can I remove it?

Frankkkkk commented 2 years ago

Hi @michaelhutter Indeed it is strange as part of the second frame looks valid enough. Is your cable shielded or near high-enough EM radiations ? Did you try changing your USB-rs485 converter ? Cheers

michaelhutter commented 2 years ago

The first adapter which I bought did not work at all. The second adapter is working but gives these extra bytes in the beginning. I think I try to change your code a little bit, so that it removes all chars until the first occurence of '~'. May be using a regex or so. I am experienced in other languages, but not in Python. So would it be possible for you to give me a hint about which Python commands could do me the job? Is there a command like "FindFirstOccurence(haystack, needle)" and "right(string, pos)"?

michaelhutter commented 2 years ago

I am still struggling with my Pylontech US3000C. If I run print(p.get_values()) then raw_frame in _decode_hw_frame() has the value b'\xf0\xff\xff\xff\xff\xbf\xff\xff\xff\xff\xff\xbf\xff\xff\xff\x1f'

If I run print(p.get_values_single(2)) then raw_frame in _decode_hw_frame() has the value b'\xf0\xff\xff\xff\xff\xbf\xff\xff\xff\xff\xff\xff\xff\xbf\xf7\xf7\x1f~20024600F07A11020F0CF60CF60CF60CF60CF60CF60CF60CF60CF60CF60CF60CF60CF60CF60CF6050B8F0B710B6E0B6E0B7E0000C26AFFFF04FFFF000000D8CC012110E1CB\r\x00'

@Frankkkkk Can you explain what is the difference between get_values() and get_values_single(2)? What does the parameter (2) mean? In both cases the CRC does not match and I don't get any result. I tried a lot of things but unfortunately was not successful until now :-(

maxx-ukoo commented 1 year ago

@michaelhutter i not python guy but I want to create pylontech emulator and investigate this scripts and pylon protocol description. Get_values and get_values_single is the same command but values hardcoded address as 255. I didn't find this address descruption in pylontech documentation. 2 is number of battery if we have more than one pattery. Do you have real battery? Could you try to execute requests on real battery with Hterm an provide responses? These request ask for protocol version from master battery from grpup 0, 1, 2, 3, 4, 5.

52 => 7E 30 30 35 32 34 36 34 46 30 30 30 30 46 44 39 35 0D 42 => 7E 30 30 34 32 34 36 34 46 30 30 30 30 46 44 39 36 0D 32 => 7E 30 30 33 32 34 36 34 46 30 30 30 30 46 44 39 37 0D 22 => 7E 30 30 32 32 34 36 34 46 30 30 30 30 46 44 39 38 0D 12 => 7E 30 30 31 32 34 36 34 46 30 30 30 30 46 44 39 39 0D 02 => 7E 30 30 30 32 34 36 34 46 30 30 30 30 46 44 39 41 0D

Could you execute these requests with HTerm 9https://www.der-hammer.info/pages/terminal.html) on real battery? Just copy 7E .. OD an send to battery?

EmCeBeh commented 1 year ago

Hello, unsure if I should make a new issue.

I tried using an ethernet - USB adapter and the unit (US3000c) just beeps like crazy.

So probably it's the wrong adapter?

Thanks!

Frankkkkk commented 1 year ago

Hi @EmCeBeh , yes please create a new issue. Unless I'm mistaken, even though they both use the same form factor (RJ45), the protocols are completely different. You need an RS485 adapter, and not ethernet ! Cheers !