Add button platform to AirGradient (#119917)

This commit is contained in:
Joost Lekkerkerker 2024-06-24 11:55:48 +02:00 committed by GitHub
parent f3a1ca6d54
commit 674dfa6e9c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 366 additions and 6 deletions

View file

@ -15,7 +15,12 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import DOMAIN
from .coordinator import AirGradientConfigCoordinator, AirGradientMeasurementCoordinator
PLATFORMS: list[Platform] = [Platform.NUMBER, Platform.SELECT, Platform.SENSOR]
PLATFORMS: list[Platform] = [
Platform.BUTTON,
Platform.NUMBER,
Platform.SELECT,
Platform.SENSOR,
]
@dataclass

View file

@ -0,0 +1,104 @@
"""Support for AirGradient buttons."""
from collections.abc import Awaitable, Callable
from dataclasses import dataclass
from airgradient import AirGradientClient, ConfigurationControl
from homeassistant.components.button import (
DOMAIN as BUTTON_DOMAIN,
ButtonEntity,
ButtonEntityDescription,
)
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 AddEntitiesCallback
from . import DOMAIN, AirGradientConfigEntry
from .coordinator import AirGradientConfigCoordinator
from .entity import AirGradientEntity
@dataclass(frozen=True, kw_only=True)
class AirGradientButtonEntityDescription(ButtonEntityDescription):
"""Describes AirGradient button entity."""
press_fn: Callable[[AirGradientClient], Awaitable[None]]
CO2_CALIBRATION = AirGradientButtonEntityDescription(
key="co2_calibration",
translation_key="co2_calibration",
entity_category=EntityCategory.CONFIG,
press_fn=lambda client: client.request_co2_calibration(),
)
LED_BAR_TEST = AirGradientButtonEntityDescription(
key="led_bar_test",
translation_key="led_bar_test",
entity_category=EntityCategory.CONFIG,
press_fn=lambda client: client.request_led_bar_test(),
)
async def async_setup_entry(
hass: HomeAssistant,
entry: AirGradientConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up AirGradient button entities based on a config entry."""
model = entry.runtime_data.measurement.data.model
coordinator = entry.runtime_data.config
added_entities = False
@callback
def _check_entities() -> None:
nonlocal added_entities
if (
coordinator.data.configuration_control is ConfigurationControl.LOCAL
and not added_entities
):
entities = [AirGradientButton(coordinator, CO2_CALIBRATION)]
if "L" in model:
entities.append(AirGradientButton(coordinator, LED_BAR_TEST))
async_add_entities(entities)
added_entities = True
elif (
coordinator.data.configuration_control is not ConfigurationControl.LOCAL
and added_entities
):
entity_registry = er.async_get(hass)
for entity_description in (CO2_CALIBRATION, LED_BAR_TEST):
unique_id = f"{coordinator.serial_number}-{entity_description.key}"
if entity_id := entity_registry.async_get_entity_id(
BUTTON_DOMAIN, DOMAIN, unique_id
):
entity_registry.async_remove(entity_id)
added_entities = False
coordinator.async_add_listener(_check_entities)
_check_entities()
class AirGradientButton(AirGradientEntity, ButtonEntity):
"""Defines an AirGradient button."""
entity_description: AirGradientButtonEntityDescription
coordinator: AirGradientConfigCoordinator
def __init__(
self,
coordinator: AirGradientConfigCoordinator,
description: AirGradientButtonEntityDescription,
) -> None:
"""Initialize airgradient button."""
super().__init__(coordinator)
self.entity_description = description
self._attr_unique_id = f"{coordinator.serial_number}-{description.key}"
async def async_press(self) -> None:
"""Press the button."""
await self.entity_description.press_fn(self.coordinator.client)

View file

@ -1,5 +1,13 @@
{
"entity": {
"button": {
"co2_calibration": {
"default": "mdi:molecule-co2"
},
"led_bar_test": {
"default": "mdi:lightbulb-on-outline"
}
},
"sensor": {
"total_volatile_organic_component_index": {
"default": "mdi:molecule"

View file

@ -88,11 +88,8 @@ async def async_setup_entry(
and added_entities
):
entity_registry = er.async_get(hass)
unique_ids = [
f"{coordinator.serial_number}-{entity_description.key}"
for entity_description in (DISPLAY_BRIGHTNESS, LED_BAR_BRIGHTNESS)
]
for unique_id in unique_ids:
for entity_description in (DISPLAY_BRIGHTNESS, LED_BAR_BRIGHTNESS):
unique_id = f"{coordinator.serial_number}-{entity_description.key}"
if entity_id := entity_registry.async_get_entity_id(
NUMBER_DOMAIN, DOMAIN, unique_id
):

View file

@ -24,6 +24,14 @@
}
},
"entity": {
"button": {
"co2_calibration": {
"name": "Calibrate CO2 sensor"
},
"led_bar_test": {
"name": "Test LED bar"
}
},
"number": {
"led_bar_brightness": {
"name": "LED bar brightness"

View file

@ -0,0 +1,139 @@
# serializer version: 1
# name: test_all_entities[indoor][button.airgradient_calibrate_co2_sensor-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'button',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'button.airgradient_calibrate_co2_sensor',
'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': 'Calibrate CO2 sensor',
'platform': 'airgradient',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'co2_calibration',
'unique_id': '84fce612f5b8-co2_calibration',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[indoor][button.airgradient_calibrate_co2_sensor-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Airgradient Calibrate CO2 sensor',
}),
'context': <ANY>,
'entity_id': 'button.airgradient_calibrate_co2_sensor',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
# name: test_all_entities[indoor][button.airgradient_test_led_bar-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'button',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'button.airgradient_test_led_bar',
'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': 'Test LED bar',
'platform': 'airgradient',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'led_bar_test',
'unique_id': '84fce612f5b8-led_bar_test',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[indoor][button.airgradient_test_led_bar-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Airgradient Test LED bar',
}),
'context': <ANY>,
'entity_id': 'button.airgradient_test_led_bar',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
# name: test_all_entities[outdoor][button.airgradient_calibrate_co2_sensor-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'button',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'button.airgradient_calibrate_co2_sensor',
'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': 'Calibrate CO2 sensor',
'platform': 'airgradient',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'co2_calibration',
'unique_id': '84fce612f5b8-co2_calibration',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[outdoor][button.airgradient_calibrate_co2_sensor-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Airgradient Calibrate CO2 sensor',
}),
'context': <ANY>,
'entity_id': 'button.airgradient_calibrate_co2_sensor',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---

View file

@ -0,0 +1,99 @@
"""Tests for the AirGradient button platform."""
from datetime import timedelta
from unittest.mock import AsyncMock, patch
from airgradient import Config
from freezegun.api import FrozenDateTimeFactory
from syrupy import SnapshotAssertion
from homeassistant.components.airgradient import DOMAIN
from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS
from homeassistant.const import ATTR_ENTITY_ID, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from . import setup_integration
from tests.common import (
MockConfigEntry,
async_fire_time_changed,
load_fixture,
snapshot_platform,
)
async def test_all_entities(
hass: HomeAssistant,
snapshot: SnapshotAssertion,
airgradient_devices: AsyncMock,
mock_config_entry: MockConfigEntry,
entity_registry: er.EntityRegistry,
) -> None:
"""Test all entities."""
with patch("homeassistant.components.airgradient.PLATFORMS", [Platform.BUTTON]):
await setup_integration(hass, mock_config_entry)
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
async def test_pressing_button(
hass: HomeAssistant,
mock_airgradient_client: AsyncMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test pressing button."""
await setup_integration(hass, mock_config_entry)
await hass.services.async_call(
BUTTON_DOMAIN,
SERVICE_PRESS,
{
ATTR_ENTITY_ID: "button.airgradient_calibrate_co2_sensor",
},
blocking=True,
)
mock_airgradient_client.request_co2_calibration.assert_called_once()
await hass.services.async_call(
BUTTON_DOMAIN,
SERVICE_PRESS,
{
ATTR_ENTITY_ID: "button.airgradient_test_led_bar",
},
blocking=True,
)
mock_airgradient_client.request_led_bar_test.assert_called_once()
async def test_cloud_creates_no_button(
hass: HomeAssistant,
mock_cloud_airgradient_client: AsyncMock,
mock_config_entry: MockConfigEntry,
freezer: FrozenDateTimeFactory,
) -> None:
"""Test cloud configuration control."""
with patch("homeassistant.components.airgradient.PLATFORMS", [Platform.BUTTON]):
await setup_integration(hass, mock_config_entry)
assert len(hass.states.async_all()) == 0
mock_cloud_airgradient_client.get_config.return_value = Config.from_json(
load_fixture("get_config_local.json", DOMAIN)
)
freezer.tick(timedelta(minutes=5))
async_fire_time_changed(hass)
await hass.async_block_till_done()
assert len(hass.states.async_all()) == 2
mock_cloud_airgradient_client.get_config.return_value = Config.from_json(
load_fixture("get_config_cloud.json", DOMAIN)
)
freezer.tick(timedelta(minutes=5))
async_fire_time_changed(hass)
await hass.async_block_till_done()
assert len(hass.states.async_all()) == 0