core/tests/components/awair/test_sensor.py

311 lines
10 KiB
Python

"""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"