Add pollen sensors to Ambee (#51702)

pull/51708/head
Franck Nijhof 2021-06-10 14:18:09 +02:00 committed by GitHub
parent 79996682e5
commit fca0446ff8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 585 additions and 111 deletions

View File

@ -9,30 +9,31 @@ from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from .const import DOMAIN, LOGGER, SCAN_INTERVAL
from .const import DOMAIN, LOGGER, SCAN_INTERVAL, SERVICE_AIR_QUALITY, SERVICE_POLLEN
PLATFORMS = (SENSOR_DOMAIN,)
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Ambee from a config entry."""
hass.data.setdefault(DOMAIN, {}).setdefault(entry.entry_id, {})
client = Ambee(
api_key=entry.data[CONF_API_KEY],
latitude=entry.data[CONF_LATITUDE],
longitude=entry.data[CONF_LONGITUDE],
)
coordinator = DataUpdateCoordinator(
for service in {SERVICE_AIR_QUALITY, SERVICE_POLLEN}:
coordinator: DataUpdateCoordinator = DataUpdateCoordinator(
hass,
LOGGER,
name=DOMAIN,
update_interval=SCAN_INTERVAL,
update_method=client.air_quality,
update_method=getattr(client, service),
)
await coordinator.async_config_entry_first_refresh()
hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN][entry.entry_id] = coordinator
hass.data[DOMAIN][entry.entry_id][service] = coordinator
hass.config_entries.async_setup_platforms(entry, PLATFORMS)
return True

View File

@ -3,67 +3,210 @@ from __future__ import annotations
from datetime import timedelta
import logging
from typing import Any, Final
from typing import Final
from homeassistant.components.sensor import ATTR_STATE_CLASS, STATE_CLASS_MEASUREMENT
from homeassistant.const import (
ATTR_DEVICE_CLASS,
ATTR_ICON,
ATTR_NAME,
ATTR_SERVICE,
ATTR_UNIT_OF_MEASUREMENT,
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
CONCENTRATION_PARTS_PER_BILLION,
CONCENTRATION_PARTS_PER_CUBIC_METER,
CONCENTRATION_PARTS_PER_MILLION,
DEVICE_CLASS_CO,
)
from .models import AmbeeSensor
DOMAIN: Final = "ambee"
LOGGER = logging.getLogger(__package__)
SCAN_INTERVAL = timedelta(minutes=180)
SERVICE_AIR_QUALITY: Final = ("air_quality", "Air Quality")
ATTR_ENABLED_BY_DEFAULT: Final = "enabled_by_default"
ATTR_ENTRY_TYPE: Final = "entry_type"
ENTRY_TYPE_SERVICE: Final = "service"
SENSORS: dict[str, dict[str, Any]] = {
DEVICE_CLASS_AMBEE_RISK: Final = "ambee__risk"
SERVICE_AIR_QUALITY: Final = "air_quality"
SERVICE_POLLEN: Final = "pollen"
SERVICES: dict[str, str] = {
SERVICE_AIR_QUALITY: "Air Quality",
SERVICE_POLLEN: "Pollen",
}
SENSORS: dict[str, dict[str, AmbeeSensor]] = {
SERVICE_AIR_QUALITY: {
"particulate_matter_2_5": {
ATTR_SERVICE: SERVICE_AIR_QUALITY,
ATTR_NAME: "Particulate Matter < 2.5 μm",
ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
},
"particulate_matter_10": {
ATTR_SERVICE: SERVICE_AIR_QUALITY,
ATTR_NAME: "Particulate Matter < 10 μm",
ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
},
"sulphur_dioxide": {
ATTR_SERVICE: SERVICE_AIR_QUALITY,
ATTR_NAME: "Sulphur Dioxide (SO2)",
ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_BILLION,
ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
},
"nitrogen_dioxide": {
ATTR_SERVICE: SERVICE_AIR_QUALITY,
ATTR_NAME: "Nitrogen Dioxide (NO2)",
ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_BILLION,
ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
},
"ozone": {
ATTR_SERVICE: SERVICE_AIR_QUALITY,
ATTR_NAME: "Ozone",
ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_BILLION,
ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
},
"carbon_monoxide": {
ATTR_SERVICE: SERVICE_AIR_QUALITY,
ATTR_NAME: "Carbon Monoxide (CO)",
ATTR_DEVICE_CLASS: DEVICE_CLASS_CO,
ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_MILLION,
ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
},
"air_quality_index": {
ATTR_SERVICE: SERVICE_AIR_QUALITY,
ATTR_NAME: "Air Quality Index (AQI)",
ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
},
},
SERVICE_POLLEN: {
"grass": {
ATTR_NAME: "Grass Pollen",
ATTR_ICON: "mdi:grass",
ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_CUBIC_METER,
},
"tree": {
ATTR_NAME: "Tree Pollen",
ATTR_ICON: "mdi:tree",
ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_CUBIC_METER,
},
"weed": {
ATTR_NAME: "Weed Pollen",
ATTR_ICON: "mdi:sprout",
ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_CUBIC_METER,
},
"grass_risk": {
ATTR_NAME: "Grass Pollen Risk",
ATTR_ICON: "mdi:grass",
ATTR_DEVICE_CLASS: DEVICE_CLASS_AMBEE_RISK,
},
"tree_risk": {
ATTR_NAME: "Tree Pollen Risk",
ATTR_ICON: "mdi:tree",
ATTR_DEVICE_CLASS: DEVICE_CLASS_AMBEE_RISK,
},
"weed_risk": {
ATTR_NAME: "Weed Pollen Risk",
ATTR_ICON: "mdi:sprout",
ATTR_DEVICE_CLASS: DEVICE_CLASS_AMBEE_RISK,
},
"grass_poaceae": {
ATTR_NAME: "Poaceae Grass Pollen",
ATTR_ICON: "mdi:grass",
ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_CUBIC_METER,
ATTR_ENABLED_BY_DEFAULT: False,
},
"tree_alder": {
ATTR_NAME: "Alder Tree Pollen",
ATTR_ICON: "mdi:tree",
ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_CUBIC_METER,
ATTR_ENABLED_BY_DEFAULT: False,
},
"tree_birch": {
ATTR_NAME: "Birch Tree Pollen",
ATTR_ICON: "mdi:tree",
ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_CUBIC_METER,
ATTR_ENABLED_BY_DEFAULT: False,
},
"tree_cypress": {
ATTR_NAME: "Cypress Tree Pollen",
ATTR_ICON: "mdi:tree",
ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_CUBIC_METER,
ATTR_ENABLED_BY_DEFAULT: False,
},
"tree_elm": {
ATTR_NAME: "Elm Tree Pollen",
ATTR_ICON: "mdi:tree",
ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_CUBIC_METER,
ATTR_ENABLED_BY_DEFAULT: False,
},
"tree_hazel": {
ATTR_NAME: "Hazel Tree Pollen",
ATTR_ICON: "mdi:tree",
ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_CUBIC_METER,
ATTR_ENABLED_BY_DEFAULT: False,
},
"tree_oak": {
ATTR_NAME: "Oak Tree Pollen",
ATTR_ICON: "mdi:tree",
ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_CUBIC_METER,
ATTR_ENABLED_BY_DEFAULT: False,
},
"tree_pine": {
ATTR_NAME: "Pine Tree Pollen",
ATTR_ICON: "mdi:tree",
ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_CUBIC_METER,
ATTR_ENABLED_BY_DEFAULT: False,
},
"tree_plane": {
ATTR_NAME: "Plane Tree Pollen",
ATTR_ICON: "mdi:tree",
ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_CUBIC_METER,
ATTR_ENABLED_BY_DEFAULT: False,
},
"tree_poplar": {
ATTR_NAME: "Poplar Tree Pollen",
ATTR_ICON: "mdi:tree",
ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_CUBIC_METER,
ATTR_ENABLED_BY_DEFAULT: False,
},
"weed_chenopod": {
ATTR_NAME: "Chenopod Weed Pollen",
ATTR_ICON: "mdi:sprout",
ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_CUBIC_METER,
ATTR_ENABLED_BY_DEFAULT: False,
},
"weed_mugwort": {
ATTR_NAME: "Mugwort Weed Pollen",
ATTR_ICON: "mdi:sprout",
ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_CUBIC_METER,
ATTR_ENABLED_BY_DEFAULT: False,
},
"weed_nettle": {
ATTR_NAME: "Nettle Weed Pollen",
ATTR_ICON: "mdi:sprout",
ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_CUBIC_METER,
ATTR_ENABLED_BY_DEFAULT: False,
},
"weed_ragweed": {
ATTR_NAME: "Ragweed Weed Pollen",
ATTR_ICON: "mdi:sprout",
ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_CUBIC_METER,
ATTR_ENABLED_BY_DEFAULT: False,
},
},
}

View File

@ -0,0 +1,15 @@
"""Models helper class for the Ambee integration."""
from __future__ import annotations
from typing import TypedDict
class AmbeeSensor(TypedDict, total=False):
"""Represent an Ambee Sensor."""
device_class: str
enabled_by_default: bool
icon: str
name: str
state_class: str
unit_of_measurement: str

View File

@ -1,18 +1,21 @@
"""Support for Ambee sensors."""
from __future__ import annotations
from homeassistant.components.sensor import ATTR_STATE_CLASS, SensorEntity
from homeassistant.components.sensor import (
ATTR_STATE_CLASS,
DOMAIN as SENSOR_DOMAIN,
SensorEntity,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
ATTR_DEVICE_CLASS,
ATTR_ICON,
ATTR_IDENTIFIERS,
ATTR_MANUFACTURER,
ATTR_NAME,
ATTR_SERVICE,
ATTR_UNIT_OF_MEASUREMENT,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import StateType
from homeassistant.helpers.update_coordinator import (
@ -20,7 +23,15 @@ from homeassistant.helpers.update_coordinator import (
DataUpdateCoordinator,
)
from .const import DOMAIN, SENSORS
from .const import (
ATTR_ENABLED_BY_DEFAULT,
ATTR_ENTRY_TYPE,
DOMAIN,
ENTRY_TYPE_SERVICE,
SENSORS,
SERVICES,
)
from .models import AmbeeSensor
async def async_setup_entry(
@ -28,42 +39,61 @@ async def async_setup_entry(
entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up Ambee sensor based on a config entry."""
coordinator: DataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
"""Set up Ambee sensors based on a config entry."""
async_add_entities(
AmbeeSensor(coordinator=coordinator, entry_id=entry.entry_id, key=sensor)
for sensor in SENSORS
AmbeeSensorEntity(
coordinator=hass.data[DOMAIN][entry.entry_id][service_key],
entry_id=entry.entry_id,
sensor_key=sensor_key,
sensor=sensor,
service_key=service_key,
service=SERVICES[service_key],
)
for service_key, service_sensors in SENSORS.items()
for sensor_key, sensor in service_sensors.items()
)
class AmbeeSensor(CoordinatorEntity, SensorEntity):
class AmbeeSensorEntity(CoordinatorEntity, SensorEntity):
"""Defines an Ambee sensor."""
def __init__(
self, *, coordinator: DataUpdateCoordinator, entry_id: str, key: str
self,
*,
coordinator: DataUpdateCoordinator,
entry_id: str,
sensor_key: str,
sensor: AmbeeSensor,
service_key: str,
service: str,
) -> None:
"""Initialize Ambee sensor."""
super().__init__(coordinator=coordinator)
self._key = key
self._entry_id = entry_id
self._service_key, self._service_name = SENSORS[key][ATTR_SERVICE]
self._sensor_key = sensor_key
self._service_key = service_key
self._attr_device_class = SENSORS[key].get(ATTR_DEVICE_CLASS)
self._attr_name = SENSORS[key][ATTR_NAME]
self._attr_state_class = SENSORS[key].get(ATTR_STATE_CLASS)
self._attr_unique_id = f"{entry_id}_{key}"
self._attr_unit_of_measurement = SENSORS[key].get(ATTR_UNIT_OF_MEASUREMENT)
self.entity_id = f"{SENSOR_DOMAIN}.{service_key}_{sensor_key}"
self._attr_device_class = sensor.get(ATTR_DEVICE_CLASS)
self._attr_entity_registry_enabled_default = sensor.get(
ATTR_ENABLED_BY_DEFAULT, True
)
self._attr_icon = sensor.get(ATTR_ICON)
self._attr_name = sensor.get(ATTR_NAME)
self._attr_state_class = sensor.get(ATTR_STATE_CLASS)
self._attr_unique_id = f"{entry_id}_{service_key}_{sensor_key}"
self._attr_unit_of_measurement = sensor.get(ATTR_UNIT_OF_MEASUREMENT)
self._attr_device_info = {
ATTR_IDENTIFIERS: {(DOMAIN, f"{entry_id}_{service_key}")},
ATTR_NAME: service,
ATTR_MANUFACTURER: "Ambee",
ATTR_ENTRY_TYPE: ENTRY_TYPE_SERVICE,
}
@property
def state(self) -> StateType:
"""Return the state of the sensor."""
return getattr(self.coordinator.data, self._key) # type: ignore[no-any-return]
@property
def device_info(self) -> DeviceInfo:
"""Return device information about this Ambee Service."""
return {
ATTR_IDENTIFIERS: {(DOMAIN, f"{self._entry_id}_{self._service_key}")},
ATTR_NAME: self._service_name,
ATTR_MANUFACTURER: "Ambee",
}
value = getattr(self.coordinator.data, self._sensor_key)
if isinstance(value, str):
return value.lower()
return value # type: ignore[no-any-return]

View File

@ -0,0 +1,10 @@
{
"state": {
"ambee__risk": {
"low": "Low",
"moderate": "Moderate",
"high": "High",
"very high": "Very High"
}
}
}

View File

@ -0,0 +1,10 @@
{
"state": {
"ambee__risk": {
"high": "High",
"low": "Low",
"moderate": "Moderate",
"very high": "Very High"
}
}
}

View File

@ -2,7 +2,7 @@
import json
from unittest.mock import AsyncMock, MagicMock, patch
from ambee import AirQuality
from ambee import AirQuality, Pollen
import pytest
from homeassistant.components.ambee.const import DOMAIN
@ -34,6 +34,9 @@ def mock_ambee(aioclient_mock: AiohttpClientMocker):
json.loads(load_fixture("ambee/air_quality.json"))
)
)
client.pollen = AsyncMock(
return_value=Pollen.from_dict(json.loads(load_fixture("ambee/pollen.json")))
)
yield ambee_mock

