"""The sma integration.""" from __future__ import annotations from datetime import timedelta import logging import pysma from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry, ConfigEntryNotReady from homeassistant.const import ( CONF_HOST, CONF_PASSWORD, CONF_PATH, CONF_SCAN_INTERVAL, CONF_SENSORS, CONF_SSL, CONF_VERIFY_SSL, EVENT_HOMEASSISTANT_STOP, ) from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import ( CONF_CUSTOM, CONF_FACTOR, CONF_GROUP, CONF_KEY, CONF_UNIT, DEFAULT_SCAN_INTERVAL, DOMAIN, PLATFORMS, PYSMA_COORDINATOR, PYSMA_DEVICE_INFO, PYSMA_OBJECT, PYSMA_REMOVE_LISTENER, PYSMA_SENSORS, ) _LOGGER = logging.getLogger(__name__) def _parse_legacy_options( entry: ConfigEntry, sensor_def: pysma.sensor.Sensors ) -> list[str]: """Parse legacy configuration options. This will parse the legacy CONF_SENSORS and CONF_CUSTOM configuration options to support deprecated yaml config from platform setup. """ # Add sensors from the custom config sensor_def.add( [ pysma.sensor.Sensor( o[CONF_KEY], n, o[CONF_UNIT], o[CONF_FACTOR], o.get(CONF_PATH) ) for n, o in entry.data.get(CONF_CUSTOM).items() ] ) # Parsing of sensors configuration if not (config_sensors := entry.data.get(CONF_SENSORS)): return [] # Support import of legacy config that should have been removed from 0.99, but was still functional # See also #25880 and #26306. Functional support was dropped in #48003 if isinstance(config_sensors, dict): config_sensors_list = [] for name, attr in config_sensors.items(): config_sensors_list.append(name) config_sensors_list.extend(attr) config_sensors = config_sensors_list # Find and replace sensors removed from pysma # This only alters the config, the actual sensor migration takes place in _migrate_old_unique_ids for sensor in config_sensors.copy(): if sensor in pysma.const.LEGACY_MAP: config_sensors.remove(sensor) config_sensors.append(pysma.const.LEGACY_MAP[sensor]["new_sensor"]) # Only sensors from config should be enabled for sensor in sensor_def: sensor.enabled = sensor.name in config_sensors return config_sensors def _migrate_old_unique_ids( hass: HomeAssistant, entry: ConfigEntry, sensor_def: pysma.sensor.Sensors, config_sensors: list[str], ) -> None: """Migrate legacy sensor entity_id format to new format.""" entity_registry = er.async_get(hass) # Create list of all possible sensor names possible_sensors = set( config_sensors + [s.name for s in sensor_def] + list(pysma.const.LEGACY_MAP) ) for sensor in possible_sensors: if sensor in sensor_def: pysma_sensor = sensor_def[sensor] original_key = pysma_sensor.key elif sensor in pysma.const.LEGACY_MAP: # If sensor was removed from pysma we will remap it to the new sensor legacy_sensor = pysma.const.LEGACY_MAP[sensor] pysma_sensor = sensor_def[legacy_sensor["new_sensor"]] original_key = legacy_sensor["old_key"] else: _LOGGER.error("%s does not exist", sensor) continue # Find entity_id using previous format of unique ID entity_id = entity_registry.async_get_entity_id( "sensor", "sma", f"sma-{original_key}-{sensor}" ) if not entity_id: continue # Change unique_id to new format using the device serial in entry.unique_id new_unique_id = f"{entry.unique_id}-{pysma_sensor.key}_{pysma_sensor.key_idx}" entity_registry.async_update_entity(entity_id, new_unique_id=new_unique_id) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up sma from a config entry.""" # Init the SMA interface protocol = "https" if entry.data[CONF_SSL] else "http" url = f"{protocol}://{entry.data[CONF_HOST]}" verify_ssl = entry.data[CONF_VERIFY_SSL] group = entry.data[CONF_GROUP] password = entry.data[CONF_PASSWORD] session = async_get_clientsession(hass, verify_ssl=verify_ssl) sma = pysma.SMA(session, url, password, group) try: # Get updated device info sma_device_info = await sma.device_info() # Get all device sensors sensor_def = await sma.get_sensors() except ( pysma.exceptions.SmaReadException, pysma.exceptions.SmaConnectionException, ) as exc: raise ConfigEntryNotReady from exc # Create DeviceInfo object from sma_device_info device_info = DeviceInfo( configuration_url=url, identifiers={(DOMAIN, entry.unique_id)}, manufacturer=sma_device_info["manufacturer"], model=sma_device_info["type"], name=sma_device_info["name"], sw_version=sma_device_info["sw_version"], ) # Parse legacy options if initial setup was done from yaml if entry.source == SOURCE_IMPORT: config_sensors = _parse_legacy_options(entry, sensor_def) _migrate_old_unique_ids(hass, entry, sensor_def, config_sensors) # Define the coordinator async def async_update_data(): """Update the used SMA sensors.""" try: await sma.read(sensor_def) except ( pysma.exceptions.SmaReadException, pysma.exceptions.SmaConnectionException, ) as exc: raise UpdateFailed(exc) from exc interval = timedelta( seconds=entry.options.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) ) coordinator = DataUpdateCoordinator( hass, _LOGGER, name="sma", update_method=async_update_data, update_interval=interval, ) try: await coordinator.async_config_entry_first_refresh() except ConfigEntryNotReady: await sma.close_session() raise # Ensure we logout on shutdown async def async_close_session(event): """Close the session.""" await sma.close_session() remove_stop_listener = hass.bus.async_listen_once( EVENT_HOMEASSISTANT_STOP, async_close_session ) hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = { PYSMA_OBJECT: sma, PYSMA_COORDINATOR: coordinator, PYSMA_SENSORS: sensor_def, PYSMA_REMOVE_LISTENER: remove_stop_listener, PYSMA_DEVICE_INFO: device_info, } hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: data = hass.data[DOMAIN].pop(entry.entry_id) await data[PYSMA_OBJECT].close_session() data[PYSMA_REMOVE_LISTENER]() return unload_ok