"""Support for AirGradient select entities.""" from collections.abc import Awaitable, Callable from dataclasses import dataclass from typing import override from airgradient import AirGradientClient, Config from airgradient.models import ConfigurationControl, LedBarMode, TemperatureUnit from homeassistant.components.select import ( DOMAIN as SELECT_DOMAIN, SelectEntity, SelectEntityDescription, ) from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from . import AirGradientConfigEntry from .const import DOMAIN, PM_STANDARD, PM_STANDARD_REVERSE from .coordinator import AirGradientCoordinator from .entity import AirGradientEntity, exception_handler PARALLEL_UPDATES = 1 @dataclass(frozen=True, kw_only=True) class AirGradientSelectEntityDescription(SelectEntityDescription): """Describes AirGradient select entity.""" value_fn: Callable[[Config], str | None] set_value_fn: Callable[[AirGradientClient, str], Awaitable[None]] CONFIG_CONTROL_ENTITY = AirGradientSelectEntityDescription( key="configuration_control", translation_key="configuration_control", options=[ConfigurationControl.CLOUD.value, ConfigurationControl.LOCAL.value], entity_category=EntityCategory.CONFIG, value_fn=lambda config: ( config.configuration_control if config.configuration_control is not ConfigurationControl.NOT_INITIALIZED else None ), set_value_fn=lambda client, value: client.set_configuration_control( ConfigurationControl(value) ), ) DISPLAY_SELECT_TYPES: tuple[AirGradientSelectEntityDescription, ...] = ( AirGradientSelectEntityDescription( key="display_temperature_unit", translation_key="display_temperature_unit", options=[x.value for x in TemperatureUnit], entity_category=EntityCategory.CONFIG, value_fn=lambda config: config.temperature_unit, set_value_fn=lambda client, value: client.set_temperature_unit( TemperatureUnit(value) ), ), AirGradientSelectEntityDescription( key="display_pm_standard", translation_key="display_pm_standard", options=list(PM_STANDARD_REVERSE), entity_category=EntityCategory.CONFIG, value_fn=lambda config: PM_STANDARD.get(config.pm_standard), set_value_fn=lambda client, value: client.set_pm_standard( PM_STANDARD_REVERSE[value] ), ), ) LED_BAR_ENTITIES: tuple[AirGradientSelectEntityDescription, ...] = ( AirGradientSelectEntityDescription( key="led_bar_mode", translation_key="led_bar_mode", options=[x.value for x in LedBarMode], entity_category=EntityCategory.CONFIG, value_fn=lambda config: config.led_bar_mode, set_value_fn=lambda client, value: client.set_led_bar_mode(LedBarMode(value)), ), ) LEARNING_TIME_OFFSET_OPTIONS = [ "12", "60", "120", "360", "720", ] ABC_DAYS = [ "1", "8", "30", "90", "180", "0", ] def _get_value(value: int, values: list[str]) -> str | None: str_value = str(value) return str_value if str_value in values else None CONTROL_ENTITIES: tuple[AirGradientSelectEntityDescription, ...] = ( AirGradientSelectEntityDescription( key="nox_index_learning_time_offset", translation_key="nox_index_learning_time_offset", options=LEARNING_TIME_OFFSET_OPTIONS, entity_category=EntityCategory.CONFIG, value_fn=lambda config: _get_value( config.nox_learning_offset, LEARNING_TIME_OFFSET_OPTIONS ), set_value_fn=lambda client, value: client.set_nox_learning_offset(int(value)), ), AirGradientSelectEntityDescription( key="voc_index_learning_time_offset", translation_key="voc_index_learning_time_offset", options=LEARNING_TIME_OFFSET_OPTIONS, entity_category=EntityCategory.CONFIG, value_fn=lambda config: _get_value( config.tvoc_learning_offset, LEARNING_TIME_OFFSET_OPTIONS ), set_value_fn=lambda client, value: client.set_tvoc_learning_offset(int(value)), ), AirGradientSelectEntityDescription( key="co2_automatic_baseline_calibration", translation_key="co2_automatic_baseline_calibration", options=ABC_DAYS, entity_category=EntityCategory.CONFIG, value_fn=lambda config: _get_value( config.co2_automatic_baseline_calibration_days, ABC_DAYS ), set_value_fn=lambda client, value: ( client.set_co2_automatic_baseline_calibration(int(value)) ), ), ) async def async_setup_entry( hass: HomeAssistant, entry: AirGradientConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up AirGradient select entities based on a config entry.""" coordinator = entry.runtime_data model = coordinator.data.measures.model async_add_entities([AirGradientSelect(coordinator, CONFIG_CONTROL_ENTITY)]) added_entities = False @callback def _async_check_entities() -> None: nonlocal added_entities if ( coordinator.data.config.configuration_control is ConfigurationControl.LOCAL and not added_entities ): entities: list[AirGradientSelect] = [ AirGradientSelect(coordinator, description) for description in CONTROL_ENTITIES ] if "I" in model: entities.extend( AirGradientSelect(coordinator, description) for description in DISPLAY_SELECT_TYPES ) if "L" in model: entities.extend( AirGradientSelect(coordinator, description) for description in LED_BAR_ENTITIES ) async_add_entities(entities) added_entities = True elif ( coordinator.data.config.configuration_control is not ConfigurationControl.LOCAL and added_entities ): entity_registry = er.async_get(hass) for entity_description in ( DISPLAY_SELECT_TYPES + LED_BAR_ENTITIES + CONTROL_ENTITIES ): unique_id = f"{coordinator.serial_number}-{entity_description.key}" if entity_id := entity_registry.async_get_entity_id( SELECT_DOMAIN, DOMAIN, unique_id ): entity_registry.async_remove(entity_id) added_entities = False coordinator.async_add_listener(_async_check_entities) _async_check_entities() class AirGradientSelect(AirGradientEntity, SelectEntity): """Defines an AirGradient select entity.""" entity_description: AirGradientSelectEntityDescription def __init__( self, coordinator: AirGradientCoordinator, description: AirGradientSelectEntityDescription, ) -> None: """Initialize AirGradient select.""" super().__init__(coordinator) self.entity_description = description self._attr_unique_id = f"{coordinator.serial_number}-{description.key}" @property @override def current_option(self) -> str | None: """Return the state of the select.""" return self.entity_description.value_fn(self.coordinator.data.config) @exception_handler @override async def async_select_option(self, option: str) -> None: """Change the selected option.""" await self.entity_description.set_value_fn(self.coordinator.client, option) await self.coordinator.async_request_refresh()