From ad0560ef378954efdd4ec059530888a6e7a34a57 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Thu, 30 Jul 2020 20:41:18 +0200 Subject: [PATCH] Improve tests for Airly integration (#38357) * Add tests * More tests * Add PARALLEL_UPDATES * Change Quality scale to platinum * Change PARALLEL_UPDATES value --- .coveragerc | 4 - homeassistant/components/airly/air_quality.py | 2 + homeassistant/components/airly/manifest.json | 3 +- homeassistant/components/airly/sensor.py | 2 + tests/components/airly/__init__.py | 31 ++++ tests/components/airly/test_air_quality.py | 113 ++++++++++++++ tests/components/airly/test_init.py | 140 ++++++++++++++++++ tests/components/airly/test_sensor.py | 128 ++++++++++++++++ 8 files changed, 418 insertions(+), 5 deletions(-) create mode 100644 tests/components/airly/test_air_quality.py create mode 100644 tests/components/airly/test_init.py create mode 100644 tests/components/airly/test_sensor.py diff --git a/.coveragerc b/.coveragerc index 90ce03476a8f..c49ac6257d8d 100644 --- a/.coveragerc +++ b/.coveragerc @@ -31,10 +31,6 @@ omit = homeassistant/components/agent_dvr/camera.py homeassistant/components/agent_dvr/const.py homeassistant/components/agent_dvr/helpers.py - homeassistant/components/airly/__init__.py - homeassistant/components/airly/air_quality.py - homeassistant/components/airly/sensor.py - homeassistant/components/airly/const.py homeassistant/components/airvisual/__init__.py homeassistant/components/airvisual/air_quality.py homeassistant/components/airvisual/sensor.py diff --git a/homeassistant/components/airly/air_quality.py b/homeassistant/components/airly/air_quality.py index deeff9af00f7..6e1e90051e05 100644 --- a/homeassistant/components/airly/air_quality.py +++ b/homeassistant/components/airly/air_quality.py @@ -31,6 +31,8 @@ LABEL_PM_2_5_PERCENT = f"{ATTR_PM_2_5}_percent_of_limit" LABEL_PM_10_LIMIT = f"{ATTR_PM_10}_limit" LABEL_PM_10_PERCENT = f"{ATTR_PM_10}_percent_of_limit" +PARALLEL_UPDATES = 1 + async def async_setup_entry(hass, config_entry, async_add_entities): """Set up Airly air_quality entity based on a config entry.""" diff --git a/homeassistant/components/airly/manifest.json b/homeassistant/components/airly/manifest.json index e86a187793fd..8140bc91c5fc 100644 --- a/homeassistant/components/airly/manifest.json +++ b/homeassistant/components/airly/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/airly", "codeowners": ["@bieniu"], "requirements": ["airly==0.0.2"], - "config_flow": true + "config_flow": true, + "quality_scale": "platinum" } diff --git a/homeassistant/components/airly/sensor.py b/homeassistant/components/airly/sensor.py index a0c5975188bf..4f8ba0f11c72 100644 --- a/homeassistant/components/airly/sensor.py +++ b/homeassistant/components/airly/sensor.py @@ -27,6 +27,8 @@ ATTR_ICON = "icon" ATTR_LABEL = "label" ATTR_UNIT = "unit" +PARALLEL_UPDATES = 1 + SENSOR_TYPES = { ATTR_API_PM1: { ATTR_DEVICE_CLASS: None, diff --git a/tests/components/airly/__init__.py b/tests/components/airly/__init__.py index f31dfb7712d4..29828bddc176 100644 --- a/tests/components/airly/__init__.py +++ b/tests/components/airly/__init__.py @@ -1 +1,32 @@ """Tests for Airly.""" +import json + +from homeassistant.components.airly.const import DOMAIN + +from tests.async_mock import patch +from tests.common import MockConfigEntry, load_fixture + + +async def init_integration(hass, forecast=False) -> MockConfigEntry: + """Set up the Airly integration in Home Assistant.""" + entry = MockConfigEntry( + domain=DOMAIN, + title="Home", + unique_id="55.55-122.12", + data={ + "api_key": "foo", + "latitude": 55.55, + "longitude": 122.12, + "name": "Home", + }, + ) + + with patch( + "airly._private._RequestsHandler.get", + return_value=json.loads(load_fixture("airly_valid_station.json")), + ): + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + return entry diff --git a/tests/components/airly/test_air_quality.py b/tests/components/airly/test_air_quality.py new file mode 100644 index 000000000000..fca2761f2f33 --- /dev/null +++ b/tests/components/airly/test_air_quality.py @@ -0,0 +1,113 @@ +"""Test air_quality of Airly integration.""" +from datetime import timedelta +import json + +from airly.exceptions import AirlyError + +from homeassistant.components.air_quality import ATTR_AQI, ATTR_PM_2_5, ATTR_PM_10 +from homeassistant.components.airly.air_quality import ( + ATTRIBUTION, + LABEL_ADVICE, + LABEL_AQI_DESCRIPTION, + LABEL_AQI_LEVEL, + LABEL_PM_2_5_LIMIT, + LABEL_PM_2_5_PERCENT, + LABEL_PM_10_LIMIT, + LABEL_PM_10_PERCENT, +) +from homeassistant.const import ( + ATTR_ATTRIBUTION, + ATTR_ENTITY_ID, + ATTR_ICON, + ATTR_UNIT_OF_MEASUREMENT, + CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + STATE_UNAVAILABLE, +) +from homeassistant.setup import async_setup_component +from homeassistant.util.dt import utcnow + +from tests.async_mock import patch +from tests.common import async_fire_time_changed, load_fixture +from tests.components.airly import init_integration + + +async def test_air_quality(hass): + """Test states of the air_quality.""" + await init_integration(hass) + registry = await hass.helpers.entity_registry.async_get_registry() + + state = hass.states.get("air_quality.home") + assert state + assert state.state == "14" + assert state.attributes.get(ATTR_AQI) == 23 + assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION + assert state.attributes.get(LABEL_ADVICE) == "Great air!" + assert state.attributes.get(ATTR_PM_10) == 19 + assert state.attributes.get(ATTR_PM_2_5) == 14 + assert state.attributes.get(LABEL_AQI_DESCRIPTION) == "Great air here today!" + assert state.attributes.get(LABEL_AQI_LEVEL) == "very low" + assert state.attributes.get(LABEL_PM_2_5_LIMIT) == 25.0 + assert state.attributes.get(LABEL_PM_2_5_PERCENT) == 55 + assert state.attributes.get(LABEL_PM_10_LIMIT) == 50.0 + assert state.attributes.get(LABEL_PM_10_PERCENT) == 37 + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER + ) + assert state.attributes.get(ATTR_ICON) == "mdi:blur" + + entry = registry.async_get("air_quality.home") + assert entry + assert entry.unique_id == "55.55-122.12" + + +async def test_availability(hass): + """Ensure that we mark the entities unavailable correctly when service causes an error.""" + await init_integration(hass) + + state = hass.states.get("air_quality.home") + assert state + assert state.state != STATE_UNAVAILABLE + assert state.state == "14" + + future = utcnow() + timedelta(minutes=60) + with patch( + "airly._private._RequestsHandler.get", + side_effect=AirlyError(500, "Unexpected error"), + ): + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + + state = hass.states.get("air_quality.home") + assert state + assert state.state == STATE_UNAVAILABLE + + future = utcnow() + timedelta(minutes=120) + with patch( + "airly._private._RequestsHandler.get", + return_value=json.loads(load_fixture("airly_valid_station.json")), + ): + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + + state = hass.states.get("air_quality.home") + assert state + assert state.state != STATE_UNAVAILABLE + assert state.state == "14" + + +async def test_manual_update_entity(hass): + """Test manual update entity via service homeasasistant/update_entity.""" + await init_integration(hass) + + await async_setup_component(hass, "homeassistant", {}) + with patch( + "homeassistant.components.airly.AirlyDataUpdateCoordinator._async_update_data" + ) as mock_update: + await hass.services.async_call( + "homeassistant", + "update_entity", + {ATTR_ENTITY_ID: ["air_quality.home"]}, + blocking=True, + ) + assert mock_update.call_count == 1 diff --git a/tests/components/airly/test_init.py b/tests/components/airly/test_init.py new file mode 100644 index 000000000000..28f2aca4fbbd --- /dev/null +++ b/tests/components/airly/test_init.py @@ -0,0 +1,140 @@ +"""Test init of Airly integration.""" +from datetime import timedelta +import json + +from homeassistant.components.airly.const import DOMAIN +from homeassistant.config_entries import ( + ENTRY_STATE_LOADED, + ENTRY_STATE_NOT_LOADED, + ENTRY_STATE_SETUP_RETRY, +) +from homeassistant.const import STATE_UNAVAILABLE + +from tests.async_mock import patch +from tests.common import MockConfigEntry, load_fixture +from tests.components.airly import init_integration + + +async def test_async_setup_entry(hass): + """Test a successful setup entry.""" + await init_integration(hass) + + state = hass.states.get("air_quality.home") + assert state is not None + assert state.state != STATE_UNAVAILABLE + assert state.state == "14" + + +async def test_config_not_ready(hass): + """Test for setup failure if connection to Airly is missing.""" + entry = MockConfigEntry( + domain=DOMAIN, + title="Home", + unique_id="55.55-122.12", + data={ + "api_key": "foo", + "latitude": 55.55, + "longitude": 122.12, + "name": "Home", + }, + ) + + with patch("airly._private._RequestsHandler.get", side_effect=ConnectionError()): + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + assert entry.state == ENTRY_STATE_SETUP_RETRY + + +async def test_config_without_unique_id(hass): + """Test for setup entry without unique_id.""" + entry = MockConfigEntry( + domain=DOMAIN, + title="Home", + data={ + "api_key": "foo", + "latitude": 55.55, + "longitude": 122.12, + "name": "Home", + }, + ) + + with patch( + "airly._private._RequestsHandler.get", + return_value=json.loads(load_fixture("airly_valid_station.json")), + ): + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + assert entry.state == ENTRY_STATE_LOADED + assert entry.unique_id == "55.55-122.12" + + +async def test_config_with_turned_off_station(hass): + """Test for setup entry for a turned off measuring station.""" + entry = MockConfigEntry( + domain=DOMAIN, + title="Home", + unique_id="55.55-122.12", + data={ + "api_key": "foo", + "latitude": 55.55, + "longitude": 122.12, + "name": "Home", + }, + ) + + with patch( + "airly._private._RequestsHandler.get", + return_value=json.loads(load_fixture("airly_no_station.json")), + ): + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + assert entry.state == ENTRY_STATE_SETUP_RETRY + + +async def test_update_interval(hass): + """Test correct update interval when the number of configured instances changes.""" + entry = await init_integration(hass) + + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + assert entry.state == ENTRY_STATE_LOADED + for instance in hass.data[DOMAIN].values(): + assert instance.update_interval == timedelta(minutes=15) + + entry = MockConfigEntry( + domain=DOMAIN, + title="Work", + unique_id="66.66-111.11", + data={ + "api_key": "foo", + "latitude": 66.66, + "longitude": 111.11, + "name": "Work", + }, + ) + + with patch( + "airly._private._RequestsHandler.get", + return_value=json.loads(load_fixture("airly_valid_station.json")), + ): + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.config_entries.async_entries(DOMAIN)) == 2 + assert entry.state == ENTRY_STATE_LOADED + for instance in hass.data[DOMAIN].values(): + assert instance.update_interval == timedelta(minutes=30) + + +async def test_unload_entry(hass): + """Test successful unload of entry.""" + entry = await init_integration(hass) + + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + assert entry.state == ENTRY_STATE_LOADED + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + assert entry.state == ENTRY_STATE_NOT_LOADED + assert not hass.data.get(DOMAIN) diff --git a/tests/components/airly/test_sensor.py b/tests/components/airly/test_sensor.py new file mode 100644 index 000000000000..3131789c6e00 --- /dev/null +++ b/tests/components/airly/test_sensor.py @@ -0,0 +1,128 @@ +"""Test sensor of Airly integration.""" +from datetime import timedelta +import json + +from homeassistant.components.airly.sensor import ATTRIBUTION +from homeassistant.const import ( + ATTR_ATTRIBUTION, + ATTR_DEVICE_CLASS, + ATTR_ENTITY_ID, + ATTR_ICON, + ATTR_UNIT_OF_MEASUREMENT, + CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_PRESSURE, + DEVICE_CLASS_TEMPERATURE, + PRESSURE_HPA, + STATE_UNAVAILABLE, + TEMP_CELSIUS, + UNIT_PERCENTAGE, +) +from homeassistant.setup import async_setup_component +from homeassistant.util.dt import utcnow + +from tests.async_mock import patch +from tests.common import async_fire_time_changed, load_fixture +from tests.components.airly import init_integration + + +async def test_sensor(hass): + """Test states of the sensor.""" + await init_integration(hass) + registry = await hass.helpers.entity_registry.async_get_registry() + + state = hass.states.get("sensor.home_humidity") + assert state + assert state.state == "92.8" + assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UNIT_PERCENTAGE + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_HUMIDITY + + entry = registry.async_get("sensor.home_humidity") + assert entry + assert entry.unique_id == "55.55-122.12-humidity" + + state = hass.states.get("sensor.home_pm1") + assert state + assert state.state == "9" + assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER + ) + assert state.attributes.get(ATTR_ICON) == "mdi:blur" + + entry = registry.async_get("sensor.home_pm1") + assert entry + assert entry.unique_id == "55.55-122.12-pm1" + + state = hass.states.get("sensor.home_pressure") + assert state + assert state.state == "1001" + assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PRESSURE_HPA + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_PRESSURE + + entry = registry.async_get("sensor.home_pressure") + assert entry + assert entry.unique_id == "55.55-122.12-pressure" + + state = hass.states.get("sensor.home_temperature") + assert state + assert state.state == "14.2" + assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_TEMPERATURE + + entry = registry.async_get("sensor.home_temperature") + assert entry + assert entry.unique_id == "55.55-122.12-temperature" + + +async def test_availability(hass): + """Ensure that we mark the entities unavailable correctly when service is offline.""" + await init_integration(hass) + + state = hass.states.get("sensor.home_humidity") + assert state + assert state.state != STATE_UNAVAILABLE + assert state.state == "92.8" + + future = utcnow() + timedelta(minutes=60) + with patch("airly._private._RequestsHandler.get", side_effect=ConnectionError()): + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + + state = hass.states.get("sensor.home_humidity") + assert state + assert state.state == STATE_UNAVAILABLE + + future = utcnow() + timedelta(minutes=120) + with patch( + "airly._private._RequestsHandler.get", + return_value=json.loads(load_fixture("airly_valid_station.json")), + ): + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + + state = hass.states.get("sensor.home_humidity") + assert state + assert state.state != STATE_UNAVAILABLE + assert state.state == "92.8" + + +async def test_manual_update_entity(hass): + """Test manual update entity via service homeasasistant/update_entity.""" + await init_integration(hass) + + await async_setup_component(hass, "homeassistant", {}) + with patch( + "homeassistant.components.airly.AirlyDataUpdateCoordinator._async_update_data" + ) as mock_update: + await hass.services.async_call( + "homeassistant", + "update_entity", + {ATTR_ENTITY_ID: ["sensor.home_humidity"]}, + blocking=True, + ) + assert mock_update.call_count == 1