modbus TCP weirdness

28 comments started 2024-04-03 last 2024-04-10
Home Automation
D
#1 DD

In a recent thread, we were talking about the TCP dongle. These references might link there ..?
https://community.givenergy.cloud/d/4218-idiot-guide-battery-idle-stuck-at-x/8
Vestas
hoggy
Rubikcube

I'm just starting out writing some givenergy_modbus code to monitor AC generation, to try to avoid clipping. (Obviously I chose day with no sun to start.) That's all working, but I was quite interested in the odd error messages I was getting. One was
Did not receive expected response type: ReadInputRegistersResponse != WriteHoldingRegisterResponse

Now what was interesting was that I had just done an API command from another window (to modify a register). So it was as if the dongle had sent the reply for that to the wrong TCP connection.

So an an experiment, I just ran a netcat instance to connect to the dongle, listening, but without sending any commands of its own. To my surprise, it seems to be receiving the responses to the commands I'm issuing from my script.

So just for kicks, I added a second netcat instance, and sure enough, it's seeing exactly the same data.
It's as if the dongle is broadcasting replies on all open TCP connections.

Is this known behaviour ? Unavoidable feature of modbus-tcp ? (The protocol does have a transaction-id field which could have been used.

#2 hoggy

DD Yes I believe its been noted before that it just spams all open connections.
If you keep the GE app open, Home Assistant etc at the same time and then use the cli.py you'll have all sorts of fun nonsense responses as they just go to everything. Think Rubikcube mentions it either on here or his website somewhere.

I'm just looking at putting the cli.py back into the newer, non released async / v2 of givenergy_modbus currently although I'll have to recreate all my "unapproved" mods too!

D
#3 DD

hoggy which givenergy_modbus tree are you using ? When I was looking around, the best one seemed to be the one bundled with britkat's giv_tcp tree.

#4 hoggy

Was the OG "Dewet" One - which works fine for my old Gen1s on non fast firmware.
Tried the one in GivTCP repo but that really doesn't seem to like any of my Gen1's no matter which flag I use (nothing, --ac, --aio) so not sure if it's even compatible anymore (assuming it's CRC related) - did you manage to get it to work on old Gen1 Firmware?

In amongst all that I found this "V2" from the original library author hiding away. Doesn't look fully finished but is much much cleaner so going to start from here now rather than try and unravel the old one. I beleive CDPUK now uses this in a fashion, it just doesn't have the cli parts which I need to recreate.
https://github.com/dewet22/givenergy-modbus/tree/getting-ready-for-1.0/givenergy_modbus

Edit: as for why, the leaner one appears to not use Pymodbus library which might open the door to ESP32 type devices although I've not looked at dependencies in too much detail.

#6 hoggy

