Add Air Pollution support to OpenWeatherMap (#137949)

Co-authored-by: Joostlek <joostlek@outlook.com>
This commit is contained in:
wittypluck 2025-05-26 21:34:48 +02:00 committed by GitHub
parent 16394061cb
commit b17d62177c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 634 additions and 30 deletions

View File

@ -12,7 +12,7 @@ from homeassistant.const import CONF_API_KEY, CONF_LANGUAGE, CONF_MODE, CONF_NAM
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from .const import CONFIG_FLOW_VERSION, DEFAULT_OWM_MODE, OWM_MODES, PLATFORMS from .const import CONFIG_FLOW_VERSION, DEFAULT_OWM_MODE, OWM_MODES, PLATFORMS
from .coordinator import WeatherUpdateCoordinator from .coordinator import OWMUpdateCoordinator, get_owm_update_coordinator
from .repairs import async_create_issue, async_delete_issue from .repairs import async_create_issue, async_delete_issue
from .utils import build_data_and_options from .utils import build_data_and_options
@ -27,7 +27,7 @@ class OpenweathermapData:
name: str name: str
mode: str mode: str
coordinator: WeatherUpdateCoordinator coordinator: OWMUpdateCoordinator
async def async_setup_entry( async def async_setup_entry(
@ -45,13 +45,13 @@ async def async_setup_entry(
async_delete_issue(hass, entry.entry_id) async_delete_issue(hass, entry.entry_id)
owm_client = create_owm_client(api_key, mode, lang=language) owm_client = create_owm_client(api_key, mode, lang=language)
weather_coordinator = WeatherUpdateCoordinator(hass, entry, owm_client) owm_coordinator = get_owm_update_coordinator(mode)(hass, entry, owm_client)
await weather_coordinator.async_config_entry_first_refresh() await owm_coordinator.async_config_entry_first_refresh()
entry.async_on_unload(entry.add_update_listener(async_update_options)) entry.async_on_unload(entry.add_update_listener(async_update_options))
entry.runtime_data = OpenweathermapData(name, mode, weather_coordinator) entry.runtime_data = OpenweathermapData(name, mode, owm_coordinator)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)

View File

@ -51,16 +51,28 @@ ATTR_API_CURRENT = "current"
ATTR_API_MINUTE_FORECAST = "minute_forecast" ATTR_API_MINUTE_FORECAST = "minute_forecast"
ATTR_API_HOURLY_FORECAST = "hourly_forecast" ATTR_API_HOURLY_FORECAST = "hourly_forecast"
ATTR_API_DAILY_FORECAST = "daily_forecast" ATTR_API_DAILY_FORECAST = "daily_forecast"
ATTR_API_AIRPOLLUTION_AQI = "aqi"
ATTR_API_AIRPOLLUTION_CO = "co"
ATTR_API_AIRPOLLUTION_NO = "no"
ATTR_API_AIRPOLLUTION_NO2 = "no2"
ATTR_API_AIRPOLLUTION_O3 = "o3"
ATTR_API_AIRPOLLUTION_SO2 = "so2"
ATTR_API_AIRPOLLUTION_PM2_5 = "pm2_5"
ATTR_API_AIRPOLLUTION_PM10 = "pm10"
ATTR_API_AIRPOLLUTION_NH3 = "nh3"
UPDATE_LISTENER = "update_listener" UPDATE_LISTENER = "update_listener"
PLATFORMS = [Platform.SENSOR, Platform.WEATHER] PLATFORMS = [Platform.SENSOR, Platform.WEATHER]
OWM_MODE_FREE_CURRENT = "current" OWM_MODE_FREE_CURRENT = "current"
OWM_MODE_FREE_FORECAST = "forecast" OWM_MODE_FREE_FORECAST = "forecast"
OWM_MODE_V30 = "v3.0" OWM_MODE_V30 = "v3.0"
OWM_MODE_AIRPOLLUTION = "air_pollution"
OWM_MODES = [ OWM_MODES = [
OWM_MODE_V30, OWM_MODE_V30,
OWM_MODE_FREE_CURRENT, OWM_MODE_FREE_CURRENT,
OWM_MODE_FREE_FORECAST, OWM_MODE_FREE_FORECAST,
OWM_MODE_AIRPOLLUTION,
] ]
DEFAULT_OWM_MODE = OWM_MODE_V30 DEFAULT_OWM_MODE = OWM_MODE_V30

View File

@ -1,12 +1,13 @@
"""Weather data coordinator for the OpenWeatherMap (OWM) service.""" """Data coordinator for the OpenWeatherMap (OWM) service."""
from __future__ import annotations from __future__ import annotations
from datetime import timedelta from datetime import timedelta
import logging import logging
from typing import TYPE_CHECKING from typing import TYPE_CHECKING, Any
from pyopenweathermap import ( from pyopenweathermap import (
CurrentAirPollution,
CurrentWeather, CurrentWeather,
DailyWeatherForecast, DailyWeatherForecast,
HourlyWeatherForecast, HourlyWeatherForecast,
@ -31,6 +32,15 @@ if TYPE_CHECKING:
from . import OpenweathermapConfigEntry from . import OpenweathermapConfigEntry
from .const import ( from .const import (
ATTR_API_AIRPOLLUTION_AQI,
ATTR_API_AIRPOLLUTION_CO,
ATTR_API_AIRPOLLUTION_NH3,
ATTR_API_AIRPOLLUTION_NO,
ATTR_API_AIRPOLLUTION_NO2,
ATTR_API_AIRPOLLUTION_O3,
ATTR_API_AIRPOLLUTION_PM2_5,
ATTR_API_AIRPOLLUTION_PM10,
ATTR_API_AIRPOLLUTION_SO2,
ATTR_API_CLOUDS, ATTR_API_CLOUDS,
ATTR_API_CONDITION, ATTR_API_CONDITION,
ATTR_API_CURRENT, ATTR_API_CURRENT,
@ -57,16 +67,20 @@ from .const import (
ATTR_API_WIND_SPEED, ATTR_API_WIND_SPEED,
CONDITION_MAP, CONDITION_MAP,
DOMAIN, DOMAIN,
OWM_MODE_AIRPOLLUTION,
OWM_MODE_FREE_CURRENT,
OWM_MODE_FREE_FORECAST,
OWM_MODE_V30,
WEATHER_CODE_SUNNY_OR_CLEAR_NIGHT, WEATHER_CODE_SUNNY_OR_CLEAR_NIGHT,
) )
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
WEATHER_UPDATE_INTERVAL = timedelta(minutes=10) OWM_UPDATE_INTERVAL = timedelta(minutes=10)
class WeatherUpdateCoordinator(DataUpdateCoordinator): class OWMUpdateCoordinator(DataUpdateCoordinator):
"""Weather data update coordinator.""" """OWM data update coordinator."""
config_entry: OpenweathermapConfigEntry config_entry: OpenweathermapConfigEntry
@ -86,9 +100,13 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator):
_LOGGER, _LOGGER,
config_entry=config_entry, config_entry=config_entry,
name=DOMAIN, name=DOMAIN,
update_interval=WEATHER_UPDATE_INTERVAL, update_interval=OWM_UPDATE_INTERVAL,
) )
class WeatherUpdateCoordinator(OWMUpdateCoordinator):
"""Weather data update coordinator."""
async def _async_update_data(self): async def _async_update_data(self):
"""Update the data.""" """Update the data."""
try: try:
@ -248,3 +266,52 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator):
return ATTR_CONDITION_CLEAR_NIGHT return ATTR_CONDITION_CLEAR_NIGHT
return CONDITION_MAP.get(weather_code) return CONDITION_MAP.get(weather_code)
class AirPollutionUpdateCoordinator(OWMUpdateCoordinator):
"""Air Pollution data update coordinator."""
async def _async_update_data(self) -> dict[str, Any]:
"""Update the data."""
try:
air_pollution_report = await self._owm_client.get_air_pollution(
self._latitude, self._longitude
)
except RequestError as error:
raise UpdateFailed(error) from error
current_air_pollution = (
self._get_current_air_pollution_data(air_pollution_report.current)
if air_pollution_report.current is not None
else {}
)
return {
ATTR_API_CURRENT: current_air_pollution,
}
def _get_current_air_pollution_data(
self, current_air_pollution: CurrentAirPollution
) -> dict[str, Any]:
return {
ATTR_API_AIRPOLLUTION_AQI: current_air_pollution.aqi,
ATTR_API_AIRPOLLUTION_CO: current_air_pollution.co,
ATTR_API_AIRPOLLUTION_NO: current_air_pollution.no,
ATTR_API_AIRPOLLUTION_NO2: current_air_pollution.no2,
ATTR_API_AIRPOLLUTION_O3: current_air_pollution.o3,
ATTR_API_AIRPOLLUTION_SO2: current_air_pollution.so2,
ATTR_API_AIRPOLLUTION_PM2_5: current_air_pollution.pm2_5,
ATTR_API_AIRPOLLUTION_PM10: current_air_pollution.pm10,
ATTR_API_AIRPOLLUTION_NH3: current_air_pollution.nh3,
}
def get_owm_update_coordinator(mode: str) -> type[OWMUpdateCoordinator]:
"""Create coordinator with a factory."""
coordinators = {
OWM_MODE_V30: WeatherUpdateCoordinator,
OWM_MODE_FREE_CURRENT: WeatherUpdateCoordinator,
OWM_MODE_FREE_FORECAST: WeatherUpdateCoordinator,
OWM_MODE_AIRPOLLUTION: AirPollutionUpdateCoordinator,
}
return coordinators[mode]

View File

@ -9,6 +9,8 @@ from homeassistant.components.sensor import (
SensorStateClass, SensorStateClass,
) )
from homeassistant.const import ( from homeassistant.const import (
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
CONCENTRATION_PARTS_PER_MILLION,
DEGREE, DEGREE,
PERCENTAGE, PERCENTAGE,
UV_INDEX, UV_INDEX,
@ -23,10 +25,17 @@ from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.typing import StateType from homeassistant.helpers.typing import StateType
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from . import OpenweathermapConfigEntry from . import OpenweathermapConfigEntry
from .const import ( from .const import (
ATTR_API_AIRPOLLUTION_AQI,
ATTR_API_AIRPOLLUTION_CO,
ATTR_API_AIRPOLLUTION_NO,
ATTR_API_AIRPOLLUTION_NO2,
ATTR_API_AIRPOLLUTION_O3,
ATTR_API_AIRPOLLUTION_PM2_5,
ATTR_API_AIRPOLLUTION_PM10,
ATTR_API_AIRPOLLUTION_SO2,
ATTR_API_CLOUDS, ATTR_API_CLOUDS,
ATTR_API_CONDITION, ATTR_API_CONDITION,
ATTR_API_CURRENT, ATTR_API_CURRENT,
@ -47,8 +56,10 @@ from .const import (
ATTRIBUTION, ATTRIBUTION,
DOMAIN, DOMAIN,
MANUFACTURER, MANUFACTURER,
OWM_MODE_AIRPOLLUTION,
OWM_MODE_FREE_FORECAST, OWM_MODE_FREE_FORECAST,
) )
from .coordinator import OWMUpdateCoordinator
WEATHER_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( WEATHER_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
SensorEntityDescription( SensorEntityDescription(
@ -151,6 +162,56 @@ WEATHER_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
), ),
) )
AIRPOLLUTION_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
SensorEntityDescription(
key=ATTR_API_AIRPOLLUTION_AQI,
device_class=SensorDeviceClass.AQI,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key=ATTR_API_AIRPOLLUTION_CO,
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
device_class=SensorDeviceClass.CO,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key=ATTR_API_AIRPOLLUTION_NO,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
device_class=SensorDeviceClass.NITROGEN_MONOXIDE,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key=ATTR_API_AIRPOLLUTION_NO2,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
device_class=SensorDeviceClass.NITROGEN_DIOXIDE,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key=ATTR_API_AIRPOLLUTION_O3,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
device_class=SensorDeviceClass.OZONE,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key=ATTR_API_AIRPOLLUTION_SO2,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
device_class=SensorDeviceClass.SULPHUR_DIOXIDE,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key=ATTR_API_AIRPOLLUTION_PM2_5,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
device_class=SensorDeviceClass.PM25,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key=ATTR_API_AIRPOLLUTION_PM10,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
device_class=SensorDeviceClass.PM10,
state_class=SensorStateClass.MEASUREMENT,
),
)
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
@ -162,7 +223,7 @@ async def async_setup_entry(
name = domain_data.name name = domain_data.name
unique_id = config_entry.unique_id unique_id = config_entry.unique_id
assert unique_id is not None assert unique_id is not None
weather_coordinator = domain_data.coordinator coordinator = domain_data.coordinator
if domain_data.mode == OWM_MODE_FREE_FORECAST: if domain_data.mode == OWM_MODE_FREE_FORECAST:
entity_registry = er.async_get(hass) entity_registry = er.async_get(hass)
@ -171,13 +232,23 @@ async def async_setup_entry(
) )
for entry in entries: for entry in entries:
entity_registry.async_remove(entry.entity_id) entity_registry.async_remove(entry.entity_id)
elif domain_data.mode == OWM_MODE_AIRPOLLUTION:
async_add_entities(
OpenWeatherMapSensor(
name,
unique_id,
description,
coordinator,
)
for description in AIRPOLLUTION_SENSOR_TYPES
)
else: else:
async_add_entities( async_add_entities(
OpenWeatherMapSensor( OpenWeatherMapSensor(
name, name,
unique_id, unique_id,
description, description,
weather_coordinator, coordinator,
) )
for description in WEATHER_SENSOR_TYPES for description in WEATHER_SENSOR_TYPES
) )
@ -195,7 +266,7 @@ class AbstractOpenWeatherMapSensor(SensorEntity):
name: str, name: str,
unique_id: str, unique_id: str,
description: SensorEntityDescription, description: SensorEntityDescription,
coordinator: DataUpdateCoordinator, coordinator: OWMUpdateCoordinator,
) -> None: ) -> None:
"""Initialize the sensor.""" """Initialize the sensor."""
self.entity_description = description self.entity_description = description

