Adds allergy/disease sensor platform from Pollen.com (#11573)
* Base platform in place * Logic in place * Requirements and coverage * Fixed some linting issues * Small attribute reorganization * Collaborator-requested changes round 1 * Updated documentationpull/11931/merge
parent
bfe259f7a0
commit
5af7666a61
|
@ -593,6 +593,7 @@ omit =
|
|||
homeassistant/components/sensor/pi_hole.py
|
||||
homeassistant/components/sensor/plex.py
|
||||
homeassistant/components/sensor/pocketcasts.py
|
||||
homeassistant/components/sensor/pollen.py
|
||||
homeassistant/components/sensor/pushbullet.py
|
||||
homeassistant/components/sensor/pvoutput.py
|
||||
homeassistant/components/sensor/pyload.py
|
||||
|
|
|
@ -61,6 +61,7 @@ homeassistant/components/sensor/airvisual.py @bachya
|
|||
homeassistant/components/sensor/gearbest.py @HerrHofrat
|
||||
homeassistant/components/sensor/irish_rail_transport.py @ttroy50
|
||||
homeassistant/components/sensor/miflora.py @danielhiversen @ChristianKuehnel
|
||||
homeassistant/components/sensor/pollen.py @bachya
|
||||
homeassistant/components/sensor/sytadin.py @gautric
|
||||
homeassistant/components/sensor/tibber.py @danielhiversen
|
||||
homeassistant/components/sensor/waqi.py @andrey-git
|
||||
|
|
|
@ -0,0 +1,322 @@
|
|||
"""
|
||||
Support for Pollen.com allergen and cold/flu sensors.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/sensor.pollen/
|
||||
"""
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
from statistics import mean
|
||||
|
||||
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_MONITORED_CONDITIONS
|
||||
)
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
REQUIREMENTS = ['pypollencom==1.1.1']
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTR_ALLERGEN_GENUS = 'primary_allergen_genus'
|
||||
ATTR_ALLERGEN_NAME = 'primary_allergen_name'
|
||||
ATTR_ALLERGEN_TYPE = 'primary_allergen_type'
|
||||
ATTR_CITY = 'city'
|
||||
ATTR_OUTLOOK = 'outlook'
|
||||
ATTR_RATING = 'rating'
|
||||
ATTR_SEASON = 'season'
|
||||
ATTR_TREND = 'trend'
|
||||
ATTR_ZIP_CODE = 'zip_code'
|
||||
|
||||
CONF_ZIP_CODE = 'zip_code'
|
||||
|
||||
DEFAULT_ATTRIBUTION = 'Data provided by IQVIA™'
|
||||
|
||||
MIN_TIME_UPDATE_AVERAGES = timedelta(hours=12)
|
||||
MIN_TIME_UPDATE_INDICES = timedelta(minutes=10)
|
||||
|
||||
CONDITIONS = {
|
||||
'allergy_average_forecasted': (
|
||||
'Allergy Index: Forecasted Average',
|
||||
'AllergyAverageSensor',
|
||||
'allergy_average_data',
|
||||
{'data_attr': 'extended_data'},
|
||||
'mdi:flower'
|
||||
),
|
||||
'allergy_average_historical': (
|
||||
'Allergy Index: Historical Average',
|
||||
'AllergyAverageSensor',
|
||||
'allergy_average_data',
|
||||
{'data_attr': 'historic_data'},
|
||||
'mdi:flower'
|
||||
),
|
||||
'allergy_index_today': (
|
||||
'Allergy Index: Today',
|
||||
'AllergyIndexSensor',
|
||||
'allergy_index_data',
|
||||
{'key': 'Today'},
|
||||
'mdi:flower'
|
||||
),
|
||||
'allergy_index_tomorrow': (
|
||||
'Allergy Index: Tomorrow',
|
||||
'AllergyIndexSensor',
|
||||
'allergy_index_data',
|
||||
{'key': 'Tomorrow'},
|
||||
'mdi:flower'
|
||||
),
|
||||
'allergy_index_yesterday': (
|
||||
'Allergy Index: Yesterday',
|
||||
'AllergyIndexSensor',
|
||||
'allergy_index_data',
|
||||
{'key': 'Yesterday'},
|
||||
'mdi:flower'
|
||||
),
|
||||
'disease_average_forecasted': (
|
||||
'Cold & Flu: Forecasted Average',
|
||||
'AllergyAverageSensor',
|
||||
'disease_average_data',
|
||||
{'data_attr': 'extended_data'},
|
||||
'mdi:snowflake'
|
||||
)
|
||||
}
|
||||
|
||||
RATING_MAPPING = [{
|
||||
'label': 'Low',
|
||||
'minimum': 0.0,
|
||||
'maximum': 2.4
|
||||
}, {
|
||||
'label': 'Low/Medium',
|
||||
'minimum': 2.5,
|
||||
'maximum': 4.8
|
||||
}, {
|
||||
'label': 'Medium',
|
||||
'minimum': 4.9,
|
||||
'maximum': 7.2
|
||||
}, {
|
||||
'label': 'Medium/High',
|
||||
'minimum': 7.3,
|
||||
'maximum': 9.6
|
||||
}, {
|
||||
'label': 'High',
|
||||
'minimum': 9.7,
|
||||
'maximum': 12
|
||||
}]
|
||||
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_ZIP_CODE): cv.positive_int,
|
||||
vol.Required(CONF_MONITORED_CONDITIONS):
|
||||
vol.All(cv.ensure_list, [vol.In(CONDITIONS)]),
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Configure the platform and add the sensors."""
|
||||
from pypollencom import Client
|
||||
|
||||
_LOGGER.debug('Configuration data: %s', config)
|
||||
|
||||
client = Client(config[CONF_ZIP_CODE])
|
||||
datas = {
|
||||
'allergy_average_data': AllergyAveragesData(client),
|
||||
'allergy_index_data': AllergyIndexData(client),
|
||||
'disease_average_data': DiseaseData(client)
|
||||
}
|
||||
|
||||
for data in datas.values():
|
||||
data.update()
|
||||
|
||||
sensors = []
|
||||
for condition in config[CONF_MONITORED_CONDITIONS]:
|
||||
name, sensor_class, data_key, params, icon = CONDITIONS[condition]
|
||||
sensors.append(globals()[sensor_class](
|
||||
datas[data_key],
|
||||
params,
|
||||
name,
|
||||
icon
|
||||
))
|
||||
|
||||
add_devices(sensors, True)
|
||||
|
||||
|
||||
def calculate_trend(list_of_nums):
|
||||
"""Calculate the most common rating as a trend."""
|
||||
ratings = list(
|
||||
r['label'] for n in list_of_nums
|
||||
for r in RATING_MAPPING
|
||||
if r['minimum'] <= n <= r['maximum'])
|
||||
return max(set(ratings), key=ratings.count)
|
||||
|
||||
|
||||
class BaseSensor(Entity):
|
||||
"""Define a base class for all of our sensors."""
|
||||
|
||||
def __init__(self, data, data_params, name, icon):
|
||||
"""Initialize the sensor."""
|
||||
self._attrs = {}
|
||||
self._icon = icon
|
||||
self._name = name
|
||||
self._data_params = data_params
|
||||
self._state = None
|
||||
self._unit = None
|
||||
self.data = data
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the device state attributes."""
|
||||
self._attrs.update({ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION})
|
||||
return self._attrs
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
"""Return the icon."""
|
||||
return self._icon
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
"""Return the unit the value is expressed in."""
|
||||
return self._unit
|
||||
|
||||
|
||||
class AllergyAverageSensor(BaseSensor):
|
||||
"""Define a sensor to show allergy average information."""
|
||||
|
||||
def update(self):
|
||||
"""Update the status of the sensor."""
|
||||
self.data.update()
|
||||
|
||||
data_attr = getattr(self.data, self._data_params['data_attr'])
|
||||
indices = [
|
||||
p['Index']
|
||||
for p in data_attr['Location']['periods']
|
||||
]
|
||||
average = round(mean(indices), 1)
|
||||
|
||||
self._attrs[ATTR_TREND] = calculate_trend(indices)
|
||||
self._attrs[ATTR_CITY] = data_attr['Location']['City'].title()
|
||||
self._attrs[ATTR_STATE] = data_attr['Location']['State']
|
||||
self._attrs[ATTR_ZIP_CODE] = data_attr['Location']['ZIP']
|
||||
|
||||
[rating] = [
|
||||
i['label'] for i in RATING_MAPPING
|
||||
if i['minimum'] <= average <= i['maximum']
|
||||
]
|
||||
self._attrs[ATTR_RATING] = rating
|
||||
|
||||
self._state = average
|
||||
self._unit = 'index'
|
||||
|
||||
|
||||
class AllergyIndexSensor(BaseSensor):
|
||||
"""Define a sensor to show allergy index information."""
|
||||
|
||||
def update(self):
|
||||
"""Update the status of the sensor."""
|
||||
self.data.update()
|
||||
|
||||
location_data = self.data.current_data['Location']
|
||||
[period] = [
|
||||
p for p in location_data['periods']
|
||||
if p['Type'] == self._data_params['key']
|
||||
]
|
||||
|
||||
self._attrs[ATTR_ALLERGEN_GENUS] = period['Triggers'][0]['Genus']
|
||||
self._attrs[ATTR_ALLERGEN_NAME] = period['Triggers'][0]['Name']
|
||||
self._attrs[ATTR_ALLERGEN_TYPE] = period['Triggers'][0]['PlantType']
|
||||
self._attrs[ATTR_OUTLOOK] = self.data.outlook_data['Outlook']
|
||||
self._attrs[ATTR_SEASON] = self.data.outlook_data['Season']
|
||||
self._attrs[ATTR_TREND] = self.data.outlook_data[
|
||||
'Trend'].title()
|
||||
self._attrs[ATTR_CITY] = location_data['City'].title()
|
||||
self._attrs[ATTR_STATE] = location_data['State']
|
||||
self._attrs[ATTR_ZIP_CODE] = location_data['ZIP']
|
||||
|
||||
[rating] = [
|
||||
i['label'] for i in RATING_MAPPING
|
||||
if i['minimum'] <= period['Index'] <= i['maximum']
|
||||
]
|
||||
self._attrs[ATTR_RATING] = rating
|
||||
|
||||
self._state = period['Index']
|
||||
self._unit = 'index'
|
||||
|
||||
|
||||
class DataBase(object):
|
||||
"""Define a generic data object."""
|
||||
|
||||
def __init__(self, client):
|
||||
"""Initialize."""
|
||||
self._client = client
|
||||
|
||||
def _get_client_data(self, module, operation):
|
||||
"""Get data from a particular point in the API."""
|
||||
from pypollencom.exceptions import HTTPError
|
||||
|
||||
try:
|
||||
data = getattr(getattr(self._client, module), operation)()
|
||||
_LOGGER.debug('Received "%s_%s" data: %s', module,
|
||||
operation, data)
|
||||
except HTTPError as exc:
|
||||
_LOGGER.error('An error occurred while retrieving data')
|
||||
_LOGGER.debug(exc)
|
||||
|
||||
return data
|
||||
|
||||
|
||||
class AllergyAveragesData(DataBase):
|
||||
"""Define an object to averages on future and historical allergy data."""
|
||||
|
||||
def __init__(self, client):
|
||||
"""Initialize."""
|
||||
super().__init__(client)
|
||||
self.extended_data = None
|
||||
self.historic_data = None
|
||||
|
||||
@Throttle(MIN_TIME_UPDATE_AVERAGES)
|
||||
def update(self):
|
||||
"""Update with new data."""
|
||||
self.extended_data = self._get_client_data('allergens', 'extended')
|
||||
self.historic_data = self._get_client_data('allergens', 'historic')
|
||||
|
||||
|
||||
class AllergyIndexData(DataBase):
|
||||
"""Define an object to retrieve current allergy index info."""
|
||||
|
||||
def __init__(self, client):
|
||||
"""Initialize."""
|
||||
super().__init__(client)
|
||||
self.current_data = None
|
||||
self.outlook_data = None
|
||||
|
||||
@Throttle(MIN_TIME_UPDATE_INDICES)
|
||||
def update(self):
|
||||
"""Update with new index data."""
|
||||
self.current_data = self._get_client_data('allergens', 'current')
|
||||
self.outlook_data = self._get_client_data('allergens', 'outlook')
|
||||
|
||||
|
||||
class DiseaseData(DataBase):
|
||||
"""Define an object to retrieve current disease index info."""
|
||||
|
||||
def __init__(self, client):
|
||||
"""Initialize."""
|
||||
super().__init__(client)
|
||||
self.extended_data = None
|
||||
|
||||
@Throttle(MIN_TIME_UPDATE_INDICES)
|
||||
def update(self):
|
||||
"""Update with new cold/flu data."""
|
||||
self.extended_data = self._get_client_data('disease', 'extended')
|
|
@ -830,6 +830,9 @@ pyotp==2.2.6
|
|||
# homeassistant.components.weather.openweathermap
|
||||
pyowm==2.8.0
|
||||
|
||||
# homeassistant.components.sensor.pollen
|
||||
pypollencom==1.1.1
|
||||
|
||||
# homeassistant.components.qwikswitch
|
||||
pyqwikswitch==0.4
|
||||
|
||||
|
|
Loading…
Reference in New Issue