DD yeah I saw that (although I didn't connect the dots to it being you)
Ultimately I'm relatively ok sticking with what works but had hoped that we werent still in library spaghetti still & there was simply one library that "just works" for all but it appears not.
I'm unsure now how GivTCP gets it to work on Gen1 (hence my question on github this morning) but other than presumably disabling CRC and switching slave addresses back (which kinda puts us back to broken for Gen3 etc...and therefore pointless) I think it might be time better spent on this other V2 one instead.

(I can't really share my version as it does just let you do anything!)

R
#7 Rubikcube

It is a bus protocol, not client server. Every message must go to every device on the bus. The behavior is correct, just 50 years out of date.

D
#8 DD

Rubikcube It is a bus protocol, not client server. Every message must go to every device on the bus.

I understand what you're saying, but I'm not quite sure I agree. I guess it depends on whether you view the dongle as a single device on the modbus bus, and accepts external requests from TCP clients; or whether it's just a gateway device extending the modbus out into the wider world. Both approaches are probably valid.

Anyway, not important... this is the behaviour we get.

What I'm now wondering is whether the modbus module drains the socket before sending out a request. Otherwise, it will contain all the junk from previous transactions. I don't immediately see code doing that, which would explain why I can get long sequences of unexpected replies.

Just instrumenting things now, when it's just my app running, the socket is empty at the start of a request, and it's working okay. But once something else runs, the socket is already readable at the start of a request, and so inevitably the requests and replies get out of sync. That can't be good. Eventually it closes the socket and opens a new one, and things get back on track.

Opening a new socket for every request seems a bit clumsy, but maybe it's necessary ?

D
#9 DD

hoggy Yes, that v2 code look a lot more elegant. Though I've not yet explored async python - something new to learn.

D
#10 DD

DD What I'm now wondering is whether the modbus module drains the socket before sending out a request. Otherwise, it will contain all the junk from previous transactions. I don't immediately see code doing that, which would explain why I can get long sequences of unexpected replies.

And even if it does find a response that matches the one expects, it could be quite stale if it's been sitting in the TCP buffers for a while.

I've added code to drain the socket prior to sending a request, and it's so much more reliable.

D
#11 DD

hoggy No idea if this is important, but I notice that the dewet code uses a modbus slave address of 0x32, whereas the britkat giv_tcp copy seems to use a slave address of 0x31 for normal inverters, or 0x11 for AIO.

(The dewet branch got rid of ModbusTcpClient dependency back in April '22, even in main branch. Not quite sure when the giv_tcp copy was taken.)

#12 hoggy

DD yeah tried that trick, setting it to various addresses used before but still didn't like Gen1 so pretty sure it's the CRC stuff.
If you set it to 0x11 on a normal inverter I believe it spams the portal too everything you call it. Found that one out by mistake early on! Guess the AIO doesn't do that now (or maybe it does, I wouldn't know)

I'm not at my desk in regards to branch used, I was on whichever pypi/pip gives you!

D
#13 DD

hoggy Looks like it's 0.10.1 from 2022-03-03

There is code even back then to add a crc field, so it seems it was always there in the frame. But perhaps it's just that older firmwares didn't bother to check it.

In giv_tcp, it was the same commit that changed the crc algorithm that also adjusted the slave address. I'm wondering if the slave address is somehow used to encode information about the algorithm being used?

#15 hoggy

it's a little off & I believe I need to expand the registers it's looking for (hence why a lot are reported back as "none" - SOC being an odd omission) but it does at least "work" and not return nonsense porting the cli over to the newer async library. Lets see where this goes....

modbus-dump.txt
32kB
D
#16 DD

hoggy That's good. Looking at the dewet tree history, I was a little disheartened that the getting-ready-for-v1.0 branch was taken at the start of 2022, yet both branches had a lot of changes since then. But it seems that they've proceeded in exact lockstop, so that of 2023-03-04, they contained identical code - git diff 555d46dcffa d8d9a075018a011 reports no differences, where those are the commits of 'Update battery.py (#16)' in the two branches.

Since then, only the branch has had any updates. Slightly unfortuantely, I think givenergy-local took a copy of the main branch, rather than the development branch, at the start of 2024, though they have made on;y a couple of minor changes since then. I have no idea whether they'd have any interest in taking later changes. (Since I don't use home-assistant, I'd have no way to test it.)

It would be really nice to get a consolodated givenergy-modbus-1.0 release put together. But one thing that slightly concerns me: in https://community.givenergy.cloud/d/1839-android-inverter-monitor/13 GivEnergy caution against sharing unsanctioned code. I think I read somewhere that they do officially support giv_tcp, and therefore by implication the copy of givenergy_modbus inside there.

#17 hoggy

So I've made a start, first thing is this method seems way quicker/more reliable.
4 second updates reliably on my old Gen1 on a full register refresh! So I think carefully selected register calls could enable a bit faster as a lot of those are pretty static. However I suspect this (like most other local controls) may be spamming the portal, so I think I'll have a chat with GE before I get too excited.

D
#18 DD

hoggy Faster than ..? I had assumed that doing a block-read of registers with a single transaction is probably better than getting just a subset if it means more round-trip transactions.

I've been playing with just getting a snoop client going: connect to the dongle and just passively decode the frames that get sent out, without making any active requests. (I'm having to learn python async along the way, for one thing.) I'm getting disconnected after only two heartbeat requests, rather than the 3 mentioned in the comments.

I was going to suggest: did you want to move discussion to a fork on github, rather than here ?

#19 hoggy

DD can do, my (poor) efforts are over on: https://github.com/hoggyhoggy/givenergy-modbus-async

Was talking more about blocks. It's at least 3 block calls of 60 holding regs for Gen 1, presumably more for Gen 3. Most of the useful "quick" updating stuff falls under the first 120 so can reduce refresh time by pulling first 2x 60 regs only (that's got it down to 3sec from a quick test).
At least that's how it looks but need to work through the register list as I'm sure I'm not that lucky.

D
#20 DD

hoggy One of the changes I'd been considering was, rather than the boolean flag 'full_refresh', allow you to specify how many of each register set you needed. So you might pass [60,0,0] to say 60 input registers, no holding registers or battery registers, or that sort of thing. 'True' would then be equivalent to [999, 999, 999] and 'False' to [999, 0, 999] (for backwards compatability).

