Skip to content
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

US3000 parsing failure #4

Open
Vards0 opened this issue Oct 17, 2021 · 33 comments
Open

US3000 parsing failure #4

Vards0 opened this issue Oct 17, 2021 · 33 comments

Comments

@Vards0
Copy link

Vards0 commented Oct 17, 2021

Hi.
I'm getting the following eror when I import the library:-

>>> import pylontech Traceback (most recent call last): File "<stdin>", line 1, in <module> File "pylontech.py", line 6 def _decode(self, obj, context, path) -> bytes:

Any ideas?

Thanks

@Frankkkkk
Copy link
Owner

Frankkkkk commented Oct 17, 2021 via email

@Frankkkkk
Copy link
Owner

Frankkkkk commented Oct 17, 2021 via email

@Vards0
Copy link
Author

Vards0 commented Oct 17, 2021

Thanks very much

I was running ver 2.7.

I'm now running ver 3.5. I've installed both dependencies but now I am getting this error:-

`Python 3.5.3 (default, Apr 5 2021, 09:00:41)
[GCC 6.3.0 20170516] on linux
Type "help", "copyright", "credits" or "license" for more information.

import pylontech
p = pylontech.Pylontech()
print(p.get_values())
Traceback (most recent call last):
File "", line 1, in
File "/pylontech/pylontech.py", line 211, in get_values
f = self.read_frame()
File "/pylontech/pylontech.py", line 170, in read_frame
f = self._decode_hw_frame(raw_frame=raw_frame)
File "/pylontech/pylontech.py", line 151, in _decode_hw_frame
assert got_frame_checksum == int(frame_chksum, 16)
ValueError: invalid literal for int() with base 16: b''`

@Frankkkkk
Copy link
Owner

I should add more debug verbosity, but in the meantime you could follow #2 (comment) which was the same problem

What is your battery topology ?

@Vards0
Copy link
Author

Vards0 commented Oct 17, 2021

My battery topology is 4x US3000

@Frankkkkk
Copy link
Owner

Okay, so maybe you've got the same problem as @stuartornum .

Can you give me a raw frame example by editing _decode_hw_frame:

def _decode_hw_frame(self, raw_frame: bytes) -> bytes:
  print(raw_frame)

and launching get_values again ?

That way we can try and support the US3000 batteries alone.

@stuartornum
Copy link

I managed to get my setup working by having the US2000 as the primary and the US3000 secondary. Although, I still have some weirdness when the SOC of the US3000 is above ~89% it reports as some very lower number (when I know for a fact it’s not!) and when it dips below 89% it reports fine again….? Very stalrange. Here’s my grafana graph https://ibb.co/yV2N2B9

@Vards0
Copy link
Author

Vards0 commented Oct 18, 2021

Hi Frankkkkk...

Sorry for the delay in getting back to you.

I edited the code to be as follows:-


    def _decode_hw_frame(self, raw_frame: bytes) -> bytes:
        print(raw_frame)
        # XXX construct
        frame_data = raw_frame[1:len(raw_frame) - 5]
        frame_chksum = raw_frame[len(raw_frame) - 5:-1]

        got_frame_checksum = Pylontech.get_frame_checksum(frame_data)
        assert got_frame_checksum == int(frame_chksum, 16)

        return frame_data

I'm not 100% sure if this is what I needed to do. Please correct me if I am wrong.

Now when I run the get_values, I am getting the below:-

[GCC 6.3.0 20170516] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import pylontech
>>> p = pylontech.Pylontech()
>>> print(p.get_values())
b'~2002460061DC11040F0CFD0CFC0CFC0CFB0CFC0CFB0CFD0CFC0CFC0CFB0CFA0CFD0CFB0CFE0CFA050BE10BCD0BCD0BCD0BCD0000C2C1FFFF04FFFF002F00EFEC0121100F0CEB0CEB0CEB0CEA0CEA0CEC0CEB0CEB0CE90CE80CE60CE90CE90CEA0CE8050BE10BCD0BCD0BCD0BCDFFBCC1B2FFFF04FFFF002800F2D00121100F0CE80CE90CEA0CEA0CEA0CE90CEA0CEA0CEB0CEC0CEB0CEB0CEB0CEA0CEA050BE10BC30BC30BC30BC3FFB7C1B8FFFF04FFFF007100E7400121100F0CE90CEC0CEB0CEA0CEA0CEB0CE90CE80CEA0CEA0CEA0CEB0CEC0CEA0CEA050BD70BC30BC30BC30BB9FFBBC1B9FFFF04FFFF006B00ED080121108D63\r'

@stuartornum, are you using mqtt to bring your values into grafana?

@stuartornum
Copy link

stuartornum commented Oct 18, 2021

@stuartornum, are you using mqtt to bring your values into grafana?

Hi @Vards0 ! I'm using @Frankkkkk code (thank you again!) on a RPi3 polling every minute via cron, this then dumps the output into an nginx html directory... which Prometheus then comes along and pulls the data out for Grafana. I'm using Prometheus for other stuff like Shelly.cloud devices.

@Vards0
Copy link
Author

Vards0 commented Oct 18, 2021

Hi Frank

I realised that I needed to change to '/dev/ttyUSB1'

I think I'm now getting more meainingful output:-

Type "help", "copyright", "credits" or "license" for more information.
>>> import pylontech
>>> p = pylontech.Pylontech()
>>> print(p.get_values())
b'~2002460061DC11040F0CFD0CFC0CFC0CFB0CFC0CFB0CFD0CFC0CFC0CFB0CFA0CFD0CFB0CFE0CFA050BE10BCD0BCD0BCD0BCD0000C2C1FFFF04FFFF002F00EFEC0121100F0CEB0CEB0CEB0CEA0CEA0CEC0CEB0CEB0CE90CE80CE60CE90CE90CEA0CE8050BE10BCD0BCD0BCD0BCDFFBCC1B2FFFF04FFFF002800F2D00121100F0CE80CE90CEA0CEA0CEA0CE90CEA0CEA0CEB0CEC0CEB0CEB0CEB0CEA0CEA050BE10BC30BC30BC30BC3FFB7C1B8FFFF04FFFF007100E7400121100F0CE90CEC0CEB0CEA0CEA0CEB0CE90CE80CEA0CEA0CEA0CEB0CEC0CEA0CEA050BD70BC30BC30BC30BB9FFBBC1B9FFFF04FFFF006B00ED080121108D63\r'

@Frankkkkk
Copy link
Owner

Hi, thanks for the debug output ! I'll take a look tonight hopefully and try to adapt the data structure :-)

Cheers

@Vards0
Copy link
Author

Vards0 commented Oct 19, 2021

Thanks Frank

I also noticed this in the Pylontech RS485 manual.

image

I'm not sure if this will be of any use to you or not. It goes right over my head.

Cheers

@Frankkkkk
Copy link
Owner

