diff --git a/custom_components/enphase_envoy_custom/__init__.py b/custom_components/enphase_envoy_custom/__init__.py index 2219b9e..be75d07 100644 --- a/custom_components/enphase_envoy_custom/__init__.py +++ b/custom_components/enphase_envoy_custom/__init__.py @@ -73,6 +73,8 @@ async def async_update_data(): )() data["grid_status"] = await envoy_reader.grid_status() + data["charge"] = await envoy_reader.charge() + data["discharge"] = await envoy_reader.discharge() _LOGGER.debug("Retrieved data from API: %s", data) diff --git a/custom_components/enphase_envoy_custom/const.py b/custom_components/enphase_envoy_custom/const.py index 86b3fbf..0901c80 100644 --- a/custom_components/enphase_envoy_custom/const.py +++ b/custom_components/enphase_envoy_custom/const.py @@ -10,7 +10,7 @@ SensorEntityDescription, SensorStateClass, ) -from homeassistant.const import ENERGY_WATT_HOUR, POWER_WATT, Platform, PERCENTAGE +from homeassistant.const import ENERGY_WATT_HOUR, POWER_WATT, Platform, PERCENTAGE, ELECTRIC_POTENTIAL_VOLT DOMAIN = "enphase_envoy" @@ -52,6 +52,34 @@ state_class=SensorStateClass.TOTAL_INCREASING, device_class=SensorDeviceClass.ENERGY, ), + SensorEntityDescription( + key="discharge", + name="Battery Power Discharge", + native_unit_of_measurement=POWER_WATT, + state_class=SensorStateClass.MEASUREMENT, + device_class=SensorDeviceClass.POWER, + ), + SensorEntityDescription( + key="charge", + name="Battery Power Charge", + native_unit_of_measurement=POWER_WATT, + state_class=SensorStateClass.MEASUREMENT, + device_class=SensorDeviceClass.POWER, + ), + SensorEntityDescription( + key="grid_import", + name="Grid Power Import", + native_unit_of_measurement=POWER_WATT, + state_class=SensorStateClass.MEASUREMENT, + device_class=SensorDeviceClass.POWER, + ), + SensorEntityDescription( + key="grid_export", + name="Grid Power Export", + native_unit_of_measurement=POWER_WATT, + state_class=SensorStateClass.MEASUREMENT, + device_class=SensorDeviceClass.POWER, + ), SensorEntityDescription( key="consumption", name="Current Power Consumption", @@ -96,7 +124,8 @@ key="total_battery_percentage", name="Total Battery Percentage", native_unit_of_measurement=PERCENTAGE, - state_class=SensorStateClass.MEASUREMENT + state_class=SensorStateClass.MEASUREMENT, + device_class=SensorDeviceClass.BATTERY ), SensorEntityDescription( key="current_battery_capacity", @@ -105,23 +134,30 @@ state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.ENERGY ), + SensorEntityDescription( + key="rmsvoltage", + name="Inverter Voltage", + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + state_class=SensorStateClass.MEASUREMENT, + device_class=SensorDeviceClass.VOLTAGE + ), ) -BATTERY_ENERGY_DISCHARGED_SENSOR = SensorEntityDescription( - key="battery_energy_discharged", - name="Battery Energy Discharged", - native_unit_of_measurement=ENERGY_WATT_HOUR, - state_class=SensorStateClass.TOTAL, - device_class=SensorDeviceClass.ENERGY -) +# BATTERY_ENERGY_DISCHARGED_SENSOR = SensorEntityDescription( +# key="battery_energy_discharged", +# name="Battery Energy Discharged", +# native_unit_of_measurement=ENERGY_WATT_HOUR, +# state_class=SensorStateClass.TOTAL, +# device_class=SensorDeviceClass.ENERGY +# ) -BATTERY_ENERGY_CHARGED_SENSOR = SensorEntityDescription( - key="battery_energy_charged", - name="Battery Energy Charged", - native_unit_of_measurement=ENERGY_WATT_HOUR, - state_class=SensorStateClass.TOTAL, - device_class=SensorDeviceClass.ENERGY -) +# BATTERY_ENERGY_CHARGED_SENSOR = SensorEntityDescription( +# key="battery_energy_charged", +# name="Battery Energy Charged", +# native_unit_of_measurement=ENERGY_WATT_HOUR, +# state_class=SensorStateClass.TOTAL, +# device_class=SensorDeviceClass.ENERGY +# ) GRID_STATUS_BINARY_SENSOR = BinarySensorEntityDescription( key="grid_status", diff --git a/custom_components/enphase_envoy_custom/envoy_reader.py b/custom_components/enphase_envoy_custom/envoy_reader.py index b241cf6..5abf3bc 100644 --- a/custom_components/enphase_envoy_custom/envoy_reader.py +++ b/custom_components/enphase_envoy_custom/envoy_reader.py @@ -32,6 +32,8 @@ 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_ENSEMBLE_POWER = "http{}://{}/ivp/ensemble/power" +ENDPOINT_URL_ARF_PROFILE = "http{}://{}/ivp/arf/profile" # pylint: disable=pointless-string-statement @@ -110,6 +112,8 @@ def __init__( # pylint: disable=too-many-arguments self.endpoint_production_inverters = None self.endpoint_production_results = None self.endpoint_ensemble_json_results = None + self.endpoint_ensemble_power_results = None + self.endpoint_arf_profile_results = None self.endpoint_home_json_results = None self.isMeteringEnabled = False # pylint: disable=invalid-name self._async_client = async_client @@ -124,6 +128,10 @@ def __init__( # pylint: disable=too-many-arguments self._token = "" self.use_enlighten_owner_token = use_enlighten_owner_token self.token_refresh_buffer_seconds = token_refresh_buffer_seconds + self.battery_power = 0 + self.grid_power = 0 + self.pv_power = 0 + self.voltage = None @property def async_client(self): @@ -151,6 +159,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_ensemble_power_results", ENDPOINT_URL_ENSEMBLE_POWER + ) await self._update_endpoint( "endpoint_home_json_results", ENDPOINT_URL_HOME_JSON ) @@ -535,8 +546,46 @@ async def production(self): production = float(match.group(1)) else: raise RuntimeError("No match for production, check REGEX " + text) + self.pv_power = int(production) return int(production) + async def rmsvoltage(self): + if self.endpoint_type == ENVOY_MODEL_S: + raw_json = self.endpoint_production_json_results.json() + if raw_json["production"][1]["type"] == "eim": + readvoltage = raw_json["production"][1]["rmsVoltage"] + else: + raise RuntimeError("No match for voltage, check REGEX " + text) + self.voltage = int(readvoltage) + return int(readvoltage) + + async def grid_export(self): + if self.endpoint_type == ENVOY_MODEL_S: + raw_json = self.endpoint_production_json_results.json() + if raw_json["consumption"][1]["measurementType"] == "net-consumption": + grid = raw_json["consumption"][1]["wNow"] + else: + raise RuntimeError("No match for grid meter, check REGEX " + text) + self.grid_power = int(grid) + if grid < 0: + return int(grid * -1 ) + else: + return 0 + + async def grid_import(self): + if self.endpoint_type == ENVOY_MODEL_S: + raw_json = self.endpoint_production_json_results.json() + if raw_json["consumption"][1]["measurementType"] == "net-consumption": + grid = raw_json["consumption"][1]["wNow"] + else: + raise RuntimeError("No match for grid meter, check REGEX " + text) + self.grid_power = int(grid) + if grid > 0: + return int(grid) + else: + return 0 + + async def consumption(self): """Running getData() beforehand will set self.enpoint_type and self.isDataRetrieved""" """so that this method will only read data from stored variables""" @@ -550,8 +599,47 @@ async def consumption(self): raw_json = self.endpoint_production_json_results.json() consumption = raw_json["consumption"][0]["wNow"] + if self.battery_power != 0: + consumption = self.battery_power + self.pv_power + self.grid_power return int(consumption) + async def discharge(self): + """Return battery discharge data from Envoys that support and have batteries installed""" + try: + raw_json = self.endpoint_ensemble_power_results.json() + except JSONDecodeError: + return None + + power = 0 + try: + for item in raw_json["devices:"]: + power += item["real_power_mw"] + except (JSONDecodeError, KeyError, IndexError, TypeError, AttributeError): + return None + self.battery_power = int(power / 1000) + if power > 0: + return int(power / 1000) + else: + return 0 + + async def charge(self): + """Return battery discharge data from Envoys that support and have batteries installed""" + try: + raw_json = self.endpoint_ensemble_power_results.json() + except JSONDecodeError: + return None + + power = 0 + try: + for item in raw_json["devices:"]: + power += item["real_power_mw"] + except (JSONDecodeError, KeyError, IndexError, TypeError, AttributeError): + return None + if power < 0: + return int(power / -1000) + else: + return 0 + async def daily_production(self): """Running getData() beforehand will set self.enpoint_type and self.isDataRetrieved""" """so that this method will only read data from stored variables""" @@ -763,6 +851,8 @@ def run_in_console(self): self.lifetime_consumption(), self.inverters_production(), self.battery_storage(), + self.current_battery_charge(), + self.current_battery_discharge(), return_exceptions=False, ) ) diff --git a/custom_components/enphase_envoy_custom/sensor.py b/custom_components/enphase_envoy_custom/sensor.py index 447c933..4a3f250 100644 --- a/custom_components/enphase_envoy_custom/sensor.py +++ b/custom_components/enphase_envoy_custom/sensor.py @@ -14,7 +14,7 @@ from homeassistant.helpers.event import async_track_state_change_event from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import BATTERY_ENERGY_DISCHARGED_SENSOR, BATTERY_ENERGY_CHARGED_SENSOR, COORDINATOR, DOMAIN, NAME, SENSORS, ICON +from .const import COORDINATOR, DOMAIN, NAME, SENSORS, ICON async def async_setup_entry( hass: HomeAssistant, @@ -72,29 +72,29 @@ async def async_setup_entry( ) entities.append(battery_capacity_entity) - entities.append( - BatteryEnergyChangeEntity( - BATTERY_ENERGY_CHARGED_SENSOR, - f"{name} {BATTERY_ENERGY_CHARGED_SENSOR.name}", - name, - config_entry.unique_id, - None, - battery_capacity_entity, - True - ) - ) - - entities.append( - BatteryEnergyChangeEntity( - BATTERY_ENERGY_DISCHARGED_SENSOR, - f"{name} {BATTERY_ENERGY_DISCHARGED_SENSOR.name}", - name, - config_entry.unique_id, - None, - battery_capacity_entity, - False - ) - ) + # entities.append( + # BatteryEnergyChangeEntity( + # BATTERY_ENERGY_CHARGED_SENSOR, + # f"{name} {BATTERY_ENERGY_CHARGED_SENSOR.name}", + # name, + # config_entry.unique_id, + # None, + # battery_capacity_entity, + # True + # ) + # ) + + # entities.append( + # BatteryEnergyChangeEntity( + # BATTERY_ENERGY_DISCHARGED_SENSOR, + # f"{name} {BATTERY_ENERGY_DISCHARGED_SENSOR.name}", + # name, + # config_entry.unique_id, + # None, + # battery_capacity_entity, + # False + # ) + # ) elif (sensor_description.key == "total_battery_percentage"): if (coordinator.data.get("batteries") is not None): @@ -366,73 +366,73 @@ def native_value(self): return None -class BatteryEnergyChangeEntity(EnvoyEntity): - def __init__( - self, - description, - name, - device_name, - device_serial_number, - serial_number, - total_battery_capacity_entity, - positive: bool - ): - super().__init__( - description=description, - name=name, - device_name=device_name, - device_serial_number=device_serial_number, - serial_number=serial_number, - ) - - self._sensor_source = total_battery_capacity_entity - self._positive = positive - self._state = 0 - self._attr_last_reset = datetime.datetime.now() - - async def async_added_to_hass(self): - """Handle entity which will be added.""" - await super().async_added_to_hass() - - @callback - def calc_change(event): - """Handle the sensor state changes.""" - old_state = event.data.get("old_state") - new_state = event.data.get("new_state") - - if ( - old_state is None - or old_state.state in (STATE_UNKNOWN, STATE_UNAVAILABLE) - or new_state.state in (STATE_UNKNOWN, STATE_UNAVAILABLE) - ): - self._state = 0 - - else: - old_state_value = int(old_state.state) - new_state_value = int(new_state.state) - - if (self._positive): - if (new_state_value > old_state_value): - self._state = new_state_value - old_state_value - else: - self._state = 0 - - else: - if (old_state_value > new_state_value): - self._state = old_state_value - new_state_value - else: - self._state = 0 - - self._attr_last_reset = datetime.datetime.now() - self.async_write_ha_state() - - self.async_on_remove( - async_track_state_change_event( - self.hass, self._sensor_source.entity_id, calc_change - ) - ) - - @property - def native_value(self): - """Return the state of the sensor.""" - return self._state +# class BatteryEnergyChangeEntity(EnvoyEntity): +# def __init__( +# self, +# description, +# name, +# device_name, +# device_serial_number, +# serial_number, +# total_battery_capacity_entity, +# positive: bool +# ): +# super().__init__( +# description=description, +# name=name, +# device_name=device_name, +# device_serial_number=device_serial_number, +# serial_number=serial_number, +# ) + +# self._sensor_source = total_battery_capacity_entity +# self._positive = positive +# self._state = 0 +# self._attr_last_reset = datetime.datetime.now() + +# async def async_added_to_hass(self): +# """Handle entity which will be added.""" +# await super().async_added_to_hass() + +# @callback +# def calc_change(event): +# """Handle the sensor state changes.""" +# old_state = event.data.get("old_state") +# new_state = event.data.get("new_state") + +# if ( +# old_state is None +# or old_state.state in (STATE_UNKNOWN, STATE_UNAVAILABLE) +# or new_state.state in (STATE_UNKNOWN, STATE_UNAVAILABLE) +# ): +# self._state = 0 + +# else +# old_state_value = int(old_state.state) +# new_state_value = int(new_state.state) + +# if (self._positive): +# if (new_state_value > old_state_value): +# self._state = new_state_value - old_state_value +# else: +# self._state = 0 + +# else: +# if (old_state_value > new_state_value): +# self._state = old_state_value - new_state_value +# else: +# self._state = 0 + +# self._attr_last_reset = datetime.datetime.now() +# self.async_write_ha_state() + +# self.async_on_remove( +# async_track_state_change_event( +# self.hass, self._sensor_source.entity_id, calc_change +# ) +# ) +# +# @property +# def native_value(self): +# """Return the state of the sensor.""" +# return self._state diff --git a/custom_components/enphase_envoy_custom/translations/it.json b/custom_components/enphase_envoy_custom/translations/it.json index 98d8520..f985a85 100644 --- a/custom_components/enphase_envoy_custom/translations/it.json +++ b/custom_components/enphase_envoy_custom/translations/it.json @@ -15,7 +15,9 @@ "data": { "host": "Host", "password": "Password", - "username": "Nome utente" + "username": "Nome utente", + "use_enlighten": "Usa Enlighten", + "serial": "Numero di serie envoy" }, "description": "Per i modelli pi\u00f9 recenti, inserisci il nome utente \"envoy\" senza password. Per i modelli pi\u00f9 vecchi, inserisci il nome utente \"installer\" senza password. Per tutti gli altri modelli, inserisci un nome utente e una password validi." }