View File

@ -41,10 +41,11 @@ from .const import (
DEFAULT_NAME, DEFAULT_NAME,
DOMAIN, DOMAIN,
MANUFACTURER, MANUFACTURER,
OWM_MODE_AIRPOLLUTION,
OWM_MODE_FREE_FORECAST, OWM_MODE_FREE_FORECAST,
OWM_MODE_V30, OWM_MODE_V30,
) )
from .coordinator import WeatherUpdateCoordinator from .coordinator import OWMUpdateCoordinator
SERVICE_GET_MINUTE_FORECAST = "get_minute_forecast" SERVICE_GET_MINUTE_FORECAST = "get_minute_forecast"
@ -58,23 +59,25 @@ async def async_setup_entry(
domain_data = config_entry.runtime_data domain_data = config_entry.runtime_data
name = domain_data.name name = domain_data.name
mode = domain_data.mode mode = domain_data.mode
weather_coordinator = domain_data.coordinator
unique_id = f"{config_entry.unique_id}" if mode != OWM_MODE_AIRPOLLUTION:
owm_weather = OpenWeatherMapWeather(name, unique_id, mode, weather_coordinator) weather_coordinator = domain_data.coordinator
async_add_entities([owm_weather], False) unique_id = f"{config_entry.unique_id}"
owm_weather = OpenWeatherMapWeather(name, unique_id, mode, weather_coordinator)
platform = entity_platform.async_get_current_platform() async_add_entities([owm_weather], False)
platform.async_register_entity_service(
name=SERVICE_GET_MINUTE_FORECAST, platform = entity_platform.async_get_current_platform()
schema=None, platform.async_register_entity_service(
func="async_get_minute_forecast", name=SERVICE_GET_MINUTE_FORECAST,
supports_response=SupportsResponse.ONLY, schema=None,
) func="async_get_minute_forecast",
supports_response=SupportsResponse.ONLY,
)
class OpenWeatherMapWeather(SingleCoordinatorWeatherEntity[WeatherUpdateCoordinator]): class OpenWeatherMapWeather(SingleCoordinatorWeatherEntity[OWMUpdateCoordinator]):
"""Implementation of an OpenWeatherMap sensor.""" """Implementation of an OpenWeatherMap sensor."""
_attr_attribution = ATTRIBUTION _attr_attribution = ATTRIBUTION
@ -93,7 +96,7 @@ class OpenWeatherMapWeather(SingleCoordinatorWeatherEntity[WeatherUpdateCoordina
name: str, name: str,
unique_id: str, unique_id: str,
mode: str, mode: str,
weather_coordinator: WeatherUpdateCoordinator, weather_coordinator: OWMUpdateCoordinator,
) -> None: ) -> None:
"""Initialize the sensor.""" """Initialize the sensor."""
super().__init__(weather_coordinator) super().__init__(weather_coordinator)

View File

@ -5,6 +5,8 @@ from datetime import UTC, datetime
from unittest.mock import AsyncMock from unittest.mock import AsyncMock
from pyopenweathermap import ( from pyopenweathermap import (
AirPollutionReport,
CurrentAirPollution,
CurrentWeather, CurrentWeather,
DailyTemperature, DailyTemperature,
DailyWeatherForecast, DailyWeatherForecast,
@ -132,6 +134,21 @@ def owm_client_mock() -> Generator[AsyncMock]:
client.get_weather.return_value = WeatherReport( client.get_weather.return_value = WeatherReport(
current_weather, minutely_weather_forecast, [], [daily_weather_forecast] current_weather, minutely_weather_forecast, [], [daily_weather_forecast]
) )
current_air_pollution = CurrentAirPollution(
date_time=datetime.fromtimestamp(1714063537, tz=UTC),
aqi=3,
co=125.55,
no=0.11,
no2=0.78,
o3=101.98,
so2=0.59,
pm2_5=4.48,
pm10=4.77,
nh3=4.62,
)
client.get_air_pollution.return_value = AirPollutionReport(
current_air_pollution, []
)
client.validate_key.return_value = True client.validate_key.return_value = True
with ( with (
patch( patch(

View File

@ -1,4 +1,435 @@
# serializer version: 1 # serializer version: 1
# name: test_sensor_states[air_pollution][sensor.openweathermap_air_quality_index-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.openweathermap_air_quality_index',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <SensorDeviceClass.AQI: 'aqi'>,
'original_icon': None,
'original_name': 'Air quality index',
'platform': 'openweathermap',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': '12.34-56.78-aqi',
'unit_of_measurement': None,
})
# ---
# name: test_sensor_states[air_pollution][sensor.openweathermap_air_quality_index-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'attribution': 'Data provided by OpenWeatherMap',
'device_class': 'aqi',
'friendly_name': 'openweathermap Air quality index',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'context': <ANY>,
'entity_id': 'sensor.openweathermap_air_quality_index',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '3',
})
# ---
# name: test_sensor_states[air_pollution][sensor.openweathermap_carbon_monoxide-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.openweathermap_carbon_monoxide',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <SensorDeviceClass.CO: 'carbon_monoxide'>,
'original_icon': None,
'original_name': 'Carbon monoxide',
'platform': 'openweathermap',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': '12.34-56.78-co',
'unit_of_measurement': 'ppm',
})
# ---
# name: test_sensor_states[air_pollution][sensor.openweathermap_carbon_monoxide-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'attribution': 'Data provided by OpenWeatherMap',
'device_class': 'carbon_monoxide',
'friendly_name': 'openweathermap Carbon monoxide',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'ppm',
}),
'context': <ANY>,
'entity_id': 'sensor.openweathermap_carbon_monoxide',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '125.55',
})
# ---
# name: test_sensor_states[air_pollution][sensor.openweathermap_nitrogen_dioxide-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.openweathermap_nitrogen_dioxide',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <SensorDeviceClass.NITROGEN_DIOXIDE: 'nitrogen_dioxide'>,
'original_icon': None,
'original_name': 'Nitrogen dioxide',
'platform': 'openweathermap',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': '12.34-56.78-no2',
'unit_of_measurement': 'µg/m³',
})
# ---
# name: test_sensor_states[air_pollution][sensor.openweathermap_nitrogen_dioxide-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'attribution': 'Data provided by OpenWeatherMap',
'device_class': 'nitrogen_dioxide',
'friendly_name': 'openweathermap Nitrogen dioxide',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'µg/m³',
}),
'context': <ANY>,
'entity_id': 'sensor.openweathermap_nitrogen_dioxide',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '0.78',
})
# ---
# name: test_sensor_states[air_pollution][sensor.openweathermap_nitrogen_monoxide-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.openweathermap_nitrogen_monoxide',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <SensorDeviceClass.NITROGEN_MONOXIDE: 'nitrogen_monoxide'>,
'original_icon': None,
'original_name': 'Nitrogen monoxide',
'platform': 'openweathermap',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': '12.34-56.78-no',
'unit_of_measurement': 'µg/m³',
})
# ---
# name: test_sensor_states[air_pollution][sensor.openweathermap_nitrogen_monoxide-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'attribution': 'Data provided by OpenWeatherMap',
'device_class': 'nitrogen_monoxide',
'friendly_name': 'openweathermap Nitrogen monoxide',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'µg/m³',
}),
'context': <ANY>,
'entity_id': 'sensor.openweathermap_nitrogen_monoxide',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '0.11',
})
# ---
# name: test_sensor_states[air_pollution][sensor.openweathermap_ozone-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.openweathermap_ozone',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <SensorDeviceClass.OZONE: 'ozone'>,
'original_icon': None,
'original_name': 'Ozone',
'platform': 'openweathermap',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': '12.34-56.78-o3',
'unit_of_measurement': 'µg/m³',
})
# ---
# name: test_sensor_states[air_pollution][sensor.openweathermap_ozone-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'attribution': 'Data provided by OpenWeatherMap',
'device_class': 'ozone',
'friendly_name': 'openweathermap Ozone',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'µg/m³',
}),
'context': <ANY>,
'entity_id': 'sensor.openweathermap_ozone',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '101.98',
})
# ---
# name: test_sensor_states[air_pollution][sensor.openweathermap_pm10-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.openweathermap_pm10',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <SensorDeviceClass.PM10: 'pm10'>,
'original_icon': None,
'original_name': 'PM10',
'platform': 'openweathermap',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': '12.34-56.78-pm10',
'unit_of_measurement': 'µg/m³',
})
# ---
# name: test_sensor_states[air_pollution][sensor.openweathermap_pm10-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'attribution': 'Data provided by OpenWeatherMap',
'device_class': 'pm10',
'friendly_name': 'openweathermap PM10',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'µg/m³',
}),
'context': <ANY>,
'entity_id': 'sensor.openweathermap_pm10',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '4.77',
})
# ---
# name: test_sensor_states[air_pollution][sensor.openweathermap_pm2_5-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.openweathermap_pm2_5',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <SensorDeviceClass.PM25: 'pm25'>,
'original_icon': None,
'original_name': 'PM2.5',
'platform': 'openweathermap',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': '12.34-56.78-pm2_5',
'unit_of_measurement': 'µg/m³',
})
# ---
# name: test_sensor_states[air_pollution][sensor.openweathermap_pm2_5-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'attribution': 'Data provided by OpenWeatherMap',
'device_class': 'pm25',
'friendly_name': 'openweathermap PM2.5',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'µg/m³',
}),
'context': <ANY>,
'entity_id': 'sensor.openweathermap_pm2_5',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '4.48',
})
# ---
# name: test_sensor_states[air_pollution][sensor.openweathermap_sulphur_dioxide-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.openweathermap_sulphur_dioxide',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <SensorDeviceClass.SULPHUR_DIOXIDE: 'sulphur_dioxide'>,
'original_icon': None,
'original_name': 'Sulphur dioxide',
'platform': 'openweathermap',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': '12.34-56.78-so2',
'unit_of_measurement': 'µg/m³',
})
# ---
# name: test_sensor_states[air_pollution][sensor.openweathermap_sulphur_dioxide-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'attribution': 'Data provided by OpenWeatherMap',
'device_class': 'sulphur_dioxide',
'friendly_name': 'openweathermap Sulphur dioxide',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'µg/m³',
}),
'context': <ANY>,
'entity_id': 'sensor.openweathermap_sulphur_dioxide',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '0.59',
})
# ---
# name: test_sensor_states[current][sensor.openweathermap_cloud_coverage-entry] # name: test_sensor_states[current][sensor.openweathermap_cloud_coverage-entry]
EntityRegistryEntrySnapshot({ EntityRegistryEntrySnapshot({
'aliases': set({ 'aliases': set({

View File

@ -6,6 +6,7 @@ import pytest
from syrupy.assertion import SnapshotAssertion from syrupy.assertion import SnapshotAssertion
from homeassistant.components.openweathermap.const import ( from homeassistant.components.openweathermap.const import (
OWM_MODE_AIRPOLLUTION,
OWM_MODE_FREE_CURRENT, OWM_MODE_FREE_CURRENT,
OWM_MODE_FREE_FORECAST, OWM_MODE_FREE_FORECAST,
OWM_MODE_V30, OWM_MODE_V30,
@ -19,7 +20,9 @@ from . import setup_platform
from tests.common import MockConfigEntry, snapshot_platform from tests.common import MockConfigEntry, snapshot_platform
@pytest.mark.parametrize("mode", [OWM_MODE_V30, OWM_MODE_FREE_CURRENT], indirect=True) @pytest.mark.parametrize(
"mode", [OWM_MODE_V30, OWM_MODE_FREE_CURRENT, OWM_MODE_AIRPOLLUTION], indirect=True
)
async def test_sensor_states( async def test_sensor_states(
hass: HomeAssistant, hass: HomeAssistant,
snapshot: SnapshotAssertion, snapshot: SnapshotAssertion,