diff --git a/README.md b/README.md index 85a25f9..ae3051b 100644 --- a/README.md +++ b/README.md @@ -96,6 +96,7 @@ What data is available depends on how many current transformer clamps (CT) are i - Current power production and consumption, today's, last 7 days and lifetime energy production and consumption over all phases. - Current power production and consumption, today's, last 7 days and lifetime energy production and consumption for each individual phase named L1, L2 and L3. - Current power production for each connected inverter. +- Energy Import and Export Index over all phases and for each phase individually from meters readings **Note** If you have CT clamps on a single phase / breaker circuit only, the L1 production and consumption phase sensors will show same data as the over all phases sensors. @@ -128,7 +129,7 @@ A device `Envoy ` is created with sensor entities for accessible d |Envoy \ Last Seven Days Energy Consumption|sensor.Envoy_\_last_seven_days_energy_consumption|Wh|4,5| |Envoy \ Lifetime Energy Consumption|sensor.Envoy_\_lifetime_energy_consumption|Wh|4,5| |Grid Status |binary_sensor.grid_status|On/Off|3| -Envoy \ Current Power Production L\|sensor.Envoy_\_current_power_production_L\|W|4,5| +|Envoy \ Current Power Production L\|sensor.Envoy_\_current_power_production_L\|W|4,5| |Envoy \ Today's Energy production L\|sensor.Envoy_\_todays_energy_production_L\|Wh|4,5| |Envoy \ Last Seven Days Energy Production L\|sensor.Envoy_\_last_seven_days_energy_production L\|Wh|4,5| |Envoy \ Lifetime Energy Production L\|sensor.Envoy_\_lifetime_energy_consumption_L\|Wh|4,5| @@ -136,6 +137,10 @@ Envoy \ Current Power Production L\|sensor.Envoy_\_current_power_p |Envoy \ Today's Energy Consumption L\|sensor.Envoy_\_todays_energy_consumption_L\|Wh|4,5,6| |Envoy \ Last Seven Days Energy Consumption L\|sensor.Envoy_\_last_seven_days_energy_consumption L\|Wh|4,5,6| |Envoy \ Lifetime Energy Consumption L\|sensor.Envoy_\_lifetime_energy_consumption_L\|Wh|4,5,6| +|Index Import|sensor.Envoy_\_index_import|Wh|4,5| +|Index Export|sensor.Envoy_\_index_export|Wh|4,5| +|Index Import L\|sensor.Envoy_\_index_import_L\|Wh|4,5| +|Index Export L\|sensor.Envoy_\_index_export_L\|Wh|4,5| 1 Always zero for Envoy Metered without meters. 2 Reportedly resets to zero when reaching ~1.92MWh for Envoy Metered without meters. diff --git a/custom_components/enphase_envoy_custom/__init__.py b/custom_components/enphase_envoy_custom/__init__.py index 5bea10b..a47da0e 100644 --- a/custom_components/enphase_envoy_custom/__init__.py +++ b/custom_components/enphase_envoy_custom/__init__.py @@ -113,6 +113,14 @@ async def async_update_data(): data[ description.key ] = await envoy_reader.lifetime_consumption_phase(description.key) + elif description.key.startswith("import_index_"): + data[ + description.key + ] = await envoy_reader.import_index_phase(description.key) + elif description.key.startswith("export_index_"): + data[ + description.key + ] = await envoy_reader.export_index_phase(description.key) data["grid_status"] = await envoy_reader.grid_status() data["envoy_info"] = await envoy_reader.envoy_info() diff --git a/custom_components/enphase_envoy_custom/const.py b/custom_components/enphase_envoy_custom/const.py index 9a691f2..baabc12 100644 --- a/custom_components/enphase_envoy_custom/const.py +++ b/custom_components/enphase_envoy_custom/const.py @@ -112,6 +112,20 @@ state_class=SensorStateClass.TOTAL, device_class=SensorDeviceClass.ENERGY ), + SensorEntityDescription( + key="import_index", + name="Index Import", + native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, + state_class=SensorStateClass.TOTAL_INCREASING, + device_class=SensorDeviceClass.ENERGY, + ), + SensorEntityDescription( + key="export_index", + name="Index Export", + native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, + state_class=SensorStateClass.TOTAL_INCREASING, + device_class=SensorDeviceClass.ENERGY, + ), ) BINARY_SENSORS = ( @@ -243,6 +257,48 @@ state_class=SensorStateClass.TOTAL_INCREASING, device_class=SensorDeviceClass.ENERGY, ), + SensorEntityDescription( + key="import_index_l1", + name="Index Import L1", + native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, + state_class=SensorStateClass.TOTAL_INCREASING, + device_class=SensorDeviceClass.ENERGY, + ), + SensorEntityDescription( + key="export_index_l1", + name="Index Export L1", + native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, + state_class=SensorStateClass.TOTAL_INCREASING, + device_class=SensorDeviceClass.ENERGY, + ), + SensorEntityDescription( + key="import_index_l2", + name="Index Import L2", + native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, + state_class=SensorStateClass.TOTAL_INCREASING, + device_class=SensorDeviceClass.ENERGY, + ), + SensorEntityDescription( + key="export_index_l2", + name="Index Export L2", + native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, + state_class=SensorStateClass.TOTAL_INCREASING, + device_class=SensorDeviceClass.ENERGY, + ), + SensorEntityDescription( + key="import_index_l3", + name="Index Import L3", + native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, + state_class=SensorStateClass.TOTAL_INCREASING, + device_class=SensorDeviceClass.ENERGY, + ), + SensorEntityDescription( + key="export_index_l3", + name="Index Export L3", + native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, + state_class=SensorStateClass.TOTAL_INCREASING, + device_class=SensorDeviceClass.ENERGY, + ), ) BATTERY_ENERGY_DISCHARGED_SENSOR = SensorEntityDescription( diff --git a/custom_components/enphase_envoy_custom/envoy_reader.py b/custom_components/enphase_envoy_custom/envoy_reader.py index 39f6d8e..a9b39fc 100644 --- a/custom_components/enphase_envoy_custom/envoy_reader.py +++ b/custom_components/enphase_envoy_custom/envoy_reader.py @@ -34,6 +34,7 @@ ENDPOINT_URL_ENSEMBLE_INVENTORY = "http{}://{}/ivp/ensemble/inventory" ENDPOINT_URL_HOME_JSON = "http{}://{}/home.json" ENDPOINT_URL_INFO_XML = "http{}://{}/info" +ENDPOINT_URL_METERS = "http{}://{}/ivp/meters/readings" # pylint: disable=pointless-string-statement @@ -84,6 +85,10 @@ class EnvoyReader: # pylint: disable=too-many-instance-attributes "Grid status not available for your Envoy device." ) + message_import_export_not_available = ( + "Import Export data not available for your Envoy device." + ) + def __init__( # pylint: disable=too-many-arguments self, host, @@ -119,6 +124,7 @@ def __init__( # pylint: disable=too-many-arguments self.endpoint_production_json_results = None self.endpoint_production_v1_results = None self.endpoint_production_inverters = None + self.endpoint_meters_json_results = None self.endpoint_production_results = None self.endpoint_ensemble_json_results = None self.endpoint_home_json_results = None @@ -196,6 +202,9 @@ async def _update_from_pc_endpoint(self): await self._update_endpoint( "endpoint_ensemble_json_results", ENDPOINT_URL_ENSEMBLE_INVENTORY ) + await self._update_endpoint( + "endpoint_meters_json_results", ENDPOINT_URL_METERS + ) if self.has_grid_status: await self._update_endpoint( "endpoint_home_json_results", ENDPOINT_URL_HOME_JSON @@ -902,6 +911,71 @@ async def battery_storage(self): return raw_json["storage"][0] + async def import_index(self): + """import index""" + """Running getData() beforehand will set self.enpoint_type and self.isDataRetrieved""" + """so that this method will only read data from stored variables""" + if self.endpoint_type in [ENVOY_MODEL_C,ENVOY_MODEL_LEGACY]: + return self.message_import_export_not_available + + raw_json = self.endpoint_meters_json_results.json() + index_imp = raw_json[1]["actEnergyDlvd"] + return int(index_imp) + + async def import_index_phase(self, phase): + """import index""" + """Running getData() beforehand will set self.enpoint_type and self.isDataRetrieved""" + """so that this method will only read data from stored variables""" + phase_map = {"import_index_l1": 0, "import_index_l2": 1, "import_index_l3": 2} + if self.endpoint_type in [ENVOY_MODEL_C,ENVOY_MODEL_LEGACY]: + return None + + raw_json = self.endpoint_meters_json_results.json() + if raw_json[1]["channels"][1]["voltage"] < 50: + return None + try: + return int( + raw_json[1]["channels"][phase_map[phase]]["actEnergyDlvd"] + ) + except (KeyError, IndexError): + return None + + return None + + async def export_index(self): + """import export""" + """Running getData() beforehand will set self.enpoint_type and self.isDataRetrieved""" + """so that this method will only read data from stored variables""" + + if self.endpoint_type in [ENVOY_MODEL_C,ENVOY_MODEL_LEGACY]: + return self.message_import_export_not_available + + raw_json = self.endpoint_meters_json_results.json() + index_exp = raw_json[1]['actEnergyRcvd'] + return int(index_exp) + + async def export_index_phase(self, phase): + """import export""" + """Running getData() beforehand will set self.enpoint_type and self.isDataRetrieved""" + """so that this method will only read data from stored variables""" + phase_map = {"export_index_l1": 0, "export_index_l2": 1, "export_index_l3": 2} + + if self.endpoint_type in [ENVOY_MODEL_C,ENVOY_MODEL_LEGACY]: + return None + + raw_json = self.endpoint_meters_json_results.json() + if raw_json[1]["channels"][1]["voltage"] < 50: + return None + + try: + return int( + raw_json[1]["channels"][phase_map[phase]]["actEnergyRcvd"] + ) + except (KeyError, IndexError): + return None + + return None + async def grid_status(self): """Return grid status reported by Envoy""" if self.has_grid_status and self.endpoint_home_json_results is not None: @@ -954,6 +1028,10 @@ async def envoy_info(self): device_data["Endpoint-production"] = self.endpoint_production_results.text else: device_data["Endpoint-production"] = self.endpoint_production_results + if self.endpoint_meters_json_results: + device_data["Endpoint-meters"] = self.endpoint_meters_json_results.text + else: + device_data["Endpoint-meters"] = self.endpoint_meters_json_results if self.endpoint_production_inverters: device_data[ "Endpoint-production_inverters" @@ -998,6 +1076,8 @@ def run_in_console(self): self.seven_days_consumption(), self.lifetime_production(), self.lifetime_consumption(), + self.import_index(), + self.export_index(), self.inverters_production(), self.battery_storage(), return_exceptions=False, @@ -1012,17 +1092,19 @@ def run_in_console(self): print(f"seven_days_consumption: {results[5]}") print(f"lifetime_production: {results[6]}") print(f"lifetime_consumption: {results[7]}") + print(f"index_import: {results[8]}") + print(f"index_export: {results[9]}") if "401" in str(data_results): print( "inverters_production: Unable to retrieve inverter data - Authentication failure" ) - elif results[8] is None: + elif results[10] is None: print( "inverters_production: Inverter data not available for your Envoy device." ) else: - print(f"inverters_production: {results[8]}") - print(f"battery_storage: {results[9]}") + print(f"inverters_production: {results[10]}") + print(f"battery_storage: {results[11]}") if __name__ == "__main__": diff --git a/custom_components/enphase_envoy_custom/manifest.json b/custom_components/enphase_envoy_custom/manifest.json index b3d3c6a..d012e04 100644 --- a/custom_components/enphase_envoy_custom/manifest.json +++ b/custom_components/enphase_envoy_custom/manifest.json @@ -6,5 +6,5 @@ "codeowners": ["@briancmpbll"], "config_flow": true, "iot_class": "local_polling", - "version": "0.0.16" + "version": "0.0.17" } diff --git a/custom_components/enphase_envoy_custom/translations/fr.json b/custom_components/enphase_envoy_custom/translations/fr.json index e9a7803..1ce8bf0 100644 --- a/custom_components/enphase_envoy_custom/translations/fr.json +++ b/custom_components/enphase_envoy_custom/translations/fr.json @@ -21,5 +21,18 @@ } } } - } + }, + "options": { + "step": { + "user": { + "title": "Options Envoy", + "data": { + "data_interval": "Temps entre 2 rafraichissement [s]" + }, + "data_description": { + "data_interval": "Temps entre 2 rafraichissement, minimum 5 sec. Relancer apres un changement" + } + } + } + } } \ No newline at end of file