Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds allergy/disease sensor platform from Pollen.com #11573

Merged
merged 7 commits into from
Jan 26, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -587,6 +587,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
Expand Down
1 change: 1 addition & 0 deletions CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
322 changes: 322 additions & 0 deletions homeassistant/components/sensor/pollen.py
Original file line number Diff line number Diff line change
@@ -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')
3 changes: 3 additions & 0 deletions requirements_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -824,6 +824,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

Expand Down