diff --git a/custom_components/enphase_envoy/const.py b/custom_components/enphase_envoy/const.py index bdacbd6..a87e506 100644 --- a/custom_components/enphase_envoy/const.py +++ b/custom_components/enphase_envoy/const.py @@ -275,6 +275,22 @@ def get_model_name(model, hardware_id): icon="mdi:transmission-tower", entity_category=EntityCategory.DIAGNOSTIC, ), + SensorEntityDescription( + key="inverters_communication_level", + name="Communication Level", + device_class=SensorDeviceClass.POWER_FACTOR, + state_class=SensorStateClass.MEASUREMENT, + suggested_display_precision=0, + entity_category=EntityCategory.DIAGNOSTIC, + ), + SensorEntityDescription( + key="relays_communication_level", + name="Communication Level", + device_class=SensorDeviceClass.POWER_FACTOR, + state_class=SensorStateClass.MEASUREMENT, + suggested_display_precision=0, + entity_category=EntityCategory.DIAGNOSTIC, + ), ) ADDITIONAL_METRICS.extend( [ diff --git a/custom_components/enphase_envoy/envoy_endpoints.py b/custom_components/enphase_envoy/envoy_endpoints.py index 9a67e00..f2b28a9 100644 --- a/custom_components/enphase_envoy/envoy_endpoints.py +++ b/custom_components/enphase_envoy/envoy_endpoints.py @@ -19,6 +19,7 @@ # Inverter endpoints ENDPOINT_URL_INVENTORY = "https://{}/inventory.json" ENDPOINT_URL_DEVSTATUS = "https://{}/ivp/peb/devstatus" +ENDPOINT_URL_COMM_STATUS = "https://{}/installer/pcu_comm_check" # Netprofile endpoints ENDPOINT_URL_INSTALLER_AGF = "https://{}/installer/agf/index.json" diff --git a/custom_components/enphase_envoy/envoy_reader.py b/custom_components/enphase_envoy/envoy_reader.py index 3129020..6ec5d45 100644 --- a/custom_components/enphase_envoy/envoy_reader.py +++ b/custom_components/enphase_envoy/envoy_reader.py @@ -32,6 +32,7 @@ ENDPOINT_URL_ENSEMBLE_SECCTRL, ENDPOINT_URL_ENSEMBLE_POWER, ENDPOINT_URL_INVENTORY, + ENDPOINT_URL_COMM_STATUS, ENDPOINT_URL_DEVSTATUS, ENDPOINT_URL_INSTALLER_AGF, ENDPOINT_URL_INSTALLER_AGF_SET_PROFILE, @@ -420,6 +421,8 @@ def grid_status(self): installer="endpoint_devstatus.pcu[?(@.devType==1)]", ) + pcu_availability_value = "endpoint_pcu_comm_status" + @envoy_property def inverters_production(self): # We will use the endpoint based on the token_type, which is automatically resolved by the inverters_data property @@ -684,6 +687,7 @@ def url(endpoint, *a, **kw): url("ensemble_inventory", ENDPOINT_URL_ENSEMBLE_INVENTORY, cache=20) url("ensemble_secctrl", ENDPOINT_URL_ENSEMBLE_SECCTRL, cache=20) url("ensemble_power", ENDPOINT_URL_ENSEMBLE_POWER, cache=20) + iurl("pcu_comm_status", ENDPOINT_URL_COMM_STATUS, cache=90) iurl("devstatus", ENDPOINT_URL_DEVSTATUS, cache=20) iurl("production_power", ENDPOINT_URL_PRODUCTION_POWER, cache=20) url("info", ENDPOINT_URL_INFO_XML, cache=86400) diff --git a/custom_components/enphase_envoy/sensor.py b/custom_components/enphase_envoy/sensor.py index cb8687f..a66bcb5 100644 --- a/custom_components/enphase_envoy/sensor.py +++ b/custom_components/enphase_envoy/sensor.py @@ -60,6 +60,44 @@ async def async_setup_entry( config_entry.unique_id, ) ) + + elif sensor_description.key == "inverters_communication_level": + if coordinator.data.get("pcu_availability") is not None: + for inverter in coordinator.data["inverters_production"]: + device_name = f"Inverter {inverter}" + entity_name = f"{device_name} {sensor_description.name}" + serial_number = inverter + entities.append( + EnvoyInverterSignalEntity( + description=sensor_description, + name=f"{device_name} {sensor_description.name}", + device_name=device_name, + device_serial_number=serial_number, + serial_number=None, + coordinator=coordinator, + parent_device=config_entry.unique_id, + ) + ) + + elif sensor_description.key == "relays_communication_level": + if ( + coordinator.data.get("relays") != None + and coordinator.data.get("pcu_availability") is not None + ): + for serial_number in coordinator.data["relays"].keys(): + device_name = f"Relay {serial_number}" + entities.append( + EnvoyRelaySignalEntity( + description=sensor_description, + name=f"{device_name} {sensor_description.name}", + device_name=device_name, + device_serial_number=serial_number, + serial_number=None, + coordinator=coordinator, + parent_device=config_entry.unique_id, + ) + ) + elif sensor_description.key.startswith("inverters_"): if coordinator.data.get("inverters_status") is not None: for inverter in coordinator.data["inverters_status"].keys(): @@ -238,6 +276,8 @@ def device_info(self) -> DeviceInfo | None: class EnvoyInverterEntity(CoordinatorEntity, SensorEntity): """Envoy inverter entity.""" + MODEL = "Inverter" + def __init__( self, description, @@ -314,34 +354,82 @@ def device_info(self) -> DeviceInfo | None: """Return the device_info of the device.""" if not self._device_serial_number: return None + device_info_kw = {} + if self._parent_device: + device_info_kw["via_device"] = (DOMAIN, self._parent_device) + + model_name = self.MODEL + if self.MODEL == "Envoy": + model = self.coordinator.data.get("envoy_info", {}).get("model", "Standard") + model_name = f"Envoy-S {model}" + + elif self.MODEL == "Inverter": + if self.coordinator.data.get( + "inverters_info" + ) and self.coordinator.data.get("inverters_info").get( + self._device_serial_number + ): + device_info_kw["sw_version"] = ( + self.coordinator.data.get("inverters_info") + .get(self._device_serial_number) + .get("img_pnum_running") + ) + device_info_kw["hw_version"] = ( + self.coordinator.data.get("inverters_info") + .get(self._device_serial_number) + .get("part_num") + ) + model_name = (get_model_name("Inverter", device_info_kw["hw_version"]),) - sw_version = None - hw_version = None - if self.coordinator.data.get("inverters_info") and self.coordinator.data.get( - "inverters_info" - ).get(self._device_serial_number): - sw_version = ( - self.coordinator.data.get("inverters_info") - .get(self._device_serial_number) - .get("img_pnum_running") + elif self.MODEL == "Relay": + info = self.coordinator.data.get("relay_info", {}).get( + self._device_serial_number, {} ) - hw_version = ( - self.coordinator.data.get("inverters_info") - .get(self._device_serial_number) - .get("part_num") + device_info_kw["sw_version"] = info.get("img_pnum_running", None) + device_info_kw["hw_version"] = resolve_hardware_id( + info.get("part_num", None) ) + model_name = get_model_name(model_name, info.get("part_num", None)) return DeviceInfo( identifiers={(DOMAIN, str(self._device_serial_number))}, manufacturer="Enphase", - model=get_model_name("Inverter", hw_version), + model=model_name, name=self._device_name, - via_device=(DOMAIN, self._parent_device), - sw_version=sw_version, - hw_version=resolve_hardware_id(hw_version), + **device_info_kw, ) +class EnvoyInverterSignalEntity(EnvoyInverterEntity): + + @property + def icon(self): + return { + 5: "mdi:wifi-strength-4", + 4: "mdi:wifi-strength-3", + 3: "mdi:wifi-strength-2", + 2: "mdi:wifi-strength-1", + 1: "mdi:wifi-strength-outline", + 0: "mdi:wifi-strength-off-outline", + }.get(self.native_value) + + @property + def extra_state_attributes(self): + return None + + @property + def native_value(self) -> int: + """Return the status of the requested attribute.""" + data = self.coordinator.data.get("pcu_availability") + if data is None: + return 0 + return int(data.get(self._device_serial_number, 0)) + + +class EnvoyRelaySignalEntity(EnvoyInverterSignalEntity): + MODEL = "Relay" + + class EnvoyBatteryEntity(CoordinatorEntity, SensorEntity): """Envoy battery entity.""" diff --git a/test_data/envoy_metered/endpoint_pcu_comm_check.json b/test_data/envoy_metered/endpoint_pcu_comm_check.json new file mode 100644 index 0000000..2b09faf --- /dev/null +++ b/test_data/envoy_metered/endpoint_pcu_comm_check.json @@ -0,0 +1,14 @@ +{"999999913010": 5, +"999999913012": 5, +"999999912750": 5, +"999999912983": 5, +"999999908520": 5, +"999999909983": 5, +"999999908521": 3, +"999999912669": 4, +"999999913748": 5, +"999999909985": 5, +"999999915285": 5, +"999999915246": 5, +"999999912590": 5, +"999999910862": 5} \ No newline at end of file