From 81b2f135ae63923d62d5e1eada4a62c2ff7f7b63 Mon Sep 17 00:00:00 2001 From: Arie Catsman <120491684+catsmanac@users.noreply.github.com> Date: Mon, 19 Feb 2024 08:55:44 +0100 Subject: [PATCH] Add active inverter count for legacy Envoy (#182) * feat: Add active inverter count for legacy Envoy, disabled by default --- README.md | 6 ++- .../enphase_envoy_custom/const.py | 7 ++++ .../enphase_envoy_custom/envoy_reader.py | 40 +++++++++++++++++-- .../enphase_envoy_custom/manifest.json | 2 +- 4 files changed, 48 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 12a0557..4fc3716 100644 --- a/README.md +++ b/README.md @@ -91,7 +91,7 @@ This integration supports various models but as models have different features t ## ENVOY C / R / LCD -- Current power production, today's, last 7 days and lifetime energy production. +- Current power production, today's, last 7 days and lifetime energy production. And Active inverter count, which is disabled by default. ## IQ Gateway / ENVOY S standard (non metered) @@ -159,6 +159,7 @@ A device `Envoy ` is created with sensor entities for accessible d |Envoy \ Frequency|sensor.Envoy_\_frequency|Wh|4,9| |Envoy \ Consumption Current|sensor.Envoy_\_consumption_Current|A|4,9| |Envoy \ Production Current|sensor.Envoy_\_production_Current|A|4,9| +|Envoy \ Active Inverter Count|sensor.Envoy_\_active_inverter_count||9,10| |||| |Grid Status |binary_sensor.grid_status|On/Off|3| |||| @@ -188,7 +189,8 @@ A device `Envoy ` is created with sensor entities for accessible d 6 Reportedly always zero on Envoy metered with Firmware D8. 7 In V0.0.18 renamed to Lifetime Net Energy Consumption /Production from Export Index/Import Import in v0.0.17. Old Entities will show as unavailable. 8 Only when consumption CT is installed in 'Load with Solar' mode. In 'Load only' mode values have no meaning. -9 Disabled by default and must be enabled in the entities configuration screen. These are values from the consumption CT. +9 Disabled by default and must be enabled in the entities configuration screen. These are values from the consumption CT. +10 Only available on legacy Envoy. ## Inverter Sensors diff --git a/custom_components/enphase_envoy_custom/const.py b/custom_components/enphase_envoy_custom/const.py index 64556e5..3ae508b 100644 --- a/custom_components/enphase_envoy_custom/const.py +++ b/custom_components/enphase_envoy_custom/const.py @@ -173,6 +173,13 @@ device_class=SensorDeviceClass.CURRENT, entity_registry_enabled_default=False, ), + SensorEntityDescription( + key="active_inverter_count", + name="Active Inverter Count", + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=False, + ), + ) BINARY_SENSORS = ( diff --git a/custom_components/enphase_envoy_custom/envoy_reader.py b/custom_components/enphase_envoy_custom/envoy_reader.py index 8236a7e..4f0b4f3 100644 --- a/custom_components/enphase_envoy_custom/envoy_reader.py +++ b/custom_components/enphase_envoy_custom/envoy_reader.py @@ -29,6 +29,7 @@ r"Since Installation\s+\s*(\d+|\d+\.\d+)\s*(Wh|kWh|MWh)" ) SERIAL_REGEX = re.compile(r"Envoy\s*Serial\s*Number:\s*([0-9]+)") +ACTIVE_INVERTER_COUNT_REGEX = r"Number of Microinverters Online\s*\s*(\d*)\s*" ENDPOINT_URL_PRODUCTION_JSON = "http{}://{}/production.json?details=1" ENDPOINT_URL_PRODUCTION_V1 = "http{}://{}/api/v1/production" @@ -37,6 +38,7 @@ ENDPOINT_URL_CHECK_JWT = "https://{}/auth/check_jwt" ENDPOINT_URL_ENSEMBLE_INVENTORY = "http{}://{}/ivp/ensemble/inventory" ENDPOINT_URL_HOME_JSON = "http{}://{}/home.json" +ENDPOINT_URL_HOME = "http{}://{}/home" ENDPOINT_URL_INFO_XML = "http{}://{}/info" ENDPOINT_URL_METERS = "http{}://{}/ivp/meters" ENDPOINT_URL_METERS_REPORTS = "http{}://{}/ivp/meters/reports" @@ -145,6 +147,10 @@ class EnvoyReader: # pylint: disable=too-many-instance-attributes "Amps production data not available for your Envoy device." ) + message_active_inverters_not_available = ( + "Active Inverter count not available for your Envoy device." + ) + def __init__( # pylint: disable=too-many-arguments self, @@ -187,6 +193,7 @@ def __init__( # pylint: disable=too-many-arguments self.endpoint_production_results = None self.endpoint_ensemble_json_results = None self.endpoint_home_json_results = None + self.endpoint_home_results = None self.isProductionMeteringEnabled = False # pylint: disable=invalid-name self.isConsumptionMeteringEnabled = False # pylint: disable=invalid-name self.net_consumption_meters_type = False @@ -303,6 +310,9 @@ async def _update_from_p0_endpoint(self): await self._update_endpoint( "endpoint_production_results", ENDPOINT_URL_PRODUCTION ) + await self._update_endpoint( + "endpoint_home_results", ENDPOINT_URL_HOME + ) async def _update_info_endpoint(self): """Update from info endpoint if next time expired.""" @@ -1137,6 +1147,20 @@ async def grid_status(self): self.has_grid_status = False return None + async def active_inverter_count(self) -> int|str: + """Return active inverter count from /home html for legacy envoy""" + if (self.endpoint_type == ENVOY_MODEL_LEGACY + and self.endpoint_home_results + and self.endpoint_home_results.status_code == 200): + + text = self.endpoint_home_results.text + match = re.search(ACTIVE_INVERTER_COUNT_REGEX, text, re.MULTILINE) + if match: + active_count = int(match.group(1)) + return active_count + + return self.message_active_inverters_not_available + async def envoy_info(self): """Return information reported by Envoy info.xml.""" device_data = {} @@ -1214,6 +1238,10 @@ async def envoy_info(self): device_data["Endpoint-info"] = self.endpoint_info_results.text else: device_data["Endpoint-info"] = self.endpoint_info_results + if self.endpoint_home_results: + device_data["legacy-home"] = self.endpoint_home_results.text + else: + device_data["legacy-home"] = self.endpoint_home_results return device_data @@ -1232,7 +1260,7 @@ def run_in_console(self, dumpraw=False,loopcount=1,waittime=1): loop = asyncio.get_event_loop() results = loop.run_until_complete( asyncio.gather( - self.production(), + self.production(), #0 self.consumption(), self.net_consumption(), self.daily_production(), @@ -1242,7 +1270,7 @@ def run_in_console(self, dumpraw=False,loopcount=1,waittime=1): self.lifetime_production(), self.lifetime_net_production(), self.lifetime_consumption(), - self.lifetime_net_consumption(), + self.lifetime_net_consumption(), #10 self.battery_storage(), self.inverters_production(), self.envoy_info(), @@ -1253,7 +1281,7 @@ def run_in_console(self, dumpraw=False,loopcount=1,waittime=1): self.production_Current(), #get values for phase L2 self.production_phase("l2"), - self.consumption("l2"), + self.consumption("l2"), #20 self.net_consumption("l2"), self.daily_production_phase("l2"), self.daily_consumption_phase("l2"), @@ -1263,9 +1291,11 @@ def run_in_console(self, dumpraw=False,loopcount=1,waittime=1): self.lifetime_net_consumption("l2"), self.pf("l2"), self.voltage("l2"), - self.frequency("l2"), + self.frequency("l2"), #30 self.consumption_Current("l2"), self.production_Current("l2"), + self.grid_status(), + self.active_inverter_count(), #34 return_exceptions=False, ) ) @@ -1303,6 +1333,8 @@ def run_in_console(self, dumpraw=False,loopcount=1,waittime=1): print(f"frequency: {results[30]}") print(f"consumption_Current: {results[31]}") print(f"production_Current: {results[32]}") + print(f"grid_status: {results[33]}") + print(f"active_inverters: {results[34]}") if "401" in str(data_results): print( "inverters_production: Unable to retrieve inverter data - Authentication failure" diff --git a/custom_components/enphase_envoy_custom/manifest.json b/custom_components/enphase_envoy_custom/manifest.json index 6c59c79..4f9f95e 100644 --- a/custom_components/enphase_envoy_custom/manifest.json +++ b/custom_components/enphase_envoy_custom/manifest.json @@ -11,5 +11,5 @@ "codeowners": ["@briancmpbll"], "config_flow": true, "iot_class": "local_polling", - "version": "0.0.19" + "version": "0.0.20" }