Also pondering how best to specify the number of registers available on the different generations of hardware. On the version in giv_tcp, they use an entirely different register array for AC vs the others. But the difference just appears to be that AC has a subset of the registers. I assume it's harmless to ask for registers beyond what's available, you just get 0's back ?

#22 hoggy

DD there are some limitations such that max is 60 registers per call.
You just seem to get a timeout if you ask for registers that don't exist.

(Edit, yes they are now enabled)

#23 hoggy

DD if possible / you want to, can you contact me via email or something? Github doesn't allow DM's and I find it's discussions a bit long winded for quick bouncing of ideas around. https://terravolt.co.uk/contact/

D
#24 DD

hoggy Looks like once a discussion has started, you can just use email to reply to the notifications, and they get added to the discussion automatically.

D
#25 DD

@Rubikcube Sorry to pester you. I'm learning about the protocol by watching the unsolicited responses going by.

One thing confusing me... for multiple batteries, it uses a base address + idx to iterate over the batteries. Most uses seem to agree on (0x32 + i). However, unless I'm mistaken, your app seems to start iterating at 0x30. (The responses for 0x30 and 0x31 are 60 0 values.)

Do different inverter models use a different base address for the battery input registers ?

Just in passing, I also notice that the official GE app seems to query input registers at offset 60 from address 0x01. I'm not sure that's supposed to be - doesn't look like a battery, since the first 16 values look nothing like voltages.

R
#26 Rubikcube

DD With modbus, every device is supposed to have a unique address. Since every device sees every message, a device can check the address to see if the message is directed at it.

When i first looked at this, I noticed the GE app used address 0 (which I don't think is valid!), the portal used address 17 (0x11), and GivTCP used address 49 (0x31). For my app I chose address 48 (0x30) so when I send a request I give my address as 48, and then I can check the address in replies to see if they are for me. The batteries are on addresses 50 thru 54(0x32..0x36) although address 55 seems to be reserved for a 6th battery.

Then came the AIO which doesn't really have a dongle, but still has some "dongle" firmware that connects to the network. This firmware will only communicate if the address is 17; any other address and it doesn't respond! Maybe this was some misguided attempt to lock the AIO to the portal and block third party software. It blocked "home" mode on the GE app, so the GE developers changed the app to use address 17, resulting in confusion for the portal and the spamming effect that we see.

GivTCP added the "isAIO" parameter to set the address to 17. Note that you can set isAIO=Y on any inverter and it will work, just messes up the portal. For my app I devised an algorithm, that starts off using address 17 to make sure the IP address is right and communication established, then tries a few requests with address 48, which if successful causes it to switch to using 48, else it sticks with 17. I hope this will save me trouble if any of the other inverter firmware gets a upgrade with this innovative feature.

Don't be discouraged by your difficulty understanding modbus. The way it has been implemented on this kit suggests the GE developers don't understand it either.

D
#27 DD

Rubikcube For my app I chose address 48 (0x30) so when I send a request I give my address as 48, and then I can check the address in replies to see if they are for me.

Yes, I was thinking along exactly the same lines - if I am free to use a unique address for requests, then it'll be easier to filter out those responses which are acknowledging my own requests.

It's true that if you are a on a shared modbus, you need to be careful in addressing requests to a particular node. But this is slightly different - we're already using TCP to direct our requests to one particular device, and TCP is of course point to point. So the node address isn't really needed to identify a member on a bus as such.

How the dongle then speaks to the inverter is a different matter.

R
#28 Rubikcube

A bit of history ....

Once upon a time there was the typewriter, a mechanical device with some limitations. Two annoying features were if you typed too quick the mechanism jammed (allegedly the reason for QWERTY keyboard layout), and if you didn't hit each key with even pressure, some letters were bold and some faint.

These two problems were solved by the electric typewriter, essentially a keyboard attached to a printing mechanism by a wire. Then one day, someone thought what would happen if you joined the wires of two electric typewriters together? The result, whatever is typed on either keyboard comes out on both printers, and so the teletype was invented. Put them in separate rooms and you can have a conversation, like an ancient version of whatsapp.

You could connect 3 or 4 to make a network with everything entered on any keyboard coming out on all of the machines. This was networking before computers! Bus networks expand on this idea of just connecting things together. It is very different from modern client-server systems in that nothing is really in control.