Frankkkkk commented Oct 19, 2021

You were right @Vards0 Indeed I completely missed this part. Here's the output with the correct parsing with the PDU you gave me above:

    NumberOfModules = 4
    Module = ListContainer: 
        Container: 
            NumberOfCells = 15
            CellVoltages = ListContainer: 
                3.325
                3.324
                3.324
                3.323
                3.324
                3.323
                3.325
                3.324
                3.324
                3.323
                3.322
                3.325
                3.323
                3.326
                3.322
            NumberOfTemperatures = 5
            AverageBMSTemperature = 30.41
            GroupedCellsTemperatures = ListContainer: 
                30.21
                30.21
                30.21
                30.21
            Current = 0.0
            Voltage = 49.857
            Power = 0.0
            RemainingCapacity = 65.535
            UserDefinedItems = 4
            TotalCapacity = 65.535
            CycleNumber = 47
            US3000 = Container: 
                Remaining = 61.42
                Total = 74.0
        Container: 
            NumberOfCells = 15
            CellVoltages = ListContainer: 
                3.307
                3.307
                3.307
                3.306
                3.306
                3.308
                3.307
                3.307
                3.305
                3.304
                3.302
                3.305
                3.305
                3.306
                3.304
            NumberOfTemperatures = 5
            AverageBMSTemperature = 30.41
            GroupedCellsTemperatures = ListContainer: 
                30.21
                30.21
                30.21
                30.21
            Current = -6.8
            Voltage = 49.586
            Power = -337.1848
            RemainingCapacity = 65.535
            UserDefinedItems = 4
            TotalCapacity = 65.535
            CycleNumber = 40
            US3000 = Container: 
                Remaining = 62.16
                Total = 74.0
        Container: 
            NumberOfCells = 15
            CellVoltages = ListContainer: 
                3.304
                3.305
                3.306
                3.306
                3.306
                3.305
                3.306
                3.306
                3.307
                3.308
                3.307
                3.307
                3.307
                3.306
                3.306
            NumberOfTemperatures = 5
            AverageBMSTemperature = 30.41
            GroupedCellsTemperatures = ListContainer: 
                30.11
                30.11
                30.11
                30.11
            Current = -7.3
            Voltage = 49.592
            Power = -362.0216
            RemainingCapacity = 65.535
            UserDefinedItems = 4
            TotalCapacity = 65.535
            CycleNumber = 113
            US3000 = Container: 
                Remaining = 59.2
                Total = 74.0
        Container: 
            NumberOfCells = 15
            CellVoltages = ListContainer: 
                3.305
                3.308
                3.307
                3.306
                3.306
                3.307
                3.305
                3.304
                3.306
                3.306
                3.306
                3.307
                3.308
                3.306
                3.306
            NumberOfTemperatures = 5
            AverageBMSTemperature = 30.31
            GroupedCellsTemperatures = ListContainer: 
                30.11
                30.11
                30.11
                30.01
            Current = -6.9
            Voltage = 49.593
            Power = -342.1917
            RemainingCapacity = 65.535
            UserDefinedItems = 4
            TotalCapacity = 65.535
            CycleNumber = 107
            US3000 = Container: 
                Remaining = 60.68
                Total = 74.0
    TotalPower = -1041.3981
    StateOfCharge = 1.0

Could you try the code on the fvd/us3000 branch (MR here #5 ) and tell me if it works for you ? I'm not merging just yet because I'd like to make it nice in order to have the same structure for US2000/US3000 batteries.

PS: SOC Is wrong because using the US2000 values

Cheers

@Frankkkkk Frankkkkk changed the title Cannot import library US3000 parsing failure Oct 19, 2021
@Vards0
Copy link
Author

Vards0 commented Oct 20, 2021

Frank you're a genius!!

I ran the updated code at it looks to be working a treat:-

Is there any way of separating the battery identities and their respective values so that I can pipe these into mqtt?

[GCC 6.3.0 20170516] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import pylontech
>>> print(p.get_values())
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'p' is not defined
>>> p = pylontech.Pylontech()
>>> print(p.get_values())
Container:
    NumberOfModules = 4
    Module = ListContainer:
        Container:
            NumberOfCells = 15
            CellVoltages = ListContainer:
                3.531
                3.53
                3.531
                3.528
                3.531
                3.531
                3.532
                3.515
                3.533
                3.533
                3.53
                3.533
                3.533
                3.533
                3.533
            NumberOfTemperatures = 5
            AverageBMSTemperature = 30.11
            GroupedCellsTemperatures = ListContainer:
                29.91
                29.91
                30.01
                29.91
            Current = 0.0
            Voltage = 52.957
            Power = 0.0
            RemainingCapacity = 65.535
            UserDefinedItems = 4
            TotalCapacity = 65.535
            CycleNumber = 48
            US3000 = Container:
                Remaining = 74.0
                Total = 74.0
        Container:
            NumberOfCells = 15
            CellVoltages = ListContainer:
                3.523
                3.528
                3.534
                3.53
                3.531
                3.531
                3.532
                3.517
                3.535
                3.535
                3.533
                3.534
                3.534
                3.536
                3.532
            NumberOfTemperatures = 5
            AverageBMSTemperature = 30.11
            GroupedCellsTemperatures = ListContainer:
                30.01
                30.01
                30.01
                29.91
            Current = 0.0
            Voltage = 52.965
            Power = 0.0
            RemainingCapacity = 65.535
            UserDefinedItems = 4
            TotalCapacity = 65.535
            CycleNumber = 41
            US3000 = Container:
                Remaining = 74.0
                Total = 74.0
        Container:
            NumberOfCells = 15
            CellVoltages = ListContainer:
                3.529
                3.528
                3.519
                3.533
                3.529
                3.532
                3.53
                3.532
                3.536
                3.536
                3.537
                3.536
                3.537
                3.533
                3.535
            NumberOfTemperatures = 5
            AverageBMSTemperature = 30.11
            GroupedCellsTemperatures = ListContainer:
                29.91
                29.91
                29.91
                29.91
            Current = 0.0
            Voltage = 52.982
            Power = 0.0
            RemainingCapacity = 65.535
            UserDefinedItems = 4
            TotalCapacity = 65.535
            CycleNumber = 114
            US3000 = Container:
                Remaining = 74.74
                Total = 74.0
        Container:
            NumberOfCells = 15
            CellVoltages = ListContainer:
                3.533
                3.525
                3.532
                3.53
                3.532
                3.522
                3.521
                3.524
                3.539
                3.539
                3.522
                3.537
                3.538
                3.54
                3.539
            NumberOfTemperatures = 5
            AverageBMSTemperature = 30.01
            GroupedCellsTemperatures = ListContainer:
                29.81
                29.81
                29.91
                29.81
            Current = 0.0
            Voltage = 52.973
            Power = 0.0
            RemainingCapacity = 65.535
            UserDefinedItems = 4
            TotalCapacity = 65.535
            CycleNumber = 108
            US3000 = Container:
                Remaining = 74.0
                Total = 74.0
    TotalPower = 0.0
    StateOfCharge = 1.0
>>>

@Frankkkkk
Copy link
Owner

Frankkkkk commented Oct 22, 2021 via email

@Vards0
Copy link
Author

Vards0 commented Oct 22, 2021

Thanks once again Frank.

Unfortunately I may not be able to use the script after all of this.

For some reason it causes my master battery to go into protection mode (Solid red light) as soon as I run it.

I can't figure out what is causing this as I have another python script (which again only works with US2000 batteries) which runs without any issues. It pulls the values and the master battery continues running as normal......

Do you have any ideas as to what may be causing this?

Cheers

@Frankkkkk
Copy link
Owner

Hi @Vards0 could you please send me which script you use please ? I have the same issue too for some time now. Don't know why because it worked fine before. Maybe I introduced a regression ?

Cheers !

@Vards0
Copy link
Author

Vards0 commented Dec 6, 2021

Hi Frank.

This is the script that I am using that causes the master battery to go into protection mode when I run it....

Pylontech.py

import serial
import construct


class HexToByte(construct.Adapter):
    def _decode(self, obj, context, path) -> bytes:
        hexstr = ''.join([chr(x) for x in obj])
        return bytes.fromhex(hexstr)


class JoinBytes(construct.Adapter):
    def _decode(self, obj, context, path) -> bytes:
        return ''.join([chr(x) for x in obj]).encode()


class DivideBy1000(construct.Adapter):
    def _decode(self, obj, context, path) -> float:
        return obj / 1000


class DivideBy100(construct.Adapter):
    def _decode(self, obj, context, path) -> float:
        return obj / 100


class ToVolt(construct.Adapter):
    def _decode(self, obj, context, path) -> float:
        return obj / 1000

class ToAmp(construct.Adapter):
    def _decode(self, obj, context, path) -> float:
        return obj / 10

class ToCelsius(construct.Adapter):
    def _decode(self, obj, context, path) -> float:
        return obj / 100



class Pylontech:
    manufacturer_info_fmt = construct.Struct(
        "DeviceName" / JoinBytes(construct.Array(10, construct.Byte)),
        "SoftwareVersion" / construct.Array(2, construct.Byte),
        "ManufacturerName" / JoinBytes(construct.GreedyRange(construct.Byte)),
    )

    system_parameters_fmt = construct.Struct(
        "CellHighVoltageLimit" / ToVolt(construct.Int16ub),
        "CellLowVoltageLimit" / ToVolt(construct.Int16ub),
        "CellUnderVoltageLimit" / ToVolt(construct.Int16sb),
        "ChargeHighTemperatureLimit" / ToCelsius(construct.Int16sb),
        "ChargeLowTemperatureLimit" / ToCelsius(construct.Int16sb),
        "ChargeCurrentLimit" / DivideBy100(construct.Int16sb),
        "ModuleHighVoltageLimit" / ToVolt(construct.Int16ub),
        "ModuleLowVoltageLimit" / ToVolt(construct.Int16ub),
        "ModuleUnderVoltageLimit" / ToVolt(construct.Int16ub),
        "DischargeHighTemperatureLimit" / ToCelsius(construct.Int16sb),
        "DischargeLowTemperatureLimit" / ToCelsius(construct.Int16sb),
        "DischargeCurrentLimit" / DivideBy100(construct.Int16sb),
    )

    management_info_fmt = construct.Struct(
        "CommandValue" / construct.Byte,
        "ChargeVoltageLimit" / construct.Array(2, construct.Byte),
        "DischargeVoltageLimit" / construct.Array(2, construct.Byte),
        "ChargeCurrentLimit" / construct.Array(2, construct.Byte),
        "DishargeCurrentLimit" / construct.Array(2, construct.Byte),
        "Status" / construct.Byte,
    )

    module_serial_number_fmt = construct.Struct(
        "CommandValue" / construct.Byte,
        "ModuleSerialNumber" / JoinBytes(construct.Array(16, construct.Byte)),
    )


    get_values_fmt = construct.Struct(
        "NumberOfModules" / construct.Byte,
        "Module" / construct.Array(construct.this.NumberOfModules, 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),
            "UserDefinedItems" / construct.Int8ub,
            "TotalCapacity" / DivideBy1000(construct.Int16ub),
            "CycleNumber" / construct.Int16ub,
            "US3000" / construct.If(construct.this.UserDefinedItems > 2,
                "Capacity" / construct.Struct(
                    "Remaining" / DivideBy1000(construct.Int24ub),
                    "Total" / DivideBy1000(construct.Int24ub),
                )
            ),
        )),
        "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])),
    )

    def __init__(self, serial_port='/dev/ttyUSB1', baudrate=115200):
        self.s = serial.Serial(serial_port, baudrate, bytesize=8, parity=serial.PARITY_NONE, stopbits=1, timeout=2)


    @staticmethod
    def get_frame_checksum(frame: bytes):
        assert isinstance(frame, bytes)

        sum = 0
        for byte in frame:
            sum += byte
        sum = ~sum
        sum %= 0x10000
        sum += 1
        return sum

    @staticmethod
    def get_info_length(info: bytes) -> int:
        lenid = len(info)
        if lenid == 0:
            return 0

        lenid_sum = (lenid & 0xf) + ((lenid >> 4) & 0xf) + ((lenid >> 8) & 0xf)
        lenid_modulo = lenid_sum % 16
        lenid_invert_plus_one = 0b1111 - lenid_modulo + 1

        return (lenid_invert_plus_one << 12) + lenid


    def send_cmd(self, address: int, cmd, info: bytes = b''):
        raw_frame = self._encode_cmd(address, cmd, info)
        self.s.write(raw_frame)


    def _encode_cmd(self, address: int, cid2: int, info: bytes = b''):
        cid1 = 0x46

        info_length = Pylontech.get_info_length(info)

        frame = "{:02X}{:02X}{:02X}{:02X}{:04X}".format(0x20, address, cid1, cid2, info_length).encode()
        frame += info

        frame_chksum = Pylontech.get_frame_checksum(frame)
        whole_frame = (b"~" + frame + "{:04X}".format(frame_chksum).encode() + b"\r")
        return whole_frame


    def _decode_hw_frame(self, raw_frame: bytes) -> bytes:
        # XXX construct
        frame_data = raw_frame[1:len(raw_frame) - 5]
        frame_chksum = raw_frame[len(raw_frame) - 5:-1]

        got_frame_checksum = Pylontech.get_frame_checksum(frame_data)
        assert got_frame_checksum == int(frame_chksum, 16)

        return frame_data

    def _decode_frame(self, frame):
        format = construct.Struct(
            "ver" / HexToByte(construct.Array(2, construct.Byte)),
            "adr" / HexToByte(construct.Array(2, construct.Byte)),
            "cid1" / HexToByte(construct.Array(2, construct.Byte)),
            "cid2" / HexToByte(construct.Array(2, construct.Byte)),
            "infolength" / HexToByte(construct.Array(4, construct.Byte)),
            "info" / HexToByte(construct.GreedyRange(construct.Byte)),
        )

        return format.parse(frame)


    def read_frame(self):
        raw_frame = self.s.readline()
        f = self._decode_hw_frame(raw_frame=raw_frame)
        parsed = self._decode_frame(f)
        return parsed



    def get_protocol_version(self):
        self.send_cmd(0, 0x4f)
        return self.read_frame()


    def get_manufacturer_info(self):
        self.send_cmd(0, 0x51)
        f = self.read_frame()
        return self.manufacturer_info_fmt.parse(f.info)


    def get_system_parameters(self):
        self.send_cmd(2, 0x47)
        f = self.read_frame()
        return self.system_parameters_fmt.parse(f.info[1:])

    def get_management_info(self):
        raise Exception('Dont touch this for now')
        self.send_cmd(2, 0x92)
        f = self.read_frame()

        print(f.info)
        print(len(f.info))
        ff = self.management_info_fmt.parse(f.info)
        print(ff)
        return ff

    def get_module_serial_number(self):
        self.send_cmd(2, 0x93)
        f = self.read_frame()
        # infoflag = f.info[0]
        return self.module_serial_number_fmt.parse(f.info[0:])

    def get_values(self):
        self.send_cmd(2, 0x42, b'FF')
        f = self.read_frame()

        # infoflag = f.info[0]
        d = self.get_values_fmt.parse(f.info[1:])
        return d


