Add pollen sensors to Ambee (#51702)
parent
79996682e5
commit
fca0446ff8
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
@ -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
|
|
@ -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]
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"state": {
|
||||
"ambee__risk": {
|
||||
"low": "Low",
|
||||
"moderate": "Moderate",
|
||||
"high": "High",
|
||||
"very high": "Very High"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"state": {
|
||||
"ambee__risk": {
|
||||
"high": "High",
|
||||
"low": "Low",
|
||||
"moderate": "Moderate",
|
||||
"very high": "Very High"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
Loading…
Reference in New Issue