Skip to content

Instantly share code, notes, and snippets.

@nagisa
Forked from pavelmaca/block_map.md
Last active May 9, 2025 04:37
Show Gist options
  • Save nagisa/435bdf783e4b13b8e810106b0852081e to your computer and use it in GitHub Desktop.
Save nagisa/435bdf783e4b13b8e810106b0852081e to your computer and use it in GitHub Desktop.
Pylontech Force H2 - Solarman Modbus RTU registry map
Start End Block description
3972 3972 Unknown
4096 4111 Brand and model name
4112 4239 Empty
4320 4320 Date and time
4352 4415 Same as 5120-5183
4416 4607 Empty
4608 4671 Unknown, maybe settings (seems static)
4672 4863 Empty
5120 5183 BMS status
5184 5199 Empty
5200 5215 BMS SN
5216 5231 Pack 0-x - Module 0-x Voltage
5232 5295 Empty
5296 5311 Pack 0-x - Module 0-x Temperature
5312 5375 Empty
5376 5503 Pack 0 Module 0-4 Cell Voltage
5504 6143 ?? Pack 1-5 Module 0-4 Cell Voltage
6144 6271 Pack 0 Module 0-4 Cell Temperature
6272 6911 ?? Pack 1-5 Module 0-4 Cell Temperature
requests:
# Brand, Device name, FW Version
- start: 4096
end: 4106
mb_functioncode: 0x03
# Device Date and Time - TODO
#- start: 4320
# end: 4325
# mb_functioncode: 0x03
# BMS Status
- start: 5120
end: 5135
mb_functioncode: 0x03
# Cell/Module Max/Min Temperature and Voltage
- start: 5136
end: 5151
mb_functioncode: 0x03
# Total Charge, Discharge
- start: 5152
end: 5167
mb_functioncode: 0x03
# Pack, Module, Cell count
- start: 5174
end: 5175
mb_functioncode: 0x03
# Device SN
- start: 5200
end: 5207
mb_functioncode: 0x03
# Pack 0 Module 0-3 Voltage
- start: 5216
end: 5219
mb_functioncode: 0x03
# Pack 0 Module 0-3 Temperature
- start: 5296
end: 5299
mb_functioncode: 0x03
# Pack 0 Module 0-3 Cells Voltage
#- start: 5376
# end: 5465
# mb_functioncode: 0x03
# Pack 0 Module 0-3 Cells Temperature
#- start: 6144
# end: 6233
# mb_functioncode: 0x03
parameters:
- group: Basic information
items:
- name: "Device SN"
class: ""
state_class: ""
uom: ""
scale: 1
rule: 5
registers: [ 5200,5201,5202,5203,5204,5205,5206,5207 ]
isstr: true
#- name: "Device Time" # TODO - cant parse format is [y, m, d, h, m, s]
# class: ""
# state_class: ""
# uom: ""
# scale: 1
# rule: 8
# registers: [4320,4321,4322,4323,4324,4325]
# isstr: true
- name: "Brand"
class: ""
state_class: ""
uom: ""
scale: 1
rule: 5
registers: [ 4096,4097,4098 ]
isstr: true
- name: "Device Name"
class: ""
state_class: ""
uom: ""
scale: 1
rule: 5
registers: [ 4101,4102,4103,4104,4105 ]
isstr: true
- name: "FW Version"
class: ""
state_class: ""
uom: ""
scale: 1
rule: 7
registers: [ 4106 ] # 00000001 00000110 = V1.6
isstr: true
# NOTE: buggy, counter jumps up by a couple MWh once in a while
- name: "Total Charge"
class: "energy"
state_class: "total_increasing"
uom: "kWh"
scale: 1
rule: 1
registers: [ 5164 ]
icon: 'mdi:battery-plus'
# NOTE: buggy, counter jumps up by a couple MWh once in a while
- name: "Total Discharge"
class: "energy"
state_class: "total_increasing"
uom: "kWh"
scale: 1
rule: 1
registers: [ 5166 ]
icon: 'mdi:battery-minus'
#- name: "Battery Pack (parallel)" # not tested
# class: ""
# state_class: ""
# uom: ""
# scale: 1
# rule: 1
# registers: [5173]
- name: "Battery Module (series)"
class: ""
state_class: ""
uom: ""
scale: 1
rule: 1
registers: [ 5174 ]
- name: "Battery Cell (series)"
class: ""
state_class: ""
uom: ""
scale: 1
rule: 1
registers: [ 5175 ]
- group: Battery
items:
- name: "Battery Voltage"
class: "voltage"
state_class: "measurement"
uom: "V"
scale: 0.1
rule: 1
registers: [ 5123 ]
icon: 'mdi:battery'
- name: "Battery Current"
class: "current"
state_class: "measurement"
uom: "A"
scale: 0.01
rule: 2
registers: [ 5125 ]
icon: 'mdi:current-dc'
- name: "Battery Temperature"
class: "temperature"
state_class: "measurement"
uom: "°C"
scale: 0.1
rule: 2
registers: [ 5126 ]
icon: 'mdi:thermometer'
- name: "Battery Charge"
class: "battery"
state_class: "measurement"
uom: "%"
scale: 1
rule: 1
registers: [ 5127 ]
icon: 'mdi:battery'
- name: "Battery Cycle Times"
class: ""
state_class: ""
uom: ""
scale: 1
rule: 1
registers: [ 5128 ]
icon: 'mdi:battery-heart'
- name: "Max Charging Voltage"
class: "voltage"
state_class: "measurement"
uom: "V"
scale: 0.1
rule: 1
registers: [ 5129 ]
- name: "Max Charging Current"
class: "current"
state_class: "measurement"
uom: "A"
scale: 0.01
rule: 2
registers: [ 5131 ]
icon: 'mdi:current-dc'
- name: "Min Discharging Voltage"
class: "voltage"
state_class: "measurement"
uom: "V"
scale: 0.1
rule: 1
registers: [ 5132 ]
- name: "Max Discharging Current"
class: "current"
state_class: "measurement"
uom: "A"
scale: 0.01
rule: 2
registers: [ 5134 ]
icon: 'mdi:current-dc'
- name: "Max Cell Voltage"
class: "voltage"
state_class: "measurement"
uom: "V"
scale: 0.001
rule: 1
registers: [ 5136 ]
icon: 'mdi:battery'
- name: "Min Cell Voltage"
class: "voltage"
state_class: "measurement"
uom: "V"
scale: 0.001
rule: 1
registers: [ 5137 ]
icon: 'mdi:battery'
- name: "Max Cell Voltage ID"
class: ""
state_class: ""
uom: ""
scale: 1
rule: 1
registers: [ 5138 ]
- name: "Min Cell Voltage ID"
class: ""
state_class: ""
uom: ""
scale: 1
rule: 1
registers: [ 5139 ]
- name: "Max Cell Temperature"
class: "temperature"
state_class: "measurement"
uom: "°C"
scale: 0.1
rule: 2
registers: [ 5140 ]
icon: 'mdi:thermometer'
- name: "Min Cell Temperature"
class: "temperature"
state_class: "measurement"
uom: "°C"
scale: 0.1
rule: 2
registers: [ 5141 ]
icon: 'mdi:thermometer'
- name: "Max Cell Temperature ID"
class: ""
state_class: ""
uom: ""
scale: 1
rule: 1
registers: [ 5142 ]
- name: "Min Cell Temperature ID"
class: ""
state_class: ""
uom: ""
scale: 1
rule: 1
registers: [ 5143 ]
- name: "Max Module Voltage"
class: "voltage"
state_class: "measurement"
uom: "V"
scale: 0.01
rule: 1
registers: [ 5144 ]
icon: 'mdi:battery'
- name: "Min Module Voltage"
class: "voltage"
state_class: "measurement"
uom: "V"
scale: 0.01
rule: 1
registers: [ 5145 ]
icon: 'mdi:battery'
- name: "Max Module Voltage ID"
class: ""
state_class: ""
uom: ""
scale: 1
rule: 1
registers: [ 5146 ]
- name: "Min Module Voltage ID"
class: ""
state_class: ""
uom: ""
scale: 1
rule: 1
registers: [ 5147 ]
- name: "Max Module Temperature"
class: "temperature"
state_class: "measurement"
uom: "°C"
scale: 0.1
rule: 2
registers: [ 5148 ]
icon: 'mdi:thermometer'
- name: "Min Module Temperature"
class: "temperature"
state_class: "measurement"
uom: "°C"
scale: 0.1
rule: 2
registers: [ 5149 ]
icon: 'mdi:thermometer'
- name: "Max Module Temperature ID"
class: ""
state_class: ""
uom: ""
scale: 1
rule: 1
registers: [ 5150 ]
- name: "Min Module Temperature ID"
class: ""
state_class: ""
uom: ""
scale: 1
rule: 1
registers: [ 5151 ]
- name: "Battery SOH"
class: "battery"
state_class: "measurement"
uom: "%"
scale: 1
rule: 1
registers: [ 5152 ]
icon: 'mdi:battery'
# NOTE: buggy, counter jumps up by a couple MWh once in a while
- name: "Today Charge"
class: "energy"
state_class: "total_increasing"
uom: "kWh"
scale: 0.001
rule: 1
registers: [ 5160 ]
# NOTE: buggy, counter jumps up by a couple MWh once in a while
- name: "Today Discharge"
class: "energy"
state_class: "total_increasing"
uom: "kWh"
scale: 0.001
rule: 1
registers: [ 5162 ]
- group: Battery Module 0
items:
- name: "Battery Module 0 Voltage"
class: "voltage"
state_class: "measurement"
uom: "V"
scale: 0.01
rule: 1
registers: [ 5216 ]
icon: 'mdi:battery'
- name: "Battery Module 0 Temperature"
class: "temperature"
state_class: "measurement"
uom: "°C"
scale: 0.1
rule: 2
registers: [ 5296 ]
icon: 'mdi:thermometer'
- group: Battery Module 1
items:
- name: "Battery Module 1 Voltage"
class: "voltage"
state_class: "measurement"
uom: "V"
scale: 0.01
rule: 1
registers: [ 5217 ]
icon: 'mdi:battery'
- name: "Battery Module 1 Temperature"
class: "temperature"
state_class: "measurement"
uom: "°C"
scale: 0.1
rule: 2
registers: [ 5297 ]
icon: 'mdi:thermometer'
- group: Battery Module 2
items:
- name: "Battery Module 2 Voltage"
class: "voltage"
state_class: "measurement"
uom: "V"
scale: 0.01
rule: 1
registers: [ 5218 ]
icon: 'mdi:battery'
- name: "Battery Module 2 Temperature"
class: "temperature"
state_class: "measurement"
uom: "°C"
scale: 0.1
rule: 2
registers: [ 5298 ]
icon: 'mdi:thermometer'
""" Scan Modbus registers to find valid registers"""
from pysolarmanv5 import PySolarmanV5, V5FrameError
import umodbus.exceptions
import argparse
# Docs:
# - https://pysolarmanv5.readthedocs.io/en/stable/solarmanv5_protocol.html
# - https://github.com/jmccrohan/pysolarmanv5
#
# Usage:
# pip install pysolarmanv5
# python solarmanPylontechScan.py 5120 5199 > scan_240224_10_10.csv
deviceIP="192.168.x.x" # string IP address
deviceSerialNumber=123456789 # int device serial number
def main():
parser = argparse.ArgumentParser()
parser.add_argument("address", help="Address to start scanning from", type=int)
parser.add_argument("addressStop", help="Last address to scan", type=int)
args = parser.parse_args()
if args.address < 0:
print("Address must be greater than or equal to 0")
return
if args.addressStop < 0:
print("AddressStop must be greater than or equal to 0")
return
if args.addressStop < args.address:
print("AddressStop must be greater than or equal to address")
return
modbus = PySolarmanV5(
deviceIP, deviceSerialNumber, port=8899, mb_slave_id=1, verbose=False
)
# print as csv header
print("Register, Register Hex, Length, Value Int, Value Hex, Value Bin1, Value Bin2")
for x in range(args.address, args.addressStop):
try:
val = modbus.read_holding_registers(register_addr=x, quantity=1)[0]
binLeft = (val >> 8) & 0xff
binRight = val & 0xff
# csv row
print(f"{x}, {x:#06x}, 1, {val:05}, {val:#06x}, {binLeft:08b}, {binRight:08b}")
except (V5FrameError, umodbus.exceptions.IllegalDataAddressError):
print(f"{x}, {x:#06x}, 1")
continue
modbus.disconnect()
if __name__ == "__main__":
main()
@nagisa
Copy link
Author

nagisa commented Feb 17, 2025

I have seen a similar thing happen to my batteries, and I believe this is a bug in the BMS firmware. I see the total discharge amount jump by a couple MWh once in a while.

Unfortunately, this affects window discharge counters too (i.e. "discharge today",) so there isn't really a reliable discharge metric from the battery, unfortunately. The best that can be done AFAICT is detecting when such jump occurs, accumulating a correction factor and subtracting it from the total reading, but that requires non-trivial logic :)

@liborhavlak
Copy link

liborhavlak commented Feb 17, 2025

The data itself is very meaningless. It is an order of magnitude higher than is realistic. This is data from inverter:
image
Total charge: 3490 kWh / Discharge 2639 kWh

@nagisa
Copy link
Author

nagisa commented Feb 17, 2025

Hm, so one thing: I won't claim that the HA stuff is accurate: I don't use HA and the file comes from the gist I forked from (https://gist.github.com/pavelmaca/170c282bdb9e20b6c624e8c7633be3b2.)

That being said I believe you're correct that the absolute numbers from the battery on its charge/discharge are meaningless, precisely because of the jumps I mentioned in my previous comment. However at the same time I think these counters are meant and do to represent the amount charged/discharged – they're just somewhat buggy.

I determine this by taking a short term derivative function of the total counters. It for the most part matches the values from an inverter (blue & orange are data from a pylontech battery, green & yellow -- from my inverter):

graph

Notice how the scale and the "shape" of the graphs between the inverter and the battery sorta match. This being a visualization of a derivative means that the total watt-hour counters are going up at a similar rate & similar time.


I personally am not using the totals from the batteries anyway, because among the problem of these counters occasionally "jumping" up, they are also updated relatively infrequently by the BMS. This makes the data really difficult to put into graphs which is my primary use-case.

@stheid
Copy link

stheid commented Mar 6, 2025

from where did you get the the registry map? I am googling for an hour now to find a official protocoll documentation..

@nagisa
Copy link
Author

nagisa commented Mar 7, 2025

There isn't anything published. I imagine the person whom I forked this from determined a basic layout by tracing the I/O between the battery and the wifi module when it is working in the cloud mode and then the remainder is an informed guess.

I have also asked support a couple times for some official docs, only to get ghosted.

@stheid
Copy link

stheid commented Mar 7, 2025

amazing work than, thanks a lot for sharing.

@pavelmaca
Copy link

There isn't anything published. I imagine the person whom I forked this from determined a basic layout by tracing the I/O between the battery and the wifi module when it is working in the cloud mode and then the remainder is an informed guess.

I have also asked support a couple times for some official docs, only to get ghosted.

I had to read all the registry values and compare them to the official Solarman Business app data. There were some predictions based on the layout for additional batteries, even though I have only three connected in parallel.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment