From 7b5ec23889caefeb385aead09b9af101fed83109 Mon Sep 17 00:00:00 2001 From: Vincent Wolsink Date: Mon, 28 Oct 2024 17:22:34 +0100 Subject: [PATCH 1/3] Add firmware version sensors for all devices --- custom_components/enphase_envoy/const.py | 24 +++ .../enphase_envoy/envoy_endpoints.py | 2 +- custom_components/enphase_envoy/sensor.py | 200 ++++++++++++------ 3 files changed, 159 insertions(+), 67 deletions(-) diff --git a/custom_components/enphase_envoy/const.py b/custom_components/enphase_envoy/const.py index c63470b..b2edcab 100644 --- a/custom_components/enphase_envoy/const.py +++ b/custom_components/enphase_envoy/const.py @@ -290,6 +290,30 @@ def get_model_name(model, hardware_id): suggested_display_precision=0, entity_category=EntityCategory.DIAGNOSTIC, ), + SensorEntityDescription( + key="envoy_software", + name="Firmware Version", + icon="mdi:memory", + entity_category=EntityCategory.DIAGNOSTIC, + ), + SensorEntityDescription( + key="inverters_software", + name="Firmware Version", + icon="mdi:memory", + entity_category=EntityCategory.DIAGNOSTIC, + ), + SensorEntityDescription( + key="relays_software", + name="Firmware Version", + icon="mdi:memory", + entity_category=EntityCategory.DIAGNOSTIC, + ), + SensorEntityDescription( + key="batteries_software", + name="Firmware Version", + icon="mdi:memory", + 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 eebcf47..d5cacea 100644 --- a/custom_components/enphase_envoy/envoy_endpoints.py +++ b/custom_components/enphase_envoy/envoy_endpoints.py @@ -2,7 +2,7 @@ # Generic endpoints "info": { "url": "https://{}/info.xml", - "cache": 86400, + "cache": 3600, "installer_required": False, "optional": False, }, diff --git a/custom_components/enphase_envoy/sensor.py b/custom_components/enphase_envoy/sensor.py index acb8a6b..0c92f1f 100644 --- a/custom_components/enphase_envoy/sensor.py +++ b/custom_components/enphase_envoy/sensor.py @@ -61,6 +61,38 @@ async def async_setup_entry( ) ) + elif sensor_description.key == "inverters_software": + if coordinator.data.get("inverters_info") is not None: + for serial_number in coordinator.data["inverters_production"]: + device_name = f"Inverter {serial_number}" + entities.append( + EnvoyInverterFirmwareEntity( + 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_software": + if coordinator.data.get("relays") != None: + for serial_number in coordinator.data["relays"].keys(): + device_name = f"Relay {serial_number}" + entities.append( + EnvoyRelayFirmwareEntity( + 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 == "inverters_communication_level": if coordinator.data.get("pcu_availability") is not None: for serial_number in coordinator.data["inverters_production"]: @@ -114,6 +146,24 @@ async def async_setup_entry( ) ) + elif sensor_description.key == "batteries_software": + if coordinator.data.get("batteries") is not None: + for battery in coordinator.data["batteries"].keys(): + device_name = f"Battery {battery}" + entity_name = f"{device_name} {sensor_description.name}" + serial_number = battery + entities.append( + EnvoyBatteryFirmwareEntity( + 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("batteries_"): if coordinator.data.get("batteries") is not None: for battery in coordinator.data["batteries"].keys(): @@ -271,10 +321,7 @@ def device_info(self) -> DeviceInfo | None: ) -class EnvoyInverterEntity(CoordinatorEntity, SensorEntity): - """Envoy inverter entity.""" - - MODEL = "Inverter" +class EnvoyDeviceEntity(CoordinatorEntity, SensorEntity): def __init__( self, @@ -307,6 +354,9 @@ def unique_id(self): if self._device_serial_number: return f"{self._device_serial_number}_{self.entity_description.key}" + +class EnvoyInverterEntity(EnvoyDeviceEntity): + @property def native_value(self): """Return the state of the sensor.""" @@ -356,38 +406,47 @@ def device_info(self) -> DeviceInfo | None: 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"]),) - - elif self.MODEL == "Relay": - info = self.coordinator.data.get("relay_info", {}).get( - self._device_serial_number, {} + 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["sw_version"] = info.get("img_pnum_running", None) - device_info_kw["hw_version"] = resolve_hardware_id( - info.get("part_num", None) + device_info_kw["hw_version"] = ( + self.coordinator.data.get("inverters_info") + .get(self._device_serial_number) + .get("part_num") ) - model_name = get_model_name(model_name, info.get("part_num", None)) + model_name = (get_model_name("Inverter", device_info_kw["hw_version"]),) + + return DeviceInfo( + identifiers={(DOMAIN, str(self._device_serial_number))}, + manufacturer="Enphase", + model=model_name, + name=self._device_name, + **device_info_kw, + ) + + +class EnvoyRelayEntity(EnvoyDeviceEntity): + + @property + 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) + + info = self.coordinator.data.get("relay_info", {}).get( + self._device_serial_number, {} + ) + 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("Relay", info.get("part_num", None)) return DeviceInfo( identifiers={(DOMAIN, str(self._device_serial_number))}, @@ -398,7 +457,7 @@ def device_info(self) -> DeviceInfo | None: ) -class EnvoyInverterSignalEntity(EnvoyInverterEntity): +class EnvoySignalEntity(EnvoyDeviceEntity): @property def icon(self): @@ -424,43 +483,38 @@ def native_value(self) -> int: return int(data.get(self._device_serial_number, 0)) -class EnvoyRelaySignalEntity(EnvoyInverterSignalEntity): - MODEL = "Relay" +class EnvoyInverterFirmwareEntity(EnvoyInverterEntity): + @property + def native_value(self) -> str: + return ( + self.coordinator.data.get("inverters_info") + .get(self._device_serial_number) + .get("img_pnum_running") + ) -class EnvoyBatteryEntity(CoordinatorEntity, SensorEntity): - """Envoy battery entity.""" - def __init__( - self, - description, - name, - device_name, - device_serial_number, - serial_number, - coordinator, - parent_device, - ): - self.entity_description = description - self._name = name - self._serial_number = serial_number - self._device_name = device_name - self._device_serial_number = device_serial_number - self._parent_device = parent_device - CoordinatorEntity.__init__(self, coordinator) +class EnvoyRelayFirmwareEntity(EnvoyRelayEntity): @property - def name(self): - """Return the name of the sensor.""" - return self._name + def native_value(self) -> str: + return ( + self.coordinator.data.get("relay_info", {}) + .get(self._device_serial_number, {}) + .get("img_pnum_running", None) + ) + + +class EnvoyInverterSignalEntity(EnvoySignalEntity, EnvoyInverterEntity): + pass - @property - def unique_id(self): - """Return the unique id of the sensor.""" - if self._serial_number: - return self._serial_number - if self._device_serial_number: - return f"{self._device_serial_number}_{self.entity_description.key}" + +class EnvoyRelaySignalEntity(EnvoySignalEntity, EnvoyRelayEntity): + pass + + +class EnvoyBatteryEntity(EnvoyDeviceEntity): + """Envoy battery entity.""" @property def native_value(self): @@ -530,3 +584,17 @@ def device_info(self) -> DeviceInfo | None: sw_version=sw_version, hw_version=resolve_hardware_id(hw_version), ) + + +class EnvoyBatteryFirmwareEntity(EnvoyBatteryEntity): + + @property + def native_value(self) -> str: + if self.coordinator.data.get("batteries") and self.coordinator.data.get( + "batteries" + ).get(self._device_serial_number): + return ( + self.coordinator.data.get("batteries") + .get(self._device_serial_number) + .get("img_pnum_running") + ) From 91035ef666e2db4d83ab2541f5a40a1725689e3e Mon Sep 17 00:00:00 2001 From: Vincent Wolsink Date: Mon, 28 Oct 2024 17:41:23 +0100 Subject: [PATCH 2/3] Uniform argument passing --- custom_components/enphase_envoy/sensor.py | 91 +++++++++++------------ 1 file changed, 42 insertions(+), 49 deletions(-) diff --git a/custom_components/enphase_envoy/sensor.py b/custom_components/enphase_envoy/sensor.py index 0c92f1f..af1ebfa 100644 --- a/custom_components/enphase_envoy/sensor.py +++ b/custom_components/enphase_envoy/sensor.py @@ -47,17 +47,16 @@ async def async_setup_entry( if coordinator.data.get("inverters_production") 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( EnvoyInverterEntity( - sensor_description, - entity_name, - device_name, - serial_number, - serial_number, - coordinator, - config_entry.unique_id, + description=sensor_description, + name=f"{device_name} {sensor_description.name}", + device_name=device_name, + device_serial_number=serial_number, + serial_number=serial_number, + coordinator=coordinator, + parent_device=config_entry.unique_id, ) ) @@ -132,17 +131,16 @@ async def async_setup_entry( if coordinator.data.get("inverters_status") is not None: for inverter in coordinator.data["inverters_status"].keys(): device_name = f"Inverter {inverter}" - entity_name = f"{device_name} {sensor_description.name}" serial_number = inverter entities.append( EnvoyInverterEntity( - sensor_description, - entity_name, - device_name, - serial_number, - None, - coordinator, - config_entry.unique_id, + 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, ) ) @@ -150,7 +148,6 @@ async def async_setup_entry( if coordinator.data.get("batteries") is not None: for battery in coordinator.data["batteries"].keys(): device_name = f"Battery {battery}" - entity_name = f"{device_name} {sensor_description.name}" serial_number = battery entities.append( EnvoyBatteryFirmwareEntity( @@ -168,32 +165,30 @@ async def async_setup_entry( if coordinator.data.get("batteries") is not None: for battery in coordinator.data["batteries"].keys(): device_name = f"Battery {battery}" - entity_name = f"{device_name} {sensor_description.name}" serial_number = battery entities.append( EnvoyBatteryEntity( - sensor_description, - entity_name, - device_name, - serial_number, - None, - coordinator, - config_entry.unique_id, + 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("agg_batteries_"): if coordinator.data.get("batteries") is not None: - entity_name = f"{name} {sensor_description.name}" entities.append( CoordinatedEnvoyEntity( - sensor_description, - entity_name, - name, - config_entry.unique_id, - None, - coordinator, - config_entry.data[CONF_HOST], + description=sensor_description, + name=f"{name} {sensor_description.name}", + device_name=name, + device_serial_number=config_entry.unique_id, + serial_number=None, + coordinator=coordinator, + device_host=config_entry.data[CONF_HOST], ) ) @@ -202,16 +197,15 @@ async def async_setup_entry( if data is None: continue - entity_name = f"{name} {sensor_description.name}" entities.append( CoordinatedEnvoyEntity( - sensor_description, - entity_name, - name, - config_entry.unique_id, - None, - coordinator, - config_entry.data[CONF_HOST], + description=sensor_description, + name=f"{name} {sensor_description.name}", + device_name=name, + device_serial_number=config_entry.unique_id, + serial_number=None, + coordinator=coordinator, + device_host=config_entry.data[CONF_HOST], ) ) @@ -224,15 +218,14 @@ async def async_setup_entry( if data is None: continue - entity_name = f"{name} {sensor_description.name}" live_entities[sensor_description.key] = CoordinatedEnvoyEntity( - sensor_description, - entity_name, - name, - config_entry.unique_id, - None, - coordinator, - config_entry.data[CONF_HOST], + description=sensor_description, + name=f"{name} {sensor_description.name}", + device_name=name, + device_serial_number=config_entry.unique_id, + serial_number=None, + coordinator=coordinator, + device_host=config_entry.data[CONF_HOST], ) entities.append(live_entities[sensor_description.key]) From 354661a78aae5c1fb9c96e2b8da4fba34b582780 Mon Sep 17 00:00:00 2001 From: Vincent Wolsink Date: Mon, 28 Oct 2024 17:53:33 +0100 Subject: [PATCH 3/3] Recommended is not None --- custom_components/enphase_envoy/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/custom_components/enphase_envoy/sensor.py b/custom_components/enphase_envoy/sensor.py index af1ebfa..844a88b 100644 --- a/custom_components/enphase_envoy/sensor.py +++ b/custom_components/enphase_envoy/sensor.py @@ -77,7 +77,7 @@ async def async_setup_entry( ) elif sensor_description.key == "relays_software": - if coordinator.data.get("relays") != None: + if coordinator.data.get("relays") is not None: for serial_number in coordinator.data["relays"].keys(): device_name = f"Relay {serial_number}" entities.append( @@ -110,7 +110,7 @@ async def async_setup_entry( elif sensor_description.key == "relays_communication_level": if ( - coordinator.data.get("relays") != None + coordinator.data.get("relays") is not None and coordinator.data.get("pcu_availability") is not None ): for serial_number in coordinator.data["relays"].keys():