View File

@ -29,11 +29,11 @@ async def test_load_unload_config_entry(
@patch(
"homeassistant.components.ambee.Ambee.air_quality",
"homeassistant.components.ambee.Ambee.request",
side_effect=AmbeeConnectionError,
)
async def test_config_entry_not_ready(
mock_air_quality: MagicMock,
mock_request: MagicMock,
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
) -> None:
@ -42,5 +42,5 @@ async def test_config_entry_not_ready(
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
assert mock_air_quality.call_count == 1
assert mock_request.call_count == 1
assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY

View File

@ -1,12 +1,26 @@
"""Tests for the sensors provided by the Ambee integration."""
from homeassistant.components.ambee.const import DOMAIN
from homeassistant.components.sensor import ATTR_STATE_CLASS, STATE_CLASS_MEASUREMENT
from unittest.mock import AsyncMock
import pytest
from homeassistant.components.ambee.const import (
DEVICE_CLASS_AMBEE_RISK,
DOMAIN,
ENTRY_TYPE_SERVICE,
)
from homeassistant.components.sensor import (
ATTR_STATE_CLASS,
DOMAIN as SENSOR_DOMAIN,
STATE_CLASS_MEASUREMENT,
)
from homeassistant.const import (
ATTR_DEVICE_CLASS,
ATTR_FRIENDLY_NAME,
ATTR_ICON,
ATTR_UNIT_OF_MEASUREMENT,
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
CONCENTRATION_PARTS_PER_BILLION,
CONCENTRATION_PARTS_PER_CUBIC_METER,
CONCENTRATION_PARTS_PER_MILLION,
DEVICE_CLASS_CO,
)
@ -25,11 +39,11 @@ async def test_air_quality(
entity_registry = er.async_get(hass)
device_registry = dr.async_get(hass)
state = hass.states.get("sensor.particulate_matter_2_5_mm")
entry = entity_registry.async_get("sensor.particulate_matter_2_5_mm")
state = hass.states.get("sensor.air_quality_particulate_matter_2_5")
entry = entity_registry.async_get("sensor.air_quality_particulate_matter_2_5")
assert entry
assert state
assert entry.unique_id == f"{entry_id}_particulate_matter_2_5"
assert entry.unique_id == f"{entry_id}_air_quality_particulate_matter_2_5"
assert state.state == "3.14"
assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Particulate Matter < 2.5 μm"
assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT
@ -38,12 +52,13 @@ async def test_air_quality(
== CONCENTRATION_MICROGRAMS_PER_CUBIC_METER
)
assert ATTR_DEVICE_CLASS not in state.attributes
assert ATTR_ICON not in state.attributes
state = hass.states.get("sensor.particulate_matter_10_mm")
entry = entity_registry.async_get("sensor.particulate_matter_10_mm")
state = hass.states.get("sensor.air_quality_particulate_matter_10")
entry = entity_registry.async_get("sensor.air_quality_particulate_matter_10")
assert entry
assert state
assert entry.unique_id == f"{entry_id}_particulate_matter_10"
assert entry.unique_id == f"{entry_id}_air_quality_particulate_matter_10"
assert state.state == "5.24"
assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Particulate Matter < 10 μm"
assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT
@ -52,12 +67,13 @@ async def test_air_quality(
== CONCENTRATION_MICROGRAMS_PER_CUBIC_METER
)
assert ATTR_DEVICE_CLASS not in state.attributes
assert ATTR_ICON not in state.attributes
state = hass.states.get("sensor.sulphur_dioxide_so2")
entry = entity_registry.async_get("sensor.sulphur_dioxide_so2")
state = hass.states.get("sensor.air_quality_sulphur_dioxide")
entry = entity_registry.async_get("sensor.air_quality_sulphur_dioxide")
assert entry
assert state
assert entry.unique_id == f"{entry_id}_sulphur_dioxide"
assert entry.unique_id == f"{entry_id}_air_quality_sulphur_dioxide"
assert state.state == "0.031"
assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Sulphur Dioxide (SO2)"
assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT
@ -66,12 +82,13 @@ async def test_air_quality(
== CONCENTRATION_PARTS_PER_BILLION
)
assert ATTR_DEVICE_CLASS not in state.attributes
assert ATTR_ICON not in state.attributes
state = hass.states.get("sensor.nitrogen_dioxide_no2")
entry = entity_registry.async_get("sensor.nitrogen_dioxide_no2")
state = hass.states.get("sensor.air_quality_nitrogen_dioxide")
entry = entity_registry.async_get("sensor.air_quality_nitrogen_dioxide")
assert entry
assert state
assert entry.unique_id == f"{entry_id}_nitrogen_dioxide"
assert entry.unique_id == f"{entry_id}_air_quality_nitrogen_dioxide"
assert state.state == "0.66"
assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Nitrogen Dioxide (NO2)"
assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT
@ -80,12 +97,13 @@ async def test_air_quality(
== CONCENTRATION_PARTS_PER_BILLION
)
assert ATTR_DEVICE_CLASS not in state.attributes
assert ATTR_ICON not in state.attributes
state = hass.states.get("sensor.ozone")
entry = entity_registry.async_get("sensor.ozone")
state = hass.states.get("sensor.air_quality_ozone")
entry = entity_registry.async_get("sensor.air_quality_ozone")
assert entry
assert state
assert entry.unique_id == f"{entry_id}_ozone"
assert entry.unique_id == f"{entry_id}_air_quality_ozone"
assert state.state == "17.067"
assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Ozone"
assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT
@ -94,12 +112,13 @@ async def test_air_quality(
== CONCENTRATION_PARTS_PER_BILLION
)
assert ATTR_DEVICE_CLASS not in state.attributes
assert ATTR_ICON not in state.attributes
state = hass.states.get("sensor.carbon_monoxide_co")
entry = entity_registry.async_get("sensor.carbon_monoxide_co")
state = hass.states.get("sensor.air_quality_carbon_monoxide")
entry = entity_registry.async_get("sensor.air_quality_carbon_monoxide")
assert entry
assert state
assert entry.unique_id == f"{entry_id}_carbon_monoxide"
assert entry.unique_id == f"{entry_id}_air_quality_carbon_monoxide"
assert state.state == "0.105"
assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_CO
assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Carbon Monoxide (CO)"
@ -108,17 +127,19 @@ async def test_air_quality(
state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
== CONCENTRATION_PARTS_PER_MILLION
)
assert ATTR_ICON not in state.attributes
state = hass.states.get("sensor.air_quality_index_aqi")
entry = entity_registry.async_get("sensor.air_quality_index_aqi")
state = hass.states.get("sensor.air_quality_air_quality_index")
entry = entity_registry.async_get("sensor.air_quality_air_quality_index")
assert entry
assert state
assert entry.unique_id == f"{entry_id}_air_quality_index"
assert entry.unique_id == f"{entry_id}_air_quality_air_quality_index"
assert state.state == "13"
assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Air Quality Index (AQI)"
assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT
assert ATTR_DEVICE_CLASS not in state.attributes
assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes
assert ATTR_ICON not in state.attributes
assert entry.device_id
device_entry = device_registry.async_get(entry.device_id)
@ -126,5 +147,203 @@ async def test_air_quality(
assert device_entry.identifiers == {(DOMAIN, f"{entry_id}_air_quality")}
assert device_entry.manufacturer == "Ambee"
assert device_entry.name == "Air Quality"
assert device_entry.entry_type == ENTRY_TYPE_SERVICE
assert not device_entry.model
assert not device_entry.sw_version
async def test_pollen(
hass: HomeAssistant,
init_integration: MockConfigEntry,
) -> None:
"""Test the Ambee Pollen sensors."""
entry_id = init_integration.entry_id
entity_registry = er.async_get(hass)
device_registry = dr.async_get(hass)
state = hass.states.get("sensor.pollen_grass")
entry = entity_registry.async_get("sensor.pollen_grass")
assert entry
assert state
assert entry.unique_id == f"{entry_id}_pollen_grass"
assert state.state == "190"
assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Grass Pollen"
assert state.attributes.get(ATTR_ICON) == "mdi:grass"
assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT
assert (
state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
== CONCENTRATION_PARTS_PER_CUBIC_METER
)
assert ATTR_DEVICE_CLASS not in state.attributes
state = hass.states.get("sensor.pollen_tree")
entry = entity_registry.async_get("sensor.pollen_tree")
assert entry
assert state
assert entry.unique_id == f"{entry_id}_pollen_tree"
assert state.state == "127"
assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Tree Pollen"
assert state.attributes.get(ATTR_ICON) == "mdi:tree"
assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT
assert (
state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
== CONCENTRATION_PARTS_PER_CUBIC_METER
)
assert ATTR_DEVICE_CLASS not in state.attributes
state = hass.states.get("sensor.pollen_weed")
entry = entity_registry.async_get("sensor.pollen_weed")
assert entry
assert state
assert entry.unique_id == f"{entry_id}_pollen_weed"
assert state.state == "95"
assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Weed Pollen"
assert state.attributes.get(ATTR_ICON) == "mdi:sprout"
assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT
assert (
state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
== CONCENTRATION_PARTS_PER_CUBIC_METER
)
assert ATTR_DEVICE_CLASS not in state.attributes
state = hass.states.get("sensor.pollen_grass_risk")
entry = entity_registry.async_get("sensor.pollen_grass_risk")
assert entry
assert state
assert entry.unique_id == f"{entry_id}_pollen_grass_risk"
assert state.state == "high"
assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_AMBEE_RISK
assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Grass Pollen Risk"
assert state.attributes.get(ATTR_ICON) == "mdi:grass"
assert ATTR_STATE_CLASS not in state.attributes
assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes
state = hass.states.get("sensor.pollen_tree_risk")
entry = entity_registry.async_get("sensor.pollen_tree_risk")
assert entry
assert state
assert entry.unique_id == f"{entry_id}_pollen_tree_risk"
assert state.state == "moderate"
assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_AMBEE_RISK
assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Tree Pollen Risk"
assert state.attributes.get(ATTR_ICON) == "mdi:tree"
assert ATTR_STATE_CLASS not in state.attributes
assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes
state = hass.states.get("sensor.pollen_weed_risk")
entry = entity_registry.async_get("sensor.pollen_weed_risk")
assert entry
assert state
assert entry.unique_id == f"{entry_id}_pollen_weed_risk"
assert state.state == "high"
assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_AMBEE_RISK
assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Weed Pollen Risk"
assert state.attributes.get(ATTR_ICON) == "mdi:sprout"
assert ATTR_STATE_CLASS not in state.attributes
assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes
assert entry.device_id
device_entry = device_registry.async_get(entry.device_id)
assert device_entry
assert device_entry.identifiers == {(DOMAIN, f"{entry_id}_pollen")}
assert device_entry.manufacturer == "Ambee"
assert device_entry.name == "Pollen"
assert device_entry.entry_type == ENTRY_TYPE_SERVICE
assert not device_entry.model
assert not device_entry.sw_version
@pytest.mark.parametrize(
"entity_id",
(
"sensor.pollen_grass_poaceae",
"sensor.pollen_tree_alder",
"sensor.pollen_tree_birch",
"sensor.pollen_tree_cypress",
"sensor.pollen_tree_elm",
"sensor.pollen_tree_hazel",
"sensor.pollen_tree_oak",
"sensor.pollen_tree_pine",
"sensor.pollen_tree_plane",
"sensor.pollen_tree_poplar",
"sensor.pollen_weed_chenopod",
"sensor.pollen_weed_mugwort",
"sensor.pollen_weed_nettle",
"sensor.pollen_weed_ragweed",
),
)
async def test_pollen_disabled_by_default(
hass: HomeAssistant, init_integration: MockConfigEntry, entity_id: str
) -> None:
"""Test the Ambee Pollen sensors that are disabled by default."""
entity_registry = er.async_get(hass)
state = hass.states.get(entity_id)
assert state is None
entry = entity_registry.async_get(entity_id)
assert entry
assert entry.disabled
assert entry.disabled_by == er.DISABLED_INTEGRATION
@pytest.mark.parametrize(
"key,icon,name,value",
[
("grass_poaceae", "mdi:grass", "Poaceae Grass Pollen", "190"),
("tree_alder", "mdi:tree", "Alder Tree Pollen", "0"),
("tree_birch", "mdi:tree", "Birch Tree Pollen", "35"),
("tree_cypress", "mdi:tree", "Cypress Tree Pollen", "0"),
("tree_elm", "mdi:tree", "Elm Tree Pollen", "0"),
("tree_hazel", "mdi:tree", "Hazel Tree Pollen", "0"),
("tree_oak", "mdi:tree", "Oak Tree Pollen", "55"),
("tree_pine", "mdi:tree", "Pine Tree Pollen", "30"),
("tree_plane", "mdi:tree", "Plane Tree Pollen", "5"),
("tree_poplar", "mdi:tree", "Poplar Tree Pollen", "0"),
("weed_chenopod", "mdi:sprout", "Chenopod Weed Pollen", "0"),
("weed_mugwort", "mdi:sprout", "Mugwort Weed Pollen", "1"),
("weed_nettle", "mdi:sprout", "Nettle Weed Pollen", "88"),
("weed_ragweed", "mdi:sprout", "Ragweed Weed Pollen", "3"),
],
)
async def test_pollen_enable_disable_by_defaults(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_ambee: AsyncMock,
key: str,
icon: str,
name: str,
value: str,
) -> None:
"""Test the Ambee Pollen sensors that are disabled by default."""
entry_id = mock_config_entry.entry_id
entity_id = f"{SENSOR_DOMAIN}.pollen_{key}"
entity_registry = er.async_get(hass)
# Pre-create registry entry for disabled by default sensor
entity_registry.async_get_or_create(
SENSOR_DOMAIN,
DOMAIN,
f"{entry_id}_pollen_{key}",
suggested_object_id=f"pollen_{key}",
disabled_by=None,
)
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
state = hass.states.get(entity_id)
entry = entity_registry.async_get(entity_id)
assert entry
assert state
assert entry.unique_id == f"{entry_id}_pollen_{key}"
assert state.state == value
assert state.attributes.get(ATTR_FRIENDLY_NAME) == name
assert state.attributes.get(ATTR_ICON) == icon
assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT
assert (
state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
== CONCENTRATION_PARTS_PER_CUBIC_METER
)
assert ATTR_DEVICE_CLASS not in state.attributes

43
tests/fixtures/ambee/pollen.json vendored Normal file
View File

@ -0,0 +1,43 @@
{
"message": "Success",
"lat": 52.42,
"lng": 6.42,
"data": [
{
"Count": {
"grass_pollen": 190,
"tree_pollen": 127,
"weed_pollen": 95
},
"Risk": {
"grass_pollen": "High",
"tree_pollen": "Moderate",
"weed_pollen": "High"
},
"Species": {
"Grass": {
"Grass / Poaceae": 190
},
"Others": 5,
"Tree": {
"Alder": 0,
"Birch": 35,
"Cypress": 0,
"Elm": 0,
"Hazel": 0,
"Oak": 55,
"Pine": 30,
"Plane": 5,
"Poplar / Cottonwood": 0
},
"Weed": {
"Chenopod": 0,
"Mugwort": 1,
"Nettle": 88,
"Ragweed": 3
}
},
"updatedAt": "2021-06-09T16:24:27.000Z"
}
]
}