Skip to content

Commit

Permalink
Merge pull request #174 from krbaker/use-more-open-inverter-data
Browse files Browse the repository at this point in the history
Add more open per-inverter endpoint
  • Loading branch information
vincentwolsink authored Nov 25, 2024
2 parents 9188c8f + 0b82fd9 commit bde89bf
Show file tree
Hide file tree
Showing 5 changed files with 303 additions and 16 deletions.
24 changes: 23 additions & 1 deletion custom_components/enphase_envoy/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,23 @@ async def async_setup_entry(

entities = []
for sensor_description in BINARY_SENSORS:
if sensor_description.key.startswith("inverters_"):
if sensor_description.key.startswith("inverter_data_"):
if coordinator.data.get("inverter_device_data"):
for inverter in coordinator.data["inverter_device_data"].keys():
device_name = f"Inverter {inverter}"
entity_name = f"{device_name} {sensor_description.name}"
entities.append(
EnvoyInverterEntity(
sensor_description,
entity_name,
device_name,
inverter,
None,
coordinator,
)
)

elif sensor_description.key.startswith("inverters_"):
if coordinator.data.get("inverters_status"):
for inverter in coordinator.data["inverters_status"].keys():
device_name = f"Inverter {inverter}"
Expand Down Expand Up @@ -188,6 +204,12 @@ def device_info(self) -> DeviceInfo or None:
@property
def is_on(self) -> bool:
"""Return the status of the requested attribute."""
if self.entity_description.key.startswith("inverter_data_"):
return (
self.coordinator.data.get("inverter_device_data")
.get(self._device_serial_number)
.get(self.entity_description.key[14:])
)
if self.coordinator.data.get("inverters_status"):
return (
self.coordinator.data.get("inverters_status")
Expand Down
192 changes: 179 additions & 13 deletions custom_components/enphase_envoy/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,10 @@
UnitOfElectricPotential,
UnitOfElectricCurrent,
UnitOfTemperature,
UnitOfTime,
UnitOfReactivePower,
EntityCategory,
SIGNAL_STRENGTH_DECIBELS,
)

DOMAIN = "enphase_envoy"
Expand Down Expand Up @@ -149,34 +151,184 @@ def get_model_name(model, hardware_id):
device_class=SensorDeviceClass.POWER,
suggested_display_precision=0,
),
# I think this data requires installer perms where inverter_data does not
# SensorEntityDescription(
# key="inverters_ac_voltage",
# name="AC Voltage",
# native_unit_of_measurement=UnitOfElectricPotential.VOLT,
# state_class=SensorStateClass.MEASUREMENT,
# device_class=SensorDeviceClass.VOLTAGE,
# ),
# I think this data requires installer perms where inverter_data does not
# SensorEntityDescription(
# key="inverters_dc_voltage",
# name="DC Voltage",
# native_unit_of_measurement=UnitOfElectricPotential.VOLT,
# state_class=SensorStateClass.MEASUREMENT,
# device_class=SensorDeviceClass.VOLTAGE,
# ),
# I think this data requires installer perms where inverter_data does not
# SensorEntityDescription(
# key="inverters_dc_current",
# name="DC Current",
# icon="mdi:current-dc",
# native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
# state_class=SensorStateClass.MEASUREMENT,
# device_class=SensorDeviceClass.CURRENT,
# ),
# I think this data requires installer perms where inverter_data does not
# SensorEntityDescription(
# key="inverters_temperature",
# name="Temperature",
# native_unit_of_measurement=UnitOfTemperature.CELSIUS,
# state_class=SensorStateClass.MEASUREMENT,
# device_class=SensorDeviceClass.TEMPERATURE,
# ),
SensorEntityDescription(
key="inverters_ac_voltage",
name="AC Voltage",
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
key="inverter_data_dc_current",
name="DC Current",
icon="mdi:current-dc",
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.VOLTAGE,
device_class=SensorDeviceClass.CURRENT,
suggested_display_precision=3,
),
SensorEntityDescription(
key="inverters_dc_voltage",
name="DC Voltage",
key="inverter_data_ac_frequency",
name="Frequency",
native_unit_of_measurement=UnitOfFrequency.HERTZ,
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.FREQUENCY,
suggested_display_precision=3,
),
# This is a dupe of other data
# SensorEntityDescription(
# key="inverter_data_watts",
# name="Data Watts",
# native_unit_of_measurement=UnitOfPower.WATT,
# state_class=SensorStateClass.MEASUREMENT,
# device_class=SensorDeviceClass.POWER,
# suggested_display_precision=0,
# ),
SensorEntityDescription(
key="inverter_data_watts_max",
name="Production Max",
native_unit_of_measurement=UnitOfPower.WATT,
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.POWER,
suggested_display_precision=0,
),
SensorEntityDescription(
key="inverter_data_ac_voltage",
name="Voltage",
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.VOLTAGE,
),
suggested_display_precision=1,
),
# This never got data in my system
# SensorEntityDescription(
# key="inverter_data_ac_current",
# name="Data Current",
# native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
# state_class=SensorStateClass.MEASUREMENT,
# device_class=SensorDeviceClass.CURRENT,
# suggested_display_precision=1,
# ),
SensorEntityDescription(
key="inverters_dc_current",
name="DC Current",
icon="mdi:current-dc",
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
key="inverter_data_dc_voltage",
name="DC Voltage",
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.CURRENT,
device_class=SensorDeviceClass.VOLTAGE,
suggested_display_precision=1,
),
SensorEntityDescription(
key="inverters_temperature",
key="inverter_data_temperature",
name="Temperature",
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.TEMPERATURE,
suggested_display_precision=1,
),
# I was hoping for interesting data here, never saw anything useful, just a documentaiton of it existing
# SensorEntityDescription(
# key="inverter_data_rssi",
# name="Data RSSI",
# native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS,
# state_class=SensorStateClass.MEASUREMENT,
# device_class=SensorDeviceClass.SIGNAL_STRENGTH,
# suggested_display_precision=1,
# entity_category=EntityCategory.DIAGNOSTIC,
# ),
# I was hoping for interesting data here, never saw anything useful, just a documentaiton of it existing
# SensorEntityDescription(
# key="inverter_data_issi",
# name="Data ISSI",
# native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS,
# state_class=SensorStateClass.MEASUREMENT,
# device_class=SensorDeviceClass.SIGNAL_STRENGTH,
# suggested_display_precision=1,
# entity_category=EntityCategory.DIAGNOSTIC,
# ),
SensorEntityDescription(
key="inverter_data_lifetime_power",
name="Lifetime Energy Production",
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
state_class=SensorStateClass.TOTAL_INCREASING,
device_class=SensorDeviceClass.ENERGY,
suggested_display_precision=0,
),
SensorEntityDescription(
key="inverter_data_watt_hours_today",
name="Today's Energy Production",
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
state_class=SensorStateClass.TOTAL,
device_class=SensorDeviceClass.ENERGY,
suggested_display_precision=1,
),
SensorEntityDescription(
key="inverter_data_watt_hours_yesterday",
name="Yesterday's Energy Production",
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
state_class=SensorStateClass.TOTAL,
device_class=SensorDeviceClass.ENERGY,
suggested_display_precision=1,
),
SensorEntityDescription(
key="inverter_data_watt_hours_week",
name="This Week's Energy Production",
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
state_class=SensorStateClass.TOTAL,
device_class=SensorDeviceClass.ENERGY,
suggested_display_precision=1,
),
# This data is in attributes too, but seemed helpful to be in diagnostics
SensorEntityDescription(
key="inverter_data_last_reading",
name="Last Reading",
native_unit_of_measurement=None,
state_class=None,
device_class=SensorDeviceClass.TIMESTAMP,
entity_category=EntityCategory.DIAGNOSTIC,
),
SensorEntityDescription(
key="inverter_data_conversion_error_cycles",
name="Power Conversion Error Cycles",
native_unit_of_measurement=None,
state_class=SensorStateClass.MEASUREMENT,
device_class=None,
suggested_display_precision=0,
entity_category=EntityCategory.DIAGNOSTIC,
),
SensorEntityDescription(
key="inverter_data_conversion_error",
name="Power Conversion Error Seconds",
native_unit_of_measurement=UnitOfTime.SECONDS,
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.DURATION,
suggested_display_precision=0,
entity_category=EntityCategory.DIAGNOSTIC,
),
SensorEntityDescription(
key="batteries_power",
Expand Down Expand Up @@ -435,6 +587,20 @@ def get_model_name(model, hardware_id):
)

