Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add IQ Storage power (charge/discharge) and correct consumption #55

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions custom_components/enphase_envoy_custom/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
68 changes: 52 additions & 16 deletions custom_components/enphase_envoy_custom/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand All @@ -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",
Expand Down
90 changes: 90 additions & 0 deletions custom_components/enphase_envoy_custom/envoy_reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand All @@ -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):
Expand Down Expand Up @@ -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
)
Expand Down Expand Up @@ -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"""
Expand All @@ -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"""
Expand Down Expand Up @@ -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,
)
)
Expand Down
Loading