2018-01-26 17:40:02 +00:00
|
|
|
"""
|
|
|
|
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
|
2018-02-14 00:25:10 +00:00
|
|
|
from homeassistant.util import Throttle, slugify
|
2018-01-26 17:40:02 +00:00
|
|
|
|
2018-04-17 18:03:22 +00:00
|
|
|
REQUIREMENTS = ['pypollencom==1.1.2']
|
2018-01-26 17:40:02 +00:00
|
|
|
_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({
|
2018-03-06 03:47:45 +00:00
|
|
|
vol.Required(CONF_ZIP_CODE): str,
|
2018-01-26 17:40:02 +00:00
|
|
|
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)
|
|
|
|
}
|
2018-02-14 00:25:10 +00:00
|
|
|
classes = {
|
|
|
|
'AllergyAverageSensor': AllergyAverageSensor,
|
|
|
|
'AllergyIndexSensor': AllergyIndexSensor
|
|
|
|
}
|
2018-01-26 17:40:02 +00:00
|
|
|
|
|
|
|
for data in datas.values():
|
|
|
|
data.update()
|
|
|
|
|
|
|
|
sensors = []
|
|
|
|
for condition in config[CONF_MONITORED_CONDITIONS]:
|
|
|
|
name, sensor_class, data_key, params, icon = CONDITIONS[condition]
|
2018-02-14 00:25:10 +00:00
|
|
|
sensors.append(classes[sensor_class](
|
2018-01-26 17:40:02 +00:00
|
|
|
datas[data_key],
|
|
|
|
params,
|
|
|
|
name,
|
2018-02-14 00:25:10 +00:00
|
|
|
icon,
|
|
|
|
config[CONF_ZIP_CODE]
|
2018-01-26 17:40:02 +00:00
|
|
|
))
|
|
|
|
|
|
|
|
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."""
|
|
|
|
|
2018-02-14 00:25:10 +00:00
|
|
|
def __init__(self, data, data_params, name, icon, unique_id):
|
2018-01-26 17:40:02 +00:00
|
|
|
"""Initialize the sensor."""
|
2018-04-21 08:16:52 +00:00
|
|
|
self._attrs = {ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION}
|
2018-01-26 17:40:02 +00:00
|
|
|
self._icon = icon
|
|
|
|
self._name = name
|
|
|
|
self._data_params = data_params
|
|
|
|
self._state = None
|
|
|
|
self._unit = None
|
2018-02-14 00:25:10 +00:00
|
|
|
self._unique_id = unique_id
|
2018-01-26 17:40:02 +00:00
|
|
|
self.data = data
|
|
|
|
|
|
|
|
@property
|
|
|
|
def device_state_attributes(self):
|
|
|
|
"""Return the device state attributes."""
|
|
|
|
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
|
|
|
|
|
2018-02-14 00:25:10 +00:00
|
|
|
@property
|
|
|
|
def unique_id(self):
|
|
|
|
"""Return a unique, HASS-friendly identifier for this entity."""
|
|
|
|
return '{0}_{1}'.format(self._unique_id, slugify(self._name))
|
|
|
|
|
2018-01-26 17:40:02 +00:00
|
|
|
@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()
|
|
|
|
|
2018-02-28 21:09:57 +00:00
|
|
|
try:
|
|
|
|
data_attr = getattr(self.data, self._data_params['data_attr'])
|
2018-03-05 22:15:54 +00:00
|
|
|
indices = [p['Index'] for p in data_attr['Location']['periods']]
|
|
|
|
self._attrs[ATTR_TREND] = calculate_trend(indices)
|
2018-02-28 21:09:57 +00:00
|
|
|
except KeyError:
|
|
|
|
_LOGGER.error("Pollen.com API didn't return any data")
|
|
|
|
return
|
|
|
|
|
2018-03-05 22:15:54 +00:00
|
|
|
try:
|
|
|
|
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']
|
|
|
|
except KeyError:
|
|
|
|
_LOGGER.debug('Location data not included in API response')
|
|
|
|
self._attrs[ATTR_CITY] = None
|
|
|
|
self._attrs[ATTR_STATE] = None
|
|
|
|
self._attrs[ATTR_ZIP_CODE] = None
|
2018-01-26 17:40:02 +00:00
|
|
|
|
2018-03-05 22:15:54 +00:00
|
|
|
average = round(mean(indices), 1)
|
2018-01-26 17:40:02 +00:00
|
|
|
[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()
|
|
|
|
|
2018-02-28 21:09:57 +00:00
|
|
|
try:
|
|
|
|
location_data = self.data.current_data['Location']
|
2018-03-05 22:15:54 +00:00
|
|
|
[period] = [
|
|
|
|
p for p in location_data['periods']
|
|
|
|
if p['Type'] == self._data_params['key']
|
|
|
|
]
|
|
|
|
[rating] = [
|
|
|
|
i['label'] for i in RATING_MAPPING
|
|
|
|
if i['minimum'] <= period['Index'] <= i['maximum']
|
|
|
|
]
|
2018-04-21 08:16:52 +00:00
|
|
|
|
|
|
|
for i in range(3):
|
|
|
|
index = i + 1
|
|
|
|
try:
|
|
|
|
data = period['Triggers'][i]
|
|
|
|
self._attrs['{0}_{1}'.format(
|
|
|
|
ATTR_ALLERGEN_GENUS, index)] = data['Genus']
|
|
|
|
self._attrs['{0}_{1}'.format(
|
|
|
|
ATTR_ALLERGEN_NAME, index)] = data['Name']
|
|
|
|
self._attrs['{0}_{1}'.format(
|
|
|
|
ATTR_ALLERGEN_TYPE, index)] = data['PlantType']
|
|
|
|
except IndexError:
|
|
|
|
self._attrs['{0}_{1}'.format(
|
|
|
|
ATTR_ALLERGEN_GENUS, index)] = None
|
|
|
|
self._attrs['{0}_{1}'.format(
|
|
|
|
ATTR_ALLERGEN_NAME, index)] = None
|
|
|
|
self._attrs['{0}_{1}'.format(
|
|
|
|
ATTR_ALLERGEN_TYPE, index)] = None
|
|
|
|
|
2018-03-05 22:15:54 +00:00
|
|
|
self._attrs[ATTR_RATING] = rating
|
|
|
|
|
2018-02-28 21:09:57 +00:00
|
|
|
except KeyError:
|
|
|
|
_LOGGER.error("Pollen.com API didn't return any data")
|
|
|
|
return
|
|
|
|
|
2018-03-05 22:15:54 +00:00
|
|
|
try:
|
|
|
|
self._attrs[ATTR_CITY] = location_data['City'].title()
|
|
|
|
self._attrs[ATTR_STATE] = location_data['State']
|
|
|
|
self._attrs[ATTR_ZIP_CODE] = location_data['ZIP']
|
|
|
|
except KeyError:
|
|
|
|
_LOGGER.debug('Location data not included in API response')
|
|
|
|
self._attrs[ATTR_CITY] = None
|
|
|
|
self._attrs[ATTR_STATE] = None
|
|
|
|
self._attrs[ATTR_ZIP_CODE] = None
|
2018-01-26 17:40:02 +00:00
|
|
|
|
2018-03-05 22:15:54 +00:00
|
|
|
try:
|
|
|
|
self._attrs[ATTR_OUTLOOK] = self.data.outlook_data['Outlook']
|
|
|
|
except KeyError:
|
|
|
|
_LOGGER.debug('Outlook data not included in API response')
|
|
|
|
self._attrs[ATTR_OUTLOOK] = None
|
2018-01-26 17:40:02 +00:00
|
|
|
|
2018-03-05 22:15:54 +00:00
|
|
|
try:
|
|
|
|
self._attrs[ATTR_SEASON] = self.data.outlook_data['Season']
|
|
|
|
except KeyError:
|
|
|
|
_LOGGER.debug('Season data not included in API response')
|
|
|
|
self._attrs[ATTR_SEASON] = None
|
|
|
|
|
|
|
|
try:
|
|
|
|
self._attrs[ATTR_TREND] = self.data.outlook_data['Trend'].title()
|
|
|
|
except KeyError:
|
|
|
|
_LOGGER.debug('Trend data not included in API response')
|
|
|
|
self._attrs[ATTR_TREND] = None
|
2018-01-26 17:40:02 +00:00
|
|
|
|
|
|
|
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
|
|
|
|
|
2018-02-28 21:09:57 +00:00
|
|
|
data = {}
|
2018-01-26 17:40:02 +00:00
|
|
|
try:
|
|
|
|
data = getattr(getattr(self._client, module), operation)()
|
2018-03-05 22:15:54 +00:00
|
|
|
_LOGGER.debug('Received "%s_%s" data: %s', module, operation, data)
|
2018-01-26 17:40:02 +00:00
|
|
|
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')
|