if __name__ == '__main__':
    p = Pylontech()
    # print(p.get_protocol_version())
    # print(p.get_manufacturer_info())
    # print(p.get_system_parameters())
    # print(p.get_management_info())
    # print(p.get_module_serial_number())
    # print(p.get_values())
    vals = p.get_values()
    for module in vals.Module:
      print (module.Voltage)

@Vards0
Copy link
Author

Vards0 commented Dec 6, 2021

And these are the scripts that runs without issue but imports the wrong values because it is written for US2000 and not for US3000 batteries:-

Both scripts needed for the communication to work.

Pyloncom.py

#! /usr/bin/python3

import sys
import pylonpacket
import logging
import serial
import time

import json
import paho.mqtt.client as mqtt


class PylonCom:

    PORT = "/dev/ttyUSB1"
    BAUD = 115200

    def __init__(self):
        self.sp = serial.Serial(PylonCom.PORT,PylonCom.BAUD,timeout=0.5)

    def GetReply(self, request, reply_type):
        self.sp.write(request.GetAsciiBytes())
        line = bytearray()
        while True:
            c = self.sp.read(1)
            if c:
                line.extend(c)
                if c[0] == 0x0D:
                    break
            else:
                break
        logging.debug("Received sentence %s",line)
        preply = pylonpacket.PylonPacket.Parse(line, reply_type)
        return preply

    def close(self):
        self.sp.close()

def connect():
    global client
    # Needs to be adjusted to set where your MQTT server is found:
    client = mqtt.Client(client_id="pylonbatteries")
    client.username_pw_set('emonpi', 'emonpimqtt2016')
    client.connect('10.1.1.32')
    
def send_data(data, topic):
    try:
        client.publish(topic, data)
    except Exception as e:
        print("error sending to emoncms...:" + str(e))
        return 0
    return 1

def main(argv=None):
    if argv is None:
        argv = sys.argv[1:]

    connect();
    pc = PylonCom()


    #for adr in range(0,255):
    #    ppIn = pylonpacket.PPGetVersionInfo()
    #    ppIn.ADR=adr
    #    ppOut=pc.GetReply(ppIn, pylonpacket.PPVersionInfo)
    #    if ppOut: print("Get protocol version reply:",ppOut)
    #return
    


    #ppIn=pylonpacket.PPGetManufacturerInfo()
    #print("Get manufacturer info:",ppIn)
    #ppOut=pc.GetReply(ppIn, pylonpacket.PPManufacturerInfo)
    #print("Get manufacturer info reply:",ppOut)

    batteries = []
    batteryInfo = {}

    print("*** Scanning for batteries")
    for adr in range(1,9):
#        print("*** Polling adress ", adr)
        ppIn = pylonpacket.PPGetSystemParameter()
        ppIn.Command = adr
        ppIn.ADR = adr
#        print("Get system parameter:",ppIn)
        ppSystemParams = pc.GetReply(ppIn, pylonpacket.PPSystemParameter)
        if not ppSystemParams is None:
            print("Get system parameter reply for address ", adr, ":",ppSystemParams)
            ppIn = pylonpacket.PPGetSeriesNumber()
            ppIn.Command = adr
            ppIn.ADR = adr
