Files

298 lines
10 KiB
Python

"""Support for AirGradient select entities."""
from collections.abc import Awaitable, Callable
from dataclasses import dataclass
from typing import override
from airgradient import AirGradientClient, Config, GpsMode
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, supports_config
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."""
config_key: str
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,
config_key="configuration_control",
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,
config_key="temperature_unit",
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,
config_key="pm_standard",
value_fn=lambda config: (
PM_STANDARD.get(config.pm_standard) if config.pm_standard else None
),
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,
config_key="led_bar_mode",
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",
"7",
"8",
"30",
"90",
"180",
"0",
]
def _get_value(value: int | None, values: list[str]) -> str | None:
str_value = str(value)
return str_value if str_value in values else None
def _get_led_level(value: int | None, options: list[str]) -> str | None:
"""Return a semantic LED-level option."""
return options[value] if value is not None and 0 <= value < len(options) else None
LED_BRIGHTNESS_OPTIONS = ["off", "dim", "mid", "bright"]
TOUCH_LED_INTENSITY_OPTIONS = ["off", "dim", "bright"]
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,
config_key="nox_learning_offset",
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,
config_key="tvoc_learning_offset",
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,
config_key="co2_automatic_baseline_calibration_days",
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))
),
),
)
GO_SELECT_ENTITIES: tuple[AirGradientSelectEntityDescription, ...] = (
AirGradientSelectEntityDescription(
key="gps_mode",
translation_key="gps_mode",
options=[mode.value for mode in GpsMode],
entity_category=EntityCategory.CONFIG,
config_key="gps_mode",
value_fn=lambda config: config.gps_mode,
set_value_fn=lambda client, value: client.set_gps_mode(GpsMode(value)),
),
AirGradientSelectEntityDescription(
key="front_led_brightness",
translation_key="front_led_brightness",
options=LED_BRIGHTNESS_OPTIONS,
entity_category=EntityCategory.CONFIG,
config_key="front_led_brightness",
value_fn=lambda config: _get_led_level(
config.front_led_brightness, LED_BRIGHTNESS_OPTIONS
),
set_value_fn=lambda client, value: client.set_front_led_brightness(
LED_BRIGHTNESS_OPTIONS.index(value)
),
),
AirGradientSelectEntityDescription(
key="back_led_brightness",
translation_key="back_led_brightness",
options=LED_BRIGHTNESS_OPTIONS,
entity_category=EntityCategory.CONFIG,
config_key="back_led_brightness",
value_fn=lambda config: _get_led_level(
config.back_led_brightness, LED_BRIGHTNESS_OPTIONS
),
set_value_fn=lambda client, value: client.set_back_led_brightness(
LED_BRIGHTNESS_OPTIONS.index(value)
),
),
AirGradientSelectEntityDescription(
key="touch_led_intensity",
translation_key="touch_led_intensity",
options=TOUCH_LED_INTENSITY_OPTIONS,
entity_category=EntityCategory.CONFIG,
config_key="touch_led_intensity",
value_fn=lambda config: _get_led_level(
config.touch_led_intensity, TOUCH_LED_INTENSITY_OPTIONS
),
set_value_fn=lambda client, value: client.set_touch_led_intensity(
TOUCH_LED_INTENSITY_OPTIONS.index(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
descriptions = (
CONFIG_CONTROL_ENTITY,
*DISPLAY_SELECT_TYPES,
*LED_BAR_ENTITIES,
*CONTROL_ENTITIES,
*GO_SELECT_ENTITIES,
)
descriptions_by_key = {description.key: description for description in descriptions}
added_entities: set[str] = set()
@callback
def _async_check_entities() -> None:
nonlocal added_entities
config = coordinator.data.config
desired_entities = {
description.key
for description in descriptions
if supports_config(
model, coordinator.client.api_version, config, description.config_key
)
and (
description is CONFIG_CONTROL_ENTITY
or config.configuration_control is ConfigurationControl.LOCAL
)
}
if entities_to_add := desired_entities - added_entities:
async_add_entities(
[
AirGradientSelect(coordinator, descriptions_by_key[key])
for key in entities_to_add
]
)
if entities_to_remove := added_entities - desired_entities:
entity_registry = er.async_get(hass)
for key in entities_to_remove:
unique_id = f"{coordinator.serial_number}-{key}"
if entity_id := entity_registry.async_get_entity_id(
SELECT_DOMAIN, DOMAIN, unique_id
):
entity_registry.async_remove(entity_id)
added_entities = desired_entities
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()