"""Pyenphase Ensemble updater class.""" import logging from typing import Any from ..const import ( ENSEMBLE_MIN_VERSION, URL_DRY_CONTACT_SETTINGS, URL_DRY_CONTACT_STATUS, URL_ENCHARGE_BATTERY, URL_ENSEMBLE_INVENTORY, URL_ENSEMBLE_SECCTRL, SupportedFeatures, ) from ..exceptions import ENDPOINT_PROBE_EXCEPTIONS, EnvoyAuthenticationRequired from ..models.acb import EnvoyBatteryAggregate from ..models.c6combiner import EnvoyC6CC from ..models.collar import EnvoyCollar from ..models.dry_contacts import EnvoyDryContactSettings, EnvoyDryContactStatus from ..models.encharge import EnvoyEncharge, EnvoyEnchargeAggregate, EnvoyEnchargePower from ..models.enpower import EnvoyEnpower from ..models.envoy import EnvoyData from .base import EnvoyUpdater _LOGGER = logging.getLogger(__name__) class EnvoyEnembleUpdater(EnvoyUpdater): """Class to handle updates for Ensemble devices.""" async def probe( self, discovered_features: SupportedFeatures ) -> SupportedFeatures | None: """Probe the Envoy for this updater and return SupportedFeatures.""" if self._envoy_version < ENSEMBLE_MIN_VERSION: _LOGGER.debug("Firmware too old for Ensemble support") return None # Check for various Ensemble support # The Ensemble Inventory endpoint will tell us if we have Enpower or Encharge support try: result = await self._json_probe_request(URL_ENSEMBLE_INVENTORY) except ENDPOINT_PROBE_EXCEPTIONS as e: _LOGGER.debug("Ensemble Inventory endpoint not found: %s", e) except EnvoyAuthenticationRequired as e: # For URL_ENSEMBLE_INVENTORY some early V7 versions # return 401 when using aiohttp. _LOGGER.debug( "Skipping Ensemble Inventory endpoint as user does" " not have access to %s: %s", URL_ENSEMBLE_INVENTORY, e, ) return None else: if not result or "error" in result: # Newer firmware with no Ensemble devices returns an empty list _LOGGER.debug("No Ensemble devices found") return None for item in result: if item["type"] == "ENPOWER": self._supported_features |= SupportedFeatures.ENPOWER if item["type"] == "ENCHARGE": self._supported_features |= SupportedFeatures.ENCHARGE if item["type"] == "COLLAR": self._supported_features |= SupportedFeatures.COLLAR if item["type"] == "C6 COMBINER CONTROLLER": self._supported_features |= SupportedFeatures.C6CC return self._supported_features async def update(self, envoy_data: EnvoyData) -> None: """Update the Envoy for this updater.""" # Update Enpower and Encharge data if supported supported_features = self._supported_features ensemble_inventory_data: list[dict[str, Any]] = await self._json_request( URL_ENSEMBLE_INVENTORY ) envoy_data.raw[URL_ENSEMBLE_INVENTORY] = ensemble_inventory_data ensemble_secctrl_data: dict[str, Any] = await self._json_request( URL_ENSEMBLE_SECCTRL ) envoy_data.raw[URL_ENSEMBLE_SECCTRL] = await self._json_request( URL_ENSEMBLE_SECCTRL ) if supported_features & SupportedFeatures.ENCHARGE: encharge_power_data: dict[str, Any] = await self._json_request( URL_ENCHARGE_BATTERY ) envoy_data.raw[URL_ENCHARGE_BATTERY] = encharge_power_data power: dict[str, Any] = { device["serial_num"]: device for device in encharge_power_data["devices:"] } inventory: dict[str, Any] = {} for item in ensemble_inventory_data: if item["type"] != "ENCHARGE": continue inventory = {device["serial_num"]: device for device in item["devices"]} envoy_data.encharge_inventory = { serial: EnvoyEncharge.from_api(inventory[serial]) for serial in inventory } envoy_data.encharge_power = { serial: EnvoyEnchargePower.from_api(power[serial]) for serial in power } envoy_data.encharge_aggregate = EnvoyEnchargeAggregate.from_api( ensemble_secctrl_data ) if supported_features & SupportedFeatures.ENPOWER: # Update Enpower data for item in ensemble_inventory_data: if item["type"] != "ENPOWER": continue enpower_data = item["devices"][0] envoy_data.enpower = EnvoyEnpower.from_api(enpower_data) # Update dry contact data dry_contact_status_data: dict[str, Any] = await self._json_request( URL_DRY_CONTACT_STATUS ) envoy_data.raw[URL_DRY_CONTACT_STATUS] = dry_contact_status_data dry_contact_settings_data: dict[str, Any] = await self._json_request( URL_DRY_CONTACT_SETTINGS ) envoy_data.raw[URL_DRY_CONTACT_SETTINGS] = dry_contact_settings_data envoy_data.dry_contact_status = { relay["id"]: EnvoyDryContactStatus.from_api(relay) for relay in dry_contact_status_data["dry_contacts"] } envoy_data.dry_contact_settings = { relay["id"]: EnvoyDryContactSettings.from_api(relay) for relay in dry_contact_settings_data["dry_contacts"] } # production updater will set common_properties.ACB_batteries_reported # to count of ACB batteries. Check if production report found # acb batteries and if so report combined soc and max capacity # for Encharge and ACB batteries if self._common_properties.acb_batteries_reported: envoy_data.battery_aggregate = EnvoyBatteryAggregate.from_api( ensemble_secctrl_data ) # IQ Meter collar seems like a single instance only if supported_features & SupportedFeatures.COLLAR: # Update Collar data for item in ensemble_inventory_data: if item["type"] != "COLLAR": continue if item.get("devices"): collar_data = item["devices"][0] envoy_data.collar = EnvoyCollar.from_api(collar_data) # C6 Combiner seems like a single instance only if supported_features & SupportedFeatures.C6CC: # Update C6CC data for item in ensemble_inventory_data: if item["type"] != "C6 COMBINER CONTROLLER": continue if item.get("devices"): c6cc_data = item["devices"][0] envoy_data.c6cc = EnvoyC6CC.from_api(c6cc_data)