#            print("Get serial number:",ppIn)
            ppSerial = pc.GetReply(ppIn, pylonpacket.PPSeriesNumber)
            if not ppSerial is None:
                print("Get serial number reply for address ", adr, ":",ppSerial) #,ppOut.info.hex())
                batteries.append(adr)
                batteryInfo[adr] = { "adr": adr, "systemParams": ppSystemParams, "serialNumber": ppSerial }

    # Get system parameter reply for address  2 : VER: 0x20, ADR: 0x02, CID1: 0x46, CID2: 0x00, LENGTH: 50, len(INFO): 25, CHKSUM: 0xF224
    # >  FLAG: 0b10001, UnitCellVoltage: 3.7, UnitCellLowVoltage 3.05, UnitCellUnderVoltage: 2.9
    # Get serial number reply for address  2 : VER: 0x20, ADR: 0x02, CID1: 0x46, CID2: 0x00, LENGTH: 34, len(INFO): 17, CHKSUM: 0xF6C5
    # > Series Number: PPTBH02198838124

    print("*** We found batteries at:", batteries)
    print("*** BatteryInfo:", batteryInfo)

    if len(batteries) != 4:
        print("Quitting since we didn't find 4 batteries")
        exit(1)

    while True:
        start=time.time()
        for adr in batteries:
            print("Conecting to addr",adr)
            ppSystemParams = batteryInfo[adr]['systemParams']
            ppIn = pylonpacket.PPGetChargeManagementInformation()
            ppIn.Command = adr
            ppIn.ADR = adr
            #print("Get charge info:",ppIn)
            ppChargeInfo = pc.GetReply(ppIn, pylonpacket.PPChargeManagementInformation)
            print("Get charge info reply, addr", adr, ":", ppChargeInfo)

            if ppChargeInfo is None:
                continue

            #return

            ppIn = pylonpacket.PPGetAnalogValue()
            ppIn.Command = adr
            ppIn.ADR = adr
            print("Get analog:",ppIn)
            ppAnalogue = pc.GetReply(ppIn, pylonpacket.PPAnalogValue)
            print("Get analog reply, addr", adr, ":", ppAnalogue)

            if ppAnalogue is None:
                continue

            # Conecting to addr 2
            # Get charge info reply, addr 2 : 02d002b7980032ff06c0
            # VER: 0x20, ADR: 0x02, CID1: 0x46, CID2: 0x00, LENGTH: 20, len(INFO): 10, CHKSUM: 0xF94F
            # >  VoltageUpLimit: 53.25, VoltageDownLimit: 47.0, MaxChargeCurrent: 50.0, MaxDischargeCurrent: -250.0, Status: 0xc0
            # Get analog: VER: 0x20, ADR: 0x02, CID1: 0x46, CID2: 0x42, LENGTH: 2, len(INFO): 1, CHKSUM: 0x0000, Command: 2
            # Get analog reply, addr 2 : VER: 0x20, ADR: 0x02, CID1: 0x46, CID2: 0x00, LENGTH: 110, len(INFO): 55, CHKSUM: 0xE46E
            # >  CellsCount: 15, TemperaturesCount: 5
            # >  TotalCurrent: 0.000A, TotalVoltage: 52.456V, RemainingCapacity: 50.000Ah, P: 0.00
            # >  Quantity: 2, TotalCapacity: 50.0Ah, Cycles: 0
            # >  CellVoltages: [3.499, 3.498, 3.493, 3.494, 3.498, 3.483, 3.497, 3.498, 3.499, 3.5, 3.499, 3.499, 3.499, 3.501, 3.499]
            # >  Temperatures: [27.0, 25.0, 25.0, 25.0, 25.0]

            j = '{ '
            j += '"time":' + str(int(time.time()))
            j += ', "VoltageUpperLimit":' + str(ppChargeInfo.VoltageUpLimit)
            j += ', "VoltageLowerLimit":' + str(ppChargeInfo.VoltageDownLimit)
            j += ', "MaxChargeAmps":' + str(ppChargeInfo.MaxChargeCurrent)
            j += ', "MaxDischargeAmps":' + str(ppChargeInfo.MaxDischargeCurrent)
            j += ', "BatteryCycles":' + str(ppAnalogue.Cycles)
            j += ', "BatteryVoltage":' + str(ppAnalogue.TotalVoltage)
            j += ', "BatteryAmps":' + str(ppAnalogue.TotalCurrent)
            j += ', "BatteryWatts":' + str(ppAnalogue.TotalCurrent*ppAnalogue.TotalVoltage)
            j += ', "BatterySOC":' + str(ppAnalogue.RemainingCapacity*100.0/50.0)
            j += ', "RemainingAh":' + str(ppAnalogue.RemainingCapacity)
            j += ', "RemainingWh":' + str(int(ppAnalogue.RemainingCapacity*ppAnalogue.CellsCount*3.33))
            j += ', "MinutesToRun":' + ( '9999' if ppAnalogue.TotalCurrent >= 0 else str(int(ppAnalogue.RemainingCapacity*60/-ppAnalogue.TotalCurrent)) )
            j += ', "CellMaxVoltage":' + str(ppSystemParams.UnitCellVoltage)
            j += ', "CellLowVoltage":' + str(ppSystemParams.UnitCellLowVoltage)
            j += ', "CellUnderVoltage":' + str(ppSystemParams.UnitCellUnderVoltage)
            lowestCell = 99
            highestCell = 0
            cells = ppAnalogue.CellVoltages
            for cell in range(ppAnalogue.CellsCount):
                j += ', "cellVoltage' + str(cell) + '":' + str(cells[cell])
                if cells[cell] < lowestCell:
                    lowestCell = cells[cell]
                if cells[cell] > highestCell:
                    highestCell = cells[cell]
            j += ', "highestCellVoltage":' + str(highestCell)
            j += ', "lowestCellVoltage":' + str(lowestCell)
            imbalance = int( (highestCell/lowestCell - 1) * 1000) / 10.0
            j += ', "cellImbalancePct":' + str(imbalance)
            temps = ppAnalogue.Temperatures
            lowestTemp = 99
            highestTemp = 0
            for temp in range(ppAnalogue.TemperaturesCount):
                j += ', "temp' + str(temp) + '":' + str(temps[temp])
                if temps[temp] < lowestTemp:
                    lowestTemp = temps[temp]
                if temps[temp] > highestTemp:
                    highestTemp = temps[temp]
            j += ', "highestTemp":' + str(highestTemp)
            j += ', "lowestTemp":' + str(lowestTemp)
            j += ' }'
            print('Send ' + j + ' to emon/pylon' + str(batteryInfo[adr]['serialNumber'].SeriesNumber))
            send = send_data(j, 'emon/pylon' + str(batteryInfo[adr]['serialNumber'].SeriesNumber))
            time.sleep(1)

        print("")
        time.sleep(max(1,10-(time.time()-start)))

    #ppIn=pylonpacket.PPGetChargeManagementInformation()
    #ppIn.Command=0x02
    #print("Get charge info:",ppIn)
    #ppOut=pc.GetReply(ppIn, pylonpacket.PPChargeManagementInformation)
    #print("Get charge info reply:",ppOut)
      

    ppIn = pylonpacket.PPGetAlarmInformation()
    ppIn.Command = 0x02
    print("Get alarm info:",ppIn)
    ppOut = pc.GetReply(ppIn, pylonpacket.PPAlarmInformation)
    print("Get alarm info reply:",ppOut) #,ppOut.info.hex())

    
    ppIn = pylonpacket.PPTurnOff()
    ppIn.Command = 0x02
    print("Turn off:",ppIn)
    ppOut = pc.GetReply(ppIn, pylonpacket.PPTurnOffReply)
    print("Turn off reply:",ppOut)
    

if __name__ == "__main__":
    root = logging.getLogger()
    #root.setLevel(logging.DEBUG)
    main(sys.argv[1:])

pylonpacket.py

import logging


