core/homeassistant/components/airvisual/sensor.py

274 lines
9.0 KiB
Python
Raw Normal View History

"""Support for AirVisual air quality sensors."""
from logging import getLogger
from datetime import timedelta
import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.const import (
2019-07-31 19:25:30 +00:00
ATTR_ATTRIBUTION,
ATTR_LATITUDE,
ATTR_LONGITUDE,
CONF_API_KEY,
CONF_LATITUDE,
CONF_LONGITUDE,
CONF_MONITORED_CONDITIONS,
CONF_SCAN_INTERVAL,
CONF_STATE,
CONF_SHOW_ON_MAP,
)
from homeassistant.helpers import aiohttp_client, config_validation as cv
from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle
2017-10-07 13:11:41 +00:00
_LOGGER = getLogger(__name__)
2019-07-31 19:25:30 +00:00
ATTR_CITY = "city"
ATTR_COUNTRY = "country"
ATTR_POLLUTANT_SYMBOL = "pollutant_symbol"
ATTR_POLLUTANT_UNIT = "pollutant_unit"
ATTR_REGION = "region"
2019-07-31 19:25:30 +00:00
CONF_CITY = "city"
CONF_COUNTRY = "country"
DEFAULT_ATTRIBUTION = "Data provided by AirVisual"
DEFAULT_SCAN_INTERVAL = timedelta(minutes=10)
2019-07-31 19:25:30 +00:00
MASS_PARTS_PER_MILLION = "ppm"
MASS_PARTS_PER_BILLION = "ppb"
VOLUME_MICROGRAMS_PER_CUBIC_METER = "µg/m3"
2019-07-31 19:25:30 +00:00
SENSOR_TYPE_LEVEL = "air_pollution_level"
SENSOR_TYPE_AQI = "air_quality_index"
SENSOR_TYPE_POLLUTANT = "main_pollutant"
SENSORS = [
2019-07-31 19:25:30 +00:00
(SENSOR_TYPE_LEVEL, "Air Pollution Level", "mdi:gauge", None),
(SENSOR_TYPE_AQI, "Air Quality Index", "mdi:chart-line", "AQI"),
(SENSOR_TYPE_POLLUTANT, "Main Pollutant", "mdi:chemical-weapon", None),
]
2019-07-31 19:25:30 +00:00
POLLUTANT_LEVEL_MAPPING = [
{"label": "Good", "icon": "mdi:emoticon-excited", "minimum": 0, "maximum": 50},
{"label": "Moderate", "icon": "mdi:emoticon-happy", "minimum": 51, "maximum": 100},
{
"label": "Unhealthy for sensitive groups",
"icon": "mdi:emoticon-neutral",
"minimum": 101,
"maximum": 150,
},
2019-07-31 19:25:30 +00:00
{"label": "Unhealthy", "icon": "mdi:emoticon-sad", "minimum": 151, "maximum": 200},
{
"label": "Very Unhealthy",
"icon": "mdi:emoticon-dead",
"minimum": 201,
"maximum": 300,
},
2019-07-31 19:25:30 +00:00
{"label": "Hazardous", "icon": "mdi:biohazard", "minimum": 301, "maximum": 10000},
]
POLLUTANT_MAPPING = {
"co": {"label": "Carbon Monoxide", "unit": MASS_PARTS_PER_MILLION},
"n2": {"label": "Nitrogen Dioxide", "unit": MASS_PARTS_PER_BILLION},
"o3": {"label": "Ozone", "unit": MASS_PARTS_PER_BILLION},
"p1": {"label": "PM10", "unit": VOLUME_MICROGRAMS_PER_CUBIC_METER},
"p2": {"label": "PM2.5", "unit": VOLUME_MICROGRAMS_PER_CUBIC_METER},
"s2": {"label": "Sulfur Dioxide", "unit": MASS_PARTS_PER_BILLION},
}
2019-07-31 19:25:30 +00:00
SENSOR_LOCALES = {"cn": "Chinese", "us": "U.S."}
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
vol.Required(CONF_API_KEY): cv.string,
vol.Required(CONF_MONITORED_CONDITIONS, default=list(SENSOR_LOCALES)): vol.All(
cv.ensure_list, [vol.In(SENSOR_LOCALES)]
),
vol.Inclusive(CONF_CITY, "city"): cv.string,
vol.Inclusive(CONF_COUNTRY, "city"): cv.string,
vol.Inclusive(CONF_LATITUDE, "coords"): cv.latitude,
vol.Inclusive(CONF_LONGITUDE, "coords"): cv.longitude,
vol.Optional(CONF_SHOW_ON_MAP, default=True): cv.boolean,
vol.Inclusive(CONF_STATE, "city"): cv.string,
vol.Optional(CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL): cv.time_period,
}
)
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Configure the platform and add the sensors."""
from pyairvisual import Client
city = config.get(CONF_CITY)
state = config.get(CONF_STATE)
country = config.get(CONF_COUNTRY)
latitude = config.get(CONF_LATITUDE, hass.config.latitude)
longitude = config.get(CONF_LONGITUDE, hass.config.longitude)
websession = aiohttp_client.async_get_clientsession(hass)
if city and state and country:
2017-10-07 13:11:41 +00:00
_LOGGER.debug(
2019-07-31 19:25:30 +00:00
"Using city, state, and country: %s, %s, %s", city, state, country
)
location_id = ",".join((city, state, country))
data = AirVisualData(
2019-02-28 05:35:14 +00:00
Client(websession, api_key=config[CONF_API_KEY]),
city=city,
state=state,
country=country,
show_on_map=config[CONF_SHOW_ON_MAP],
2019-07-31 19:25:30 +00:00
scan_interval=config[CONF_SCAN_INTERVAL],
)
else:
2019-07-31 19:25:30 +00:00
_LOGGER.debug("Using latitude and longitude: %s, %s", latitude, longitude)
location_id = ",".join((str(latitude), str(longitude)))
data = AirVisualData(
2019-02-28 05:35:14 +00:00
Client(websession, api_key=config[CONF_API_KEY]),
latitude=latitude,
longitude=longitude,
show_on_map=config[CONF_SHOW_ON_MAP],
2019-07-31 19:25:30 +00:00
scan_interval=config[CONF_SCAN_INTERVAL],
)
await data.async_update()
sensors = []
for locale in config[CONF_MONITORED_CONDITIONS]:
for kind, name, icon, unit in SENSORS:
sensors.append(
2019-07-31 19:25:30 +00:00
AirVisualSensor(data, kind, name, icon, unit, locale, location_id)
)
async_add_entities(sensors, True)
class AirVisualSensor(Entity):
"""Define an AirVisual sensor."""
def __init__(self, airvisual, kind, name, icon, unit, locale, location_id):
"""Initialize."""
self._attrs = {ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION}
self._icon = icon
self._locale = locale
self._location_id = location_id
self._name = name
self._state = None
self._type = kind
self._unit = unit
self.airvisual = airvisual
@property
def device_state_attributes(self):
2017-10-07 13:11:41 +00:00
"""Return the device state attributes."""
if self.airvisual.show_on_map:
self._attrs[ATTR_LATITUDE] = self.airvisual.latitude
self._attrs[ATTR_LONGITUDE] = self.airvisual.longitude
else:
2019-07-31 19:25:30 +00:00
self._attrs["lati"] = self.airvisual.latitude
self._attrs["long"] = self.airvisual.longitude
return self._attrs
@property
def available(self):
"""Return True if entity is available."""
return bool(self.airvisual.pollution_info)
@property
def icon(self):
"""Return the icon."""
return self._icon
@property
def name(self):
"""Return the name."""
2019-07-31 19:25:30 +00:00
return "{0} {1}".format(SENSOR_LOCALES[self._locale], self._name)
@property
def state(self):
"""Return the state."""
return self._state
@property
def unique_id(self):
"""Return a unique, HASS-friendly identifier for this entity."""
return f"{self._location_id}_{self._locale}_{self._type}"
@property
def unit_of_measurement(self):
"""Return the unit the value is expressed in."""
return self._unit
async def async_update(self):
"""Update the sensor."""
await self.airvisual.async_update()
data = self.airvisual.pollution_info
if not data:
return
if self._type == SENSOR_TYPE_LEVEL:
aqi = data[f"aqi{self._locale}"]
[level] = [
2019-07-31 19:25:30 +00:00
i
for i in POLLUTANT_LEVEL_MAPPING
if i["minimum"] <= aqi <= i["maximum"]
]
2019-07-31 19:25:30 +00:00
self._state = level["label"]
self._icon = level["icon"]
elif self._type == SENSOR_TYPE_AQI:
self._state = data[f"aqi{self._locale}"]
elif self._type == SENSOR_TYPE_POLLUTANT:
symbol = data[f"main{self._locale}"]
2019-07-31 19:25:30 +00:00
self._state = POLLUTANT_MAPPING[symbol]["label"]
self._attrs.update(
{
ATTR_POLLUTANT_SYMBOL: symbol,
ATTR_POLLUTANT_UNIT: POLLUTANT_MAPPING[symbol]["unit"],
}
)
class AirVisualData:
"""Define an object to hold sensor data."""
def __init__(self, client, **kwargs):
"""Initialize."""
self._client = client
self.city = kwargs.get(CONF_CITY)
self.country = kwargs.get(CONF_COUNTRY)
self.latitude = kwargs.get(CONF_LATITUDE)
self.longitude = kwargs.get(CONF_LONGITUDE)
self.pollution_info = {}
self.show_on_map = kwargs.get(CONF_SHOW_ON_MAP)
self.state = kwargs.get(CONF_STATE)
2019-07-31 19:25:30 +00:00
self.async_update = Throttle(kwargs[CONF_SCAN_INTERVAL])(self._async_update)
async def _async_update(self):
"""Update AirVisual data."""
from pyairvisual.errors import AirVisualError
try:
if self.city and self.state and self.country:
2019-07-31 19:25:30 +00:00
resp = await self._client.api.city(self.city, self.state, self.country)
self.longitude, self.latitude = resp["location"]["coordinates"]
else:
resp = await self._client.api.nearest_city(
2019-07-31 19:25:30 +00:00
self.latitude, self.longitude
)
2017-10-07 13:11:41 +00:00
_LOGGER.debug("New data retrieved: %s", resp)
2019-07-31 19:25:30 +00:00
self.pollution_info = resp["current"]["pollution"]
except (KeyError, AirVisualError) as err:
if self.city and self.state and self.country:
location = (self.city, self.state, self.country)
else:
location = (self.latitude, self.longitude)
2019-07-31 19:25:30 +00:00
_LOGGER.error("Can't retrieve data for location: %s (%s)", location, err)
self.pollution_info = {}