core/homeassistant/components/tomorrowio/sensor.py

366 lines
12 KiB
Python

"""Sensor component that handles additional Tomorrowio data for your location."""
from __future__ import annotations
from abc import abstractmethod
from collections.abc import Callable
from dataclasses import dataclass
from typing import Any
from pytomorrowio.const import (
HealthConcernType,
PollenIndex,
PrecipitationType,
PrimaryPollutantType,
)
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorEntityDescription,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
ATTR_ATTRIBUTION,
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
CONCENTRATION_PARTS_PER_MILLION,
CONF_NAME,
IRRADIATION_BTUS_PER_HOUR_SQUARE_FOOT,
IRRADIATION_WATTS_PER_SQUARE_METER,
LENGTH_KILOMETERS,
LENGTH_METERS,
LENGTH_MILES,
PERCENTAGE,
PRESSURE_HPA,
PRESSURE_INHG,
SPEED_METERS_PER_SECOND,
SPEED_MILES_PER_HOUR,
TEMP_CELSIUS,
TEMP_FAHRENHEIT,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.util import slugify
from homeassistant.util.distance import convert as distance_convert
from homeassistant.util.pressure import convert as pressure_convert
from homeassistant.util.temperature import convert as temp_convert
from . import TomorrowioDataUpdateCoordinator, TomorrowioEntity
from .const import (
DOMAIN,
TMRW_ATTR_CARBON_MONOXIDE,
TMRW_ATTR_CHINA_AQI,
TMRW_ATTR_CHINA_HEALTH_CONCERN,
TMRW_ATTR_CHINA_PRIMARY_POLLUTANT,
TMRW_ATTR_CLOUD_BASE,
TMRW_ATTR_CLOUD_CEILING,
TMRW_ATTR_CLOUD_COVER,
TMRW_ATTR_DEW_POINT,
TMRW_ATTR_EPA_AQI,
TMRW_ATTR_EPA_HEALTH_CONCERN,
TMRW_ATTR_EPA_PRIMARY_POLLUTANT,
TMRW_ATTR_FEELS_LIKE,
TMRW_ATTR_FIRE_INDEX,
TMRW_ATTR_NITROGEN_DIOXIDE,
TMRW_ATTR_OZONE,
TMRW_ATTR_PARTICULATE_MATTER_10,
TMRW_ATTR_PARTICULATE_MATTER_25,
TMRW_ATTR_POLLEN_GRASS,
TMRW_ATTR_POLLEN_TREE,
TMRW_ATTR_POLLEN_WEED,
TMRW_ATTR_PRECIPITATION_TYPE,
TMRW_ATTR_PRESSURE_SURFACE_LEVEL,
TMRW_ATTR_SOLAR_GHI,
TMRW_ATTR_SULPHUR_DIOXIDE,
TMRW_ATTR_WIND_GUST,
)
@dataclass
class TomorrowioSensorEntityDescription(SensorEntityDescription):
"""Describes a Tomorrow.io sensor entity."""
unit_imperial: str | None = None
unit_metric: str | None = None
metric_conversion: Callable[[float], float] | float = 1.0
is_metric_check: bool | None = None
value_map: Any | None = None
SENSOR_TYPES = (
TomorrowioSensorEntityDescription(
key=TMRW_ATTR_FEELS_LIKE,
name="Feels Like",
unit_imperial=TEMP_FAHRENHEIT,
unit_metric=TEMP_CELSIUS,
metric_conversion=lambda val: temp_convert(val, TEMP_FAHRENHEIT, TEMP_CELSIUS),
is_metric_check=True,
device_class=SensorDeviceClass.TEMPERATURE,
),
TomorrowioSensorEntityDescription(
key=TMRW_ATTR_DEW_POINT,
name="Dew Point",
unit_imperial=TEMP_FAHRENHEIT,
unit_metric=TEMP_CELSIUS,
metric_conversion=lambda val: temp_convert(val, TEMP_FAHRENHEIT, TEMP_CELSIUS),
is_metric_check=True,
device_class=SensorDeviceClass.TEMPERATURE,
),
TomorrowioSensorEntityDescription(
key=TMRW_ATTR_PRESSURE_SURFACE_LEVEL,
name="Pressure (Surface Level)",
unit_metric=PRESSURE_HPA,
metric_conversion=lambda val: pressure_convert(
val, PRESSURE_INHG, PRESSURE_HPA
),
is_metric_check=True,
device_class=SensorDeviceClass.PRESSURE,
),
TomorrowioSensorEntityDescription(
key=TMRW_ATTR_SOLAR_GHI,
name="Global Horizontal Irradiance",
unit_imperial=IRRADIATION_BTUS_PER_HOUR_SQUARE_FOOT,
unit_metric=IRRADIATION_WATTS_PER_SQUARE_METER,
metric_conversion=3.15459,
is_metric_check=True,
),
TomorrowioSensorEntityDescription(
key=TMRW_ATTR_CLOUD_BASE,
name="Cloud Base",
unit_imperial=LENGTH_MILES,
unit_metric=LENGTH_KILOMETERS,
metric_conversion=lambda val: distance_convert(
val, LENGTH_MILES, LENGTH_KILOMETERS
),
is_metric_check=True,
),
TomorrowioSensorEntityDescription(
key=TMRW_ATTR_CLOUD_CEILING,
name="Cloud Ceiling",
unit_imperial=LENGTH_MILES,
unit_metric=LENGTH_KILOMETERS,
metric_conversion=lambda val: distance_convert(
val, LENGTH_MILES, LENGTH_KILOMETERS
),
is_metric_check=True,
),
TomorrowioSensorEntityDescription(
key=TMRW_ATTR_CLOUD_COVER,
name="Cloud Cover",
unit_imperial=PERCENTAGE,
unit_metric=PERCENTAGE,
),
TomorrowioSensorEntityDescription(
key=TMRW_ATTR_WIND_GUST,
name="Wind Gust",
unit_imperial=SPEED_MILES_PER_HOUR,
unit_metric=SPEED_METERS_PER_SECOND,
metric_conversion=lambda val: distance_convert(val, LENGTH_MILES, LENGTH_METERS)
/ 3600,
is_metric_check=True,
),
TomorrowioSensorEntityDescription(
key=TMRW_ATTR_PRECIPITATION_TYPE,
name="Precipitation Type",
value_map=PrecipitationType,
device_class="tomorrowio__precipitation_type",
icon="mdi:weather-snowy-rainy",
),
TomorrowioSensorEntityDescription(
key=TMRW_ATTR_OZONE,
name="Ozone",
unit_metric=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
metric_conversion=2.03,
is_metric_check=True,
device_class=SensorDeviceClass.OZONE,
),
TomorrowioSensorEntityDescription(
key=TMRW_ATTR_PARTICULATE_MATTER_25,
name="Particulate Matter < 2.5 μm",
unit_metric=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
metric_conversion=3.2808399**3,
is_metric_check=True,
device_class=SensorDeviceClass.PM25,
),
TomorrowioSensorEntityDescription(
key=TMRW_ATTR_PARTICULATE_MATTER_10,
name="Particulate Matter < 10 μm",
unit_metric=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
metric_conversion=3.2808399**3,
is_metric_check=True,
device_class=SensorDeviceClass.PM10,
),
TomorrowioSensorEntityDescription(
key=TMRW_ATTR_NITROGEN_DIOXIDE,
name="Nitrogen Dioxide",
unit_metric=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
metric_conversion=1.95,
is_metric_check=True,
device_class=SensorDeviceClass.NITROGEN_DIOXIDE,
),
TomorrowioSensorEntityDescription(
key=TMRW_ATTR_CARBON_MONOXIDE,
name="Carbon Monoxide",
unit_imperial=CONCENTRATION_PARTS_PER_MILLION,
unit_metric=CONCENTRATION_PARTS_PER_MILLION,
device_class=SensorDeviceClass.CO,
),
TomorrowioSensorEntityDescription(
key=TMRW_ATTR_SULPHUR_DIOXIDE,
name="Sulphur Dioxide",
unit_metric=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
metric_conversion=2.71,
is_metric_check=True,
device_class=SensorDeviceClass.SULPHUR_DIOXIDE,
),
TomorrowioSensorEntityDescription(
key=TMRW_ATTR_EPA_AQI,
name="US EPA Air Quality Index",
device_class=SensorDeviceClass.AQI,
),
TomorrowioSensorEntityDescription(
key=TMRW_ATTR_EPA_PRIMARY_POLLUTANT,
name="US EPA Primary Pollutant",
value_map=PrimaryPollutantType,
),
TomorrowioSensorEntityDescription(
key=TMRW_ATTR_EPA_HEALTH_CONCERN,
name="US EPA Health Concern",
value_map=HealthConcernType,
device_class="tomorrowio__health_concern",
icon="mdi:hospital",
),
TomorrowioSensorEntityDescription(
key=TMRW_ATTR_CHINA_AQI,
name="China MEP Air Quality Index",
device_class=SensorDeviceClass.AQI,
),
TomorrowioSensorEntityDescription(
key=TMRW_ATTR_CHINA_PRIMARY_POLLUTANT,
name="China MEP Primary Pollutant",
value_map=PrimaryPollutantType,
),
TomorrowioSensorEntityDescription(
key=TMRW_ATTR_CHINA_HEALTH_CONCERN,
name="China MEP Health Concern",
value_map=HealthConcernType,
device_class="tomorrowio__health_concern",
icon="mdi:hospital",
),
TomorrowioSensorEntityDescription(
key=TMRW_ATTR_POLLEN_TREE,
name="Tree Pollen Index",
value_map=PollenIndex,
device_class="tomorrowio__pollen_index",
icon="mdi:flower-pollen",
),
TomorrowioSensorEntityDescription(
key=TMRW_ATTR_POLLEN_WEED,
name="Weed Pollen Index",
value_map=PollenIndex,
device_class="tomorrowio__pollen_index",
icon="mdi:flower-pollen",
),
TomorrowioSensorEntityDescription(
key=TMRW_ATTR_POLLEN_GRASS,
name="Grass Pollen Index",
value_map=PollenIndex,
device_class="tomorrowio__pollen_index",
icon="mdi:flower-pollen",
),
TomorrowioSensorEntityDescription(
TMRW_ATTR_FIRE_INDEX,
name="Fire Index",
icon="mdi:fire",
),
)
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up a config entry."""
coordinator = hass.data[DOMAIN][config_entry.entry_id]
entities = [
TomorrowioSensorEntity(hass, config_entry, coordinator, 4, description)
for description in SENSOR_TYPES
]
async_add_entities(entities)
class BaseTomorrowioSensorEntity(TomorrowioEntity, SensorEntity):
"""Base Tomorrow.io sensor entity."""
entity_description: TomorrowioSensorEntityDescription
_attr_entity_registry_enabled_default = False
def __init__(
self,
hass: HomeAssistant,
config_entry: ConfigEntry,
coordinator: TomorrowioDataUpdateCoordinator,
api_version: int,
description: TomorrowioSensorEntityDescription,
) -> None:
"""Initialize Tomorrow.io Sensor Entity."""
super().__init__(config_entry, coordinator, api_version)
self.entity_description = description
self._attr_name = f"{self._config_entry.data[CONF_NAME]} - {description.name}"
self._attr_unique_id = (
f"{self._config_entry.unique_id}_{slugify(description.name)}"
)
self._attr_extra_state_attributes = {ATTR_ATTRIBUTION: self.attribution}
# Fallback to metric always in case imperial isn't defined (for metric only
# sensors)
self._attr_native_unit_of_measurement = (
description.unit_metric
if hass.config.units.is_metric
else description.unit_imperial
) or description.unit_metric
@property
@abstractmethod
def _state(self) -> str | int | float | None:
"""Return the raw state."""
@property
def native_value(self) -> str | int | float | None:
"""Return the state."""
state = self._state
# If an imperial unit isn't provided, we always want to convert to metric since
# that is what the UI expects
if state is not None and (
(
self.entity_description.metric_conversion != 1.0
and self.entity_description.is_metric_check is not None
and self.hass.config.units.is_metric
== self.entity_description.is_metric_check
)
or (
self.entity_description.unit_imperial is None
and self.entity_description.unit_metric is not None
)
):
conversion = self.entity_description.metric_conversion
# When conversion is a callable, we assume it's a single input function
if callable(conversion):
return round(conversion(float(state)), 2)
return round(float(state) * conversion, 2)
if self.entity_description.value_map is not None and state is not None:
return self.entity_description.value_map(state).name.lower()
return state
class TomorrowioSensorEntity(BaseTomorrowioSensorEntity):
"""Sensor entity that talks to Tomorrow.io v4 API to retrieve non-weather data."""
@property
def _state(self) -> str | int | float | None:
"""Return the raw state."""
return self._get_current_property(self.entity_description.key)