diff --git a/homeassistant/components/airgradient/__init__.py b/homeassistant/components/airgradient/__init__.py index 76e11c05527..b1b5a28ef67 100644 --- a/homeassistant/components/airgradient/__init__.py +++ b/homeassistant/components/airgradient/__init__.py @@ -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 diff --git a/homeassistant/components/airgradient/button.py b/homeassistant/components/airgradient/button.py new file mode 100644 index 00000000000..b59188ebdd4 --- /dev/null +++ b/homeassistant/components/airgradient/button.py @@ -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) diff --git a/homeassistant/components/airgradient/icons.json b/homeassistant/components/airgradient/icons.json index cf0c80c873e..45d1e12d46e 100644 --- a/homeassistant/components/airgradient/icons.json +++ b/homeassistant/components/airgradient/icons.json @@ -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" diff --git a/homeassistant/components/airgradient/number.py b/homeassistant/components/airgradient/number.py index e065b76ed51..139357f3753 100644 --- a/homeassistant/components/airgradient/number.py +++ b/homeassistant/components/airgradient/number.py @@ -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 ): diff --git a/homeassistant/components/airgradient/strings.json b/homeassistant/components/airgradient/strings.json index 6c079419839..0b5c245f04c 100644 --- a/homeassistant/components/airgradient/strings.json +++ b/homeassistant/components/airgradient/strings.json @@ -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" diff --git a/tests/components/airgradient/snapshots/test_button.ambr b/tests/components/airgradient/snapshots/test_button.ambr new file mode 100644 index 00000000000..fa3f8994c3c --- /dev/null +++ b/tests/components/airgradient/snapshots/test_button.ambr @@ -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': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.airgradient_calibrate_co2_sensor', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + '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': , + 'entity_id': 'button.airgradient_calibrate_co2_sensor', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_entities[indoor][button.airgradient_test_led_bar-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.airgradient_test_led_bar', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + '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': , + 'entity_id': 'button.airgradient_test_led_bar', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_entities[outdoor][button.airgradient_calibrate_co2_sensor-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.airgradient_calibrate_co2_sensor', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + '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': , + 'entity_id': 'button.airgradient_calibrate_co2_sensor', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- diff --git a/tests/components/airgradient/test_button.py b/tests/components/airgradient/test_button.py new file mode 100644 index 00000000000..7901c3a067b --- /dev/null +++ b/tests/components/airgradient/test_button.py @@ -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