Add pollen sensors to Ambee (#51702)

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

View File

@ -3,67 +3,210 @@ from __future__ import annotations
from datetime import timedelta from datetime import timedelta
import logging import logging
from typing import Any, Final from typing import Final
from homeassistant.components.sensor import ATTR_STATE_CLASS, STATE_CLASS_MEASUREMENT from homeassistant.components.sensor import ATTR_STATE_CLASS, STATE_CLASS_MEASUREMENT
from homeassistant.const import ( from homeassistant.const import (
ATTR_DEVICE_CLASS, ATTR_DEVICE_CLASS,
ATTR_ICON,
ATTR_NAME, ATTR_NAME,
ATTR_SERVICE,
ATTR_UNIT_OF_MEASUREMENT, ATTR_UNIT_OF_MEASUREMENT,
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
CONCENTRATION_PARTS_PER_BILLION, CONCENTRATION_PARTS_PER_BILLION,
CONCENTRATION_PARTS_PER_CUBIC_METER,
CONCENTRATION_PARTS_PER_MILLION, CONCENTRATION_PARTS_PER_MILLION,
DEVICE_CLASS_CO, DEVICE_CLASS_CO,
) )
from .models import AmbeeSensor
DOMAIN: Final = "ambee" DOMAIN: Final = "ambee"
LOGGER = logging.getLogger(__package__) LOGGER = logging.getLogger(__package__)
SCAN_INTERVAL = timedelta(minutes=180) 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"
"particulate_matter_2_5": {
ATTR_SERVICE: SERVICE_AIR_QUALITY, SERVICE_AIR_QUALITY: Final = "air_quality"
ATTR_NAME: "Particulate Matter < 2.5 μm", SERVICE_POLLEN: Final = "pollen"
ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, 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_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_NAME: "Particulate Matter < 10 μm",
ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
},
"sulphur_dioxide": {
ATTR_NAME: "Sulphur Dioxide (SO2)",
ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_BILLION,
ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
},
"nitrogen_dioxide": {
ATTR_NAME: "Nitrogen Dioxide (NO2)",
ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_BILLION,
ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
},
"ozone": {
ATTR_NAME: "Ozone",
ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_BILLION,
ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
},
"carbon_monoxide": {
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_NAME: "Air Quality Index (AQI)",
ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
},
}, },
"particulate_matter_10": { SERVICE_POLLEN: {
ATTR_SERVICE: SERVICE_AIR_QUALITY, "grass": {
ATTR_NAME: "Particulate Matter < 10 μm", ATTR_NAME: "Grass Pollen",
ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, ATTR_ICON: "mdi:grass",
ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
}, ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_CUBIC_METER,
"sulphur_dioxide": { },
ATTR_SERVICE: SERVICE_AIR_QUALITY, "tree": {
ATTR_NAME: "Sulphur Dioxide (SO2)", ATTR_NAME: "Tree Pollen",
ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_BILLION, ATTR_ICON: "mdi:tree",
ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
}, ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_CUBIC_METER,
"nitrogen_dioxide": { },
ATTR_SERVICE: SERVICE_AIR_QUALITY, "weed": {
ATTR_NAME: "Nitrogen Dioxide (NO2)", ATTR_NAME: "Weed Pollen",
ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_BILLION, ATTR_ICON: "mdi:sprout",
ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
}, ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_CUBIC_METER,
"ozone": { },
ATTR_SERVICE: SERVICE_AIR_QUALITY, "grass_risk": {
ATTR_NAME: "Ozone", ATTR_NAME: "Grass Pollen Risk",
ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_BILLION, ATTR_ICON: "mdi:grass",
ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, ATTR_DEVICE_CLASS: DEVICE_CLASS_AMBEE_RISK,
}, },
"carbon_monoxide": { "tree_risk": {
ATTR_SERVICE: SERVICE_AIR_QUALITY, ATTR_NAME: "Tree Pollen Risk",
ATTR_NAME: "Carbon Monoxide (CO)", ATTR_ICON: "mdi:tree",
ATTR_DEVICE_CLASS: DEVICE_CLASS_CO, ATTR_DEVICE_CLASS: DEVICE_CLASS_AMBEE_RISK,
ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_MILLION, },
ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, "weed_risk": {
}, ATTR_NAME: "Weed Pollen Risk",
"air_quality_index": { ATTR_ICON: "mdi:sprout",
ATTR_SERVICE: SERVICE_AIR_QUALITY, ATTR_DEVICE_CLASS: DEVICE_CLASS_AMBEE_RISK,
ATTR_NAME: "Air Quality Index (AQI)", },
ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, "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.""" """Support for Ambee sensors."""
from __future__ import annotations 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.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
ATTR_DEVICE_CLASS, ATTR_DEVICE_CLASS,
ATTR_ICON,
ATTR_IDENTIFIERS, ATTR_IDENTIFIERS,
ATTR_MANUFACTURER, ATTR_MANUFACTURER,
ATTR_NAME, ATTR_NAME,
ATTR_SERVICE,
ATTR_UNIT_OF_MEASUREMENT, ATTR_UNIT_OF_MEASUREMENT,
) )
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import StateType from homeassistant.helpers.typing import StateType
from homeassistant.helpers.update_coordinator import ( from homeassistant.helpers.update_coordinator import (
@ -20,7 +23,15 @@ from homeassistant.helpers.update_coordinator import (
DataUpdateCoordinator, 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( async def async_setup_entry(
@ -28,42 +39,61 @@ async def async_setup_entry(
entry: ConfigEntry, entry: ConfigEntry,
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Set up Ambee sensor based on a config entry.""" """Set up Ambee sensors based on a config entry."""
coordinator: DataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
async_add_entities( async_add_entities(
AmbeeSensor(coordinator=coordinator, entry_id=entry.entry_id, key=sensor) AmbeeSensorEntity(
for sensor in SENSORS 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.""" """Defines an Ambee sensor."""
def __init__( 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: ) -> None:
"""Initialize Ambee sensor.""" """Initialize Ambee sensor."""
super().__init__(coordinator=coordinator) super().__init__(coordinator=coordinator)
self._key = key self._sensor_key = sensor_key
self._entry_id = entry_id self._service_key = service_key
self._service_key, self._service_name = SENSORS[key][ATTR_SERVICE]
self._attr_device_class = SENSORS[key].get(ATTR_DEVICE_CLASS) self.entity_id = f"{SENSOR_DOMAIN}.{service_key}_{sensor_key}"
self._attr_name = SENSORS[key][ATTR_NAME] self._attr_device_class = sensor.get(ATTR_DEVICE_CLASS)
self._attr_state_class = SENSORS[key].get(ATTR_STATE_CLASS) self._attr_entity_registry_enabled_default = sensor.get(
self._attr_unique_id = f"{entry_id}_{key}" ATTR_ENABLED_BY_DEFAULT, True
self._attr_unit_of_measurement = SENSORS[key].get(ATTR_UNIT_OF_MEASUREMENT) )
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 @property
def state(self) -> StateType: def state(self) -> StateType:
"""Return the state of the sensor.""" """Return the state of the sensor."""
return getattr(self.coordinator.data, self._key) # type: ignore[no-any-return] value = getattr(self.coordinator.data, self._sensor_key)
if isinstance(value, str):
@property return value.lower()
def device_info(self) -> DeviceInfo: return value # type: ignore[no-any-return]
"""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",
}

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 import json
from unittest.mock import AsyncMock, MagicMock, patch from unittest.mock import AsyncMock, MagicMock, patch
from ambee import AirQuality from ambee import AirQuality, Pollen
import pytest import pytest
from homeassistant.components.ambee.const import DOMAIN 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")) 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 yield ambee_mock

View File

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

View File

@ -1,12 +1,26 @@
"""Tests for the sensors provided by the Ambee integration.""" """Tests for the sensors provided by the Ambee integration."""
from homeassistant.components.ambee.const import DOMAIN from unittest.mock import AsyncMock
from homeassistant.components.sensor import ATTR_STATE_CLASS, STATE_CLASS_MEASUREMENT
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 ( from homeassistant.const import (
ATTR_DEVICE_CLASS, ATTR_DEVICE_CLASS,
ATTR_FRIENDLY_NAME, ATTR_FRIENDLY_NAME,
ATTR_ICON,
ATTR_UNIT_OF_MEASUREMENT, ATTR_UNIT_OF_MEASUREMENT,
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
CONCENTRATION_PARTS_PER_BILLION, CONCENTRATION_PARTS_PER_BILLION,
CONCENTRATION_PARTS_PER_CUBIC_METER,
CONCENTRATION_PARTS_PER_MILLION, CONCENTRATION_PARTS_PER_MILLION,
DEVICE_CLASS_CO, DEVICE_CLASS_CO,
) )
@ -25,11 +39,11 @@ async def test_air_quality(
entity_registry = er.async_get(hass) entity_registry = er.async_get(hass)
device_registry = dr.async_get(hass) device_registry = dr.async_get(hass)
state = hass.states.get("sensor.particulate_matter_2_5_mm") state = hass.states.get("sensor.air_quality_particulate_matter_2_5")
entry = entity_registry.async_get("sensor.particulate_matter_2_5_mm") entry = entity_registry.async_get("sensor.air_quality_particulate_matter_2_5")
assert entry assert entry
assert state 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.state == "3.14"
assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Particulate Matter < 2.5 μm" assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Particulate Matter < 2.5 μm"
assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT
@ -38,12 +52,13 @@ async def test_air_quality(
== CONCENTRATION_MICROGRAMS_PER_CUBIC_METER == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER
) )
assert ATTR_DEVICE_CLASS not in state.attributes assert ATTR_DEVICE_CLASS not in state.attributes
assert ATTR_ICON not in state.attributes
state = hass.states.get("sensor.particulate_matter_10_mm") state = hass.states.get("sensor.air_quality_particulate_matter_10")
entry = entity_registry.async_get("sensor.particulate_matter_10_mm") entry = entity_registry.async_get("sensor.air_quality_particulate_matter_10")
assert entry assert entry
assert state 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.state == "5.24"
assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Particulate Matter < 10 μm" assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Particulate Matter < 10 μm"
assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT
@ -52,12 +67,13 @@ async def test_air_quality(
== CONCENTRATION_MICROGRAMS_PER_CUBIC_METER == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER
) )
assert ATTR_DEVICE_CLASS not in state.attributes assert ATTR_DEVICE_CLASS not in state.attributes
assert ATTR_ICON not in state.attributes
state = hass.states.get("sensor.sulphur_dioxide_so2") state = hass.states.get("sensor.air_quality_sulphur_dioxide")
entry = entity_registry.async_get("sensor.sulphur_dioxide_so2") entry = entity_registry.async_get("sensor.air_quality_sulphur_dioxide")
assert entry assert entry
assert state 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.state == "0.031"
assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Sulphur Dioxide (SO2)" assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Sulphur Dioxide (SO2)"
assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT
@ -66,12 +82,13 @@ async def test_air_quality(
== CONCENTRATION_PARTS_PER_BILLION == CONCENTRATION_PARTS_PER_BILLION
) )
assert ATTR_DEVICE_CLASS not in state.attributes assert ATTR_DEVICE_CLASS not in state.attributes
assert ATTR_ICON not in state.attributes
state = hass.states.get("sensor.nitrogen_dioxide_no2") state = hass.states.get("sensor.air_quality_nitrogen_dioxide")
entry = entity_registry.async_get("sensor.nitrogen_dioxide_no2") entry = entity_registry.async_get("sensor.air_quality_nitrogen_dioxide")
assert entry assert entry
assert state 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.state == "0.66"
assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Nitrogen Dioxide (NO2)" assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Nitrogen Dioxide (NO2)"
assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT
@ -80,12 +97,13 @@ async def test_air_quality(
== CONCENTRATION_PARTS_PER_BILLION == CONCENTRATION_PARTS_PER_BILLION
) )
assert ATTR_DEVICE_CLASS not in state.attributes assert ATTR_DEVICE_CLASS not in state.attributes
assert ATTR_ICON not in state.attributes
state = hass.states.get("sensor.ozone") state = hass.states.get("sensor.air_quality_ozone")
entry = entity_registry.async_get("sensor.ozone") entry = entity_registry.async_get("sensor.air_quality_ozone")
assert entry assert entry
assert state 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.state == "17.067"
assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Ozone" assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Ozone"
assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT
@ -94,12 +112,13 @@ async def test_air_quality(
== CONCENTRATION_PARTS_PER_BILLION == CONCENTRATION_PARTS_PER_BILLION
) )
assert ATTR_DEVICE_CLASS not in state.attributes assert ATTR_DEVICE_CLASS not in state.attributes
assert ATTR_ICON not in state.attributes
state = hass.states.get("sensor.carbon_monoxide_co") state = hass.states.get("sensor.air_quality_carbon_monoxide")
entry = entity_registry.async_get("sensor.carbon_monoxide_co") entry = entity_registry.async_get("sensor.air_quality_carbon_monoxide")
assert entry assert entry
assert state 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.state == "0.105"
assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_CO assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_CO
assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Carbon Monoxide (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) state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
== CONCENTRATION_PARTS_PER_MILLION == CONCENTRATION_PARTS_PER_MILLION
) )
assert ATTR_ICON not in state.attributes
state = hass.states.get("sensor.air_quality_index_aqi") state = hass.states.get("sensor.air_quality_air_quality_index")
entry = entity_registry.async_get("sensor.air_quality_index_aqi") entry = entity_registry.async_get("sensor.air_quality_air_quality_index")
assert entry assert entry
assert state 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.state == "13"
assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Air Quality Index (AQI)" assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Air Quality Index (AQI)"
assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT
assert ATTR_DEVICE_CLASS not in state.attributes assert ATTR_DEVICE_CLASS not in state.attributes
assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes
assert ATTR_ICON not in state.attributes
assert entry.device_id assert entry.device_id
device_entry = device_registry.async_get(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.identifiers == {(DOMAIN, f"{entry_id}_air_quality")}
assert device_entry.manufacturer == "Ambee" assert device_entry.manufacturer == "Ambee"
assert device_entry.name == "Air Quality" assert device_entry.name == "Air Quality"
assert device_entry.entry_type == ENTRY_TYPE_SERVICE
assert not device_entry.model assert not device_entry.model
assert not device_entry.sw_version 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"
}
]
}