From ce6b759b70de4d029b4151f7ee30b8a6fb5a35f0 Mon Sep 17 00:00:00 2001 From: Charles Garwood Date: Tue, 8 Aug 2023 23:05:52 -0400 Subject: [PATCH] Add Envoy enpower sensors (#98086) --- .../components/enphase_envoy/binary_sensor.py | 164 ++++++++++++++++-- .../components/enphase_envoy/manifest.json | 2 +- .../components/enphase_envoy/sensor.py | 71 ++++++++ .../components/enphase_envoy/strings.json | 5 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 6 files changed, 225 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/enphase_envoy/binary_sensor.py b/homeassistant/components/enphase_envoy/binary_sensor.py index af57a4da6af..4e893050b16 100644 --- a/homeassistant/components/enphase_envoy/binary_sensor.py +++ b/homeassistant/components/enphase_envoy/binary_sensor.py @@ -8,7 +8,9 @@ import logging from pyenphase import ( EnvoyData, EnvoyEncharge, + EnvoyEnpower, ) +from pyenphase.models.dry_contacts import DryContactStatus from homeassistant.components.binary_sensor import ( BinarySensorDeviceClass, @@ -66,6 +68,47 @@ ENCHARGE_SENSORS = ( ), ) +RELAY_STATUS_SENSOR = BinarySensorEntityDescription( + key="relay_status", icon="mdi:power-plug", has_entity_name=True +) + + +@dataclass +class EnvoyEnpowerRequiredKeysMixin: + """Mixin for required keys.""" + + value_fn: Callable[[EnvoyEnpower], bool] + + +@dataclass +class EnvoyEnpowerBinarySensorEntityDescription( + BinarySensorEntityDescription, EnvoyEnpowerRequiredKeysMixin +): + """Describes an Envoy Enpower binary sensor entity.""" + + +ENPOWER_SENSORS = ( + EnvoyEnpowerBinarySensorEntityDescription( + key="communicating", + translation_key="communicating", + device_class=BinarySensorDeviceClass.CONNECTIVITY, + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda enpower: enpower.communicating, + ), + EnvoyEnpowerBinarySensorEntityDescription( + key="operating", + translation_key="operating", + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda enpower: enpower.operating, + ), + EnvoyEnpowerBinarySensorEntityDescription( + key="mains_oper_state", + translation_key="grid_status", + icon="mdi:transmission-tower", + value_fn=lambda enpower: enpower.mains_oper_state == "closed", + ), +) + async def async_setup_entry( hass: HomeAssistant, @@ -86,15 +129,50 @@ async def async_setup_entry( for encharge in envoy_data.encharge_inventory ) + if envoy_data.enpower: + entities.extend( + EnvoyEnpowerBinarySensorEntity(coordinator, description) + for description in ENPOWER_SENSORS + ) + + if envoy_data.dry_contact_status: + entities.extend( + EnvoyRelayBinarySensorEntity(coordinator, RELAY_STATUS_SENSOR, relay) + for relay in envoy_data.dry_contact_status + ) async_add_entities(entities) -class EnvoyEnchargeBinarySensorEntity( +class EnvoyBaseBinarySensorEntity( CoordinatorEntity[EnphaseUpdateCoordinator], BinarySensorEntity ): """Defines a base envoy binary_sensor entity.""" _attr_has_entity_name = True + + def __init__( + self, + coordinator: EnphaseUpdateCoordinator, + description: BinarySensorEntityDescription, + ) -> None: + """Init the Enphase base binary_sensor entity.""" + self.entity_description = description + serial_number = coordinator.envoy.serial_number + assert serial_number is not None + self.envoy_serial_num = serial_number + super().__init__(coordinator) + + @property + def data(self) -> EnvoyData: + """Return envoy data.""" + data = self.coordinator.envoy.data + assert data is not None + return data + + +class EnvoyEnchargeBinarySensorEntity(EnvoyBaseBinarySensorEntity): + """Defines an Encharge binary_sensor entity.""" + entity_description: EnvoyEnchargeBinarySensorEntityDescription def __init__( @@ -104,13 +182,7 @@ class EnvoyEnchargeBinarySensorEntity( serial_number: str, ) -> None: """Init the Encharge base entity.""" - self.entity_description = description - self.coordinator = coordinator - assert serial_number is not None - - self.envoy_serial_num = coordinator.envoy.serial_number - assert self.envoy_serial_num is not None - + super().__init__(coordinator, description) self._serial_number = serial_number self._attr_unique_id = f"{serial_number}_{description.key}" encharge_inventory = self.data.encharge_inventory @@ -124,18 +196,76 @@ class EnvoyEnchargeBinarySensorEntity( via_device=(DOMAIN, self.envoy_serial_num), ) - super().__init__(coordinator) - - @property - def data(self) -> EnvoyData: - """Return envoy data.""" - data = self.coordinator.envoy.data - assert data is not None - return data - @property def is_on(self) -> bool: """Return the state of the Encharge binary_sensor.""" encharge_inventory = self.data.encharge_inventory assert encharge_inventory is not None return self.entity_description.value_fn(encharge_inventory[self._serial_number]) + + +class EnvoyEnpowerBinarySensorEntity(EnvoyBaseBinarySensorEntity): + """Defines an Enpower binary_sensor entity.""" + + entity_description: EnvoyEnpowerBinarySensorEntityDescription + + def __init__( + self, + coordinator: EnphaseUpdateCoordinator, + description: EnvoyEnpowerBinarySensorEntityDescription, + ) -> None: + """Init the Enpower base entity.""" + super().__init__(coordinator, description) + enpower = self.data.enpower + assert enpower is not None + self._serial_number = enpower.serial_number + self._attr_unique_id = f"{self._serial_number}_{description.key}" + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, self._serial_number)}, + manufacturer="Enphase", + model="Enpower", + name=f"Enpower {self._serial_number}", + sw_version=str(enpower.firmware_version), + via_device=(DOMAIN, self.envoy_serial_num), + ) + + @property + def is_on(self) -> bool: + """Return the state of the Enpower binary_sensor.""" + enpower = self.data.enpower + assert enpower is not None + return self.entity_description.value_fn(enpower) + + +class EnvoyRelayBinarySensorEntity(EnvoyBaseBinarySensorEntity): + """Defines an Enpower dry contact binary_sensor entity.""" + + def __init__( + self, + coordinator: EnphaseUpdateCoordinator, + description: BinarySensorEntityDescription, + relay: str, + ) -> None: + """Init the Enpower base entity.""" + super().__init__(coordinator, description) + enpower = self.data.enpower + assert enpower is not None + self.relay = self.data.dry_contact_status[relay] + self._serial_number = enpower.serial_number + self._attr_unique_id = f"{self._serial_number}_relay_{self.relay.id}" + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, self._serial_number)}, + manufacturer="Enphase", + model="Enpower", + name=f"Enpower {self._serial_number}", + sw_version=str(enpower.firmware_version), + via_device=(DOMAIN, self.envoy_serial_num), + ) + self._attr_name = ( + f"{self.data.dry_contact_settings[self.relay.id].load_name} Relay" + ) + + @property + def is_on(self) -> bool: + """Return the state of the Enpower binary_sensor.""" + return self.relay.status == DryContactStatus.CLOSED diff --git a/homeassistant/components/enphase_envoy/manifest.json b/homeassistant/components/enphase_envoy/manifest.json index 9656dbe9084..36faae38228 100644 --- a/homeassistant/components/enphase_envoy/manifest.json +++ b/homeassistant/components/enphase_envoy/manifest.json @@ -6,7 +6,7 @@ "documentation": "https://www.home-assistant.io/integrations/enphase_envoy", "iot_class": "local_polling", "loggers": ["pyenphase"], - "requirements": ["pyenphase==1.1.3"], + "requirements": ["pyenphase==1.2.1"], "zeroconf": [ { "type": "_enphase-envoy._tcp.local." diff --git a/homeassistant/components/enphase_envoy/sensor.py b/homeassistant/components/enphase_envoy/sensor.py index 37063f5e53f..019af1d393e 100644 --- a/homeassistant/components/enphase_envoy/sensor.py +++ b/homeassistant/components/enphase_envoy/sensor.py @@ -10,6 +10,7 @@ from pyenphase import ( EnvoyData, EnvoyEncharge, EnvoyEnchargePower, + EnvoyEnpower, EnvoyInverter, EnvoySystemConsumption, EnvoySystemProduction, @@ -259,6 +260,36 @@ ENCHARGE_POWER_SENSORS = ( ) +@dataclass +class EnvoyEnpowerRequiredKeysMixin: + """Mixin for required keys.""" + + value_fn: Callable[[EnvoyEnpower], datetime.datetime | int | float] + + +@dataclass +class EnvoyEnpowerSensorEntityDescription( + SensorEntityDescription, EnvoyEnpowerRequiredKeysMixin +): + """Describes an Envoy Encharge sensor entity.""" + + +ENPOWER_SENSORS = ( + EnvoyEnpowerSensorEntityDescription( + key="temperature", + native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, + device_class=SensorDeviceClass.TEMPERATURE, + value_fn=lambda enpower: enpower.temperature, + ), + EnvoyEnpowerSensorEntityDescription( + key=LAST_REPORTED_KEY, + translation_key=LAST_REPORTED_KEY, + device_class=SensorDeviceClass.TIMESTAMP, + value_fn=lambda enpower: dt_util.utc_from_timestamp(enpower.last_report_date), + ), +) + + async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, @@ -300,6 +331,11 @@ async def async_setup_entry( for description in ENCHARGE_POWER_SENSORS for encharge in envoy_data.encharge_power ) + if envoy_data.enpower: + entities.extend( + EnvoyEnpowerEntity(coordinator, description) + for description in ENPOWER_SENSORS + ) async_add_entities(entities) @@ -469,3 +505,38 @@ class EnvoyEnchargePowerEntity(EnvoyEnchargeEntity): encharge_power = self.data.encharge_power assert encharge_power is not None return self.entity_description.value_fn(encharge_power[self._serial_number]) + + +class EnvoyEnpowerEntity(EnvoyBaseEntity, SensorEntity): + """Envoy Enpower sensor entity.""" + + _attr_has_entity_name = True + entity_description: EnvoyEnpowerSensorEntityDescription + + def __init__( + self, + coordinator: EnphaseUpdateCoordinator, + description: EnvoyEnpowerSensorEntityDescription, + ) -> None: + """Initialize Enpower entity.""" + super().__init__(coordinator, description) + assert coordinator.envoy.data is not None + enpower_data = coordinator.envoy.data.enpower + assert enpower_data is not None + self._serial_number = enpower_data.serial_number + self._attr_unique_id = f"{self._serial_number}_{description.key}" + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, self._serial_number)}, + manufacturer="Enphase", + model="Enpower", + name=f"Enpower {self._serial_number}", + sw_version=str(enpower_data.firmware_version), + via_device=(DOMAIN, self.envoy_serial_num), + ) + + @property + def native_value(self) -> datetime.datetime | int | float | None: + """Return the state of the power sensors.""" + enpower = self.data.enpower + assert enpower is not None + return self.entity_description.value_fn(enpower) diff --git a/homeassistant/components/enphase_envoy/strings.json b/homeassistant/components/enphase_envoy/strings.json index 46ec7d9607f..915fee94e2a 100644 --- a/homeassistant/components/enphase_envoy/strings.json +++ b/homeassistant/components/enphase_envoy/strings.json @@ -27,10 +27,13 @@ "name": "Communicating" }, "dc_switch": { - "name": "DC Switch" + "name": "DC switch" }, "operating": { "name": "Operating" + }, + "grid_status": { + "name": "Grid status" } }, "sensor": { diff --git a/requirements_all.txt b/requirements_all.txt index 0496849e477..7516b2e35a7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1662,7 +1662,7 @@ pyedimax==0.2.1 pyefergy==22.1.1 # homeassistant.components.enphase_envoy -pyenphase==1.1.3 +pyenphase==1.2.1 # homeassistant.components.envisalink pyenvisalink==4.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 15ef1794dfe..6b1a1728fed 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1229,7 +1229,7 @@ pyeconet==0.1.20 pyefergy==22.1.1 # homeassistant.components.enphase_envoy -pyenphase==1.1.3 +pyenphase==1.2.1 # homeassistant.components.everlights pyeverlights==0.1.0