Skip to content

Commit

Permalink
Add active inverter count for legacy Envoy (#182)
Browse files Browse the repository at this point in the history
* feat: Add active inverter count for legacy Envoy, disabled by default
  • Loading branch information
catsmanac authored Feb 19, 2024
1 parent eda6a0f commit 81b2f13
Show file tree
Hide file tree
Showing 4 changed files with 48 additions and 7 deletions.
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -159,6 +159,7 @@ A device `Envoy <serialnumber>` is created with sensor entities for accessible d
|Envoy \<sn\> Frequency|sensor.Envoy_\<sn\>_frequency|Wh|4,9|
|Envoy \<sn\> Consumption Current|sensor.Envoy_\<sn\>_consumption_Current|A|4,9|
|Envoy \<sn\> Production Current|sensor.Envoy_\<sn\>_production_Current|A|4,9|
|Envoy \<sn\> Active Inverter Count|sensor.Envoy_\<sn\>_active_inverter_count||9,10|
||||
|Grid Status |binary_sensor.grid_status|On/Off|3|
||||
Expand Down Expand Up @@ -188,7 +189,8 @@ A device `Envoy <serialnumber>` 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

Expand Down
7 changes: 7 additions & 0 deletions custom_components/enphase_envoy_custom/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = (
Expand Down
40 changes: 36 additions & 4 deletions custom_components/enphase_envoy_custom/envoy_reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
r"<td>Since Installation</td>\s+<td>\s*(\d+|\d+\.\d+)\s*(Wh|kWh|MWh)</td>"
)
SERIAL_REGEX = re.compile(r"Envoy\s*Serial\s*Number:\s*([0-9]+)")
ACTIVE_INVERTER_COUNT_REGEX = r"<td>Number of Microinverters Online</td>\s*<td>\s*(\d*)\s*</td>"

ENDPOINT_URL_PRODUCTION_JSON = "http{}://{}/production.json?details=1"
ENDPOINT_URL_PRODUCTION_V1 = "http{}://{}/api/v1/production"
Expand All @@ -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"
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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."""
Expand Down Expand Up @@ -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 = {}
Expand Down Expand Up @@ -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

Expand All @@ -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(),
Expand All @@ -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(),
Expand All @@ -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"),
Expand All @@ -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,
)
)
Expand Down Expand Up @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion custom_components/enphase_envoy_custom/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,5 @@
"codeowners": ["@briancmpbll"],
"config_flow": true,
"iot_class": "local_polling",
"version": "0.0.19"
"version": "0.0.20"
}

0 comments on commit 81b2f13

Please sign in to comment.