solaredge_modbus
solaredge_modbus copied to clipboard
Adding additional parameters
I'm not sure if this would work, but here is a list of ModBus parameters that include additional ones (for example related to the battery) which are currently not supported: Power Control Open Protocol for SolarEdge Inverters.pdf (from https://www.photovoltaikforum.com/core/attachment/157903-power-control-open-protocol-for-solaredge-inverters-pdf/)
I would specifically be interested in those from the Global StorEdge Control Block (starting from E004 onwards), if possible in both read and write mode. This could be used to implement a software-controlled charge management either consumption/time-based or with even more complex dependencies on other devices.
Would this be possible to implement?
Interesting! Thanks for sharing the document. Most of the battery registers are already supported, and I will take a look at the power control registers. Displaying the status should be no problem, while the writable registers I will need to have a better look at to make sure people won't be setting their houses on fire.
Maybe this could be done with a command line switch or something like that? If you have a look at my BSB-LAN project which deals with various heating systems, you can see that this also enables lots of features usually not available to the everyday user, and I was in doubt as well whether I should make these accessible, but in the end these devices (must) have security measures in place that prevent hazardous results. Plus, this is an official document which lists these parameters as read/writeable, so external software is allowed to make use of this.
I just tried adding a line in init.py such as
"c_storage_control_mode": (0xe004 + self.offset, 1, registerType.HOLDING, registerDataType.UINT16, int, "Storage Control Mode", "", 1),
and I can read the value without a problem then using read()
, but it is not listed when running example.py --json.
Thanks for the new version! But did I get it right that parameter e004 is not yet implemented?
I haven't had time to work through all of the storage and export control document. Feel free to send a pr.
I just tried adding a line in init.py such as
"c_storage_control_mode": (0xe004 + self.offset, 1, registerType.HOLDING, registerDataType.UINT16, int, "Storage Control Mode", "", 1),
That should work, but you will need to set the last variable (batch number) to 3 or 4.
Sorry, I didn't mean to push at all, I just read about the added parameters in the new release and wasn't sure if the code was working correctly with my battery. I've never coded in Python, so I'd be a bit reluctant to send a PR, but if it's just adding lines like the one you quoted, I could do that, however, I'm off-site until mid-August, so I cannot really test until then. As for the batch number, how do I know whether it's 3 or 4? And is there a mechanism that decodes the values into human-readable options?
Sorry, I didn't mean to push at all, I just read about the added parameters in the new release and wasn't sure if the code was working correctly with my battery.
No worries.
As for the batch number, how do I know whether it's 3 or 4? And is there a mechanism that decodes the values into human-readable options?
The batch number is based on a maximum number of register that can be polled sequentially. It can be 3 if it fits in the range of the other registers in batch 3. Otherwise, it will become batch 4. This should only incur a small performance penalty.
Human readable options can be specified in the second to last parameter in a register definition. Usually this field is for a human readable unit (W, kW, %, for example), unless it is a list, in which case integer values can be mapped to human readable strings. Actually translating the register value to this mapped string still requires a bit of work, see examply.py line 51 for example.
Sorry, I still don't get the concept behind the batch number: When I look at all the entries, the number seems to increase from 1 to 4 every few entries. How/where is one batch defined/configured? As for human readable options, I think I got it. (At least) for my purpose, numeric values will probably be sufficient, but for the sake of completeness, it may be good to add them nevertheless.
Sorry, I still don't get the concept behind the batch number: When I look at all the entries, the number seems to increase from 1 to 4 every few entries. How/where is one batch defined/configured?
Modbus registers are read from a device in sequential blocks. These blocks have a maximum size. The maximum size is not large enough to read all the registers we want in one read request. Therefore we split the reads so the total size of the request is less than the maximum, but reads as many registers in one go. This is faster than sending a read request for each register individually. Therefore, the batch number increases if the address to be read plus its size minus the first address read in that batch is greater than the maximum request size.
Ah, ok, thanks, I think I got it. Is the maximum request size defined somewhere or do I have to find out by trial and error? And should I add new parameter lines based on their address or add them at the end of each block?
I'm sorry, I don't think I get the way the self.registers works. If I add
"storage_control_mode": (0xe004, 1, registerType.HOLDING, registerDataType.UINT16, int, "Storage Control Mode", "", 3),
"storage_ac_charge_policy": (0xe005, 1, registerType.HOLDING, registerDataType.UINT16, int, "Storage AC Charge Policy", "", 3),
"storage_ac_charge_limit": (0xe006, 2, registerType.HOLDING, registerDataType.FLOAT32, float, "Storage AC Charge Limit", "", 3),
"storage_backup_reserved_setting": (0xe008, 2, registerType.HOLDING, registerDataType.FLOAT32, float, "Storage Backup Reserved Setting", "%", 3),
"storage_default_mode": (0xe00a, 1, registerType.HOLDING, registerDataType.UINT16, int, "Storage Charge/Discharge Default Mode", "", 3),
"rc_cmd_timeout": (0xe00b, 2, registerType.HOLDING, registerDataType.UINT32, int, "Remote Control Command Timeout", "s", 3),
"rc_cmd_mode": (0xe00d, 1, registerType.HOLDING, registerDataType.UINT16, int, "Remote Control Command Mode", "", 3),
"rc_charge_limit": (0xe00e, 2, registerType.HOLDING, registerDataType.FLOAT32, float, "Remote Control Command Charge Limit", "W", 3),
"rc_discharge_limit": (0xe010, 2, registerType.HOLDING, registerDataType.FLOAT32, float, "Remote Control Command Discharge Limit", "W", 3),
before
"rrcr_state": (0xf000, 1, registerType.HOLDING, registerDataType.UINT16, int, "RRCR State", "", 3),
the additional parameters are not shown when running example.py. However, if I comment out the lines from rrcr_state until cosphi, the new parameters show up, but some other values show up completely messed up:
"rated_energy": -3.4028234663852886e+38,
"maximum_charge_continuous_power": -3.4028234663852886e+38,
"maximum_discharge_continuous_power": -3.4028234663852886e+38,
"maximum_charge_peak_power": -3.4028234663852886e+38,
"maximum_discharge_peak_power": -3.4028234663852886e+38,
"average_temperature": -3.4028234663852886e+38,
"maximum_temperature": 0.0,
"instantaneous_voltage": -3.4028234663852886e+38,
"instantaneous_current": -3.4028234663852886e+38,
"instantaneous_power": 0.0,
"lifetime_export_energy_counter": 0,
"lifetime_import_energy_counter": 0,
"maximum_energy": -3.4028234663852886e+38,
"available_energy": -3.4028234663852886e+38,
"soh": -3.4028234663852886e+38,
"soe": -3.4028234663852886e+38,
I've tried batch 3 and 4, but no difference. Maybe you could try and add the lines above and then I know how to do it?
Correction, the invalid values seem to only come up occasionally, it now seems to work, but I still have to comment out the rrcr_state until cosphi lines.
I hope to have time this weekend to start adding some of the other registers.
Thanks! In the meantime, can I somehow use a "fixed" attribute for the data type encoding when writing? For now, I only want to write UINT16 values to a holding register, so I assume that no encoding is necessary if I just pass the value directly?
Thanks! In the meantime, can I somehow use a "fixed" attribute for the data type encoding when writing? For now, I only want to write UINT16 values to a holding register, so I assume that no encoding is necessary if I just pass the value directly?
You provide the register name to the write function. The encoding is then done depending on the register's data type. So, if for example you were able to write to the power_ac
register, which has the following definition:
"power_ac": (0x9c93, 1, registerType.HOLDING, registerDataType.INT16, int, "Power", "W", 2)
The function inverter.write("power_ac", 1234)
will have 1234
encoded as an int
. I wouldn't recommend passing a string "1234"
, the modbus library might not be kind to you if you do this.
Thanks, passing an int works fine, I'm now wondering how to pass 32-Bit integer - these occupy two ModBus registers, if I'm not mistaken, and the endian-ness seems to be different. Could I still just do it the cheap way by sending the two bytes reversed to inverter.write()
or do I have to call it twice with an increased address or completely different?
Thanks, passing an int works fine,
Really, how? I just noticed the necessary function to encode data to a register (_encode_value
) was missing.
Could I still just do it the cheap way by sending the two bytes reversed to
inverter.write()
or do I have to call it twice with an increased address or completely different?
Have a look at the _write
function, and check out _encode_value
. You'll see that your input is encoded according to the register type as defined in the register definition.
Yes, the function is (was?) missing, I just removed the encode_value
function and passed the data directly like this:
return self._write_holding_register(address, data)
This works for int16 values, but not other data types. But I just saw that you added that function an hour ago, thanks!
And did you have a chance to figure out why adding parameters results in such a strange behaviour that some existing parameters have to be removed for new ones to work? And what bugs me more is the fact that non-INT16 values seem to be quite a bit off:
"rc_cmd_timeout": 235929600,
"rc_charge_limit": 2.00424861907959,
"rc_discharge_limit": 2.00424861907959,
rc_cmd_timeout
for example should be 3600 (the default value), and the charge and discharge limit also make no sense...
I think it's a matter of endianness: 3600 is 0x00000E10, the above value of 235929600 is 0x0E100000. However, I have not installed the most recent version, so in case this is also covered by your changes, please ignore!
I am interested in also changing the options on the inverter also (i.e when the feed in tariff for solar goes negative (getting charged for exporting) I would like to automate the option to change the inverter to zero export.
I am already this to read the data from the inverters, but it would be great to get some assistance on being able to "push" the option changes too. I don't have the experience, or the knowledge apart from basic python changes. do you or anyone have an example in python of pushing changes back to the inverter?
there is the repo https://github.com/binsentsu/home-assistant-solaredge-modbus where it is able to change the settings on the inverter by writing the changes to the inverter, but it is for homeassistant - is there something similar that could be implemented easily?
@herbi3, have you tried using the write()
function on the active_power_limit
register?
thanks @nmakel, I got the active power limit to work, however i am trying to change the export control mode, limit and site limit (I have x2 inverters, trying to get it to still generate but only to match the load demand from the SE meter.)
I tried adding
"export_control_mode": (0xe000, 1, registerType.HOLDING, registerDataType.UINT16, int, "Export Control Mode", "", 1),
export_control_limit_mode": (0xe001, 1, registerType.HOLDING, registerDataType.UINT16, int, "Export Control Limit Mode", "", 1),
export_control_site_limit": (0xe002, 2, registerType.HOLDING, registerDataType.FLOAT32, int, "Export Control Site Limit", "W", 2),
ultimately I don't know what I'm doing, but I am giving it a good crack.
I tried adding
"export_control_mode": (0xe000, 1, registerType.HOLDING, registerDataType.UINT16, int, "Export Control Mode", "", 1),
export_control_limit_mode": (0xe001, 1, registerType.HOLDING, registerDataType.UINT16, int, "Export Control Limit Mode", "", 1),
export_control_site_limit": (0xe002, 2, registerType.HOLDING, registerDataType.FLOAT32, int, "Export Control Site Limit", "W", 2),
ultimately I don't know what I'm doing, but I am giving it a good crack.
Trying this yourself is very much appreciated. I had a quick look at the documentation and it looks like it's a bit more convoluted than just writing to export_control_site_limit
. There's a number of other registers that need to be set to the correct value for this to work.
I will have a look at defining these registers in the next day or so. After that it should be as simple as calling write in the correct order to a number of registers to do what you need.
Ok @herbi3. Attached is a modified __init__.py
which has the required registers and supporting functionality added.
Let me state the following before you begin: I have not tested this. I do not know what unintended consequences can result from messing with these values. Only do this if you have access to the inverter in order to revert any changes you make.
According to the documentation you'll need to:
- Write
1
toadvanced_power_control_enable
. - Write
4
(RRCR mode) toreactive_power_config
. - Write
1
tocommit_power_control_settings
to commit the above. - Decide how you want to configure export control (see page 15 of the document). In order to generate the correct value from the bitmap, use a tool like this. Write the
uint16
value toexport_control_mode
. - If you want to set a manual export limit, you can write the wattage to
export_control_site_limit
.
thank you @nmakel I really appreciate this and all the work you've put into this!
I tried the changing of settings and commit, however it I get this error from enum
Traceback (most recent call last): File "./solaredge-emoncms.py.bak.20221006", line 29, in <module> master = solaredge_modbus.Inverter(host="10.0.2.100", port=1502) File "/home/administrator/.local/lib/python3.8/site-packages/solaredge_modbus/__init__.py", line 493, in __init__ "reactive_power_config": (0xf104, 2, registerType.HOLDING, registerDataType.INT32, int, "Reactive Power Config", REACTIVE_POWER_CONFIG_MAP, 4), File "/usr/lib/python3.8/enum.py", line 384, in __getattr__ raise AttributeError(name) from None AttributeError: INT32
I saw that INT32 is missing from registerDataType, but im not cleaver enough to figure out what value to put into the register, I tried INT32 = 4
at first and got ValueError: not enough values to unpack (expected 8, got 7)
when trying to write the data
Interesting - I didn't have to enable the advanced power control or change the RRCR mode (Although it is explicitly stated in the Solaredge documentation)
I wrote the changes for export_control_mode & export_control_limit_mode - once that was enabled, it started to try and read from export_control_site_limit
which gave not a number NaN
- After writing the value, it was then able to read the value during the inverter read all function.
thank you @nmakel I really appreciate this and all the work you've put into this!
I tried the changing of settings and commit, however it I get this error from enum
Traceback (most recent call last): File "./solaredge-emoncms.py.bak.20221006", line 29, in <module> master = solaredge_modbus.Inverter(host="10.0.2.100", port=1502) File "/home/administrator/.local/lib/python3.8/site-packages/solaredge_modbus/__init__.py", line 493, in __init__ "reactive_power_config": (0xf104, 2, registerType.HOLDING, registerDataType.INT32, int, "Reactive Power Config", REACTIVE_POWER_CONFIG_MAP, 4), File "/usr/lib/python3.8/enum.py", line 384, in __getattr__ raise AttributeError(name) from None AttributeError: INT32
I saw that INT32 is missing from registerDataType, but im not cleaver enough to figure out what value to put into the register, I tried
INT32 = 4
at first and gotValueError: not enough values to unpack (expected 8, got 7)
when trying to write the data
Try 8.
Interesting - I didn't have to enable the advanced power control or change the RRCR mode (Although it is explicitly stated in the Solaredge documentation)
I wrote the changes for export_control_mode & export_control_limit_mode - once that was enabled, it started to try and read from
export_control_site_limit
which gave not a numberNaN
- After writing the value, it was then able to read the value during the inverter read all function.
Sounds good. Does it work?
Interesting - I didn't have to enable the advanced power control or change the RRCR mode (Although it is explicitly stated in the Solaredge documentation)
I wrote the changes for export_control_mode & export_control_limit_mode - once that was enabled, it started to try and read from export_control_site_limit which gave not a number NaN - After writing the value, it was then able to read the value during the inverter read all function. Sounds good. Does it work?
this particular part works just as intended, thank you!