class PylonPacket:
    
    def __init__(self):
        self.header = bytearray(6)
        self.info = bytearray()
        self.checksum = bytearray(2)
        self.VER=0x20
        self.ADR=0x02

    @property
    def VER(self):
        return self.header[0]

    @VER.setter
    def VER(self, value):
        self.header[0] = value

    @property
    def ADR(self):
        return self.header[1]

    @ADR.setter
    def ADR(self, value):
        self.header[1] = value

    @property
    def CID1(self):
        return self.header[2]

    @CID1.setter
    def CID1(self, value):
        self.header[2] = value

    @property
    def CID2(self):
        return self.header[3]

    @CID2.setter
    def CID2(self, value):
        self.header[3] = value

    @property
    def LENGTH(self):
        return (((self.header[4] & 0x0F) << 8) | self.header[5])

    @LENGTH.setter
    def LENGTH(self, value):
        if value>0xfff or value<0: raise OverflowError("Invalid length")
        sum = (value & 0x000F) + ((value >> 4) & 0x000F) + ((value >> 8 ) & 0x000F);
        sum = sum % 16;
        sum = ~sum;
        sum = sum + 1;
        val = (sum << 12) + value;
        self.header[5] = (val & 0xff)
        self.header[4] = (val>>8) & 0xff
    

    @property
    def INFO(self):
        return self.info

    @INFO.setter
    def INFO(self, value):
        self.info = value

    @property
    def CHKSUM(self):
        return self.checksum[0:2]

    @CHKSUM.setter
    def CHKSUM(self, value):
        self.checksum[0:2] = value

    def UpdateChecksum(self):
        sum=0
        for char in bytes(self.header).hex().upper():
            sum+=ord(char)
        for v in bytes(self.info).hex().upper():
            sum+=ord(v)
        sum=sum%65536
        sum=~sum
        sum=sum+1
        self.checksum[0]=(sum >>8) & 0xff
        self.checksum[1]=sum & 0xff


    def GetAsciiBytes(self):
        self.UpdateChecksum()
        ret=bytearray()
        ret.extend(self.header)
        ret.extend(self.info)
        ret.extend(self.checksum)
        rh='~'+ret.hex().upper()+"\r"
        logging.debug("Encoded sentence is %s",rh)
        return rh.encode()

    def Parse(ascii, packet_type):
        if (len(ascii)==0): return None
        #logging.debug("Value to decode is %s",ascii)
        if ascii[0] != 0x7E or ascii[-1] != 0x0D:
            raise ValueError("Invalid packet format")
        content=ascii[1:-1].decode()
        logging.debug("Content of packet: %s",content)
        bdata = bytes.fromhex(content)
        
        pret = packet_type()
        for i in range(0,len(pret.header)):
            pret.header[i] = bdata[i]
        if  pret.LENGTH>0:
            pret.info=bdata[6:-2]
            logging.debug("Info content is %s", pret.info.hex())
        else:
            pret.info=bytearray()
        pret.UpdateChecksum()
        if pret.checksum[0]!=bdata[-2] or pret.checksum[1]!=bdata[-1]:
            logging.error("Invalid checksum!")
            raise ValueError("Invalid checksum")
        pret.PostParse()
        return pret

    def PostParse(self):
        pass

    def GetInt2(self, idx):
        val=self.info[idx]<<8 | self.info[idx+1]
        return val

    def GetInt2Complement(self, idx):
        val=self.info[idx]<<8 | self.info[idx+1]
        if (val & 0x8000)==0x8000:
            val = val - 0x10000
        return val

    def __str__(self, **kwargs):
        return "VER: 0x%02x, ADR: 0x%02x, CID1: 0x%02x, CID2: 0x%02x, LENGTH: %s, len(INFO): %s, CHKSUM: 0x%02X%02X"%(self.VER,self.ADR,self.CID1,self.CID2,self.LENGTH,len(self.INFO),self.CHKSUM[0],self.CHKSUM[1])

class PPGetVersionInfo(PylonPacket):
    def __init__(self):
        super().__init__()
        self.CID1=0x46
        self.CID2=0x4F

class PPVersionInfo(PylonPacket):
    pass

class PPGetManufacturerInfo(PylonPacket):
    def __init__(self):
        super().__init__()
        self.CID1=0x46
        self.CID2=0x51

class PPManufacturerInfo(PylonPacket):
    @property
    def DeviceName(self):
        return (self.info[0:10]).decode().strip()

    @property
    def SoftwareVersion(self):
        return self.info[10:13]

    @property
    def ManufacturerName(self):
        return (self.info[12:]).decode().strip()

    def __str__(self, **kwargs):
        #print(int(self.SoftwareVersion[1]))
        return super().__str__(**kwargs)+("\r\n>  DeviceName: %s, SoftwareVersion %s.%s, ManufacturerName: %s"%(self.DeviceName,'?','?',self.ManufacturerName))


class PPGetAnalogValue(PylonPacket):

    def __init__(self):
        super().__init__()
        self.info=bytearray(1)
        self.LENGTH=0x02
        self.CID1=0x46
        self.CID2=0x42
        

    @property
    def Command(self):
        return self.info[0]

    @Command.setter
    def Command(self, value):
        self.info[0] = value
    
    def __str__(self, **kwargs):
        return super().__str__(**kwargs)+(", Command: %s"%(self.Command))


class PPAnalogValue(PylonPacket):

    def __init__(self):
        super().__init__()
        self.voltages=[]
        self.temperatures=[]

    @property
    def CellsCount(self):
        return self.info[2]

    @property
    def CellVoltages(self):
        return self.voltages

    @property
    def TemperaturesCount(self):
        idx=(self.CellsCount*2)+3
        return self.info[idx]

    @property
    def Temperatures(self):
        return self.temperatures

    @property
    def TotalCurrent(self):
        return self.GetInt2Complement(-11)/10.0 

    @property
    def TotalVoltage(self):
        return self.GetInt2(-9)/1000.0 

    @property
    def RemainingCapacity(self):
        return self.GetInt2(-7)/1000.0 

    @property
    def Quantity(self):
        return self.info[-5]

    @property
    def TotalCapacity(self):
        return self.GetInt2(-4)/1000.0  

    @property
    def Cycles(self):
        return self.GetInt2(-2) 

    def PostParse(self):
        logging.debug("Post processing parsed data %s",self.info.hex())
        self.voltages=[]
        self.temperatures=[]
        for v in range(0,self.CellsCount):
            cv=self.GetInt2(3+(2*v))/1000.0
            logging.debug("Voltage ",v,cv)
            self.voltages.append(cv)
        
        idx=(self.CellsCount*2)+3
        for t in range(0,self.TemperaturesCount):
            tv=self.GetInt2Complement(idx+1+(2*t))
            tv=(tv-2731)/10.0
            logging.debug("Temperature %s: %s",v,tv)
            self.temperatures.append(tv)
    
    def __str__(self, **kwargs):
        ret=super().__str__(**kwargs)
        ret+=("\r\n>  CellsCount: %s, TemperaturesCount: %s"%(self.CellsCount, self.TemperaturesCount))
        ret+=("\r\n>  TotalCurrent: %.3f, TotalVoltage: %.3f, RemainingCapacity: %.3f, P: %.2f"%(self.TotalCurrent, self.TotalVoltage, self.RemainingCapacity,(self.TotalCurrent*self.TotalVoltage)))
        ret+=("\r\n>  Quantity: %s, TotalCapacity: %s, Cycles: %s"%(self.Quantity, self.TotalCapacity, self.Cycles))
        ret+=("\r\n>  CellVoltages: %s"%(self.CellVoltages))
        ret+=("\r\n>  Temperatures: %s"%(self.Temperatures))
        return ret

class PPGetSystemParameter(PylonPacket):
    def __init__(self):
        super().__init__()
        self.CID1=0x46
        self.CID2=0x47

class PPSystemParameter(PylonPacket):
    @property
    def INFOFLAG(self):
        return self.info[0]

    @property
    def UnitCellVoltage(self):
        return self.GetInt2(1)/1000.0

    @property
    def UnitCellLowVoltage(self):
        return self.GetInt2(3)/1000.0

    @property
    def UnitCellUnderVoltage(self):
        return self.GetInt2(5)/1000.0

    #TODO: Doplnit dalsi neuzitecne property
    def __str__(self, **kwargs):
        return super().__str__(**kwargs)+("\r\n>  FLAG: %s, UnitCellVoltage: %s, UnitCellLowVoltage %s, UnitCellUnderVoltage: %s"%(bin(self.INFOFLAG), self.UnitCellVoltage,self.UnitCellLowVoltage,self.UnitCellUnderVoltage))


class PPGetAlarmInformation(PylonPacket):
    def __init__(self):
        super().__init__()
        self.CID1=0x46
        self.CID2=0x44
        self.info=bytearray(1)
        self.LENGTH=0x02
 
    @property
    def Command(self):
        return self.info[0]

    @Command.setter
    def Command(self, value):
        self.info[0] = value
    
    def __str__(self, **kwargs):
        return super().__str__(**kwargs)+(", Command: %s"%(self.Command))

class PPAlarmInformation(PylonPacket):
    pass


class PPGetChargeManagementInformation(PylonPacket):
    def __init__(self):
        super().__init__()
        self.CID1=0x46
        self.CID2=0x92
        self.info=bytearray(1)
        self.LENGTH=0x02
 
    @property
    def Command(self):
        return self.info[0]

    @Command.setter
    def Command(self, value):
        self.info[0] = value
    
    def __str__(self, **kwargs):
        return super().__str__(**kwargs)+(", Command: %s"%(self.Command))

class PPChargeManagementInformation(PylonPacket):

    @property
    def VoltageUpLimit(self):
        return self.GetInt2(1)/1000.0

    @property
    def VoltageDownLimit(self):
        return self.GetInt2(3)/1000.0

    @property
    def MaxChargeCurrent(self):
        return self.GetInt2Complement(5)/1.0

    @property
    def MaxDischargeCurrent(self):
        return self.GetInt2Complement(7)/1.0

    @property
    def Status(self):
        return self.info[9]

    def __str__(self, **kwargs):
        print(self.info.hex())
        return super().__str__(**kwargs)+("\r\n>  VoltageUpLimit: %s, VoltageDownLimit: %s, MaxChargeCurrent: %s, MaxDischargeCurrent: %s, Status: %s"%(self.VoltageUpLimit,self.VoltageDownLimit,self.MaxChargeCurrent,self.MaxDischargeCurrent,self.Status))



class PPGetSeriesNumber(PylonPacket):
    def __init__(self):
        super().__init__()
        self.CID1=0x46
        self.CID2=0x93
        self.info=bytearray(1)
        self.LENGTH=0x02
 
    @property
    def Command(self):
        return self.info[0]

    @Command.setter
    def Command(self, value):
        self.info[0] = value
    
    def __str__(self, **kwargs):
        return super().__str__(**kwargs)+(", Command: %s"%(self.Command))

class PPSeriesNumber(PylonPacket):
    
    @property
    def SeriesNumber(self):
        return self.info[1:].decode()


    def __str__(self, **kwargs):
        return super().__str__(**kwargs)+("\r\n> Series Number: %s"%(self.SeriesNumber))

class PPTurnOff(PylonPacket):
    def __init__(self):
        super().__init__()
        self.CID1=0x46
        self.CID2=0x95
        self.info=bytearray(1)
        self.LENGTH=0x02
 
    @property
    def Command(self):
        return self.info[0]

    @Command.setter
    def Command(self, value):
        self.info[0] = value

    def __str__(self, **kwargs):
        return super().__str__(**kwargs)+(", Command: %s"%(self.Command))


class PPTurnOffReply(PylonPacket):
    pass

@Vards0
Copy link
Author

Vards0 commented Mar 8, 2022

Hi Frank.

I'm just following up to see if you have had a chance to review the code for this.

I still haven't found any working code that can monitor the Pylontech US3000 devices.

Thanks

@hrford
Copy link

hrford commented Mar 13, 2022

I've also got a US3000 and would like to support this project. I'm looking to fix the Capacity fields and therefore have a sensible SoC output. I'm remote from the battery so would hook up a RPi or something and remote into it when I visit next. I'm very keen to get this fixed and see @Frankkkkk has a branch he's waiting to merge...

@hrford
Copy link

hrford commented Apr 17, 2022

@Vards0 I have a US3000 single module. I'm working on modifying @Frankkkkk 's script to work with larger >65Ah (3.5kWh) batteries. I noted Frank's comment about keeping the API clean so I'm working toward that goal but I think maybe an abstractin layer is required rather than making them in the binary conversion code...

Your screenshot from the Pylontech RS485 manual is super useful as it means the overflow of the existing fields is not limited to US3000 batteries, but just any which go over 65Ah capacity.

Do you have the Pylontech RS485 manual?

@Vards0
Copy link
Author

Vards0 commented Apr 18, 2022

Hi Henry.

You should be able to download it from here.

https://powerforum.co.za/applications/core/interface/file/attachment.php?id=12308

I look forward to seeing how you get on with this. Please let me know if you need anything else.

@hrford
Copy link

hrford commented Apr 18, 2022

The link doesn't work because: "This attachment is not available. It may have been removed or the person who shared it may not have permission to share it to this location. "

I've made changes to a version of Frank's pylontech.py which give me this output:
Current = -4.1 Voltage = 49.633 Power = -203.4953 RemainingCapacityLowC = 65.535 UserDefinedItems = 4 TotalCapacityLowC = 65.535 CycleNumber = 154 HighC = Container: Remaining = 72.52 Total = 74.0 RemainingCapacity = 72.52 TotalCapacity = 74.0 TotalPower = -203.4953 StateOfCharge = 0.98

You can try it at https://github.com/hrford/solar-battery/blob/main/pylontech.py

It renames the original fields of lower capacity devices to "RemainingCapacityLowC" and "TotalCapacityLowC" and computes two new fields using the existing names "RemainingCapacity" and "TotalCapacity". If you have a high-capacity device, it uses those raw fields instead.

(If you have any issues, please send raw_frame outputs and I can put them through my fake serial driver.)

@Vards0
Copy link
Author

Vards0 commented Apr 19, 2022

Thanks Henry.

If You copy the link and paste it into a new tab it should work. Github seems to alter the link somehow.

https://powerforum.co.za/applications/core/interface/file/attachment.php?id=12308

