"""Tests for the Awair sensor platform.""" from contextlib import contextmanager from datetime import timedelta import json import logging from unittest.mock import patch from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.components.awair.sensor import ( ATTR_LAST_API_UPDATE, ATTR_TIMESTAMP, DEVICE_CLASS_CARBON_DIOXIDE, DEVICE_CLASS_PM2_5, DEVICE_CLASS_SCORE, DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, ) from homeassistant.const import ( DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, STATE_UNAVAILABLE, TEMP_CELSIUS, ) from homeassistant.setup import async_setup_component from homeassistant.util.dt import parse_datetime, utcnow from tests.common import async_fire_time_changed, load_fixture, mock_coro DISCOVERY_CONFIG = {"sensor": {"platform": "awair", "access_token": "qwerty"}} MANUAL_CONFIG = { "sensor": { "platform": "awair", "access_token": "qwerty", "devices": [{"uuid": "awair_foo"}], } } _LOGGER = logging.getLogger(__name__) NOW = utcnow() AIR_DATA_FIXTURE = json.loads(load_fixture("awair_air_data_latest.json")) AIR_DATA_FIXTURE[0][ATTR_TIMESTAMP] = str(NOW) AIR_DATA_FIXTURE_UPDATED = json.loads( load_fixture("awair_air_data_latest_updated.json") ) AIR_DATA_FIXTURE_UPDATED[0][ATTR_TIMESTAMP] = str(NOW + timedelta(minutes=5)) AIR_DATA_FIXTURE_EMPTY = [] @contextmanager def alter_time(retval): """Manage multiple time mocks.""" patch_one = patch("homeassistant.util.dt.utcnow", return_value=retval) patch_two = patch("homeassistant.util.utcnow", return_value=retval) patch_three = patch( "homeassistant.components.awair.sensor.dt.utcnow", return_value=retval ) with patch_one, patch_two, patch_three: yield async def setup_awair(hass, config=None, data_fixture=AIR_DATA_FIXTURE): """Load the Awair platform.""" devices_json = json.loads(load_fixture("awair_devices.json")) devices_mock = mock_coro(devices_json) devices_patch = patch("python_awair.AwairClient.devices", return_value=devices_mock) air_data_mock = mock_coro(data_fixture) air_data_patch = patch( "python_awair.AwairClient.air_data_latest", return_value=air_data_mock ) if config is None: config = DISCOVERY_CONFIG with devices_patch, air_data_patch, alter_time(NOW): assert await async_setup_component(hass, SENSOR_DOMAIN, config) await hass.async_block_till_done() async def test_platform_manually_configured(hass): """Test that we can manually configure devices.""" await setup_awair(hass, MANUAL_CONFIG) assert len(hass.states.async_all()) == 6 # Ensure that we loaded the device with uuid 'awair_foo', not the # 'awair_12345' device that we stub out for API device discovery entity = hass.data[SENSOR_DOMAIN].get_entity("sensor.awair_co2") assert entity.unique_id == "awair_foo_CO2" async def test_platform_automatically_configured(hass): """Test that we can discover devices from the API.""" await setup_awair(hass) assert len(hass.states.async_all()) == 6 # Ensure that we loaded the device with uuid 'awair_12345', which is # the device that we stub out for API device discovery entity = hass.data[SENSOR_DOMAIN].get_entity("sensor.awair_co2") assert entity.unique_id == "awair_12345_CO2" async def test_bad_platform_setup(hass): """Tests that we throw correct exceptions when setting up Awair.""" from python_awair import AwairClient auth_patch = patch( "python_awair.AwairClient.devices", side_effect=AwairClient.AuthError ) rate_patch = patch( "python_awair.AwairClient.devices", side_effect=AwairClient.RatelimitError ) generic_patch = patch( "python_awair.AwairClient.devices", side_effect=AwairClient.GenericError ) with auth_patch: assert await async_setup_component(hass, SENSOR_DOMAIN, DISCOVERY_CONFIG) assert not hass.states.async_all() with rate_patch: assert await async_setup_component(hass, SENSOR_DOMAIN, DISCOVERY_CONFIG) assert not hass.states.async_all() with generic_patch: assert await async_setup_component(hass, SENSOR_DOMAIN, DISCOVERY_CONFIG) assert not hass.states.async_all() async def test_awair_setup_no_data(hass): """Ensure that we do not crash during setup when no data is returned.""" await setup_awair(hass, data_fixture=AIR_DATA_FIXTURE_EMPTY) assert not hass.states.async_all() async def test_awair_misc_attributes(hass): """Test that desired attributes are set.""" await setup_awair(hass) attributes = hass.states.get("sensor.awair_co2").attributes assert attributes[ATTR_LAST_API_UPDATE] == parse_datetime( AIR_DATA_FIXTURE[0][ATTR_TIMESTAMP] ) async def test_awair_score(hass): """Test that we create a sensor for the 'Awair score'.""" await setup_awair(hass) sensor = hass.states.get("sensor.awair_score") assert sensor.state == "78" assert sensor.attributes["device_class"] == DEVICE_CLASS_SCORE assert sensor.attributes["unit_of_measurement"] == "%" async def test_awair_temp(hass): """Test that we create a temperature sensor.""" await setup_awair(hass) sensor = hass.states.get("sensor.awair_temperature") assert sensor.state == "22.4" assert sensor.attributes["device_class"] == DEVICE_CLASS_TEMPERATURE assert sensor.attributes["unit_of_measurement"] == TEMP_CELSIUS async def test_awair_humid(hass): """Test that we create a humidity sensor.""" await setup_awair(hass) sensor = hass.states.get("sensor.awair_humidity") assert sensor.state == "32.7" assert sensor.attributes["device_class"] == DEVICE_CLASS_HUMIDITY assert sensor.attributes["unit_of_measurement"] == "%" async def test_awair_co2(hass): """Test that we create a CO2 sensor.""" await setup_awair(hass) sensor = hass.states.get("sensor.awair_co2") assert sensor.state == "612" assert sensor.attributes["device_class"] == DEVICE_CLASS_CARBON_DIOXIDE assert sensor.attributes["unit_of_measurement"] == "ppm" async def test_awair_voc(hass): """Test that we create a CO2 sensor.""" await setup_awair(hass) sensor = hass.states.get("sensor.awair_voc") assert sensor.state == "1012" assert sensor.attributes["device_class"] == DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS assert sensor.attributes["unit_of_measurement"] == "ppb" async def test_awair_dust(hass): """Test that we create a pm25 sensor.""" await setup_awair(hass) # The Awair Gen1 that we mock actually returns 'DUST', but that # is mapped to pm25 internally so that it shows up in Homekit sensor = hass.states.get("sensor.awair_pm2_5") assert sensor.state == "6.2" assert sensor.attributes["device_class"] == DEVICE_CLASS_PM2_5 assert sensor.attributes["unit_of_measurement"] == "µg/m3" async def test_awair_unsupported_sensors(hass): """Ensure we don't create sensors the stubbed device doesn't support.""" await setup_awair(hass) # Our tests mock an Awair Gen 1 device, which should never return # PM10 sensor readings. Assert that we didn't create a pm10 sensor, # which could happen if someone were ever to refactor incorrectly. assert hass.states.get("sensor.awair_pm10") is None async def test_availability(hass): """Ensure that we mark the component available/unavailable correctly.""" await setup_awair(hass) assert hass.states.get("sensor.awair_score").state == "78" future = NOW + timedelta(minutes=30) data_patch = patch( "python_awair.AwairClient.air_data_latest", return_value=mock_coro(AIR_DATA_FIXTURE), ) with data_patch, alter_time(future): async_fire_time_changed(hass, future) await hass.async_block_till_done() assert hass.states.get("sensor.awair_score").state == STATE_UNAVAILABLE future = NOW + timedelta(hours=1) fixture = AIR_DATA_FIXTURE_UPDATED fixture[0][ATTR_TIMESTAMP] = str(future) data_patch = patch( "python_awair.AwairClient.air_data_latest", return_value=mock_coro(fixture) ) with data_patch, alter_time(future): async_fire_time_changed(hass, future) await hass.async_block_till_done() assert hass.states.get("sensor.awair_score").state == "79" future = NOW + timedelta(minutes=90) fixture = AIR_DATA_FIXTURE_EMPTY data_patch = patch( "python_awair.AwairClient.air_data_latest", return_value=mock_coro(fixture) ) with data_patch, alter_time(future): async_fire_time_changed(hass, future) await hass.async_block_till_done() assert hass.states.get("sensor.awair_score").state == STATE_UNAVAILABLE async def test_async_update(hass): """Ensure we can update sensors.""" await setup_awair(hass) future = NOW + timedelta(minutes=10) data_patch = patch( "python_awair.AwairClient.air_data_latest", return_value=mock_coro(AIR_DATA_FIXTURE_UPDATED), ) with data_patch, alter_time(future): async_fire_time_changed(hass, future) await hass.async_block_till_done() score_sensor = hass.states.get("sensor.awair_score") assert score_sensor.state == "79" assert hass.states.get("sensor.awair_temperature").state == "23.4" assert hass.states.get("sensor.awair_humidity").state == "33.7" assert hass.states.get("sensor.awair_co2").state == "613" assert hass.states.get("sensor.awair_voc").state == "1013" assert hass.states.get("sensor.awair_pm2_5").state == "7.2" async def test_throttle_async_update(hass): """Ensure we throttle updates.""" await setup_awair(hass) future = NOW + timedelta(minutes=1) data_patch = patch( "python_awair.AwairClient.air_data_latest", return_value=mock_coro(AIR_DATA_FIXTURE_UPDATED), ) with data_patch, alter_time(future): async_fire_time_changed(hass, future) await hass.async_block_till_done() assert hass.states.get("sensor.awair_score").state == "78" future = NOW + timedelta(minutes=15) with data_patch, alter_time(future): async_fire_time_changed(hass, future) await hass.async_block_till_done() assert hass.states.get("sensor.awair_score").state == "79"