From b38f174c4863369392712957835ab13122bb94ff Mon Sep 17 00:00:00 2001 From: WillCodeForCats <48533968+WillCodeForCats@users.noreply.github.com> Date: Sat, 29 Jul 2023 22:45:46 -0700 Subject: [PATCH 1/9] Move to asyncio.timeout from async_timeout New minimum requirement HA 2023.8 and Python 3.11 --- README.md | 2 +- custom_components/solaredge_modbus_multi/__init__.py | 3 +-- hacs.json | 2 +- info.md | 2 +- 4 files changed, 4 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 2e7442da..7edef84a 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ After rebooting Home Assistant, this integration can be configured through the i ### Required Versions * Home Assistant 2023.7.0 or newer -* Python 3.10 or newer +* Python 3.11 or newer * pymodbus 3.3.1 or newer ## Specifications diff --git a/custom_components/solaredge_modbus_multi/__init__.py b/custom_components/solaredge_modbus_multi/__init__.py index e46531a3..ec50f182 100644 --- a/custom_components/solaredge_modbus_multi/__init__.py +++ b/custom_components/solaredge_modbus_multi/__init__.py @@ -6,7 +6,6 @@ from datetime import timedelta from typing import Any -import async_timeout from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_HOST, @@ -220,7 +219,7 @@ async def _refresh_modbus_data_with_retry( attempt = 1 while True: try: - async with async_timeout.timeout(self._hub.coordinator_timeout): + async with asyncio.timeout(self._hub.coordinator_timeout): return await self._hub.async_refresh_modbus_data() except Exception as ex: if not isinstance(ex, ex_type): diff --git a/hacs.json b/hacs.json index 416d38b6..dc500deb 100644 --- a/hacs.json +++ b/hacs.json @@ -1,6 +1,6 @@ { "name": "SolarEdge Modbus Multi", "content_in_root": false, - "homeassistant": "2023.7.0", + "homeassistant": "2023.8.0", "render_readme": false } diff --git a/info.md b/info.md index 7af484c2..07e3fe0a 100644 --- a/info.md +++ b/info.md @@ -22,4 +22,4 @@ Read more on the wiki: [WillCodeForCats/solaredge-modbus-multi/wiki](https://git * Supports status and error reporting sensors. * User friendly configuration through Config Flow. -Requires Home Assistant 2023.7.0 and newer. +Requires Home Assistant 2023.8.0 and newer. From 165019cc5ae3ac1868c4a7d1ff7fb9d4bad46fb8 Mon Sep 17 00:00:00 2001 From: WillCodeForCats <48533968+WillCodeForCats@users.noreply.github.com> Date: Sat, 29 Jul 2023 22:47:28 -0700 Subject: [PATCH 2/9] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7edef84a..d4207cc5 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ After rebooting Home Assistant, this integration can be configured through the i [WillCodeForCats/solaredge-modbus-multi/wiki](https://github.com/WillCodeForCats/solaredge-modbus-multi/wiki) ### Required Versions -* Home Assistant 2023.7.0 or newer +* Home Assistant 2023.8.0 or newer * Python 3.11 or newer * pymodbus 3.3.1 or newer From 60b1685448dbd50d1647dea7e9e43667913eeb81 Mon Sep 17 00:00:00 2001 From: WillCodeForCats <48533968+WillCodeForCats@users.noreply.github.com> Date: Sat, 26 Aug 2023 08:40:02 -0700 Subject: [PATCH 3/9] Add dynamic power control config settings Add number entities to set Active Power Limit and CosPhi values --- .../solaredge_modbus_multi/number.py | 117 ++++++++++++++++++ 1 file changed, 117 insertions(+) diff --git a/custom_components/solaredge_modbus_multi/number.py b/custom_components/solaredge_modbus_multi/number.py index a31c372c..1a776fe2 100644 --- a/custom_components/solaredge_modbus_multi/number.py +++ b/custom_components/solaredge_modbus_multi/number.py @@ -28,6 +28,13 @@ async def async_setup_entry( entities = [] + for inverter in hub.inverters: + if hub.option_detect_extras: + entities.append( + SolarEdgeActivePowerLimitSet(inverter, config_entry, coordinator) + ) + entities.append(SolarEdgeCosPhiSet(inverter, config_entry, coordinator)) + """ Power Control Options: Storage Control """ if hub.option_storage_control is True: for inverter in hub.inverters: @@ -471,3 +478,113 @@ async def async_set_native_value(self, value: float) -> None: address=57362, payload=builder.to_registers() ) await self.async_update() + + +class SolarEdgeActivePowerLimitSet(SolarEdgeNumberBase): + """Global Dynamic Power Control: Set Inverter Active Power Limit""" + + native_unit_of_measurement = PERCENTAGE + native_min_value = 0 + native_max_value = 100 + mode = "slider" + icon = "mdi:percent" + + def __init__(self, inverter, config_entry, coordinator): + super().__init__(inverter, config_entry, coordinator) + + @property + def unique_id(self) -> str: + return f"{self._platform.uid_base}_active_power_limit_set" + + @property + def name(self) -> str: + return "Active Power Limit" + + @property + def entity_registry_enabled_default(self) -> bool: + if self._platform.global_power_control is True: + return True + else: + return False + + @property + def available(self) -> bool: + try: + if ( + self._platform.decoded_model["I_Power_Limit"] == SunSpecNotImpl.UINT16 + or self._platform.decoded_model["I_Power_Limit"] > 100 + or self._platform.decoded_model["I_Power_Limit"] < 0 + ): + return False + + return super().available + + except KeyError: + return False + + @property + def native_value(self) -> int: + return self._platform.decoded_model["I_Power_Limit"] + + async def async_set_native_value(self, value: float) -> None: + _LOGGER.debug(f"set {self.unique_id} to {value}") + builder = BinaryPayloadBuilder(byteorder=Endian.Big, wordorder=Endian.Little) + builder.add_16bit_uint(int(value)) + await self._platform.write_registers( + address=61441, payload=builder.to_registers() + ) + await self.async_update() + + +class SolarEdgeCosPhiSet(SolarEdgeNumberBase): + """Global Dynamic Power Control: Set Inverter CosPhi""" + + native_min_value = -1.0 + native_max_value = 1.0 + native_step = 0.1 + mode = "slider" + icon = "mdi:angle-acute" + + def __init__(self, inverter, config_entry, coordinator): + super().__init__(inverter, config_entry, coordinator) + + @property + def unique_id(self) -> str: + return f"{self._platform.uid_base}_cosphi_set" + + @property + def name(self) -> str: + return "CosPhi" + + @property + def entity_registry_enabled_default(self) -> bool: + return False + + @property + def available(self) -> bool: + try: + if ( + float_to_hex(self._platform.decoded_model["I_CosPhi"]) + == hex(SunSpecNotImpl.FLOAT32) + or self._platform.decoded_model["I_CosPhi"] > 1.0 + or self._platform.decoded_model["I_CosPhi"] < -1.0 + ): + return False + + return super().available + + except KeyError: + return False + + @property + def native_value(self): + return round(self._platform.decoded_model["I_CosPhi"], 1) + + async def async_set_native_value(self, value: float) -> None: + _LOGGER.debug(f"set {self.unique_id} to {value}") + builder = BinaryPayloadBuilder(byteorder=Endian.Big, wordorder=Endian.Little) + builder.add_32bit_float(float(value)) + await self._platform.write_registers( + address=61442, payload=builder.to_registers() + ) + await self.async_update() From b30f05127a669ea7ac2ea592fc115c32d97f97c7 Mon Sep 17 00:00:00 2001 From: WillCodeForCats <48533968+WillCodeForCats@users.noreply.github.com> Date: Sat, 26 Aug 2023 08:40:28 -0700 Subject: [PATCH 4/9] Add docstrings and update registry defaults --- custom_components/solaredge_modbus_multi/sensor.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/custom_components/solaredge_modbus_multi/sensor.py b/custom_components/solaredge_modbus_multi/sensor.py index 407e9e14..cb4bb5f1 100644 --- a/custom_components/solaredge_modbus_multi/sensor.py +++ b/custom_components/solaredge_modbus_multi/sensor.py @@ -1304,6 +1304,8 @@ def extra_state_attributes(self): class SolarEdgeActivePowerLimit(SolarEdgeGlobalPowerControlBlock): + """Global Dynamic Power Control: Inverter Active Power Limit""" + state_class = SensorStateClass.MEASUREMENT native_unit_of_measurement = PERCENTAGE suggested_display_precision = 0 @@ -1323,10 +1325,7 @@ def name(self) -> str: @property def entity_registry_enabled_default(self) -> bool: - if self._platform.global_power_control is True: - return True - else: - return False + return self._platform.global_power_control @property def native_value(self): @@ -1346,6 +1345,8 @@ def native_value(self): class SolarEdgeCosPhi(SolarEdgeGlobalPowerControlBlock): + """Global Dynamic Power Control: Inverter CosPhi""" + state_class = SensorStateClass.MEASUREMENT suggested_display_precision = 1 icon = "mdi:angle-acute" @@ -1364,7 +1365,7 @@ def name(self) -> str: @property def entity_registry_enabled_default(self) -> bool: - return False + return self._platform.global_power_control @property def native_value(self): From e0c95722746b8e54f00bca6b7013f2ddcd9dc0f4 Mon Sep 17 00:00:00 2001 From: WillCodeForCats <48533968+WillCodeForCats@users.noreply.github.com> Date: Sun, 27 Aug 2023 15:22:58 -0700 Subject: [PATCH 5/9] Move inverter command delay option to top level Move inverter command delay to top level since this can now also apply to Auto-Detect Additional Entities even when Power Control Options is disabled. --- .../solaredge_modbus_multi/config_flow.py | 29 +++++++++---------- .../solaredge_modbus_multi/strings.json | 6 ++-- .../translations/de.json | 6 ++-- .../translations/en.json | 6 ++-- .../translations/fr.json | 6 ++-- .../translations/nb.json | 6 ++-- .../translations/nl.json | 6 ++-- .../translations/pl.json | 6 ++-- 8 files changed, 35 insertions(+), 36 deletions(-) diff --git a/custom_components/solaredge_modbus_multi/config_flow.py b/custom_components/solaredge_modbus_multi/config_flow.py index eb7e942a..3bfaef8c 100644 --- a/custom_components/solaredge_modbus_multi/config_flow.py +++ b/custom_components/solaredge_modbus_multi/config_flow.py @@ -114,6 +114,10 @@ async def async_step_init(self, user_input=None) -> FlowResult: errors[CONF_SCAN_INTERVAL] = "invalid_scan_interval" elif user_input[CONF_SCAN_INTERVAL] > 86400: errors[CONF_SCAN_INTERVAL] = "invalid_scan_interval" + elif user_input[ConfName.SLEEP_AFTER_WRITE] < 0: + errors[ConfName.SLEEP_AFTER_WRITE] = "invalid_sleep_interval" + elif user_input[ConfName.SLEEP_AFTER_WRITE] > 60: + errors[ConfName.SLEEP_AFTER_WRITE] = "invalid_sleep_interval" else: if user_input[ConfName.DETECT_BATTERIES] is True: self.init_info = user_input @@ -146,6 +150,9 @@ async def async_step_init(self, user_input=None) -> FlowResult: ConfName.ADV_PWR_CONTROL: self.config_entry.options.get( ConfName.ADV_PWR_CONTROL, bool(ConfDefaultFlag.ADV_PWR_CONTROL) ), + ConfName.SLEEP_AFTER_WRITE: self.config_entry.options.get( + ConfName.SLEEP_AFTER_WRITE, ConfDefaultInt.SLEEP_AFTER_WRITE + ), } return self.async_show_form( @@ -176,6 +183,10 @@ async def async_step_init(self, user_input=None) -> FlowResult: f"{ConfName.ADV_PWR_CONTROL}", default=user_input[ConfName.ADV_PWR_CONTROL], ): cv.boolean, + vol.Optional( + f"{ConfName.SLEEP_AFTER_WRITE}", + default=user_input[ConfName.SLEEP_AFTER_WRITE], + ): vol.Coerce(int), }, ), errors=errors, @@ -241,14 +252,9 @@ async def async_step_adv_pwr_ctl(self, user_input=None) -> FlowResult: errors = {} if user_input is not None: - if user_input[ConfName.SLEEP_AFTER_WRITE] < 0: - errors[ConfName.SLEEP_AFTER_WRITE] = "invalid_sleep_interval" - elif user_input[ConfName.SLEEP_AFTER_WRITE] > 60: - errors[ConfName.SLEEP_AFTER_WRITE] = "invalid_sleep_interval" - else: - return self.async_create_entry( - title="", data={**self.init_info, **user_input} - ) + return self.async_create_entry( + title="", data={**self.init_info, **user_input} + ) else: user_input = { @@ -260,9 +266,6 @@ async def async_step_adv_pwr_ctl(self, user_input=None) -> FlowResult: ConfName.ADV_SITE_LIMIT_CONTROL, bool(ConfDefaultFlag.ADV_SITE_LIMIT_CONTROL), ), - ConfName.SLEEP_AFTER_WRITE: self.config_entry.options.get( - ConfName.SLEEP_AFTER_WRITE, ConfDefaultInt.SLEEP_AFTER_WRITE - ), } return self.async_show_form( @@ -277,10 +280,6 @@ async def async_step_adv_pwr_ctl(self, user_input=None) -> FlowResult: f"{ConfName.ADV_SITE_LIMIT_CONTROL}", default=user_input[ConfName.ADV_SITE_LIMIT_CONTROL], ): cv.boolean, - vol.Optional( - f"{ConfName.SLEEP_AFTER_WRITE}", - default=user_input[ConfName.SLEEP_AFTER_WRITE], - ): vol.Coerce(int), } ), errors=errors, diff --git a/custom_components/solaredge_modbus_multi/strings.json b/custom_components/solaredge_modbus_multi/strings.json index e2bb847f..52c51469 100644 --- a/custom_components/solaredge_modbus_multi/strings.json +++ b/custom_components/solaredge_modbus_multi/strings.json @@ -36,15 +36,15 @@ "detect_meters": "Auto-Detect Meters", "detect_batteries": "Auto-Detect Batteries", "detect_extras": "Auto-Detect Additional Entities", - "advanced_power_control": "Power Control Options" + "advanced_power_control": "Power Control Options", + "sleep_after_write": "Inverter Command Delay (seconds)" } }, "adv_pwr_ctl": { "title": "Power Control Options", "data": { "adv_storage_control": "Enable Storage Control", - "adv_site_limit_control": "Enable Site Limit Control", - "sleep_after_write": "Inverter Command Delay (seconds)" + "adv_site_limit_control": "Enable Site Limit Control" }, "description": "Warning: These options can violate utility agreements, alter your utility billing, may require special equipment, and overwrite provisioning by SolarEdge or your installer. Use at your own risk!" }, diff --git a/custom_components/solaredge_modbus_multi/translations/de.json b/custom_components/solaredge_modbus_multi/translations/de.json index 330e2c8a..ec955e8f 100644 --- a/custom_components/solaredge_modbus_multi/translations/de.json +++ b/custom_components/solaredge_modbus_multi/translations/de.json @@ -36,15 +36,15 @@ "detect_meters": "Messgeräte automatisch erkennen", "detect_batteries": "Batterien automatisch erkennen", "detect_extras": "Zusätzliche Entitäten automatisch erkennen", - "advanced_power_control": "Erweiterte Leistungssteuerung" + "advanced_power_control": "Erweiterte Leistungssteuerung", + "sleep_after_write": "Befehlsverzögerung des Wechselrichters (Sekunden)" } }, "adv_pwr_ctl": { "title": "Energiesteuerungsoptionen", "data": { "adv_storage_control": "Speichersteuerung aktivieren", - "adv_site_limit_control": "Site-Limit-Kontrolle aktivieren", - "sleep_after_write": "Befehlsverzögerung des Wechselrichters (Sekunden)" + "adv_site_limit_control": "Site-Limit-Kontrolle aktivieren" }, "description": "Warnung: Diese Optionen können gegen Stromverträge verstoßen, Ihre Stromabrechnung ändern, möglicherweise spezielle Geräte erfordern und die Bereitstellung durch SolarEdge oder Ihren Installateur überschreiben. Benutzung auf eigene Gefahr!" }, diff --git a/custom_components/solaredge_modbus_multi/translations/en.json b/custom_components/solaredge_modbus_multi/translations/en.json index e2bb847f..52c51469 100644 --- a/custom_components/solaredge_modbus_multi/translations/en.json +++ b/custom_components/solaredge_modbus_multi/translations/en.json @@ -36,15 +36,15 @@ "detect_meters": "Auto-Detect Meters", "detect_batteries": "Auto-Detect Batteries", "detect_extras": "Auto-Detect Additional Entities", - "advanced_power_control": "Power Control Options" + "advanced_power_control": "Power Control Options", + "sleep_after_write": "Inverter Command Delay (seconds)" } }, "adv_pwr_ctl": { "title": "Power Control Options", "data": { "adv_storage_control": "Enable Storage Control", - "adv_site_limit_control": "Enable Site Limit Control", - "sleep_after_write": "Inverter Command Delay (seconds)" + "adv_site_limit_control": "Enable Site Limit Control" }, "description": "Warning: These options can violate utility agreements, alter your utility billing, may require special equipment, and overwrite provisioning by SolarEdge or your installer. Use at your own risk!" }, diff --git a/custom_components/solaredge_modbus_multi/translations/fr.json b/custom_components/solaredge_modbus_multi/translations/fr.json index 79afd629..0f3d9d7d 100644 --- a/custom_components/solaredge_modbus_multi/translations/fr.json +++ b/custom_components/solaredge_modbus_multi/translations/fr.json @@ -36,15 +36,15 @@ "detect_meters": "Auto-détecter les capteurs", "detect_batteries": "Auto-détecter les batteries", "detect_extras": "Détection automatique des entités supplémentaires", - "advanced_power_control": "Options de contrôle de l'alimentation" + "advanced_power_control": "Options de contrôle de l'alimentation", + "sleep_after_write": "Délai de commande de l'onduleur (en secondes)" } }, "adv_pwr_ctl": { "title": "Options de contrôle de l'alimentation", "data": { "adv_storage_control": "Activer le contrôle du stockage", - "adv_site_limit_control": "Activer le contrôle des limites du site", - "sleep_after_write": "Délai de commande de l'onduleur (en secondes)" + "adv_site_limit_control": "Activer le contrôle des limites du site" }, "description": "Avertissement : Ces options peuvent enfreindre l'accord d'utilisation, modifier la facturation de vos services, nécessiter un équipement spécial et écraser le provisionnement par SolarEdge ou votre installateur. À utiliser à vos risques et périls!" }, diff --git a/custom_components/solaredge_modbus_multi/translations/nb.json b/custom_components/solaredge_modbus_multi/translations/nb.json index 522df423..689b06b1 100644 --- a/custom_components/solaredge_modbus_multi/translations/nb.json +++ b/custom_components/solaredge_modbus_multi/translations/nb.json @@ -36,15 +36,15 @@ "detect_meters": "Automatisk oppdagelse av målere", "detect_batteries": "Automatisk gjenkjenning av batterier", "detect_extras": "Automatisk oppdage flere enheter", - "advanced_power_control": "Avansert strømkontroll" + "advanced_power_control": "Avansert strømkontroll", + "sleep_after_write": "Inverter Command Delay (sekunder)" } }, "adv_pwr_ctl": { "title": "Strømkontrollalternativer", "data": { "adv_storage_control": "Aktiver lagringskontroll", - "adv_site_limit_control": "Aktiver Site Limit Control", - "sleep_after_write": "Inverter Command Delay (sekunder)" + "adv_site_limit_control": "Aktiver Site Limit Control" }, "description": "Advarsel: Disse alternativene kan bryte forsyningsavtaler, endre forbruksfaktureringen, kan kreve spesialutstyr og overskrive klargjøring av SolarEdge eller installatøren. Bruk på eget ansvar!" }, diff --git a/custom_components/solaredge_modbus_multi/translations/nl.json b/custom_components/solaredge_modbus_multi/translations/nl.json index 6347a536..5455664f 100644 --- a/custom_components/solaredge_modbus_multi/translations/nl.json +++ b/custom_components/solaredge_modbus_multi/translations/nl.json @@ -36,15 +36,15 @@ "detect_meters": "Meters automatisch detecteren", "detect_batteries": "Batterijen automatisch detecteren", "detect_extras": "Automatische detectie van aanvullende entiteiten", - "advanced_power_control": "Geavanceerde stroomregeling" + "advanced_power_control": "Geavanceerde stroomregeling", + "sleep_after_write": "Omvormer commando vertraging (seconden)" } }, "adv_pwr_ctl": { "title": "Opties voor stroomregeling", "data": { "adv_storage_control": "Opslagbeheer inschakelen", - "adv_site_limit_control": "Beheer van sitelimiet inschakelen", - "sleep_after_write": "Omvormer commando vertraging (seconden)" + "adv_site_limit_control": "Beheer van sitelimiet inschakelen" }, "description": "Waarschuwing: deze opties kunnen in strijd zijn met nutsvoorzieningen, de facturering van uw nutsbedrijf wijzigen, mogelijk speciale apparatuur vereisen en de voorzieningen door SolarEdge of uw installateur overschrijven. Gebruik op eigen risico!" }, diff --git a/custom_components/solaredge_modbus_multi/translations/pl.json b/custom_components/solaredge_modbus_multi/translations/pl.json index ab4d56c0..7580c62b 100644 --- a/custom_components/solaredge_modbus_multi/translations/pl.json +++ b/custom_components/solaredge_modbus_multi/translations/pl.json @@ -36,15 +36,15 @@ "detect_meters": "Automatycznie wykryj liczniki", "detect_batteries": "Automatycznie wykryj baterie", "detect_extras": "Automatycznie wykrywaj dodatkowe elementy", - "advanced_power_control": "Zaawansowana kontrola mocy" + "advanced_power_control": "Zaawansowana kontrola mocy", + "sleep_after_write": "Opóźnienie polecenia falownika (sekundy)" } }, "adv_pwr_ctl": { "title": "Opcje sterowania zasilaniem", "data": { "adv_storage_control": "Włącz kontrolę pamięci", - "adv_site_limit_control": "Włącz kontrolę limitu witryny", - "sleep_after_write": "Opóźnienie polecenia falownika (sekundy)" + "adv_site_limit_control": "Włącz kontrolę limitu witryny" }, "description": "Ostrzeżenie: opcje te mogą naruszać umowy za media, zmieniać rozliczenia za media, mogą wymagać specjalnego sprzętu i nadpisać udostępnianie przez SolarEdge lub instalatora. Używaj na własne ryzyko!" }, From 8e9f41ce69bc867956312bd16d6624f661e928ba Mon Sep 17 00:00:00 2001 From: WillCodeForCats <48533968+WillCodeForCats@users.noreply.github.com> Date: Sun, 27 Aug 2023 15:33:33 -0700 Subject: [PATCH 6/9] Simplify --- custom_components/solaredge_modbus_multi/number.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/custom_components/solaredge_modbus_multi/number.py b/custom_components/solaredge_modbus_multi/number.py index 1a776fe2..8b0f3297 100644 --- a/custom_components/solaredge_modbus_multi/number.py +++ b/custom_components/solaredge_modbus_multi/number.py @@ -502,10 +502,7 @@ def name(self) -> str: @property def entity_registry_enabled_default(self) -> bool: - if self._platform.global_power_control is True: - return True - else: - return False + return self._platform.global_power_control @property def available(self) -> bool: From 6a25206ad2feff0a5e56b29b43adcae64d4defff Mon Sep 17 00:00:00 2001 From: WillCodeForCats <48533968+WillCodeForCats@users.noreply.github.com> Date: Sun, 27 Aug 2023 15:54:22 -0700 Subject: [PATCH 7/9] Bump version for release --- custom_components/solaredge_modbus_multi/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/solaredge_modbus_multi/manifest.json b/custom_components/solaredge_modbus_multi/manifest.json index 49fb8d94..48b56734 100644 --- a/custom_components/solaredge_modbus_multi/manifest.json +++ b/custom_components/solaredge_modbus_multi/manifest.json @@ -10,5 +10,5 @@ "issue_tracker": "https://github.com/WillCodeForCats/solaredge-modbus-multi/issues", "loggers": ["custom_components.solaredge_modbus_multi"], "requirements": ["pymodbus>=3.3.1"], - "version": "2.4.3-pre.7" + "version": "2.4.3" } From 04e851b085833b14479a2a80491d78958d0d42fb Mon Sep 17 00:00:00 2001 From: WillCodeForCats <48533968+WillCodeForCats@users.noreply.github.com> Date: Tue, 29 Aug 2023 10:03:21 -0700 Subject: [PATCH 8/9] Refactor async locking and write wait --- .../solaredge_modbus_multi/__init__.py | 4 + .../solaredge_modbus_multi/hub.py | 293 ++++++++++-------- 2 files changed, 162 insertions(+), 135 deletions(-) diff --git a/custom_components/solaredge_modbus_multi/__init__.py b/custom_components/solaredge_modbus_multi/__init__.py index 8ebfc50a..27105bca 100644 --- a/custom_components/solaredge_modbus_multi/__init__.py +++ b/custom_components/solaredge_modbus_multi/__init__.py @@ -188,6 +188,10 @@ def __init__( async def _async_update_data(self): try: + while self._hub.has_write: + _LOGGER.debug(f"Waiting for write {self._hub.has_write}") + await asyncio.sleep(1) + return await self._refresh_modbus_data_with_retry( ex_type=DataUpdateFailed, limit=RetrySettings.Limit, diff --git a/custom_components/solaredge_modbus_multi/hub.py b/custom_components/solaredge_modbus_multi/hub.py index b2faa181..a6ea27f0 100644 --- a/custom_components/solaredge_modbus_multi/hub.py +++ b/custom_components/solaredge_modbus_multi/hub.py @@ -137,10 +137,7 @@ def __init__( self.batteries = [] self.inverter_common = {} self.mmppt_common = {} - - self._wr_unit = None - self._wr_address = None - self._wr_payload = None + self.has_write = None self._initalized = False self._online = True @@ -165,6 +162,8 @@ def __init__( ) async def _async_init_solaredge(self) -> None: + """Detect devices and load initial modbus data from inverters.""" + if not self.is_connected: ir.async_create_issue( self._hass, @@ -208,7 +207,7 @@ async def _async_init_solaredge(self) -> None: raise HubInitFailed(f"{e}") except DeviceInvalid as e: - """Inverters are required""" + # Inverters are mandatory _LOGGER.error(f"Inverter at {self.hub_host} ID {inverter_unit_id}: {e}") raise HubInitFailed(f"{e}") @@ -374,17 +373,22 @@ async def _async_init_solaredge(self) -> None: self.initalized = True async def async_refresh_modbus_data(self) -> bool: + """Refresh modbus data from inverters.""" + if not self.is_connected: await self.connect() if not self.initalized: try: - await self._async_init_solaredge() + async with self._lock: + await self._async_init_solaredge() except ConnectionException as e: self.disconnect() raise HubInitFailed(f"Setup failed: {e}") + return True + if not self.is_connected: self.online = False ir.async_create_issue( @@ -406,12 +410,13 @@ async def async_refresh_modbus_data(self) -> bool: self.online = True try: - for inverter in self.inverters: - await inverter.read_modbus_data() - for meter in self.meters: - await meter.read_modbus_data() - for battery in self.batteries: - await battery.read_modbus_data() + async with self._lock: + for inverter in self.inverters: + await inverter.read_modbus_data() + for meter in self.meters: + await meter.read_modbus_data() + for battery in self.batteries: + await battery.read_modbus_data() except ModbusReadError as e: self.disconnect() @@ -435,6 +440,140 @@ async def async_refresh_modbus_data(self) -> bool: return True + async def connect(self) -> None: + """Connect to inverter.""" + + if self._client is None: + self._client = AsyncModbusTcpClient( + host=self._host, + port=self._port, + reconnect_delay=ModbusDefaults.ReconnectDelay, + timeout=ModbusDefaults.Timeout, + ) + + await self._client.connect() + + def disconnect(self) -> None: + """Disconnect from inverter.""" + + if self._client is not None: + self._client.close() + + async def shutdown(self) -> None: + """Shut down the hub and disconnect.""" + async with self._lock: + self.online = False + self.disconnect() + self._client = None + + async def modbus_read_holding_registers(self, unit, address, rcount): + """Read modbus registers from inverter.""" + + self._rr_unit = unit + self._rr_address = address + self._rr_count = rcount + + kwargs = {"slave": self._rr_unit} if self._rr_unit else {} + + result = await self._client.read_holding_registers( + self._rr_address, self._rr_count, **kwargs + ) + + if result.isError(): + _LOGGER.debug(f"Unit {unit}: {result}") + + if type(result) is ModbusIOException: + raise ModbusIOError(result) + + if type(result) is ExceptionResponse: + if result.exception_code == ModbusExceptions.IllegalAddress: + raise ModbusIllegalAddress(result) + + if result.exception_code == ModbusExceptions.IllegalFunction: + raise ModbusIllegalFunction(result) + + if result.exception_code == ModbusExceptions.IllegalValue: + raise ModbusIllegalValue(result) + + raise ModbusReadError(result) + + return result + + async def write_registers(self, unit: int, address: int, payload) -> None: + """Write modbus registers to inverter.""" + + self._wr_unit = unit + self._wr_address = address + self._wr_payload = payload + + try: + async with self._lock: + if not self.is_connected: + await self.connect() + + kwargs = {"slave": self._wr_unit} if self._wr_unit else {} + result = await self._client.write_registers( + self._wr_address, self._wr_payload, **kwargs + ) + + self.has_write = address + + if self.sleep_after_write > 0: + _LOGGER.debug( + f"Sleep {self.sleep_after_write} seconds after write {address}." + ) + await asyncio.sleep(self.sleep_after_write) + + self.has_write = None + _LOGGER.debug(f"Finished with write {address}.") + + except asyncio.TimeoutError: + self.disconnect() + + raise HomeAssistantError( + f"Timeout while sending command to inverter ID {self._wr_unit}." + ) + + except ConnectionException as e: + self.disconnect() + + _LOGGER.error(f"Connection failed: {e}") + raise HomeAssistantError( + f"Connection to inverter ID {self._wr_unit} failed." + ) + + if result.isError(): + if type(result) is ModbusIOException: + self.disconnect() + _LOGGER.error( + f"Write failed: No response from inverter ID {self._wr_unit}." + ) + raise HomeAssistantError( + "No response from inverter ID {self._wr_unit}." + ) + + if type(result) is ExceptionResponse: + if result.exception_code == ModbusExceptions.IllegalAddress: + _LOGGER.debug(f"Write IllegalAddress: {result}") + raise HomeAssistantError( + "Address not supported at device at ID {self._wr_unit}." + ) + + if result.exception_code == ModbusExceptions.IllegalFunction: + _LOGGER.debug(f"Write IllegalFunction: {result}") + raise HomeAssistantError( + "Function not supported by device at ID {self._wr_unit}." + ) + + if result.exception_code == ModbusExceptions.IllegalValue: + _LOGGER.debug(f"Write IllegalValue: {result}") + raise HomeAssistantError( + "Value invalid for device at ID {self._wr_unit}." + ) + + self.disconnect() + raise ModbusWriteError(result) + @property def online(self): return self._online @@ -464,14 +603,17 @@ def name(self): @property def hub_id(self) -> str: + """Return the ID of this hub.""" return self._id @property def hub_host(self) -> str: + """Return the modbus client host.""" return self._host @property def hub_port(self) -> int: + """Return the modbus client port.""" return self._port @property @@ -530,6 +672,10 @@ def keep_modbus_open(self, value: bool) -> None: _LOGGER.debug(f"keep_modbus_open={self._keep_modbus_open}") + @property + def sleep_after_write(self) -> int: + return self._sleep_after_write + @property def coordinator_timeout(self) -> int: if not self.initalized: @@ -556,129 +702,6 @@ def is_connected(self) -> bool: return self._client.connected - def disconnect(self) -> None: - if self._client is not None: - self._client.close() - - async def connect(self) -> None: - """Connect modbus client.""" - async with self._lock: - if self._client is None: - self._client = AsyncModbusTcpClient( - host=self._host, - port=self._port, - reconnect_delay=ModbusDefaults.ReconnectDelay, - timeout=ModbusDefaults.Timeout, - ) - - await self._client.connect() - - async def shutdown(self) -> None: - """Shut down the hub.""" - async with self._lock: - self.online = False - self.disconnect() - self._client = None - - async def modbus_read_holding_registers(self, unit, address, rcount): - self._rr_unit = unit - self._rr_address = address - self._rr_count = rcount - - async with self._lock: - kwargs = {"slave": self._rr_unit} if self._rr_unit else {} - - result = await self._client.read_holding_registers( - self._rr_address, self._rr_count, **kwargs - ) - - if result.isError(): - _LOGGER.debug(f"Unit {unit}: {result}") - - if type(result) is ModbusIOException: - raise ModbusIOError(result) - - if type(result) is ExceptionResponse: - if result.exception_code == ModbusExceptions.IllegalAddress: - raise ModbusIllegalAddress(result) - - if result.exception_code == ModbusExceptions.IllegalFunction: - raise ModbusIllegalFunction(result) - - if result.exception_code == ModbusExceptions.IllegalValue: - raise ModbusIllegalValue(result) - - raise ModbusReadError(result) - - return result - - async def write_registers(self, unit: int, address: int, payload) -> None: - self._wr_unit = unit - self._wr_address = address - self._wr_payload = payload - - try: - if not self.is_connected: - await self.connect() - - async with self._lock: - kwargs = {"slave": self._wr_unit} if self._wr_unit else {} - result = await self._client.write_registers( - self._wr_address, self._wr_payload, **kwargs - ) - - if self._sleep_after_write > 0: - _LOGGER.debug( - f"Sleeping {self._sleep_after_write} seconds after write." - ) - await asyncio.sleep(self._sleep_after_write) - - except asyncio.TimeoutError: - raise HomeAssistantError( - f"Timeout while tyring to send command to inverter ID {self._wr_unit}." - ) - - except ConnectionException as e: - _LOGGER.error(f"Connection failed: {e}") - raise HomeAssistantError( - f"Connection to inverter ID {self._wr_unit} failed." - ) - - if result.isError(): - if not self.keep_modbus_open: - self.disconnect() - - if type(result) is ModbusIOException: - _LOGGER.error( - f"Write failed: No response from inverter ID {self._wr_unit}." - ) - - raise HomeAssistantError( - "No response from inverter ID {self._wr_unit}." - ) - - if type(result) is ExceptionResponse: - if result.exception_code == ModbusExceptions.IllegalAddress: - _LOGGER.debug(f"Write IllegalAddress: {result}") - - raise HomeAssistantError( - "Address not supported at device at ID {self._wr_unit}." - ) - - if result.exception_code == ModbusExceptions.IllegalFunction: - _LOGGER.debug(f"Write IllegalFunction: {result}") - raise HomeAssistantError( - "Function not supported by device at ID {self._wr_unit}." - ) - - if result.exception_code == ModbusExceptions.IllegalValue: - _LOGGER.debug(f"Write IllegalValue: {result}") - raise HomeAssistantError( - "Value invalid for device at ID {self._wr_unit}." - ) - - raise ModbusWriteError(result) - class SolarEdgeInverter: def __init__(self, device_id: int, hub: SolarEdgeModbusMultiHub) -> None: From 44847c6e5106c8a0c84dc0b54537b2778e0b335d Mon Sep 17 00:00:00 2001 From: WillCodeForCats <48533968+WillCodeForCats@users.noreply.github.com> Date: Tue, 29 Aug 2023 10:08:28 -0700 Subject: [PATCH 9/9] Bump version for pre-release --- custom_components/solaredge_modbus_multi/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/solaredge_modbus_multi/manifest.json b/custom_components/solaredge_modbus_multi/manifest.json index 48b56734..088f449b 100644 --- a/custom_components/solaredge_modbus_multi/manifest.json +++ b/custom_components/solaredge_modbus_multi/manifest.json @@ -10,5 +10,5 @@ "issue_tracker": "https://github.com/WillCodeForCats/solaredge-modbus-multi/issues", "loggers": ["custom_components.solaredge_modbus_multi"], "requirements": ["pymodbus>=3.3.1"], - "version": "2.4.3" + "version": "2.4.4-pre.1" }