From 7def610ff2b983bb37c874f7032bfebd3ba09305 Mon Sep 17 00:00:00 2001 From: Johan-EU <33609553+Johan-EU@users.noreply.github.com> Date: Sat, 20 May 2023 19:52:44 +0200 Subject: [PATCH] Read/Write Active Power Limit --- README.md | 17 ++++++ .../solaredge_modbus/__init__.py | 57 +++++++++++++++---- .../solaredge_modbus/config_flow.py | 3 + custom_components/solaredge_modbus/const.py | 6 +- custom_components/solaredge_modbus/number.py | 29 +++++++++- .../solaredge_modbus/strings.json | 1 + .../solaredge_modbus/translations/de.json | 3 +- .../solaredge_modbus/translations/en.json | 3 +- .../solaredge_modbus/translations/nb.json | 3 +- .../solaredge_modbus/translations/nl.json | 3 +- 10 files changed, 107 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 5670507..392acbf 100644 --- a/README.md +++ b/README.md @@ -33,5 +33,22 @@ Appendix B of the Solaredge [power control document][2] gives the necessary step Note that if you allow the battery to be charged from the grid, via the "Storage AC Charge Policy" selector, the self-consumption metric will disappear from the Solaredge monitoring - according to Solaredge technical support, this is because "they can't tell where the energy came from". +# Control of inverter power output +The active power limit of the inverter can be set from Home Assistant. This enables limiting or completely shutting down power output. For example in case of a dynamic energy contract in periods with a negative energy price. + +The active power limit is set as the percentage of the inverter’s maximum power via `number.solaredge_active_power_limit`. For example: when you have a SE5000 inverter that has a maximum output of 5000W, setting the value of `number.solaredge_active_power_limit` to 20 will limit the inverter to 1000W which is 20% of 5000W. See [Power Control Protocol for Solaredge Inverters Technical Note][2] for detailed information. + +## Enabling Power Control in Home Assistant +Power control is disabled by default. It can be enabled in the configuration of this integration by setting `power_control` to true. When power control is enabled `number.solaredge_active_power_limit` is available in Home Assistant for reading and writing. The actual value of the active power limit is read together with the other values of the inverter. + +## Enabling Power Control on SolarEdge Inverter +With default settings the inverter will not allow power control. It can be enabled in the same way that Modbus TCP is enabled on the SolarEdge Inverter, by connecting to the inverter with a web browser. + +These to settings must be applied in the Power Control menu: + +1. Set Advanced Power Control to Enable +2. Set Reactive Power mode to RRCR + + [1]: https://www.solaredge.com/sites/default/files/sunspec-implementation-technical-note.pdf [2]: https://www.photovoltaikforum.com/core/attachment/88445-power-control-open-protocol-for-solaredge-inverters-pdf/ diff --git a/custom_components/solaredge_modbus/__init__.py b/custom_components/solaredge_modbus/__init__.py index 304cb56..148aede 100644 --- a/custom_components/solaredge_modbus/__init__.py +++ b/custom_components/solaredge_modbus/__init__.py @@ -23,11 +23,13 @@ DEFAULT_SCAN_INTERVAL, DEFAULT_MODBUS_ADDRESS, CONF_MODBUS_ADDRESS, + CONF_POWER_CONTROL, CONF_READ_METER1, CONF_READ_METER2, CONF_READ_METER3, CONF_READ_BATTERY1, CONF_READ_BATTERY2, + DEFAULT_POWER_CONTROL, DEFAULT_READ_METER1, DEFAULT_READ_METER2, DEFAULT_READ_METER3, @@ -51,6 +53,7 @@ vol.Optional( CONF_MODBUS_ADDRESS, default=DEFAULT_MODBUS_ADDRESS ): cv.positive_int, + vol.Optional(CONF_POWER_CONTROL, default=DEFAULT_POWER_CONTROL): cv.boolean, vol.Optional(CONF_READ_METER1, default=DEFAULT_READ_METER1): cv.boolean, vol.Optional(CONF_READ_METER2, default=DEFAULT_READ_METER2): cv.boolean, vol.Optional(CONF_READ_METER3, default=DEFAULT_READ_METER3): cv.boolean, @@ -82,11 +85,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): port = entry.data[CONF_PORT] address = entry.data.get(CONF_MODBUS_ADDRESS, 1) scan_interval = entry.data[CONF_SCAN_INTERVAL] - read_meter1 = entry.data.get(CONF_READ_METER1, False) - read_meter2 = entry.data.get(CONF_READ_METER2, False) - read_meter3 = entry.data.get(CONF_READ_METER3, False) - read_battery1 = entry.data.get(CONF_READ_BATTERY1, False) - read_battery2 = entry.data.get(CONF_READ_BATTERY2, False) + power_control = entry.data.get(CONF_POWER_CONTROL, DEFAULT_POWER_CONTROL), + read_meter1 = entry.data.get(CONF_READ_METER1, DEFAULT_READ_METER1) + read_meter2 = entry.data.get(CONF_READ_METER2, DEFAULT_READ_METER2) + read_meter3 = entry.data.get(CONF_READ_METER3, DEFAULT_READ_METER3) + read_battery1 = entry.data.get(CONF_READ_BATTERY1, DEFAULT_READ_BATTERY1) + read_battery2 = entry.data.get(CONF_READ_BATTERY2, DEFAULT_READ_BATTERY2) _LOGGER.debug("Setup %s.%s", DOMAIN, name) @@ -97,6 +101,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): port, address, scan_interval, + power_control, read_meter1, read_meter2, read_meter3, @@ -155,11 +160,12 @@ def __init__( port, address, scan_interval, - read_meter1=False, - read_meter2=False, - read_meter3=False, - read_battery1=False, - read_battery2=False, + power_control=DEFAULT_POWER_CONTROL, + read_meter1=DEFAULT_READ_METER1, + read_meter2=DEFAULT_READ_METER2, + read_meter3=DEFAULT_READ_METER3, + read_battery1=DEFAULT_READ_BATTERY1, + read_battery2=DEFAULT_READ_BATTERY2, ): """Initialize the Modbus hub.""" self._hass = hass @@ -167,6 +173,7 @@ def __init__( self._lock = threading.Lock() self._name = name self._address = address + self.power_control = power_control self.read_meter1 = read_meter1 self.read_meter2 = read_meter2 self.read_meter3 = read_meter3 @@ -230,6 +237,11 @@ def connect(self): with self._lock: self._client.connect() + @property + def power_control_enabled(self): + """Return true if power control has been enabled""" + return self.power_control + @property def has_meter(self): """Return true if a meter is available""" @@ -260,6 +272,7 @@ def calculate_value(self, value, sf): def read_modbus_data(self): return ( self.read_modbus_data_inverter() + and self.read_modbus_power_limit() and self.read_modbus_data_meter1() and self.read_modbus_data_meter2() and self.read_modbus_data_meter3() @@ -665,6 +678,30 @@ def read_modbus_data_inverter(self): return True + def read_modbus_power_limit(self): + """ + Read the active power limit value (%) + """ + + if not self.power_control_enabled: + return True + + inverter_data = self.read_holding_registers( + unit=self._address, address=0xF001, count=1 + ) + if inverter_data.isError(): + _LOGGER.debug("Could not read Active Power Limit") + # Don't stop reading other data, could just be advanced power management not enabled + return True + + decoder = BinaryPayloadDecoder.fromRegisters( + inverter_data.registers, byteorder=Endian.Big, wordorder=Endian.Little + ) + # 0xF001 - 1 - Active Power Limit + self.data["nominal_active_power_limit"] = decoder.decode_16bit_uint() + + return True + def read_modbus_data_storage(self): if self.has_battery: count = 0x12 # Read storedge block as well diff --git a/custom_components/solaredge_modbus/config_flow.py b/custom_components/solaredge_modbus/config_flow.py index d4b8678..b0241dc 100644 --- a/custom_components/solaredge_modbus/config_flow.py +++ b/custom_components/solaredge_modbus/config_flow.py @@ -12,11 +12,13 @@ DEFAULT_PORT, DEFAULT_MODBUS_ADDRESS, CONF_MODBUS_ADDRESS, + CONF_POWER_CONTROL, CONF_READ_METER1, CONF_READ_METER2, CONF_READ_METER3, CONF_READ_BATTERY1, CONF_READ_BATTERY2, + DEFAULT_POWER_CONTROL, DEFAULT_READ_METER1, DEFAULT_READ_METER2, DEFAULT_READ_METER3, @@ -31,6 +33,7 @@ vol.Required(CONF_HOST): str, vol.Required(CONF_PORT, default=DEFAULT_PORT): int, vol.Optional(CONF_MODBUS_ADDRESS, default=DEFAULT_MODBUS_ADDRESS): int, + vol.Optional(CONF_POWER_CONTROL, default=DEFAULT_POWER_CONTROL): bool, vol.Optional(CONF_READ_METER1, default=DEFAULT_READ_METER1): bool, vol.Optional(CONF_READ_METER2, default=DEFAULT_READ_METER2): bool, vol.Optional(CONF_READ_METER3, default=DEFAULT_READ_METER3): bool, diff --git a/custom_components/solaredge_modbus/const.py b/custom_components/solaredge_modbus/const.py index e21b8ea..e8c4341 100644 --- a/custom_components/solaredge_modbus/const.py +++ b/custom_components/solaredge_modbus/const.py @@ -3,6 +3,7 @@ DEFAULT_SCAN_INTERVAL = 30 DEFAULT_PORT = 1502 DEFAULT_MODBUS_ADDRESS = 1 +DEFAULT_POWER_CONTROL = False DEFAULT_READ_METER1 = False DEFAULT_READ_METER2 = False DEFAULT_READ_METER3 = False @@ -12,6 +13,7 @@ ATTR_STATUS_DESCRIPTION = "status_description" ATTR_MANUFACTURER = "Solaredge" CONF_MODBUS_ADDRESS = "modbus_address" +CONF_POWER_CONTROL = "power_control" CONF_READ_METER1 = "read_meter_1" CONF_READ_METER2 = "read_meter_2" CONF_READ_METER3 = "read_meter_3" @@ -331,6 +333,8 @@ ["Export control site limit", "export_control_site_limit", 0xE002, "f", {"min": 0, "max": 10000, "unit": "W"}], ] +ACTIVE_POWER_LIMIT_TYPE = ["Active Power Limit", "nominal_active_power_limit", 0xF001, "u16", {"min": 0, "max": 100, "unit": "%"}] + STORAGE_SELECT_TYPES = [ ["Storage Control Mode", "storage_contol_mode", 0xE004, STOREDGE_CONTROL_MODE], ["Storage AC Charge Policy", "storage_ac_charge_policy", 0xE005, STOREDGE_AC_CHARGE_POLICY], @@ -342,7 +346,7 @@ STORAGE_NUMBER_TYPES = [ ["Storage AC Charge Limit", "storage_ac_charge_limit", 0xE006, "f", {"min": 0, "max": 100000000000}], ["Storage Backup reserved", "storage_backup_reserved", 0xE008, "f", {"min": 0, "max": 100, "unit": "%"}], - ["Storage Remote Command Timeout", "storage_remote_command_timeout", 0xE00B, "i", {"min": 0, "max": 86400, "unit": "s"}], + ["Storage Remote Command Timeout", "storage_remote_command_timeout", 0xE00B, "u32", {"min": 0, "max": 86400, "unit": "s"}], ["Storage Remote Charge Limit", "storage_remote_charge_limit", 0xE00E, "f", {"min": 0, "max": 20000, "unit": "W"}], ["Storage Remote Discharge Limit", "storage_remote_discharge_limit", 0xE010, "f", {"min": 0, "max": 20000, "unit": "W"}], ] diff --git a/custom_components/solaredge_modbus/number.py b/custom_components/solaredge_modbus/number.py index 3a6e873..7ed1c66 100644 --- a/custom_components/solaredge_modbus/number.py +++ b/custom_components/solaredge_modbus/number.py @@ -4,6 +4,7 @@ from .const import ( DOMAIN, ATTR_MANUFACTURER, + ACTIVE_POWER_LIMIT_TYPE, EXPORT_CONTROL_NUMBER_TYPES, STORAGE_NUMBER_TYPES, ) @@ -33,6 +34,20 @@ async def async_setup_entry(hass, entry, async_add_entities) -> None: entities = [] + # If power control is enabled add power control + if hub.power_control_enabled: + number = SolarEdgeNumber( + hub_name, + hub, + device_info, + ACTIVE_POWER_LIMIT_TYPE[0], + ACTIVE_POWER_LIMIT_TYPE[1], + ACTIVE_POWER_LIMIT_TYPE[2], + ACTIVE_POWER_LIMIT_TYPE[3], + ACTIVE_POWER_LIMIT_TYPE[4] + ) + entities.append(number) + # If a meter is available add export control if hub.has_meter: for number_info in EXPORT_CONTROL_NUMBER_TYPES: @@ -127,12 +142,20 @@ async def async_set_native_value(self, value: float) -> None: """Change the selected value.""" builder = BinaryPayloadBuilder(byteorder=Endian.Big, wordorder=Endian.Little) - if self._fmt == "i": + if self._fmt == "u32": builder.add_32bit_uint(int(value)) + elif self._fmt =="u16": + builder.add_16bit_uint(int(value)) elif self._fmt == "f": builder.add_32bit_float(float(value)) - - self._hub.write_registers(unit=1, address=self._register, payload=builder.to_registers()) + else: + _LOGGER.error(f"Invalid encoding format {self._fmt} for {self._key}") + return + + response = self._hub.write_registers(unit=1, address=self._register, payload=builder.to_registers()) + if response.isError(): + _LOGGER.error(f"Could not write value {value} to {self._key}") + return self._hub.data[self._key] = value self.async_write_ha_state() diff --git a/custom_components/solaredge_modbus/strings.json b/custom_components/solaredge_modbus/strings.json index 91ac051..83ef232 100644 --- a/custom_components/solaredge_modbus/strings.json +++ b/custom_components/solaredge_modbus/strings.json @@ -9,6 +9,7 @@ "name": "The prefix to be used for your SolarEdge sensors", "port": "The TCP port on which to connect to the SolarEdge", "modbus_address": "The modbus address", + "power_control": "Enable setting active power limit", "read_meter_1": "Read meter 1 data (only for meter models)", "read_meter_2": "Read meter 2 data (only for meter models)", "read_meter_3": "Read meter 3 data (only for meter models)", diff --git a/custom_components/solaredge_modbus/translations/de.json b/custom_components/solaredge_modbus/translations/de.json index 9059a27..43caecd 100644 --- a/custom_components/solaredge_modbus/translations/de.json +++ b/custom_components/solaredge_modbus/translations/de.json @@ -8,7 +8,8 @@ "host": "Die IP-Adresse des Solaredge Wechselrichters", "name": "Das Prefix, das für die SolarEdge Sensoren verwendet werden soll", "port": "Der TCP Port des SolarEdge Wechselrichters (z.B. 502)", - "modbus_address": "Modbus-Adresse", + "modbus_address": "Modbus-Adresse", + "power_control": "Einstellung der Wirkleistungsbegrenzung aktivieren", "read_meter_1": "Lese die Stände des Zähler 1 (nur für Modelle mit Zähler)", "read_meter_2": "Lese die Stände des Zähler 2 (nur für Modelle mit Zähler)", "read_meter_3": "Lese die Stände des Zähler 3 (nur für Modelle mit Zähler)", diff --git a/custom_components/solaredge_modbus/translations/en.json b/custom_components/solaredge_modbus/translations/en.json index 69a17bd..dadc493 100644 --- a/custom_components/solaredge_modbus/translations/en.json +++ b/custom_components/solaredge_modbus/translations/en.json @@ -8,7 +8,8 @@ "host": "The ip-address of your Solaredge inverter", "name": "The prefix to be used for your SolarEdge sensors", "port": "The TCP port on which to connect to the SolarEdge inverter", - "modbus_address": "The modbus address", + "modbus_address": "The modbus address", + "power_control": "Enable setting active power limit", "read_meter_1": "Read meter 1 data (only for meter models)", "read_meter_2": "Read meter 2 data (only for meter models)", "read_meter_3": "Read meter 3 data (only for meter models)", diff --git a/custom_components/solaredge_modbus/translations/nb.json b/custom_components/solaredge_modbus/translations/nb.json index 41ad677..cb611f5 100644 --- a/custom_components/solaredge_modbus/translations/nb.json +++ b/custom_components/solaredge_modbus/translations/nb.json @@ -8,7 +8,8 @@ "host": "IP-adressen til din Solaredge-omformer", "name": "Prefikset som skal brukes til SolarEdge-sensorene dine", "port": "TCP-porten som skal kobles til SolarEdge-omformeren", - "modbus_address": "Modbus adresse", + "modbus_address": "Modbus adresse", + "power_control": "Aktiver innstilling av aktiv effektgrense", "read_meter_1": "Les måler 1-data (bare for målermodeller)", "read_meter_2": "Les måler 2-data (bare for målermodeller)", "read_meter_3": "Les måler 3-data (bare for målermodeller)", diff --git a/custom_components/solaredge_modbus/translations/nl.json b/custom_components/solaredge_modbus/translations/nl.json index 288f329..ec45603 100644 --- a/custom_components/solaredge_modbus/translations/nl.json +++ b/custom_components/solaredge_modbus/translations/nl.json @@ -8,7 +8,8 @@ "host": "Het ip-adres van uw Solaredge-omvormer", "name": "Het voorvoegsel dat moet worden gebruikt voor uw SolarEdge-sensoren", "port": "De TCP-poort waarop verbinding moet worden gemaakt met de SolarEdge-omvormer", - "modbus_address": "Modbus adres", + "modbus_address": "Modbus adres", + "power_control": "Activeer het instellen van de limiet voor het actieve vermogen", "read_meter_1": "Lees meter 1 data (enkel voor voor meter modellen)", "read_meter_2": "Lees meter 2 data (enkel voor voor meter modellen)", "read_meter_3": "Lees meter 3 data (enkel voor voor meter modellen)",