From 4ee21e66dc8975c201138c62b57bdbb5db8a29bb Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Thu, 1 Nov 2018 12:36:42 -0600 Subject: [PATCH] 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 --- homeassistant/components/sensor/pollen.py | 298 ++++++++++++++-------- requirements_all.txt | 2 +- 2 files changed, 188 insertions(+), 112 deletions(-) diff --git a/homeassistant/components/sensor/pollen.py b/homeassistant/components/sensor/pollen.py index 6df7047b353..62fdd5b4955 100644 --- a/homeassistant/components/sensor/pollen.py +++ b/homeassistant/components/sensor/pollen.py @@ -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: diff --git a/requirements_all.txt b/requirements_all.txt index e6b1d258c07..be86435ea84 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -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