Adds the AirVisual air quality sensor platform (#9320)
* Adds the AirVisual air quality sensor platform * Updated .coveragerc * Removed some un-needed code * Adding strangely-necessary pylint disable * Removing a Python3.5-specific dict combiner method * Restarting stuck coverage test * Added units to AQI sensor (to get nice graph) * Making collaborator-requested changes * Removing unnecessary parameter from data object
This commit is contained in:
parent
d2d876945b
commit
c539b5c12b
3 changed files with 293 additions and 0 deletions
|
@ -427,6 +427,7 @@ omit =
|
|||
homeassistant/components/remote/itach.py
|
||||
homeassistant/components/scene/hunterdouglas_powerview.py
|
||||
homeassistant/components/scene/lifx_cloud.py
|
||||
homeassistant/components/sensor/airvisual.py
|
||||
homeassistant/components/sensor/arest.py
|
||||
homeassistant/components/sensor/arwn.py
|
||||
homeassistant/components/sensor/bbox.py
|
||||
|
|
289
homeassistant/components/sensor/airvisual.py
Normal file
289
homeassistant/components/sensor/airvisual.py
Normal file
|
@ -0,0 +1,289 @@
|
|||
"""
|
||||
Support for AirVisual air quality sensors.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/sensor.airvisual/
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
from logging import getLogger
|
||||
from datetime import timedelta
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.sensor import PLATFORM_SCHEMA
|
||||
from homeassistant.const import (ATTR_ATTRIBUTION, ATTR_STATE, CONF_API_KEY,
|
||||
CONF_LATITUDE, CONF_LONGITUDE,
|
||||
CONF_MONITORED_CONDITIONS)
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
_LOGGER = getLogger(__name__)
|
||||
REQUIREMENTS = ['pyairvisual==0.1.0']
|
||||
|
||||
ATTR_CITY = 'city'
|
||||
ATTR_COUNTRY = 'country'
|
||||
ATTR_POLLUTANT_SYMBOL = 'pollutant_symbol'
|
||||
ATTR_POLLUTANT_UNIT = 'pollutant_unit'
|
||||
ATTR_TIMESTAMP = 'timestamp'
|
||||
|
||||
CONF_RADIUS = 'radius'
|
||||
|
||||
MASS_PARTS_PER_MILLION = 'ppm'
|
||||
MASS_PARTS_PER_BILLION = 'ppb'
|
||||
VOLUME_MICROGRAMS_PER_CUBIC_METER = 'µg/m3'
|
||||
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=10)
|
||||
|
||||
POLLUTANT_LEVEL_MAPPING = [{
|
||||
'label': 'Good',
|
||||
'minimum': 0,
|
||||
'maximum': 50
|
||||
}, {
|
||||
'label': 'Moderate',
|
||||
'minimum': 51,
|
||||
'maximum': 100
|
||||
}, {
|
||||
'label': 'Unhealthy for Sensitive Groups',
|
||||
'minimum': 101,
|
||||
'maximum': 150
|
||||
}, {
|
||||
'label': 'Unhealthy',
|
||||
'minimum': 151,
|
||||
'maximum': 200
|
||||
}, {
|
||||
'label': 'Very Unhealthy',
|
||||
'minimum': 201,
|
||||
'maximum': 300
|
||||
}, {
|
||||
'label': 'Hazardous',
|
||||
'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
|
||||
}
|
||||
}
|
||||
|
||||
SENSOR_LOCALES = {'cn': 'Chinese', 'us': 'U.S.'}
|
||||
SENSOR_TYPES = [
|
||||
('AirPollutionLevelSensor', 'Air Pollution Level', 'mdi:scale'),
|
||||
('AirQualityIndexSensor', 'Air Quality Index', 'mdi:format-list-numbers'),
|
||||
('MainPollutantSensor', 'Main Pollutant', 'mdi:chemical-weapon'),
|
||||
]
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_API_KEY):
|
||||
cv.string,
|
||||
vol.Required(CONF_MONITORED_CONDITIONS):
|
||||
vol.All(cv.ensure_list, [vol.In(SENSOR_LOCALES)]),
|
||||
vol.Optional(CONF_LATITUDE):
|
||||
cv.latitude,
|
||||
vol.Optional(CONF_LONGITUDE):
|
||||
cv.longitude,
|
||||
vol.Optional(CONF_RADIUS, default=1000):
|
||||
cv.positive_int,
|
||||
})
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Configure the platform and add the sensors."""
|
||||
import pyairvisual as pav
|
||||
|
||||
api_key = config.get(CONF_API_KEY)
|
||||
_LOGGER.debug('AirVisual API Key: %s', api_key)
|
||||
|
||||
monitored_locales = config.get(CONF_MONITORED_CONDITIONS)
|
||||
_LOGGER.debug('Monitored Conditions: %s', monitored_locales)
|
||||
|
||||
latitude = config.get(CONF_LATITUDE, hass.config.latitude)
|
||||
_LOGGER.debug('AirVisual Latitude: %s', latitude)
|
||||
|
||||
longitude = config.get(CONF_LONGITUDE, hass.config.longitude)
|
||||
_LOGGER.debug('AirVisual Longitude: %s', longitude)
|
||||
|
||||
radius = config.get(CONF_RADIUS)
|
||||
_LOGGER.debug('AirVisual Radius: %s', radius)
|
||||
|
||||
data = AirVisualData(pav.Client(api_key), latitude, longitude, radius)
|
||||
|
||||
sensors = []
|
||||
for locale in monitored_locales:
|
||||
for sensor_class, name, icon in SENSOR_TYPES:
|
||||
sensors.append(globals()[sensor_class](data, name, icon, locale))
|
||||
|
||||
async_add_devices(sensors, True)
|
||||
|
||||
|
||||
def merge_two_dicts(dict1, dict2):
|
||||
"""Merge two dicts into a new dict as a shallow copy."""
|
||||
final = dict1.copy()
|
||||
final.update(dict2)
|
||||
return final
|
||||
|
||||
|
||||
class AirVisualBaseSensor(Entity):
|
||||
"""Define a base class for all of our sensors."""
|
||||
|
||||
def __init__(self, data, name, icon, locale):
|
||||
"""Initialize."""
|
||||
self._data = data
|
||||
self._icon = icon
|
||||
self._locale = locale
|
||||
self._name = name
|
||||
self._state = None
|
||||
self._unit = None
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
if self._data:
|
||||
return {
|
||||
ATTR_ATTRIBUTION: 'AirVisual©',
|
||||
ATTR_CITY: self._data.city,
|
||||
ATTR_COUNTRY: self._data.country,
|
||||
ATTR_STATE: self._data.state,
|
||||
ATTR_TIMESTAMP: self._data.pollution_info.get('ts')
|
||||
}
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
"""Return the icon."""
|
||||
return self._icon
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name."""
|
||||
return '{0} {1}'.format(SENSOR_LOCALES[self._locale], self._name)
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state."""
|
||||
return self._state
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_update(self):
|
||||
"""Update the status of the sensor."""
|
||||
_LOGGER.debug('updating sensor: %s', self._name)
|
||||
self._data.update()
|
||||
|
||||
|
||||
class AirPollutionLevelSensor(AirVisualBaseSensor):
|
||||
"""Define a sensor to measure air pollution level."""
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_update(self):
|
||||
"""Update the status of the sensor."""
|
||||
yield from super().async_update()
|
||||
aqi = self._data.pollution_info.get('aqi{0}'.format(self._locale))
|
||||
|
||||
try:
|
||||
[level] = [
|
||||
i for i in POLLUTANT_LEVEL_MAPPING
|
||||
if i['minimum'] <= aqi <= i['maximum']
|
||||
]
|
||||
self._state = level.get('label')
|
||||
except ValueError:
|
||||
self._state = None
|
||||
|
||||
|
||||
class AirQualityIndexSensor(AirVisualBaseSensor):
|
||||
"""Define a sensor to measure AQI."""
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
"""Return the unit the value is expressed in."""
|
||||
return ''
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_update(self):
|
||||
"""Update the status of the sensor."""
|
||||
yield from super().async_update()
|
||||
self._state = self._data.pollution_info.get(
|
||||
'aqi{0}'.format(self._locale))
|
||||
|
||||
|
||||
class MainPollutantSensor(AirVisualBaseSensor):
|
||||
"""Define a sensor to the main pollutant of an area."""
|
||||
|
||||
def __init__(self, data, name, icon, locale):
|
||||
"""Initialize."""
|
||||
super().__init__(data, name, icon, locale)
|
||||
self._symbol = None
|
||||
self._unit = None
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
if self._data:
|
||||
return merge_two_dicts(super().device_state_attributes, {
|
||||
ATTR_POLLUTANT_SYMBOL: self._symbol,
|
||||
ATTR_POLLUTANT_UNIT: self._unit
|
||||
})
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_update(self):
|
||||
"""Update the status of the sensor."""
|
||||
yield from super().async_update()
|
||||
symbol = self._data.pollution_info.get('main{0}'.format(self._locale))
|
||||
pollution_info = POLLUTANT_MAPPING.get(symbol, {})
|
||||
self._state = pollution_info.get('label')
|
||||
self._unit = pollution_info.get('unit')
|
||||
self._symbol = symbol
|
||||
|
||||
|
||||
class AirVisualData(object):
|
||||
"""Define an object to hold sensor data."""
|
||||
|
||||
def __init__(self, client, latitude, longitude, radius):
|
||||
"""Initialize."""
|
||||
self.city = None
|
||||
self._client = client
|
||||
self.country = None
|
||||
self.latitude = latitude
|
||||
self.longitude = longitude
|
||||
self.pollution_info = None
|
||||
self.radius = radius
|
||||
self.state = None
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def update(self):
|
||||
"""Update with new AirVisual data."""
|
||||
import pyairvisual.exceptions as exceptions
|
||||
|
||||
try:
|
||||
resp = self._client.nearest_city(self.latitude, self.longitude,
|
||||
self.radius).get('data')
|
||||
_LOGGER.debug('New data retrieved: %s', resp)
|
||||
|
||||
self.city = resp.get('city')
|
||||
self.state = resp.get('state')
|
||||
self.country = resp.get('country')
|
||||
self.pollution_info = resp.get('current').get('pollution')
|
||||
except exceptions.HTTPError as exc_info:
|
||||
_LOGGER.error('Unable to update sensor data')
|
||||
_LOGGER.debug(exc_info)
|
|
@ -538,6 +538,9 @@ pyRFXtrx==0.20.1
|
|||
# homeassistant.components.switch.dlink
|
||||
pyW215==0.6.0
|
||||
|
||||
# homeassistant.components.sensor.airvisual
|
||||
pyairvisual==0.1.0
|
||||
|
||||
# homeassistant.components.alarm_control_panel.alarmdotcom
|
||||
pyalarmdotcom==0.3.0
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue