mirror of
https://github.com/home-assistant/core.git
synced 2025-07-24 05:37:44 +00:00
Add select entities to AirGradient (#117136)
This commit is contained in:
parent
6382cb9134
commit
c80718628e
@ -2,24 +2,47 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from airgradient import AirGradientClient
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import CONF_HOST, Platform
|
from homeassistant.const import CONF_HOST, Platform
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers import device_registry as dr
|
||||||
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
|
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
from .coordinator import AirGradientDataUpdateCoordinator
|
from .coordinator import AirGradientConfigCoordinator, AirGradientMeasurementCoordinator
|
||||||
|
|
||||||
PLATFORMS: list[Platform] = [Platform.SENSOR]
|
PLATFORMS: list[Platform] = [Platform.SELECT, Platform.SENSOR]
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
"""Set up Airgradient from a config entry."""
|
"""Set up Airgradient from a config entry."""
|
||||||
|
|
||||||
coordinator = AirGradientDataUpdateCoordinator(hass, entry.data[CONF_HOST])
|
client = AirGradientClient(
|
||||||
|
entry.data[CONF_HOST], session=async_get_clientsession(hass)
|
||||||
|
)
|
||||||
|
|
||||||
await coordinator.async_config_entry_first_refresh()
|
measurement_coordinator = AirGradientMeasurementCoordinator(hass, client)
|
||||||
|
config_coordinator = AirGradientConfigCoordinator(hass, client)
|
||||||
|
|
||||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator
|
await measurement_coordinator.async_config_entry_first_refresh()
|
||||||
|
await config_coordinator.async_config_entry_first_refresh()
|
||||||
|
|
||||||
|
device_registry = dr.async_get(hass)
|
||||||
|
device_registry.async_get_or_create(
|
||||||
|
config_entry_id=entry.entry_id,
|
||||||
|
identifiers={(DOMAIN, measurement_coordinator.serial_number)},
|
||||||
|
manufacturer="AirGradient",
|
||||||
|
model=measurement_coordinator.data.model,
|
||||||
|
serial_number=measurement_coordinator.data.serial_number,
|
||||||
|
sw_version=measurement_coordinator.data.firmware_version,
|
||||||
|
)
|
||||||
|
|
||||||
|
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = {
|
||||||
|
"measurement": measurement_coordinator,
|
||||||
|
"config": config_coordinator,
|
||||||
|
}
|
||||||
|
|
||||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||||
|
|
||||||
|
@ -2,31 +2,56 @@
|
|||||||
|
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
|
||||||
from airgradient import AirGradientClient, AirGradientError, Measures
|
from airgradient import AirGradientClient, AirGradientError, Config, Measures
|
||||||
|
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
|
||||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||||
|
|
||||||
from .const import LOGGER
|
from .const import LOGGER
|
||||||
|
|
||||||
|
|
||||||
class AirGradientDataUpdateCoordinator(DataUpdateCoordinator[Measures]):
|
class AirGradientCoordinator[_DataT](DataUpdateCoordinator[_DataT]):
|
||||||
"""Class to manage fetching AirGradient data."""
|
"""Class to manage fetching AirGradient data."""
|
||||||
|
|
||||||
def __init__(self, hass: HomeAssistant, host: str) -> None:
|
_update_interval: timedelta
|
||||||
|
config_entry: ConfigEntry
|
||||||
|
|
||||||
|
def __init__(self, hass: HomeAssistant, client: AirGradientClient) -> None:
|
||||||
"""Initialize coordinator."""
|
"""Initialize coordinator."""
|
||||||
super().__init__(
|
super().__init__(
|
||||||
hass,
|
hass,
|
||||||
logger=LOGGER,
|
logger=LOGGER,
|
||||||
name=f"AirGradient {host}",
|
name=f"AirGradient {client.host}",
|
||||||
update_interval=timedelta(minutes=1),
|
update_interval=self._update_interval,
|
||||||
)
|
)
|
||||||
session = async_get_clientsession(hass)
|
self.client = client
|
||||||
self.client = AirGradientClient(host, session=session)
|
assert self.config_entry.unique_id
|
||||||
|
self.serial_number = self.config_entry.unique_id
|
||||||
|
|
||||||
async def _async_update_data(self) -> Measures:
|
async def _async_update_data(self) -> _DataT:
|
||||||
try:
|
try:
|
||||||
return await self.client.get_current_measures()
|
return await self._update_data()
|
||||||
except AirGradientError as error:
|
except AirGradientError as error:
|
||||||
raise UpdateFailed(error) from error
|
raise UpdateFailed(error) from error
|
||||||
|
|
||||||
|
async def _update_data(self) -> _DataT:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
class AirGradientMeasurementCoordinator(AirGradientCoordinator[Measures]):
|
||||||
|
"""Class to manage fetching AirGradient data."""
|
||||||
|
|
||||||
|
_update_interval = timedelta(minutes=1)
|
||||||
|
|
||||||
|
async def _update_data(self) -> Measures:
|
||||||
|
return await self.client.get_current_measures()
|
||||||
|
|
||||||
|
|
||||||
|
class AirGradientConfigCoordinator(AirGradientCoordinator[Config]):
|
||||||
|
"""Class to manage fetching AirGradient data."""
|
||||||
|
|
||||||
|
_update_interval = timedelta(minutes=5)
|
||||||
|
|
||||||
|
async def _update_data(self) -> Config:
|
||||||
|
return await self.client.get_config()
|
||||||
|
@ -4,21 +4,17 @@ from homeassistant.helpers.device_registry import DeviceInfo
|
|||||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||||
|
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
from .coordinator import AirGradientDataUpdateCoordinator
|
from .coordinator import AirGradientCoordinator
|
||||||
|
|
||||||
|
|
||||||
class AirGradientEntity(CoordinatorEntity[AirGradientDataUpdateCoordinator]):
|
class AirGradientEntity(CoordinatorEntity[AirGradientCoordinator]):
|
||||||
"""Defines a base AirGradient entity."""
|
"""Defines a base AirGradient entity."""
|
||||||
|
|
||||||
_attr_has_entity_name = True
|
_attr_has_entity_name = True
|
||||||
|
|
||||||
def __init__(self, coordinator: AirGradientDataUpdateCoordinator) -> None:
|
def __init__(self, coordinator: AirGradientCoordinator) -> None:
|
||||||
"""Initialize airgradient entity."""
|
"""Initialize airgradient entity."""
|
||||||
super().__init__(coordinator)
|
super().__init__(coordinator)
|
||||||
self._attr_device_info = DeviceInfo(
|
self._attr_device_info = DeviceInfo(
|
||||||
identifiers={(DOMAIN, coordinator.data.serial_number)},
|
identifiers={(DOMAIN, coordinator.serial_number)},
|
||||||
model=coordinator.data.model,
|
|
||||||
manufacturer="AirGradient",
|
|
||||||
serial_number=coordinator.data.serial_number,
|
|
||||||
sw_version=coordinator.data.firmware_version,
|
|
||||||
)
|
)
|
||||||
|
119
homeassistant/components/airgradient/select.py
Normal file
119
homeassistant/components/airgradient/select.py
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
"""Support for AirGradient select entities."""
|
||||||
|
|
||||||
|
from collections.abc import Awaitable, Callable
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
from airgradient import AirGradientClient, Config
|
||||||
|
from airgradient.models import ConfigurationControl, TemperatureUnit
|
||||||
|
|
||||||
|
from homeassistant.components.select import SelectEntity, SelectEntityDescription
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.exceptions import ServiceValidationError
|
||||||
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
|
from .const import DOMAIN
|
||||||
|
from .coordinator import AirGradientConfigCoordinator, AirGradientMeasurementCoordinator
|
||||||
|
from .entity import AirGradientEntity
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, kw_only=True)
|
||||||
|
class AirGradientSelectEntityDescription(SelectEntityDescription):
|
||||||
|
"""Describes AirGradient select entity."""
|
||||||
|
|
||||||
|
value_fn: Callable[[Config], str]
|
||||||
|
set_value_fn: Callable[[AirGradientClient, str], Awaitable[None]]
|
||||||
|
requires_display: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
CONFIG_CONTROL_ENTITY = AirGradientSelectEntityDescription(
|
||||||
|
key="configuration_control",
|
||||||
|
translation_key="configuration_control",
|
||||||
|
options=[x.value for x in ConfigurationControl],
|
||||||
|
value_fn=lambda config: config.configuration_control,
|
||||||
|
set_value_fn=lambda client, value: client.set_configuration_control(
|
||||||
|
ConfigurationControl(value)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
PROTECTED_SELECT_TYPES: tuple[AirGradientSelectEntityDescription, ...] = (
|
||||||
|
AirGradientSelectEntityDescription(
|
||||||
|
key="display_temperature_unit",
|
||||||
|
translation_key="display_temperature_unit",
|
||||||
|
options=[x.value for x in TemperatureUnit],
|
||||||
|
value_fn=lambda config: config.temperature_unit,
|
||||||
|
set_value_fn=lambda client, value: client.set_temperature_unit(
|
||||||
|
TemperatureUnit(value)
|
||||||
|
),
|
||||||
|
requires_display=True,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
||||||
|
) -> None:
|
||||||
|
"""Set up AirGradient select entities based on a config entry."""
|
||||||
|
|
||||||
|
config_coordinator: AirGradientConfigCoordinator = hass.data[DOMAIN][
|
||||||
|
entry.entry_id
|
||||||
|
]["config"]
|
||||||
|
measurement_coordinator: AirGradientMeasurementCoordinator = hass.data[DOMAIN][
|
||||||
|
entry.entry_id
|
||||||
|
]["measurement"]
|
||||||
|
|
||||||
|
entities = [AirGradientSelect(config_coordinator, CONFIG_CONTROL_ENTITY)]
|
||||||
|
|
||||||
|
entities.extend(
|
||||||
|
AirGradientProtectedSelect(config_coordinator, description)
|
||||||
|
for description in PROTECTED_SELECT_TYPES
|
||||||
|
if (
|
||||||
|
description.requires_display
|
||||||
|
and measurement_coordinator.data.model.startswith("I")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
async_add_entities(entities)
|
||||||
|
|
||||||
|
|
||||||
|
class AirGradientSelect(AirGradientEntity, SelectEntity):
|
||||||
|
"""Defines an AirGradient select entity."""
|
||||||
|
|
||||||
|
entity_description: AirGradientSelectEntityDescription
|
||||||
|
coordinator: AirGradientConfigCoordinator
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
coordinator: AirGradientConfigCoordinator,
|
||||||
|
description: AirGradientSelectEntityDescription,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize AirGradient select."""
|
||||||
|
super().__init__(coordinator)
|
||||||
|
self.entity_description = description
|
||||||
|
self._attr_unique_id = f"{coordinator.serial_number}-{description.key}"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def current_option(self) -> str:
|
||||||
|
"""Return the state of the select."""
|
||||||
|
return self.entity_description.value_fn(self.coordinator.data)
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
|
||||||
|
class AirGradientProtectedSelect(AirGradientSelect):
|
||||||
|
"""Defines a protected AirGradient select entity."""
|
||||||
|
|
||||||
|
async def async_select_option(self, option: str) -> None:
|
||||||
|
"""Change the selected option."""
|
||||||
|
if (
|
||||||
|
self.coordinator.data.configuration_control
|
||||||
|
is not ConfigurationControl.LOCAL
|
||||||
|
):
|
||||||
|
raise ServiceValidationError(
|
||||||
|
translation_domain=DOMAIN,
|
||||||
|
translation_key="no_local_configuration",
|
||||||
|
)
|
||||||
|
await super().async_select_option(option)
|
@ -24,8 +24,8 @@ from homeassistant.core import HomeAssistant, callback
|
|||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
from homeassistant.helpers.typing import StateType
|
from homeassistant.helpers.typing import StateType
|
||||||
|
|
||||||
from . import AirGradientDataUpdateCoordinator
|
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
|
from .coordinator import AirGradientMeasurementCoordinator
|
||||||
from .entity import AirGradientEntity
|
from .entity import AirGradientEntity
|
||||||
|
|
||||||
|
|
||||||
@ -130,7 +130,9 @@ async def async_setup_entry(
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""Set up AirGradient sensor entities based on a config entry."""
|
"""Set up AirGradient sensor entities based on a config entry."""
|
||||||
|
|
||||||
coordinator: AirGradientDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
|
coordinator: AirGradientMeasurementCoordinator = hass.data[DOMAIN][entry.entry_id][
|
||||||
|
"measurement"
|
||||||
|
]
|
||||||
listener: Callable[[], None] | None = None
|
listener: Callable[[], None] | None = None
|
||||||
not_setup: set[AirGradientSensorEntityDescription] = set(SENSOR_TYPES)
|
not_setup: set[AirGradientSensorEntityDescription] = set(SENSOR_TYPES)
|
||||||
|
|
||||||
@ -162,16 +164,17 @@ class AirGradientSensor(AirGradientEntity, SensorEntity):
|
|||||||
"""Defines an AirGradient sensor."""
|
"""Defines an AirGradient sensor."""
|
||||||
|
|
||||||
entity_description: AirGradientSensorEntityDescription
|
entity_description: AirGradientSensorEntityDescription
|
||||||
|
coordinator: AirGradientMeasurementCoordinator
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
coordinator: AirGradientDataUpdateCoordinator,
|
coordinator: AirGradientMeasurementCoordinator,
|
||||||
description: AirGradientSensorEntityDescription,
|
description: AirGradientSensorEntityDescription,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize airgradient sensor."""
|
"""Initialize airgradient sensor."""
|
||||||
super().__init__(coordinator)
|
super().__init__(coordinator)
|
||||||
self.entity_description = description
|
self.entity_description = description
|
||||||
self._attr_unique_id = f"{coordinator.data.serial_number}-{description.key}"
|
self._attr_unique_id = f"{coordinator.serial_number}-{description.key}"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def native_value(self) -> StateType:
|
def native_value(self) -> StateType:
|
||||||
|
@ -23,6 +23,23 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"entity": {
|
"entity": {
|
||||||
|
"select": {
|
||||||
|
"configuration_control": {
|
||||||
|
"name": "Configuration source",
|
||||||
|
"state": {
|
||||||
|
"cloud": "Cloud",
|
||||||
|
"local": "Local",
|
||||||
|
"both": "Both"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"display_temperature_unit": {
|
||||||
|
"name": "Display temperature unit",
|
||||||
|
"state": {
|
||||||
|
"c": "Celsius",
|
||||||
|
"f": "Fahrenheit"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"sensor": {
|
"sensor": {
|
||||||
"total_volatile_organic_component_index": {
|
"total_volatile_organic_component_index": {
|
||||||
"name": "Total VOC index"
|
"name": "Total VOC index"
|
||||||
@ -40,5 +57,10 @@
|
|||||||
"name": "Raw nitrogen"
|
"name": "Raw nitrogen"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"exceptions": {
|
||||||
|
"no_local_configuration": {
|
||||||
|
"message": "Device should be configured with local configuration to be able to change settings."
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
from collections.abc import Generator
|
from collections.abc import Generator
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
from airgradient import Measures
|
from airgradient import Config, Measures
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from homeassistant.components.airgradient.const import DOMAIN
|
from homeassistant.components.airgradient.const import DOMAIN
|
||||||
@ -28,7 +28,7 @@ def mock_airgradient_client() -> Generator[AsyncMock, None, None]:
|
|||||||
"""Mock an AirGradient client."""
|
"""Mock an AirGradient client."""
|
||||||
with (
|
with (
|
||||||
patch(
|
patch(
|
||||||
"homeassistant.components.airgradient.coordinator.AirGradientClient",
|
"homeassistant.components.airgradient.AirGradientClient",
|
||||||
autospec=True,
|
autospec=True,
|
||||||
) as mock_client,
|
) as mock_client,
|
||||||
patch(
|
patch(
|
||||||
@ -37,9 +37,13 @@ def mock_airgradient_client() -> Generator[AsyncMock, None, None]:
|
|||||||
),
|
),
|
||||||
):
|
):
|
||||||
client = mock_client.return_value
|
client = mock_client.return_value
|
||||||
|
client.host = "10.0.0.131"
|
||||||
client.get_current_measures.return_value = Measures.from_json(
|
client.get_current_measures.return_value = Measures.from_json(
|
||||||
load_fixture("current_measures.json", DOMAIN)
|
load_fixture("current_measures.json", DOMAIN)
|
||||||
)
|
)
|
||||||
|
client.get_config.return_value = Config.from_json(
|
||||||
|
load_fixture("get_config.json", DOMAIN)
|
||||||
|
)
|
||||||
yield client
|
yield client
|
||||||
|
|
||||||
|
|
||||||
|
@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"wifi": -64,
|
||||||
|
"serialno": "84fce60bec38",
|
||||||
|
"channels": {
|
||||||
|
"1": {
|
||||||
|
"pm01": 3,
|
||||||
|
"pm02": 5,
|
||||||
|
"pm10": 5,
|
||||||
|
"pm003Count": 753,
|
||||||
|
"atmp": 18.8,
|
||||||
|
"rhum": 68,
|
||||||
|
"atmpCompensated": 17.09,
|
||||||
|
"rhumCompensated": 92
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tvocIndex": 49,
|
||||||
|
"tvocRaw": 30802,
|
||||||
|
"noxIndex": 1,
|
||||||
|
"noxRaw": 16359,
|
||||||
|
"bootCount": 1,
|
||||||
|
"ledMode": "co2",
|
||||||
|
"firmware": "3.1.1",
|
||||||
|
"model": "O-1PPT"
|
||||||
|
}
|
13
tests/components/airgradient/fixtures/get_config.json
Normal file
13
tests/components/airgradient/fixtures/get_config.json
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"country": "DE",
|
||||||
|
"pmStandard": "ugm3",
|
||||||
|
"ledBarMode": "co2",
|
||||||
|
"displayMode": "on",
|
||||||
|
"abcDays": 8,
|
||||||
|
"tvocLearningOffset": 12,
|
||||||
|
"noxLearningOffset": 12,
|
||||||
|
"mqttBrokerUrl": "",
|
||||||
|
"temperatureUnit": "c",
|
||||||
|
"configurationControl": "both",
|
||||||
|
"postDataToAirGradient": true
|
||||||
|
}
|
170
tests/components/airgradient/snapshots/test_select.ambr
Normal file
170
tests/components/airgradient/snapshots/test_select.ambr
Normal file
@ -0,0 +1,170 @@
|
|||||||
|
# serializer version: 1
|
||||||
|
# name: test_all_entities[select.airgradient_configuration_source-entry]
|
||||||
|
EntityRegistryEntrySnapshot({
|
||||||
|
'aliases': set({
|
||||||
|
}),
|
||||||
|
'area_id': None,
|
||||||
|
'capabilities': dict({
|
||||||
|
'options': list([
|
||||||
|
'cloud',
|
||||||
|
'local',
|
||||||
|
'both',
|
||||||
|
]),
|
||||||
|
}),
|
||||||
|
'config_entry_id': <ANY>,
|
||||||
|
'device_class': None,
|
||||||
|
'device_id': <ANY>,
|
||||||
|
'disabled_by': None,
|
||||||
|
'domain': 'select',
|
||||||
|
'entity_category': None,
|
||||||
|
'entity_id': 'select.airgradient_configuration_source',
|
||||||
|
'has_entity_name': True,
|
||||||
|
'hidden_by': None,
|
||||||
|
'icon': None,
|
||||||
|
'id': <ANY>,
|
||||||
|
'labels': set({
|
||||||
|
}),
|
||||||
|
'name': None,
|
||||||
|
'options': dict({
|
||||||
|
}),
|
||||||
|
'original_device_class': None,
|
||||||
|
'original_icon': None,
|
||||||
|
'original_name': 'Configuration source',
|
||||||
|
'platform': 'airgradient',
|
||||||
|
'previous_unique_id': None,
|
||||||
|
'supported_features': 0,
|
||||||
|
'translation_key': 'configuration_control',
|
||||||
|
'unique_id': '84fce612f5b8-configuration_control',
|
||||||
|
'unit_of_measurement': None,
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_all_entities[select.airgradient_configuration_source-state]
|
||||||
|
StateSnapshot({
|
||||||
|
'attributes': ReadOnlyDict({
|
||||||
|
'friendly_name': 'Airgradient Configuration source',
|
||||||
|
'options': list([
|
||||||
|
'cloud',
|
||||||
|
'local',
|
||||||
|
'both',
|
||||||
|
]),
|
||||||
|
}),
|
||||||
|
'context': <ANY>,
|
||||||
|
'entity_id': 'select.airgradient_configuration_source',
|
||||||
|
'last_changed': <ANY>,
|
||||||
|
'last_reported': <ANY>,
|
||||||
|
'last_updated': <ANY>,
|
||||||
|
'state': 'both',
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_all_entities[select.airgradient_display_temperature_unit-entry]
|
||||||
|
EntityRegistryEntrySnapshot({
|
||||||
|
'aliases': set({
|
||||||
|
}),
|
||||||
|
'area_id': None,
|
||||||
|
'capabilities': dict({
|
||||||
|
'options': list([
|
||||||
|
'c',
|
||||||
|
'f',
|
||||||
|
]),
|
||||||
|
}),
|
||||||
|
'config_entry_id': <ANY>,
|
||||||
|
'device_class': None,
|
||||||
|
'device_id': <ANY>,
|
||||||
|
'disabled_by': None,
|
||||||
|
'domain': 'select',
|
||||||
|
'entity_category': None,
|
||||||
|
'entity_id': 'select.airgradient_display_temperature_unit',
|
||||||
|
'has_entity_name': True,
|
||||||
|
'hidden_by': None,
|
||||||
|
'icon': None,
|
||||||
|
'id': <ANY>,
|
||||||
|
'labels': set({
|
||||||
|
}),
|
||||||
|
'name': None,
|
||||||
|
'options': dict({
|
||||||
|
}),
|
||||||
|
'original_device_class': None,
|
||||||
|
'original_icon': None,
|
||||||
|
'original_name': 'Display temperature unit',
|
||||||
|
'platform': 'airgradient',
|
||||||
|
'previous_unique_id': None,
|
||||||
|
'supported_features': 0,
|
||||||
|
'translation_key': 'display_temperature_unit',
|
||||||
|
'unique_id': '84fce612f5b8-display_temperature_unit',
|
||||||
|
'unit_of_measurement': None,
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_all_entities[select.airgradient_display_temperature_unit-state]
|
||||||
|
StateSnapshot({
|
||||||
|
'attributes': ReadOnlyDict({
|
||||||
|
'friendly_name': 'Airgradient Display temperature unit',
|
||||||
|
'options': list([
|
||||||
|
'c',
|
||||||
|
'f',
|
||||||
|
]),
|
||||||
|
}),
|
||||||
|
'context': <ANY>,
|
||||||
|
'entity_id': 'select.airgradient_display_temperature_unit',
|
||||||
|
'last_changed': <ANY>,
|
||||||
|
'last_reported': <ANY>,
|
||||||
|
'last_updated': <ANY>,
|
||||||
|
'state': 'c',
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_all_entities_outdoor[select.airgradient_configuration_source-entry]
|
||||||
|
EntityRegistryEntrySnapshot({
|
||||||
|
'aliases': set({
|
||||||
|
}),
|
||||||
|
'area_id': None,
|
||||||
|
'capabilities': dict({
|
||||||
|
'options': list([
|
||||||
|
'cloud',
|
||||||
|
'local',
|
||||||
|
'both',
|
||||||
|
]),
|
||||||
|
}),
|
||||||
|
'config_entry_id': <ANY>,
|
||||||
|
'device_class': None,
|
||||||
|
'device_id': <ANY>,
|
||||||
|
'disabled_by': None,
|
||||||
|
'domain': 'select',
|
||||||
|
'entity_category': None,
|
||||||
|
'entity_id': 'select.airgradient_configuration_source',
|
||||||
|
'has_entity_name': True,
|
||||||
|
'hidden_by': None,
|
||||||
|
'icon': None,
|
||||||
|
'id': <ANY>,
|
||||||
|
'labels': set({
|
||||||
|
}),
|
||||||
|
'name': None,
|
||||||
|
'options': dict({
|
||||||
|
}),
|
||||||
|
'original_device_class': None,
|
||||||
|
'original_icon': None,
|
||||||
|
'original_name': 'Configuration source',
|
||||||
|
'platform': 'airgradient',
|
||||||
|
'previous_unique_id': None,
|
||||||
|
'supported_features': 0,
|
||||||
|
'translation_key': 'configuration_control',
|
||||||
|
'unique_id': '84fce612f5b8-configuration_control',
|
||||||
|
'unit_of_measurement': None,
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_all_entities_outdoor[select.airgradient_configuration_source-state]
|
||||||
|
StateSnapshot({
|
||||||
|
'attributes': ReadOnlyDict({
|
||||||
|
'friendly_name': 'Airgradient Configuration source',
|
||||||
|
'options': list([
|
||||||
|
'cloud',
|
||||||
|
'local',
|
||||||
|
'both',
|
||||||
|
]),
|
||||||
|
}),
|
||||||
|
'context': <ANY>,
|
||||||
|
'entity_id': 'select.airgradient_configuration_source',
|
||||||
|
'last_changed': <ANY>,
|
||||||
|
'last_reported': <ANY>,
|
||||||
|
'last_updated': <ANY>,
|
||||||
|
'state': 'both',
|
||||||
|
})
|
||||||
|
# ---
|
115
tests/components/airgradient/test_select.py
Normal file
115
tests/components/airgradient/test_select.py
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
"""Tests for the AirGradient select platform."""
|
||||||
|
|
||||||
|
from unittest.mock import AsyncMock, patch
|
||||||
|
|
||||||
|
from airgradient import ConfigurationControl, Measures
|
||||||
|
import pytest
|
||||||
|
from syrupy import SnapshotAssertion
|
||||||
|
|
||||||
|
from homeassistant.components.airgradient import DOMAIN
|
||||||
|
from homeassistant.components.select import (
|
||||||
|
DOMAIN as SELECT_DOMAIN,
|
||||||
|
SERVICE_SELECT_OPTION,
|
||||||
|
)
|
||||||
|
from homeassistant.const import ATTR_ENTITY_ID, ATTR_OPTION, Platform
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.exceptions import ServiceValidationError
|
||||||
|
from homeassistant.helpers import entity_registry as er
|
||||||
|
|
||||||
|
from . import setup_integration
|
||||||
|
|
||||||
|
from tests.common import MockConfigEntry, load_fixture, snapshot_platform
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
||||||
|
async def test_all_entities(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
snapshot: SnapshotAssertion,
|
||||||
|
mock_airgradient_client: AsyncMock,
|
||||||
|
mock_config_entry: MockConfigEntry,
|
||||||
|
entity_registry: er.EntityRegistry,
|
||||||
|
) -> None:
|
||||||
|
"""Test all entities."""
|
||||||
|
with patch("homeassistant.components.airgradient.PLATFORMS", [Platform.SELECT]):
|
||||||
|
await setup_integration(hass, mock_config_entry)
|
||||||
|
|
||||||
|
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
||||||
|
async def test_all_entities_outdoor(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
snapshot: SnapshotAssertion,
|
||||||
|
mock_airgradient_client: AsyncMock,
|
||||||
|
mock_config_entry: MockConfigEntry,
|
||||||
|
entity_registry: er.EntityRegistry,
|
||||||
|
) -> None:
|
||||||
|
"""Test all entities."""
|
||||||
|
mock_airgradient_client.get_current_measures.return_value = Measures.from_json(
|
||||||
|
load_fixture("current_measures_outdoor.json", DOMAIN)
|
||||||
|
)
|
||||||
|
with patch("homeassistant.components.airgradient.PLATFORMS", [Platform.SELECT]):
|
||||||
|
await setup_integration(hass, mock_config_entry)
|
||||||
|
|
||||||
|
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_setting_value(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_airgradient_client: AsyncMock,
|
||||||
|
mock_config_entry: MockConfigEntry,
|
||||||
|
) -> None:
|
||||||
|
"""Test setting value."""
|
||||||
|
await setup_integration(hass, mock_config_entry)
|
||||||
|
|
||||||
|
await hass.services.async_call(
|
||||||
|
SELECT_DOMAIN,
|
||||||
|
SERVICE_SELECT_OPTION,
|
||||||
|
{
|
||||||
|
ATTR_ENTITY_ID: "select.airgradient_configuration_source",
|
||||||
|
ATTR_OPTION: "local",
|
||||||
|
},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
mock_airgradient_client.set_configuration_control.assert_called_once_with("local")
|
||||||
|
assert mock_airgradient_client.get_config.call_count == 2
|
||||||
|
|
||||||
|
|
||||||
|
async def test_setting_protected_value(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_airgradient_client: AsyncMock,
|
||||||
|
mock_config_entry: MockConfigEntry,
|
||||||
|
) -> None:
|
||||||
|
"""Test setting protected value."""
|
||||||
|
await setup_integration(hass, mock_config_entry)
|
||||||
|
|
||||||
|
mock_airgradient_client.get_config.return_value.configuration_control = (
|
||||||
|
ConfigurationControl.CLOUD
|
||||||
|
)
|
||||||
|
|
||||||
|
with pytest.raises(ServiceValidationError):
|
||||||
|
await hass.services.async_call(
|
||||||
|
SELECT_DOMAIN,
|
||||||
|
SERVICE_SELECT_OPTION,
|
||||||
|
{
|
||||||
|
ATTR_ENTITY_ID: "select.airgradient_display_temperature_unit",
|
||||||
|
ATTR_OPTION: "c",
|
||||||
|
},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
mock_airgradient_client.set_temperature_unit.assert_not_called()
|
||||||
|
|
||||||
|
mock_airgradient_client.get_config.return_value.configuration_control = (
|
||||||
|
ConfigurationControl.LOCAL
|
||||||
|
)
|
||||||
|
|
||||||
|
await hass.services.async_call(
|
||||||
|
SELECT_DOMAIN,
|
||||||
|
SERVICE_SELECT_OPTION,
|
||||||
|
{
|
||||||
|
ATTR_ENTITY_ID: "select.airgradient_display_temperature_unit",
|
||||||
|
ATTR_OPTION: "c",
|
||||||
|
},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
mock_airgradient_client.set_temperature_unit.assert_called_once_with("c")
|
@ -1,7 +1,7 @@
|
|||||||
"""Tests for the AirGradient sensor platform."""
|
"""Tests for the AirGradient sensor platform."""
|
||||||
|
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from unittest.mock import AsyncMock
|
from unittest.mock import AsyncMock, patch
|
||||||
|
|
||||||
from airgradient import AirGradientError, Measures
|
from airgradient import AirGradientError, Measures
|
||||||
from freezegun.api import FrozenDateTimeFactory
|
from freezegun.api import FrozenDateTimeFactory
|
||||||
@ -9,7 +9,7 @@ import pytest
|
|||||||
from syrupy import SnapshotAssertion
|
from syrupy import SnapshotAssertion
|
||||||
|
|
||||||
from homeassistant.components.airgradient import DOMAIN
|
from homeassistant.components.airgradient import DOMAIN
|
||||||
from homeassistant.const import STATE_UNAVAILABLE
|
from homeassistant.const import STATE_UNAVAILABLE, Platform
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers import entity_registry as er
|
from homeassistant.helpers import entity_registry as er
|
||||||
|
|
||||||
@ -32,6 +32,7 @@ async def test_all_entities(
|
|||||||
entity_registry: er.EntityRegistry,
|
entity_registry: er.EntityRegistry,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test all entities."""
|
"""Test all entities."""
|
||||||
|
with patch("homeassistant.components.airgradient.PLATFORMS", [Platform.SENSOR]):
|
||||||
await setup_integration(hass, mock_config_entry)
|
await setup_integration(hass, mock_config_entry)
|
||||||
|
|
||||||
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
|
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
|
||||||
@ -47,6 +48,7 @@ async def test_create_entities(
|
|||||||
mock_airgradient_client.get_current_measures.return_value = Measures.from_json(
|
mock_airgradient_client.get_current_measures.return_value = Measures.from_json(
|
||||||
load_fixture("measures_after_boot.json", DOMAIN)
|
load_fixture("measures_after_boot.json", DOMAIN)
|
||||||
)
|
)
|
||||||
|
with patch("homeassistant.components.airgradient.PLATFORMS", [Platform.SENSOR]):
|
||||||
await setup_integration(hass, mock_config_entry)
|
await setup_integration(hass, mock_config_entry)
|
||||||
|
|
||||||
assert len(hass.states.async_all()) == 0
|
assert len(hass.states.async_all()) == 0
|
||||||
|
Loading…
x
Reference in New Issue
Block a user