Add support for changing Enphase battery backup modes (#102392)

This commit is contained in:
Charles Garwood 2023-10-20 20:05:42 -04:00 committed by GitHub
parent 41b59b6990
commit 013e580c02
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 174 additions and 7 deletions

View File

@ -6,7 +6,7 @@
"documentation": "https://www.home-assistant.io/integrations/enphase_envoy", "documentation": "https://www.home-assistant.io/integrations/enphase_envoy",
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["pyenphase"], "loggers": ["pyenphase"],
"requirements": ["pyenphase==1.12.0"], "requirements": ["pyenphase==1.13.0"],
"zeroconf": [ "zeroconf": [
{ {
"type": "_enphase-envoy._tcp.local." "type": "_enphase-envoy._tcp.local."

View File

@ -1,10 +1,13 @@
"""Number platform for Enphase Envoy solar energy monitor.""" """Number platform for Enphase Envoy solar energy monitor."""
from __future__ import annotations from __future__ import annotations
from collections.abc import Callable from collections.abc import Awaitable, Callable
from dataclasses import dataclass from dataclasses import dataclass
from typing import Any
from pyenphase import EnvoyDryContactSettings from pyenphase import Envoy, EnvoyDryContactSettings
from pyenphase.const import SupportedFeatures
from pyenphase.models.tariff import EnvoyStorageSettings
from homeassistant.components.number import ( from homeassistant.components.number import (
NumberDeviceClass, NumberDeviceClass,
@ -12,7 +15,7 @@ from homeassistant.components.number import (
NumberEntityDescription, NumberEntityDescription,
) )
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EntityCategory from homeassistant.const import PERCENTAGE, EntityCategory
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
@ -36,6 +39,21 @@ class EnvoyRelayNumberEntityDescription(
"""Describes an Envoy Dry Contact Relay number entity.""" """Describes an Envoy Dry Contact Relay number entity."""
@dataclass
class EnvoyStorageSettingsRequiredKeysMixin:
"""Mixin for required keys."""
value_fn: Callable[[EnvoyStorageSettings], float]
update_fn: Callable[[Envoy, float], Awaitable[dict[str, Any]]]
@dataclass
class EnvoyStorageSettingsNumberEntityDescription(
NumberEntityDescription, EnvoyStorageSettingsRequiredKeysMixin
):
"""Describes an Envoy storage mode number entity."""
RELAY_ENTITIES = ( RELAY_ENTITIES = (
EnvoyRelayNumberEntityDescription( EnvoyRelayNumberEntityDescription(
key="soc_low", key="soc_low",
@ -53,6 +71,15 @@ RELAY_ENTITIES = (
), ),
) )
STORAGE_RESERVE_SOC_ENTITY = EnvoyStorageSettingsNumberEntityDescription(
key="reserve_soc",
translation_key="reserve_soc",
native_unit_of_measurement=PERCENTAGE,
device_class=NumberDeviceClass.BATTERY,
value_fn=lambda storage_settings: storage_settings.reserved_soc,
update_fn=lambda envoy, value: envoy.set_reserve_soc(int(value)),
)
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
@ -70,6 +97,14 @@ async def async_setup_entry(
for entity in RELAY_ENTITIES for entity in RELAY_ENTITIES
for relay in envoy_data.dry_contact_settings for relay in envoy_data.dry_contact_settings
) )
if (
envoy_data.tariff
and envoy_data.tariff.storage_settings
and coordinator.envoy.supported_features & SupportedFeatures.ENCHARGE
):
entities.append(
EnvoyStorageSettingsNumberEntity(coordinator, STORAGE_RESERVE_SOC_ENTITY)
)
async_add_entities(entities) async_add_entities(entities)
@ -114,3 +149,42 @@ class EnvoyRelayNumberEntity(EnvoyBaseEntity, NumberEntity):
{"id": self._relay_id, self.entity_description.key: int(value)} {"id": self._relay_id, self.entity_description.key: int(value)}
) )
await self.coordinator.async_request_refresh() await self.coordinator.async_request_refresh()
class EnvoyStorageSettingsNumberEntity(EnvoyBaseEntity, NumberEntity):
"""Representation of an Enphase storage settings number entity."""
entity_description: EnvoyStorageSettingsNumberEntityDescription
def __init__(
self,
coordinator: EnphaseUpdateCoordinator,
description: EnvoyStorageSettingsNumberEntityDescription,
) -> None:
"""Initialize the Enphase relay number entity."""
super().__init__(coordinator, description)
self.envoy = coordinator.envoy
assert self.data.enpower is not None
enpower = self.data.enpower
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 native_value(self) -> float:
"""Return the state of the storage setting entity."""
assert self.data.tariff is not None
assert self.data.tariff.storage_settings is not None
return self.entity_description.value_fn(self.data.tariff.storage_settings)
async def async_set_native_value(self, value: float) -> None:
"""Update the storage setting."""
await self.entity_description.update_fn(self.envoy, value)
await self.coordinator.async_request_refresh()

View File

@ -1,12 +1,14 @@
"""Select platform for Enphase Envoy solar energy monitor.""" """Select platform for Enphase Envoy solar energy monitor."""
from __future__ import annotations from __future__ import annotations
from collections.abc import Callable, Coroutine from collections.abc import Awaitable, Callable, Coroutine
from dataclasses import dataclass from dataclasses import dataclass
from typing import Any from typing import Any
from pyenphase import Envoy, EnvoyDryContactSettings from pyenphase import Envoy, EnvoyDryContactSettings
from pyenphase.const import SupportedFeatures
from pyenphase.models.dry_contacts import DryContactAction, DryContactMode from pyenphase.models.dry_contacts import DryContactAction, DryContactMode
from pyenphase.models.tariff import EnvoyStorageMode, EnvoyStorageSettings
from homeassistant.components.select import SelectEntity, SelectEntityDescription from homeassistant.components.select import SelectEntity, SelectEntityDescription
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
@ -36,6 +38,21 @@ class EnvoyRelaySelectEntityDescription(
"""Describes an Envoy Dry Contact Relay select entity.""" """Describes an Envoy Dry Contact Relay select entity."""
@dataclass
class EnvoyStorageSettingsRequiredKeysMixin:
"""Mixin for required keys."""
value_fn: Callable[[EnvoyStorageSettings], str]
update_fn: Callable[[Envoy, str], Awaitable[dict[str, Any]]]
@dataclass
class EnvoyStorageSettingsSelectEntityDescription(
SelectEntityDescription, EnvoyStorageSettingsRequiredKeysMixin
):
"""Describes an Envoy storage settings select entity."""
RELAY_MODE_MAP = { RELAY_MODE_MAP = {
DryContactMode.MANUAL: "standard", DryContactMode.MANUAL: "standard",
DryContactMode.STATE_OF_CHARGE: "battery", DryContactMode.STATE_OF_CHARGE: "battery",
@ -51,6 +68,14 @@ REVERSE_RELAY_ACTION_MAP = {v: k for k, v in RELAY_ACTION_MAP.items()}
MODE_OPTIONS = list(REVERSE_RELAY_MODE_MAP) MODE_OPTIONS = list(REVERSE_RELAY_MODE_MAP)
ACTION_OPTIONS = list(REVERSE_RELAY_ACTION_MAP) ACTION_OPTIONS = list(REVERSE_RELAY_ACTION_MAP)
STORAGE_MODE_MAP = {
EnvoyStorageMode.BACKUP: "backup",
EnvoyStorageMode.SELF_CONSUMPTION: "self_consumption",
EnvoyStorageMode.SAVINGS: "savings",
}
REVERSE_STORAGE_MODE_MAP = {v: k for k, v in STORAGE_MODE_MAP.items()}
STORAGE_MODE_OPTIONS = list(REVERSE_STORAGE_MODE_MAP)
RELAY_ENTITIES = ( RELAY_ENTITIES = (
EnvoyRelaySelectEntityDescription( EnvoyRelaySelectEntityDescription(
key="mode", key="mode",
@ -101,6 +126,15 @@ RELAY_ENTITIES = (
), ),
), ),
) )
STORAGE_MODE_ENTITY = EnvoyStorageSettingsSelectEntityDescription(
key="storage_mode",
translation_key="storage_mode",
options=STORAGE_MODE_OPTIONS,
value_fn=lambda storage_settings: STORAGE_MODE_MAP[storage_settings.mode],
update_fn=lambda envoy, value: envoy.set_storage_mode(
REVERSE_STORAGE_MODE_MAP[value]
),
)
async def async_setup_entry( async def async_setup_entry(
@ -119,6 +153,14 @@ async def async_setup_entry(
for entity in RELAY_ENTITIES for entity in RELAY_ENTITIES
for relay in envoy_data.dry_contact_settings for relay in envoy_data.dry_contact_settings
) )
if (
envoy_data.tariff
and envoy_data.tariff.storage_settings
and coordinator.envoy.supported_features & SupportedFeatures.ENCHARGE
):
entities.append(
EnvoyStorageSettingsSelectEntity(coordinator, STORAGE_MODE_ENTITY)
)
async_add_entities(entities) async_add_entities(entities)
@ -164,3 +206,43 @@ class EnvoyRelaySelectEntity(EnvoyBaseEntity, SelectEntity):
"""Update the relay.""" """Update the relay."""
await self.entity_description.update_fn(self.envoy, self.relay, option) await self.entity_description.update_fn(self.envoy, self.relay, option)
await self.coordinator.async_request_refresh() await self.coordinator.async_request_refresh()
class EnvoyStorageSettingsSelectEntity(EnvoyBaseEntity, SelectEntity):
"""Representation of an Enphase storage settings select entity."""
entity_description: EnvoyStorageSettingsSelectEntityDescription
def __init__(
self,
coordinator: EnphaseUpdateCoordinator,
description: EnvoyStorageSettingsSelectEntityDescription,
) -> None:
"""Initialize the Enphase storage settings select entity."""
super().__init__(coordinator, description)
self.envoy = coordinator.envoy
assert coordinator.envoy.data is not None
assert coordinator.envoy.data.enpower is not None
enpower = coordinator.envoy.data.enpower
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 current_option(self) -> str:
"""Return the state of the select entity."""
assert self.data.tariff is not None
assert self.data.tariff.storage_settings is not None
return self.entity_description.value_fn(self.data.tariff.storage_settings)
async def async_select_option(self, option: str) -> None:
"""Update the relay."""
await self.entity_description.update_fn(self.envoy, option)
await self.coordinator.async_request_refresh()

View File

@ -39,6 +39,9 @@
}, },
"restore_battery_level": { "restore_battery_level": {
"name": "Restore battery level" "name": "Restore battery level"
},
"reserve_soc": {
"name": "Reserve battery level"
} }
}, },
"select": { "select": {
@ -75,6 +78,14 @@
"schedule": "[%key:component::enphase_envoy::entity::select::relay_grid_action::state::schedule%]", "schedule": "[%key:component::enphase_envoy::entity::select::relay_grid_action::state::schedule%]",
"none": "[%key:component::enphase_envoy::entity::select::relay_grid_action::state::none%]" "none": "[%key:component::enphase_envoy::entity::select::relay_grid_action::state::none%]"
} }
},
"storage_mode": {
"name": "Storage mode",
"state": {
"self_consumption": "Self consumption",
"backup": "Full backup",
"savings": "Savings mode"
}
} }
}, },
"sensor": { "sensor": {

View File

@ -1691,7 +1691,7 @@ pyedimax==0.2.1
pyefergy==22.1.1 pyefergy==22.1.1
# homeassistant.components.enphase_envoy # homeassistant.components.enphase_envoy
pyenphase==1.12.0 pyenphase==1.13.0
# homeassistant.components.envisalink # homeassistant.components.envisalink
pyenvisalink==4.6 pyenvisalink==4.6

View File

@ -1273,7 +1273,7 @@ pyeconet==0.1.20
pyefergy==22.1.1 pyefergy==22.1.1
# homeassistant.components.enphase_envoy # homeassistant.components.enphase_envoy
pyenphase==1.12.0 pyenphase==1.13.0
# homeassistant.components.everlights # homeassistant.components.everlights
pyeverlights==0.1.0 pyeverlights==0.1.0