From e3438baf49b447074193bf33a5505dad209873f9 Mon Sep 17 00:00:00 2001 From: Charles Garwood Date: Mon, 14 Aug 2023 20:23:16 -0400 Subject: [PATCH] Add select platform to Enphase integration (#98368) * Add select platform to Enphase integration * Review comments pt1 * Review comments pt2 * Review comments * Additional tweaks from code review * .coveragerc --------- Co-authored-by: J. Nick Koston --- .coveragerc | 1 + .../components/enphase_envoy/const.py | 2 +- .../components/enphase_envoy/manifest.json | 2 +- .../components/enphase_envoy/select.py | 171 ++++++++++++++++++ .../components/enphase_envoy/strings.json | 36 ++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 7 files changed, 212 insertions(+), 4 deletions(-) create mode 100644 homeassistant/components/enphase_envoy/select.py diff --git a/.coveragerc b/.coveragerc index e64058d93d0..014dc2f0f39 100644 --- a/.coveragerc +++ b/.coveragerc @@ -305,6 +305,7 @@ omit = homeassistant/components/enphase_envoy/binary_sensor.py homeassistant/components/enphase_envoy/coordinator.py homeassistant/components/enphase_envoy/entity.py + homeassistant/components/enphase_envoy/select.py homeassistant/components/enphase_envoy/sensor.py homeassistant/components/enphase_envoy/switch.py homeassistant/components/entur_public_transport/* diff --git a/homeassistant/components/enphase_envoy/const.py b/homeassistant/components/enphase_envoy/const.py index 828abe8fe4c..d1c6618502e 100644 --- a/homeassistant/components/enphase_envoy/const.py +++ b/homeassistant/components/enphase_envoy/const.py @@ -5,6 +5,6 @@ from homeassistant.const import Platform DOMAIN = "enphase_envoy" -PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR, Platform.SWITCH] +PLATFORMS = [Platform.BINARY_SENSOR, Platform.SELECT, Platform.SENSOR, Platform.SWITCH] INVALID_AUTH_ERRORS = (EnvoyAuthenticationError, EnvoyAuthenticationRequired) diff --git a/homeassistant/components/enphase_envoy/manifest.json b/homeassistant/components/enphase_envoy/manifest.json index 6969dc3d6ab..62f7c73ef76 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.5.2"], + "requirements": ["pyenphase==1.6.0"], "zeroconf": [ { "type": "_enphase-envoy._tcp.local." diff --git a/homeassistant/components/enphase_envoy/select.py b/homeassistant/components/enphase_envoy/select.py new file mode 100644 index 00000000000..75c9ce0cf7c --- /dev/null +++ b/homeassistant/components/enphase_envoy/select.py @@ -0,0 +1,171 @@ +"""Select platform for Enphase Envoy solar energy monitor.""" +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass +import logging +from typing import Any + +from pyenphase import EnvoyDryContactSettings +from pyenphase.models.dry_contacts import DryContactAction, DryContactMode + +from homeassistant.components.select import SelectEntity, SelectEntityDescription +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .coordinator import EnphaseUpdateCoordinator +from .entity import EnvoyBaseEntity + +_LOGGER = logging.getLogger(__name__) + + +@dataclass +class EnvoyRelayRequiredKeysMixin: + """Mixin for required keys.""" + + value_fn: Callable[[EnvoyDryContactSettings], str] + update_fn: Callable[[Any, Any, Any], Any] + + +@dataclass +class EnvoyRelaySelectEntityDescription( + SelectEntityDescription, EnvoyRelayRequiredKeysMixin +): + """Describes an Envoy Dry Contact Relay select entity.""" + + +RELAY_MODE_MAP = { + DryContactMode.MANUAL: "standard", + DryContactMode.STATE_OF_CHARGE: "battery", +} +REVERSE_RELAY_MODE_MAP = {v: k for k, v in RELAY_MODE_MAP.items()} +RELAY_ACTION_MAP = { + DryContactAction.APPLY: "powered", + DryContactAction.SHED: "not_powered", + DryContactAction.SCHEDULE: "schedule", + DryContactAction.NONE: "none", +} +REVERSE_RELAY_ACTION_MAP = {v: k for k, v in RELAY_ACTION_MAP.items()} +MODE_OPTIONS = list(REVERSE_RELAY_MODE_MAP) +ACTION_OPTIONS = list(REVERSE_RELAY_ACTION_MAP) + +RELAY_ENTITIES = ( + EnvoyRelaySelectEntityDescription( + key="mode", + translation_key="relay_mode", + options=MODE_OPTIONS, + value_fn=lambda relay: RELAY_MODE_MAP[relay.mode], + update_fn=lambda envoy, relay, value: envoy.update_dry_contact( + { + "id": relay.id, + "mode": REVERSE_RELAY_MODE_MAP[value], + } + ), + ), + EnvoyRelaySelectEntityDescription( + key="grid_action", + translation_key="relay_grid_action", + options=ACTION_OPTIONS, + value_fn=lambda relay: RELAY_ACTION_MAP[relay.grid_action], + update_fn=lambda envoy, relay, value: envoy.update_dry_contact( + { + "id": relay.id, + "grid_action": REVERSE_RELAY_ACTION_MAP[value], + } + ), + ), + EnvoyRelaySelectEntityDescription( + key="microgrid_action", + translation_key="relay_microgrid_action", + options=ACTION_OPTIONS, + value_fn=lambda relay: RELAY_ACTION_MAP[relay.micro_grid_action], + update_fn=lambda envoy, relay, value: envoy.update_dry_contact( + { + "id": relay.id, + "micro_grid_action": REVERSE_RELAY_ACTION_MAP[value], + } + ), + ), + EnvoyRelaySelectEntityDescription( + key="generator_action", + translation_key="relay_generator_action", + options=ACTION_OPTIONS, + value_fn=lambda relay: RELAY_ACTION_MAP[relay.generator_action], + update_fn=lambda envoy, relay, value: envoy.update_dry_contact( + { + "id": relay.id, + "generator_action": REVERSE_RELAY_ACTION_MAP[value], + } + ), + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up Enphase Envoy select platform.""" + coordinator: EnphaseUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] + envoy_data = coordinator.envoy.data + assert envoy_data is not None + envoy_serial_num = config_entry.unique_id + assert envoy_serial_num is not None + entities: list[SelectEntity] = [] + if envoy_data.dry_contact_settings: + entities.extend( + EnvoyRelaySelectEntity(coordinator, entity, relay) + for entity in RELAY_ENTITIES + for relay in envoy_data.dry_contact_settings + ) + async_add_entities(entities) + + +class EnvoyRelaySelectEntity(EnvoyBaseEntity, SelectEntity): + """Representation of an Enphase Enpower select entity.""" + + entity_description: EnvoyRelaySelectEntityDescription + + def __init__( + self, + coordinator: EnphaseUpdateCoordinator, + description: EnvoyRelaySelectEntityDescription, + relay: str, + ) -> None: + """Initialize the Enphase relay select entity.""" + super().__init__(coordinator, description) + self.envoy = coordinator.envoy + assert self.envoy is not None + assert self.data is not None + self.enpower = self.data.enpower + assert self.enpower is not None + self._serial_number = self.enpower.serial_number + self.relay = self.data.dry_contact_settings[relay] + self.relay_id = relay + self._attr_unique_id = ( + f"{self._serial_number}_relay_{relay}_{self.entity_description.key}" + ) + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, relay)}, + manufacturer="Enphase", + model="Dry contact relay", + name=self.relay.load_name, + sw_version=str(self.enpower.firmware_version), + via_device=(DOMAIN, self._serial_number), + ) + + @property + def current_option(self) -> str: + """Return the state of the Enpower switch.""" + return self.entity_description.value_fn( + self.data.dry_contact_settings[self.relay_id] + ) + + async def async_select_option(self, option: str) -> None: + """Update the relay.""" + await self.entity_description.update_fn(self.envoy, self.relay, option) + await self.coordinator.async_request_refresh() diff --git a/homeassistant/components/enphase_envoy/strings.json b/homeassistant/components/enphase_envoy/strings.json index 2afd19d87d1..bab16bc6c58 100644 --- a/homeassistant/components/enphase_envoy/strings.json +++ b/homeassistant/components/enphase_envoy/strings.json @@ -36,6 +36,42 @@ "name": "Grid status" } }, + "select": { + "relay_mode": { + "name": "Mode", + "state": { + "standard": "Standard", + "battery": "Battery level" + } + }, + "relay_grid_action": { + "name": "Grid action", + "state": { + "powered": "Powered", + "not_powered": "Not powered", + "schedule": "Follow schedule", + "none": "None" + } + }, + "relay_microgrid_action": { + "name": "Microgrid action", + "state": { + "powered": "[%key:component::enphase_envoy::entity::select::relay_grid_action::state::powered%]", + "not_powered": "[%key:component::enphase_envoy::entity::select::relay_grid_action::state::not_powered%]", + "schedule": "[%key:component::enphase_envoy::entity::select::relay_grid_action::state::schedule%]", + "none": "[%key:component::enphase_envoy::entity::select::relay_grid_action::state::none%]" + } + }, + "relay_generator_action": { + "name": "Generator action", + "state": { + "powered": "[%key:component::enphase_envoy::entity::select::relay_grid_action::state::powered%]", + "not_powered": "[%key:component::enphase_envoy::entity::select::relay_grid_action::state::not_powered%]", + "schedule": "[%key:component::enphase_envoy::entity::select::relay_grid_action::state::schedule%]", + "none": "[%key:component::enphase_envoy::entity::select::relay_grid_action::state::none%]" + } + } + }, "sensor": { "last_reported": { "name": "Last reported" diff --git a/requirements_all.txt b/requirements_all.txt index f4d8381f0c1..6f755d7c860 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.5.2 +pyenphase==1.6.0 # homeassistant.components.envisalink pyenvisalink==4.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 05f81b16e78..51c87f8176a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1226,7 +1226,7 @@ pyeconet==0.1.20 pyefergy==22.1.1 # homeassistant.components.enphase_envoy -pyenphase==1.5.2 +pyenphase==1.6.0 # homeassistant.components.everlights pyeverlights==0.1.0