BINARY_SENSORS = (
# This sensor appears to mirror the 'communicating' sensor
# BinarySensorEntityDescription(
# key="inverter_data_gone",
# name="Data Gone",
# device_class=BinarySensorDeviceClass.POWER,
# entity_category=EntityCategory.DIAGNOSTIC,
# ),
# This sensor is always on, maybe this is for removed devices or something, documenting, but leaving off
# BinarySensorEntityDescription(
# key="inverter_data_active",
# name="Data Active",
# device_class=BinarySensorDeviceClass.POWER,
# entity_category=EntityCategory.DIAGNOSTIC,
# ),
BinarySensorEntityDescription(
key="inverters_producing",
name="Producing",
Expand Down
6 changes: 6 additions & 0 deletions custom_components/enphase_envoy/envoy_endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,12 @@
"installer_required": False,
"optional": False,
},
"device_data": {
"url": "https://{}/ivp/pdm/device_data",
"cache": 0,
"installer_required": False,
"optional": True,
},
"devstatus": {
"url": "https://{}/ivp/peb/devstatus",
"cache": 0,
Expand Down
39 changes: 39 additions & 0 deletions custom_components/enphase_envoy/envoy_reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,38 @@ def has_metering_setup(json):
return json["production"][1]["activeCount"] > 0


def parse_devicedata(data):
idd = {}
for key, value in data.items():
if isinstance(value, dict) and "devName" in value and value["devName"] == "pcu":
channel = value["channels"][0] # unclear when there might be a channel > 0
lifetime = channel["lifetime"]
last_reading = channel["lastReading"]
idd[value["sn"]] = {
"sn": value["sn"],
"active": value["active"],
"watts": channel["watts"]["now"],
"watts_max": channel["watts"]["max"],
"watt_hours_today": channel["wattHours"]["today"],
"watt_hours_yesterday": channel["wattHours"]["yesterday"],
"watt_hours_week": channel["wattHours"]["week"],
"ac_voltage": int(last_reading["acVoltageINmV"]) / 1000,
"ac_frequency": int(last_reading["acFrequencyINmHz"]) / 1000,
"ac_current": int(last_reading["acCurrentInmA"]) / 1000,
"dc_voltage": int(last_reading["dcVoltageINmV"]) / 1000,
"dc_current": int(last_reading["dcCurrentINmA"]) / 1000,
"temperature": last_reading["channelTemp"],
"rssi": last_reading["rssi"],
"issi": last_reading["issi"],
"lifetime_power": int(lifetime["joulesProduced"]) * 0.000277778,
"conversion_error": last_reading["pwrConvErrSecs"],
"conversion_error_cycles": last_reading["pwrConvMaxErrCycles"],
"gone": value["modGone"],
"last_reading": last_reading["endDate"],
}
return idd


def parse_devstatus(data):
def convert_dev(dev):
def iter():
Expand Down Expand Up @@ -251,6 +283,9 @@ def set_endpoint_data(self, endpoint, response):
if endpoint == "endpoint_devstatus":
# Do extra parsing, to zip the fields and values and make it a proper dict
self.data[endpoint] = parse_devstatus(response.json())
elif endpoint == "endpoint_device_data":
# Do extra parsing, to zip the fields and values and make it a proper dict
self.data[endpoint] = parse_devicedata(response.json())
elif content_type == "application/json":
self.data[endpoint] = response.json()
elif content_type in ("text/xml", "application/xml"):
Expand Down Expand Up @@ -497,6 +532,10 @@ def batteries(self):
def batteries_power(self):
return self._path_to_dict("endpoint_ensemble_power.devices:", "serial_num")

@envoy_property(required_endpoint="endpoint_device_data")
def inverter_device_data(self):
return self._resolve_path("endpoint_device_data")

@envoy_property(required_endpoint="endpoint_ensemble_power")
def agg_batteries_power(self):
batteries_data = self._resolve_path("endpoint_ensemble_power.devices:")
Expand Down
Loading

0 comments on commit bde89bf

Please sign in to comment.