Update Pollen.com sensor platform to include asthma info (#18024)

* Update Pollen.com sensor platform to include asthma info

* Updated requirements

* Bump to 2.2.2
This commit is contained in:
Aaron Bach 2018-11-01 12:36:42 -06:00 committed by Paulus Schoutsen
parent 4a3f754033
commit 4ee21e66dc
2 changed files with 188 additions and 112 deletions

View File

@ -18,9 +18,10 @@ from homeassistant.helpers import aiohttp_client
from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle
REQUIREMENTS = ['pypollencom==2.1.0']
REQUIREMENTS = ['pypollencom==2.2.2']
_LOGGER = logging.getLogger(__name__)
ATTR_ALLERGEN_AMOUNT = 'allergen_amount'
ATTR_ALLERGEN_GENUS = 'allergen_genus'
ATTR_ALLERGEN_NAME = 'allergen_name'
ATTR_ALLERGEN_TYPE = 'allergen_type'
@ -43,21 +44,35 @@ TYPE_ALLERGY_OUTLOOK = 'allergy_outlook'
TYPE_ALLERGY_TODAY = 'allergy_index_today'
TYPE_ALLERGY_TOMORROW = 'allergy_index_tomorrow'
TYPE_ALLERGY_YESTERDAY = 'allergy_index_yesterday'
TYPE_ASTHMA_FORECAST = 'asthma_average_forecasted'
TYPE_ASTHMA_HISTORIC = 'asthma_average_historical'
TYPE_ASTHMA_INDEX = 'asthma_index'
TYPE_ASTHMA_TODAY = 'asthma_index_today'
TYPE_ASTHMA_TOMORROW = 'asthma_index_tomorrow'
TYPE_ASTHMA_YESTERDAY = 'asthma_index_yesterday'
TYPE_DISEASE_FORECAST = 'disease_average_forecasted'
SENSORS = {
TYPE_ALLERGY_FORECAST: (
'Allergy Index: Forecasted Average', None, 'mdi:flower', 'index'),
'ForecastSensor', 'Allergy Index: Forecasted Average', 'mdi:flower'),
TYPE_ALLERGY_HISTORIC: (
'Allergy Index: Historical Average', None, 'mdi:flower', 'index'),
TYPE_ALLERGY_TODAY: (
'Allergy Index: Today', TYPE_ALLERGY_INDEX, 'mdi:flower', 'index'),
'HistoricalSensor', 'Allergy Index: Historical Average', 'mdi:flower'),
TYPE_ALLERGY_TODAY: ('IndexSensor', 'Allergy Index: Today', 'mdi:flower'),
TYPE_ALLERGY_TOMORROW: (
'Allergy Index: Tomorrow', TYPE_ALLERGY_INDEX, 'mdi:flower', 'index'),
'IndexSensor', 'Allergy Index: Tomorrow', 'mdi:flower'),
TYPE_ALLERGY_YESTERDAY: (
'Allergy Index: Yesterday', TYPE_ALLERGY_INDEX, 'mdi:flower', 'index'),
'IndexSensor', 'Allergy Index: Yesterday', 'mdi:flower'),
TYPE_ASTHMA_TODAY: ('IndexSensor', 'Ashma Index: Today', 'mdi:flower'),
TYPE_ASTHMA_TOMORROW: (
'IndexSensor', 'Ashma Index: Tomorrow', 'mdi:flower'),
TYPE_ASTHMA_YESTERDAY: (
'IndexSensor', 'Ashma Index: Yesterday', 'mdi:flower'),
TYPE_ASTHMA_FORECAST: (
'ForecastSensor', 'Asthma Index: Forecasted Average', 'mdi:flower'),
TYPE_ASTHMA_HISTORIC: (
'HistoricalSensor', 'Asthma Index: Historical Average', 'mdi:flower'),
TYPE_DISEASE_FORECAST: (
'Cold & Flu: Forecasted Average', None, 'mdi:snowflake', 'index')
'ForecastSensor', 'Cold & Flu: Forecasted Average', 'mdi:snowflake')
}
RATING_MAPPING = [{
@ -87,7 +102,8 @@ TREND_INCREASING = 'Increasing'
TREND_SUBSIDING = 'Subsiding'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_ZIP_CODE): str,
vol.Required(CONF_ZIP_CODE):
str,
vol.Required(CONF_MONITORED_CONDITIONS, default=list(SENSORS)):
vol.All(cv.ensure_list, [vol.In(SENSORS)])
})
@ -100,18 +116,18 @@ async def async_setup_platform(
websession = aiohttp_client.async_get_clientsession(hass)
data = PollenComData(
pollen = PollenComData(
Client(config[CONF_ZIP_CODE], websession),
config[CONF_MONITORED_CONDITIONS])
await data.async_update()
await pollen.async_update()
sensors = []
for kind in config[CONF_MONITORED_CONDITIONS]:
name, category, icon, unit = SENSORS[kind]
sensor_class, name, icon = SENSORS[kind]
sensors.append(
PollencomSensor(
data, config[CONF_ZIP_CODE], kind, category, name, icon, unit))
globals()[sensor_class](
pollen, kind, name, icon, config[CONF_ZIP_CODE]))
async_add_entities(sensors, True)
@ -124,27 +140,31 @@ def calculate_average_rating(indices):
return max(set(ratings), key=ratings.count)
class PollencomSensor(Entity):
"""Define a Pollen.com sensor."""
class BaseSensor(Entity):
"""Define a base Pollen.com sensor."""
def __init__(self, pollencom, zip_code, kind, category, name, icon, unit):
def __init__(self, pollen, kind, name, icon, zip_code):
"""Initialize the sensor."""
self._attrs = {ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION}
self._category = category
self._icon = icon
self._kind = kind
self._name = name
self._state = None
self._type = kind
self._unit = unit
self._zip_code = zip_code
self.pollencom = pollencom
self.pollen = pollen
@property
def available(self):
"""Return True if entity is available."""
return bool(
self.pollencom.data.get(self._type)
or self.pollencom.data.get(self._category))
if self._kind in (TYPE_ALLERGY_TODAY, TYPE_ALLERGY_TOMORROW,
TYPE_ALLERGY_YESTERDAY):
return bool(self.pollen.data[TYPE_ALLERGY_INDEX])
if self._kind in (TYPE_ASTHMA_TODAY, TYPE_ASTHMA_TOMORROW,
TYPE_ASTHMA_YESTERDAY):
return bool(self.pollen.data[TYPE_ASTHMA_INDEX])
return bool(self.pollen.data[self._kind])
@property
def device_state_attributes(self):
@ -169,24 +189,24 @@ class PollencomSensor(Entity):
@property
def unique_id(self):
"""Return a unique, HASS-friendly identifier for this entity."""
return '{0}_{1}'.format(self._zip_code, self._type)
return '{0}_{1}'.format(self._zip_code, self._kind)
@property
def unit_of_measurement(self):
"""Return the unit the value is expressed in."""
return self._unit
return 'index'
class ForecastSensor(BaseSensor):
"""Define sensor related to forecast data."""
async def async_update(self):
"""Update the sensor."""
await self.pollencom.async_update()
if not self.pollencom.data:
await self.pollen.async_update()
if not self.pollen.data:
return
if self._category:
data = self.pollencom.data[self._category].get('Location')
else:
data = self.pollencom.data[self._type].get('Location')
data = self.pollen.data[self._kind].get('Location')
if not data:
return
@ -196,44 +216,101 @@ class PollencomSensor(Entity):
i['label'] for i in RATING_MAPPING
if i['minimum'] <= average <= i['maximum']
]
slope = (data['periods'][-1]['Index'] - data['periods'][-2]['Index'])
trend = TREND_FLAT
if slope > 0:
trend = TREND_INCREASING
elif slope < 0:
trend = TREND_SUBSIDING
else:
trend = TREND_FLAT
if self._type == TYPE_ALLERGY_FORECAST:
outlook = self.pollencom.data[TYPE_ALLERGY_OUTLOOK]
self._attrs.update({
ATTR_CITY: data['City'].title(),
ATTR_RATING: rating,
ATTR_STATE: data['State'],
ATTR_TREND: trend,
ATTR_ZIP_CODE: data['ZIP']
})
self._attrs.update({
ATTR_CITY: data['City'].title(),
ATTR_OUTLOOK: outlook['Outlook'],
ATTR_RATING: rating,
ATTR_SEASON: outlook['Season'].title(),
ATTR_STATE: data['State'],
ATTR_TREND: outlook['Trend'].title(),
ATTR_ZIP_CODE: data['ZIP']
})
self._state = average
elif self._type == TYPE_ALLERGY_HISTORIC:
self._attrs.update({
ATTR_CITY: data['City'].title(),
ATTR_RATING: calculate_average_rating(indices),
ATTR_STATE: data['State'],
ATTR_TREND: trend,
ATTR_ZIP_CODE: data['ZIP']
})
self._state = average
elif self._type in (TYPE_ALLERGY_TODAY, TYPE_ALLERGY_TOMORROW,
TYPE_ALLERGY_YESTERDAY):
key = self._type.split('_')[-1].title()
[period] = [p for p in data['periods'] if p['Type'] == key]
[rating] = [
i['label'] for i in RATING_MAPPING
if i['minimum'] <= period['Index'] <= i['maximum']
]
if self._kind == TYPE_ALLERGY_FORECAST:
outlook = self.pollen.data[TYPE_ALLERGY_OUTLOOK]
self._attrs[ATTR_OUTLOOK] = outlook['Outlook']
self._state = average
class HistoricalSensor(BaseSensor):
"""Define sensor related to historical data."""
async def async_update(self):
"""Update the sensor."""
await self.pollen.async_update()
if not self.pollen.data:
return
data = self.pollen.data[self._kind].get('Location')
if not data:
return
indices = [p['Index'] for p in data['periods']]
average = round(mean(indices), 1)
slope = (data['periods'][-1]['Index'] - data['periods'][-2]['Index'])
if slope > 0:
trend = TREND_INCREASING
elif slope < 0:
trend = TREND_SUBSIDING
else:
trend = TREND_FLAT
self._attrs.update({
ATTR_CITY: data['City'].title(),
ATTR_RATING: calculate_average_rating(indices),
ATTR_STATE: data['State'],
ATTR_TREND: trend,
ATTR_ZIP_CODE: data['ZIP']
})
self._state = average
class IndexSensor(BaseSensor):
"""Define sensor related to indices."""
async def async_update(self):
"""Update the sensor."""
await self.pollen.async_update()
if not self.pollen.data:
return
data = {}
if self._kind in (TYPE_ALLERGY_TODAY, TYPE_ALLERGY_TOMORROW,
TYPE_ALLERGY_YESTERDAY):
data = self.pollen.data[TYPE_ALLERGY_INDEX].get('Location')
elif self._kind in (TYPE_ASTHMA_TODAY, TYPE_ASTHMA_TOMORROW,
TYPE_ASTHMA_YESTERDAY):
data = self.pollen.data[TYPE_ASTHMA_INDEX].get('Location')
if not data:
return
key = self._kind.split('_')[-1].title()
[period] = [p for p in data['periods'] if p['Type'] == key]
[rating] = [
i['label'] for i in RATING_MAPPING
if i['minimum'] <= period['Index'] <= i['maximum']
]
self._attrs.update({
ATTR_CITY: data['City'].title(),
ATTR_RATING: rating,
ATTR_STATE: data['State'],
ATTR_ZIP_CODE: data['ZIP']
})
if self._kind in (TYPE_ALLERGY_TODAY, TYPE_ALLERGY_TOMORROW,
TYPE_ALLERGY_YESTERDAY):
for idx, attrs in enumerate(period['Triggers']):
index = idx + 1
self._attrs.update({
@ -244,23 +321,18 @@ class PollencomSensor(Entity):
'{0}_{1}'.format(ATTR_ALLERGEN_TYPE, index):
attrs['PlantType'],
})
elif self._kind in (TYPE_ASTHMA_TODAY, TYPE_ASTHMA_TOMORROW,
TYPE_ASTHMA_YESTERDAY):
for idx, attrs in enumerate(period['Triggers']):
index = idx + 1
self._attrs.update({
'{0}_{1}'.format(ATTR_ALLERGEN_NAME, index):
attrs['Name'],
'{0}_{1}'.format(ATTR_ALLERGEN_AMOUNT, index):
attrs['PPM'],
})
self._attrs.update({
ATTR_CITY: data['City'].title(),
ATTR_RATING: rating,
ATTR_STATE: data['State'],
ATTR_ZIP_CODE: data['ZIP']
})
self._state = period['Index']
elif self._type == TYPE_DISEASE_FORECAST:
self._attrs.update({
ATTR_CITY: data['City'].title(),
ATTR_RATING: rating,
ATTR_STATE: data['State'],
ATTR_TREND: trend,
ATTR_ZIP_CODE: data['ZIP']
})
self._state = average
self._state = period['Index']
class PollenComData:
@ -272,10 +344,21 @@ class PollenComData:
self._sensor_types = sensor_types
self.data = {}
async def _get_data(self, method, key):
"""Return API data from a specific call."""
from pypollencom.errors import PollenComError
try:
data = await method()
self.data[key] = data
except PollenComError as err:
_LOGGER.error('Unable to get "%s" data: %s', key, err)
self.data[key] = {}
@Throttle(DEFAULT_SCAN_INTERVAL)
async def async_update(self):
"""Update Pollen.com data."""
from pypollencom.errors import InvalidZipError, PollenComError
from pypollencom.errors import InvalidZipError
# Pollen.com requires a bit more complicated error handling, given that
# it sometimes has parts (but not the whole thing) go down:
@ -285,45 +368,38 @@ class PollenComData:
try:
if TYPE_ALLERGY_FORECAST in self._sensor_types:
try:
data = await self._client.allergens.extended()
self.data[TYPE_ALLERGY_FORECAST] = data
except PollenComError as err:
_LOGGER.error('Unable to get allergy forecast: %s', err)
self.data[TYPE_ALLERGY_FORECAST] = {}
try:
data = await self._client.allergens.outlook()
self.data[TYPE_ALLERGY_OUTLOOK] = data
except PollenComError as err:
_LOGGER.error('Unable to get allergy outlook: %s', err)
self.data[TYPE_ALLERGY_OUTLOOK] = {}
await self._get_data(
self._client.allergens.extended, TYPE_ALLERGY_FORECAST)
await self._get_data(
self._client.allergens.outlook, TYPE_ALLERGY_OUTLOOK)
if TYPE_ALLERGY_HISTORIC in self._sensor_types:
try:
data = await self._client.allergens.historic()
self.data[TYPE_ALLERGY_HISTORIC] = data
except PollenComError as err:
_LOGGER.error('Unable to get allergy history: %s', err)
self.data[TYPE_ALLERGY_HISTORIC] = {}
await self._get_data(
self._client.allergens.historic, TYPE_ALLERGY_HISTORIC)
if any(s in self._sensor_types
for s in [TYPE_ALLERGY_TODAY, TYPE_ALLERGY_TOMORROW,
TYPE_ALLERGY_YESTERDAY]):
try:
data = await self._client.allergens.current()
self.data[TYPE_ALLERGY_INDEX] = data
except PollenComError as err:
_LOGGER.error('Unable to get current allergies: %s', err)
self.data[TYPE_ALLERGY_TODAY] = {}
await self._get_data(
self._client.allergens.current, TYPE_ALLERGY_INDEX)
if TYPE_ASTHMA_FORECAST in self._sensor_types:
await self._get_data(
self._client.asthma.extended, TYPE_ASTHMA_FORECAST)
if TYPE_ASTHMA_HISTORIC in self._sensor_types:
await self._get_data(
self._client.asthma.historic, TYPE_ASTHMA_HISTORIC)
if any(s in self._sensor_types
for s in [TYPE_ASTHMA_TODAY, TYPE_ASTHMA_TOMORROW,
TYPE_ASTHMA_YESTERDAY]):
await self._get_data(
self._client.asthma.current, TYPE_ASTHMA_INDEX)
if TYPE_DISEASE_FORECAST in self._sensor_types:
try:
data = await self._client.disease.extended()
self.data[TYPE_DISEASE_FORECAST] = data
except PollenComError as err:
_LOGGER.error('Unable to get disease forecast: %s', err)
self.data[TYPE_DISEASE_FORECAST] = {}
await self._get_data(
self._client.disease.extended, TYPE_DISEASE_FORECAST)
_LOGGER.debug('New data retrieved: %s', self.data)
except InvalidZipError:

View File

@ -1060,7 +1060,7 @@ pyowm==2.9.0
pypjlink2==1.2.0
# homeassistant.components.sensor.pollen
pypollencom==2.1.0
pypollencom==2.2.2
# homeassistant.components.qwikswitch
pyqwikswitch==0.8