@Vards0
Copy link
Author

Vards0 commented Apr 19, 2022

Hi Henry.

I ran your modified code. (I'm not sure if you want me to raise a new issue under your github branch)....

It gives all the correct values for all 4 of my batteries but unfortunately I am getting the same issue as with Frank's code; for some reason it causes my master battery to go into protection mode (Solid red light) as soon as I run it.

I didn't have this issue with the original code that Frank posted. It only started happening after the code was adjusted to accommodate >65AH capacities.

@hrford
Copy link

hrford commented Apr 19, 2022

I spotted that github altered the link and did what you suggested to start with, but same issue.

I'll do a comparison between my code and Frank's master branch and see where commands might be different and get back to you. However, any issues should be raised with Frank on his github using a new issue.

@hrford
Copy link

hrford commented Apr 19, 2022

@Vards0 the script that works for you is an older version and nothing like the newer reformatted one I'm working on.

I might have a go at running the old script through my serial debugger and see what is different, maybe I can help @Frankkkkk find that regression.

In the mean time, can you send me a copy of the RS485 manual please?

@Vards0
Copy link
Author

Vards0 commented Apr 20, 2022

Hi Henry. I cannot find your email address to send you the manual ....

Can you perhaps give me upload access to your repository??

Cheers

@Frankkkkk
Copy link
Owner

Hi @hrford and @Vards0 !
Sorry for the ghosting from my part :( too many things in the same time..

Indeed, I think that I introduced a regression somethere in the code that makes the battery fault themselves.. I'll try to run a git .bissect and hopefully pinpoint the problem.

In the meantime, don't hesitate to make a merge request ! I'd be happy to merge them !

Cheers to both

@hrford
Copy link

hrford commented Apr 24, 2022

@Frankkkkk
This is what @Vards0's script (On 2021-12-06) would output through my fake serial driver (1 module support only):

Fake Pylontech serial: init(serial_port=/dev/ttyUSB1, baudrate=115200, bytesize=8, parity=PARITY_NONE, stopbits=1, timeout=0.5)
*** Scanning for batteries
Fake Pylontech serial: write(data=b'~200146470000FDA8\r')
Get system parameter reply for address  1 : VER: 0x20, ADR: 0x02, CID1: 0x46, CID2: 0x00, LENGTH: 50, len(INFO): 25, CHKSUM: 0xF23D
>  FLAG: 0b10001, UnitCellVoltage: 3.65, UnitCellLowVoltage 3.05, UnitCellUnderVoltage: 2.9
Fake Pylontech serial: write(data=b'~20014693E00201FD2F\r')
Fake Pylontech serial: write(data=b'~200246470000FDA7\r')
Get system parameter reply for address  2 : VER: 0x20, ADR: 0x02, CID1: 0x46, CID2: 0x00, LENGTH: 50, len(INFO): 25, CHKSUM: 0xF23D
>  FLAG: 0b10001, UnitCellVoltage: 3.65, UnitCellLowVoltage 3.05, UnitCellUnderVoltage: 2.9
Fake Pylontech serial: write(data=b'~20024693E00202FD2D\r')
Fake Pylontech serial: write(data=b'~200346470000FDA6\r')
Fake Pylontech serial: write(data=b'~200446470000FDA5\r')
Fake Pylontech serial: write(data=b'~200546470000FDA4\r')
Fake Pylontech serial: write(data=b'~200646470000FDA3\r')
Fake Pylontech serial: write(data=b'~200746470000FDA2\r')
Fake Pylontech serial: write(data=b'~200846470000FDA1\r')
*** We found batteries at: []
*** BatteryInfo: {}
Quitting since we didn't find 4 batteries

This is what my test outputs based on your US3000 scanner:

Fake Pylontech serial: init(serial_port=/dev/ttyUSB0, baudrate=115200, bytesize=8, parity=PARITY_NONE, stopbits=1, timeout=2)
Fake Pylontech serial: write(data=b'~20024642E002FFFD09\r')
Fake Pylontech serial: readline() return b'~20024600F07A11010F0CEB0CEC0CEC0CEC0CEC0CEB0CED0CEC0CED0CEE0CEF0CEF0CED0CEF0CED050B7D0B550B550B550B55FFD7C1E1FFFF04FFFF009A011B48012110E103\r'
raw_frame: b'~20024600F07A11010F0CEB0CEC0CEC0CEC0CEC0CEB0CED0CEC0CED0CEE0CEF0CEF0CED0CEF0CED050B7D0B550B550B550B55FFD7C1E1FFFF04FFFF009A011B48012110E103\r'
Container: 
    NumberOfModules = 1
	(all good stuff)

However, my serial driver fakes only one battery, so it's not possible to tell what the issue is with @Vards0's battery is crashing.

Varda's script first output:
200146470000FDA8

Frank's script first output:
20024642E002FFFD09

@Vards0 Could you let us know where your battery is resetting?

@Vards0
Copy link
Author

Vards0 commented May 4, 2022

Hi Henry.

Apologies, I was away for a few days.

Please clarify what you mean? I am not sure exactly when the script is causing the battery to reset. It seems to be as soon as I run it....

@grepa
Copy link

grepa commented Jun 6, 2022

Hi
US3000C on comand
if name == 'main':
p = Pylontech()
# print(p.get_protocol_version())
# print(p.get_manufacturer_info())
print(p.get_system_parameters())
# print(p.get_management_info())
# print(p.get_module_serial_number())
print(p.get_values())

get_system_parameters() returns value ok , but get_values not function. Not return value :-( 



Return value:
python3 ./pylontech.py 

Container:
CellHighVoltageLimit = 3.65
CellLowVoltageLimit = 3.05
CellUnderVoltageLimit = 2.8
ChargeHighTemperatureLimit = 33.31
ChargeLowTemperatureLimit = 26.31
ChargeCurrentLimit = 9.0
ModuleHighVoltageLimit = 54.0
ModuleLowVoltageLimit = 46.0
ModuleUnderVoltageLimit = 43.0
DischargeHighTemperatureLimit = 33.31
DischargeLowTemperatureLimit = 26.31
DischargeCurrentLimit = -9.0
Traceback (most recent call last):
File "/home/ales/Stažené/python-pylontech-master/pylontech/./pylontech.py", line 225, in
print(p.get_values())
File "/home/ales/Stažené/python-pylontech-master/pylontech/./pylontech.py", line 211, in get_values
f = self.read_frame()
File "/home/ales/Stažené/python-pylontech-master/pylontech/./pylontech.py", line 170, in read_frame
f = self._decode_hw_frame(raw_frame=raw_frame)
File "/home/ales/Stažené/python-pylontech-master/pylontech/./pylontech.py", line 151, in _decode_hw_frame
assert got_frame_checksum == int(frame_chksum, 16)
ValueError: invalid literal for int() with base 16: b''

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants