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

reverse engineered shadow management and write protection registers #2

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,3 +123,32 @@ serial number as an argument.
I'm going to write a module for the excellent Victron VenusOS to display the values
of the individual inverters.

## undocumented modbus registers
Most of the modbus registers follow the SunSpec specification. However some features of the Kaco inverters are not covered by it and are not documented by Kaco otherwise. Using the procedures described above, I reverseengineered in particular the registers to control write access and shadow management.

The Kaco app is using the fdbg.cgi api to directly send/receive modbus telegrams. Picking up what is sent by the Kaco app when entering the "shadow management" section of an inverter, we find that we are receiving a POST on fdbg.cgi with data

{"data":"03030fb8000106d9"}

interpreting this using the modbus specification:

* The first byte 03 is the modbus address.
* The second byte 03 is the 'read holding registers'
* next two bytes are starting address. In this case 0x0fb8
* next two bytes are real length. In this case 0x0001. reading one register (two bytes long)
* the final two bytes are the CRC code of the entire command.

The response of the kaco inverter to this post is either of the following two

content = '{"dat":"ok","data":"0303020000c184"}' #off case
content = '{"dat":"ok","data":"03030200010044"}' #on case

and is the expected response to the read command accoring to modbus spec. Counting bytes starting with zero:
address 03 (byte 0), command 'read holding register' (byte 1), two bytes long (byte 2) response in bytes 3 and 4 and CRC code (bytes 5 and 6)

Similarily, posting {"data":"03060fb80001cad9"} on the fdbg.cgi turns on shadow management. and posting {"data":"03060fb800000b19"} turns it off.

the same manipulation can be done using the modbus TCP protocol. The register adress for shadow management is 0x0fb8 or 4024. allowed values are 0x0000 and 0x0001.

Another register that was reverse engineered like this is the write access control register which is at 0x0fba or 4026.

32 changes: 28 additions & 4 deletions kaco_inverter.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,25 +20,49 @@ def setting():

@app.route('/getdevdata.cgi', methods=['GET'])
def getdevdata(device=2, sn="1234"):
content = '{"flg":1,"tim":"20221111153846","tmp":351,"fac":4998,"pac":0,"sac":0,"qac":0,"eto":464,"etd":61,"hto":59,"pf":0,"wan":0,"err":0,"vac":[2325,2327,2341],"iac":[5,5,5],"vpv":[1500,1497],"ipv":[0,1],"str":[]}'
content = '{"flg":1,"tim":"20241124112956","tmp":284,"fac":4999,"pac":1026,"sac":1026,"qac":0,"eto":23634,"etd":17,"hto":1526,"pf":100,"wan":0,"err":0,"vac":[2236,2254,2267],"iac":[16,15,16],"vpv":[7619,0],"ipv":[132,0],"str":[]}'
return Response(content, mimetype='application/json')


@app.route('/getdev.cgi', methods=['GET'])
def getdev(device=0):
if request.args.get('device', 0) == "2":
content = '{"inv":[{"isn":"8.0NX312001234","add":3,"safety":70,"rate":8000,"msw":"V610-03043-04 ","ssw":"V610-60009-00 ","tsw":"V610-11009-01 ","pac":386,"etd":58,"eto":461,"err":0,"cmv":"V2.1.1AV2.00","mty":51,"psb_eb":1}],"num":1}'
content = '{"inv":[{"isn":"8.0NX312064373","add":3,"safety":70,"rate":8000,"msw":"V610-03043-05 ","ssw":"V610-60009-00 ","tsw":"V610-11009-02 ","pac":1026,"etd":17,"eto":23634,"err":0,"cmv":"V2.1.2 ","mty":51,"psb_eb":1}],"num":1}'
else:
content = '{"psn":"1234567890","key":"1234567890","typ":5,"nam":"Wi-Fi Stick","mod":"B32078-10","muf":"KACO","brd":"KACO","hw":"M11","sw":"21618-006R","wsw":"ESP32-WROOM-32U","tim":"2022-11-11 13:19:34","pdk":"","ser":"","protocol":"V1.0","host":"cn-shanghai","port":1883,"status":-1}'
content = '{"psn":"B3278A2C2448","key":"EFM4VBWVVXRKXMPG","typ":5,"nam":"Wi-Fi Stick","mod":"B32078-10","muf":"KACO","brd":"KACO","hw":"M11","sw":"21618-006R","wsw":"ESP32-WROOM-32U","tim":"2024-11-24 11:32:06","pdk":"","ser":"","protocol":"V1.0","host":"cn-shanghai","port":1883,"status":-1}'
return Response(content, mimetype='application/json')

@app.route('/wlanget.cgi')
def wlanget():
return Response('{"mode":"STATION","sid":"lasseredn","srh":-42,"ip":"10.0.0.2","gtw":"10.0.0.1","msk":"255.255.255.0"}', mimetype='application/json')
return Response('{"mode":"STATION","sid":"Gohle","srh":-70,"ip":"192.168.178.182","gtw":"192.168.178.1","msk":"255.255.255.0"}', mimetype='application/json')

@app.route('/fdbg.cgi', methods=['POST', 'GET'])
def fdbg(path=None):
print(path)
print(request.args, request.data, request.path, request.method)
if request.data == b'{"data":"03030fb8000106d9"}': #read shadow management status
content = '{"dat":"ok","data":"0303020000c184"}' #off case
content = '{"dat":"ok","data":"03030200010044"}' #on case
elif request.data == b'{"data":"03060fb80001cad9"}': #turn shadow management on
content = '{"dat":"ok","data":"03060fb80001cad9"}'
elif request.data == b'{"data":"03060fb800000b19"}': #turn shadow management off
content = '{"dat":"ok","data":"03060fb800000b19"}'
elif request.data == b'{"data":"03030fba0001a719"}': #read external write access status
content = '{"dat":"ok","data":"0303020000c184"}' #off case
content = '{"dat":"ok","data":"03030200010044"}' #on case
elif request.data == b'{"data":"03060fba00016b19"}': #turn external write on
content = '{"dat":"ok","data":"03060fba00016b19"}'
elif request.data == b'{"data":"03060fba0000aad9"}': #turn shadow management off
content = '{"dat":"ok","data":"03060fba0000aad9"}'
else:
content = 'fdbg.cgi: Not implemented data: %s'%request.data
print(content)
return Response(content, mimetype='application/jsoon')


@app.route('/<path:path>', methods=['POST', 'GET'])
def catch_all(path):
print("DEBUG: Before crash", path, request.data)
app.logger.info('Catch-All %s: %s', (path, request.data))
return 'Not implemented path: %s' % path

Expand Down