Additional cleanup of IQVIA integration (#23403)

* Additional cleanup of IQVIA integration

* Task

* Moved import

* Fixed exception

* Member comments (round 1)

* Member comments (round 2)

* Member comments
This commit is contained in:
Aaron Bach 2019-04-26 11:06:46 -06:00 committed by GitHub
parent 606dbb85d2
commit 8fe95f4bab
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 115 additions and 122 deletions

View file

@ -1,18 +1,22 @@
"""Support for IQVIA.""" """Support for IQVIA."""
import asyncio
from datetime import timedelta from datetime import timedelta
import logging import logging
from pyiqvia import Client
from pyiqvia.errors import IQVIAError, InvalidZipError
import voluptuous as vol import voluptuous as vol
from homeassistant.const import ( from homeassistant.const import ATTR_ATTRIBUTION, CONF_MONITORED_CONDITIONS
ATTR_ATTRIBUTION, CONF_MONITORED_CONDITIONS, CONF_SCAN_INTERVAL)
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.helpers import ( from homeassistant.helpers import aiohttp_client, config_validation as cv
aiohttp_client, config_validation as cv, discovery) from homeassistant.helpers.discovery import async_load_platform
from homeassistant.helpers.dispatcher import ( from homeassistant.helpers.dispatcher import (
async_dispatcher_connect, async_dispatcher_send) async_dispatcher_connect, async_dispatcher_send)
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.event import async_track_time_interval
from homeassistant.util.decorator import Registry
from .const import ( from .const import (
DATA_CLIENT, DATA_LISTENER, DOMAIN, SENSORS, TOPIC_DATA_UPDATE, DATA_CLIENT, DATA_LISTENER, DOMAIN, SENSORS, TOPIC_DATA_UPDATE,
@ -24,6 +28,7 @@ from .const import (
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
CONF_ZIP_CODE = 'zip_code' CONF_ZIP_CODE = 'zip_code'
DATA_CONFIG = 'config' DATA_CONFIG = 'config'
@ -31,8 +36,18 @@ DATA_CONFIG = 'config'
DEFAULT_ATTRIBUTION = 'Data provided by IQVIA™' DEFAULT_ATTRIBUTION = 'Data provided by IQVIA™'
DEFAULT_SCAN_INTERVAL = timedelta(minutes=30) DEFAULT_SCAN_INTERVAL = timedelta(minutes=30)
NOTIFICATION_ID = 'iqvia_setup' FETCHER_MAPPING = {
NOTIFICATION_TITLE = 'IQVIA Setup' (TYPE_ALLERGY_FORECAST,): (TYPE_ALLERGY_FORECAST, TYPE_ALLERGY_OUTLOOK),
(TYPE_ALLERGY_HISTORIC,): (TYPE_ALLERGY_HISTORIC,),
(TYPE_ALLERGY_TODAY, TYPE_ALLERGY_TOMORROW, TYPE_ALLERGY_YESTERDAY): (
TYPE_ALLERGY_INDEX,),
(TYPE_ASTHMA_FORECAST,): (TYPE_ASTHMA_FORECAST,),
(TYPE_ASTHMA_HISTORIC,): (TYPE_ASTHMA_HISTORIC,),
(TYPE_ASTHMA_TODAY, TYPE_ASTHMA_TOMORROW, TYPE_ASTHMA_YESTERDAY): (
TYPE_ASTHMA_INDEX,),
(TYPE_DISEASE_FORECAST,): (TYPE_DISEASE_FORECAST,),
}
CONFIG_SCHEMA = vol.Schema({ CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({ DOMAIN: vol.Schema({
@ -45,16 +60,10 @@ CONFIG_SCHEMA = vol.Schema({
async def async_setup(hass, config): async def async_setup(hass, config):
"""Set up the IQVIA component.""" """Set up the IQVIA component."""
from pyiqvia import Client
from pyiqvia.errors import IQVIAError
hass.data[DOMAIN] = {} hass.data[DOMAIN] = {}
hass.data[DOMAIN][DATA_CLIENT] = {} hass.data[DOMAIN][DATA_CLIENT] = {}
hass.data[DOMAIN][DATA_LISTENER] = {} hass.data[DOMAIN][DATA_LISTENER] = {}
if DOMAIN not in config:
return True
conf = config[DOMAIN] conf = config[DOMAIN]
websession = aiohttp_client.async_get_clientsession(hass) websession = aiohttp_client.async_get_clientsession(hass)
@ -66,17 +75,12 @@ async def async_setup(hass, config):
await iqvia.async_update() await iqvia.async_update()
except IQVIAError as err: except IQVIAError as err:
_LOGGER.error('Unable to set up IQVIA: %s', err) _LOGGER.error('Unable to set up IQVIA: %s', err)
hass.components.persistent_notification.create(
'Error: {0}<br />'
'You will need to restart hass after fixing.'
''.format(err),
title=NOTIFICATION_TITLE,
notification_id=NOTIFICATION_ID)
return False return False
hass.data[DOMAIN][DATA_CLIENT] = iqvia hass.data[DOMAIN][DATA_CLIENT] = iqvia
discovery.load_platform(hass, 'sensor', DOMAIN, {}, conf) hass.async_create_task(
async_load_platform(hass, 'sensor', DOMAIN, {}, config))
async def refresh(event_time): async def refresh(event_time):
"""Refresh IQVIA data.""" """Refresh IQVIA data."""
@ -86,9 +90,7 @@ async def async_setup(hass, config):
hass.data[DOMAIN][DATA_LISTENER] = async_track_time_interval( hass.data[DOMAIN][DATA_LISTENER] = async_track_time_interval(
hass, refresh, hass, refresh,
timedelta( DEFAULT_SCAN_INTERVAL)
seconds=conf.get(
CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL.seconds)))
return True return True
@ -103,94 +105,81 @@ class IQVIAData:
self.sensor_types = sensor_types self.sensor_types = sensor_types
self.zip_code = client.zip_code self.zip_code = client.zip_code
async def _get_data(self, method, key): self.fetchers = Registry()
"""Return API data from a specific call.""" self.fetchers.register(TYPE_ALLERGY_FORECAST)(
from pyiqvia.errors import IQVIAError self._client.allergens.extended)
self.fetchers.register(TYPE_ALLERGY_HISTORIC)(
try: self._client.allergens.historic)
data = await method() self.fetchers.register(TYPE_ALLERGY_OUTLOOK)(
self.data[key] = data self._client.allergens.outlook)
except IQVIAError as err: self.fetchers.register(TYPE_ALLERGY_INDEX)(
_LOGGER.error('Unable to get "%s" data: %s', key, err) self._client.allergens.current)
self.data[key] = {} self.fetchers.register(TYPE_ASTHMA_FORECAST)(
self._client.asthma.extended)
self.fetchers.register(TYPE_ASTHMA_HISTORIC)(
self._client.asthma.historic)
self.fetchers.register(TYPE_ASTHMA_INDEX)(self._client.asthma.current)
self.fetchers.register(TYPE_DISEASE_FORECAST)(
self._client.disease.extended)
async def async_update(self): async def async_update(self):
"""Update IQVIA data.""" """Update IQVIA data."""
from pyiqvia.errors import InvalidZipError tasks = {}
for conditions, fetcher_types in FETCHER_MAPPING.items():
if not any(c in self.sensor_types for c in conditions):
continue
for fetcher_type in fetcher_types:
tasks[fetcher_type] = self.fetchers[fetcher_type]()
results = await asyncio.gather(*tasks.values(), return_exceptions=True)
# IQVIA sites require a bit more complicated error handling, given that # IQVIA sites require a bit more complicated error handling, given that
# it sometimes has parts (but not the whole thing) go down: # they sometimes have parts (but not the whole thing) go down:
#
# 1. If `InvalidZipError` is thrown, quit everything immediately. # 1. If `InvalidZipError` is thrown, quit everything immediately.
# 2. If an individual request throws any other error, try the others. # 2. If a single request throws any other error, try the others.
try: for key, result in zip(tasks, results):
if TYPE_ALLERGY_FORECAST in self.sensor_types: if isinstance(result, InvalidZipError):
await self._get_data( _LOGGER.error("No data for ZIP: %s", self._client.zip_code)
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:
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]):
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:
await self._get_data(
self._client.disease.extended, TYPE_DISEASE_FORECAST)
_LOGGER.debug("New data retrieved: %s", self.data)
except InvalidZipError:
_LOGGER.error(
"Cannot retrieve data for ZIP code: %s", self._client.zip_code)
self.data = {} self.data = {}
return
if isinstance(result, IQVIAError):
_LOGGER.error('Unable to get %s data: %s', key, result)
self.data[key] = {}
continue
_LOGGER.debug('Loaded new %s data', key)
self.data[key] = result
class IQVIAEntity(Entity): class IQVIAEntity(Entity):
"""Define a base IQVIA entity.""" """Define a base IQVIA entity."""
def __init__(self, iqvia, kind, name, icon, zip_code): def __init__(self, iqvia, sensor_type, name, icon, zip_code):
"""Initialize the sensor.""" """Initialize the sensor."""
self._async_unsub_dispatcher_connect = None self._async_unsub_dispatcher_connect = None
self._attrs = {ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION} self._attrs = {ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION}
self._icon = icon self._icon = icon
self._iqvia = iqvia self._iqvia = iqvia
self._kind = kind
self._name = name self._name = name
self._state = None self._state = None
self._type = sensor_type
self._zip_code = zip_code self._zip_code = zip_code
@property @property
def available(self): def available(self):
"""Return True if entity is available.""" """Return True if entity is available."""
if self._kind in (TYPE_ALLERGY_TODAY, TYPE_ALLERGY_TOMORROW, if self._type in (TYPE_ALLERGY_TODAY, TYPE_ALLERGY_TOMORROW,
TYPE_ALLERGY_YESTERDAY): TYPE_ALLERGY_YESTERDAY):
return self._iqvia.data.get(TYPE_ALLERGY_INDEX) is not None return self._iqvia.data.get(TYPE_ALLERGY_INDEX) is not None
if self._kind in (TYPE_ASTHMA_TODAY, TYPE_ASTHMA_TOMORROW, if self._type in (TYPE_ASTHMA_TODAY, TYPE_ASTHMA_TOMORROW,
TYPE_ASTHMA_YESTERDAY): TYPE_ASTHMA_YESTERDAY):
return self._iqvia.data.get(TYPE_ASTHMA_INDEX) is not None return self._iqvia.data.get(TYPE_ASTHMA_INDEX) is not None
return self._iqvia.data.get(self._kind) is not None return self._iqvia.data.get(self._type) is not None
@property @property
def device_state_attributes(self): def device_state_attributes(self):
@ -215,7 +204,7 @@ class IQVIAEntity(Entity):
@property @property
def unique_id(self): def unique_id(self):
"""Return a unique, HASS-friendly identifier for this entity.""" """Return a unique, HASS-friendly identifier for this entity."""
return '{0}_{1}'.format(self._zip_code, self._kind) return '{0}_{1}'.format(self._zip_code, self._type)
@property @property
def unit_of_measurement(self): def unit_of_measurement(self):

View file

@ -22,24 +22,15 @@ TYPE_ASTHMA_YESTERDAY = 'asthma_index_yesterday'
TYPE_DISEASE_FORECAST = 'disease_average_forecasted' TYPE_DISEASE_FORECAST = 'disease_average_forecasted'
SENSORS = { SENSORS = {
TYPE_ALLERGY_FORECAST: ( TYPE_ALLERGY_FORECAST: ('Allergy Index: Forecasted Average', 'mdi:flower'),
'ForecastSensor', 'Allergy Index: Forecasted Average', 'mdi:flower'), TYPE_ALLERGY_HISTORIC: ('Allergy Index: Historical Average', 'mdi:flower'),
TYPE_ALLERGY_HISTORIC: ( TYPE_ALLERGY_TODAY: ('Allergy Index: Today', 'mdi:flower'),
'HistoricalSensor', 'Allergy Index: Historical Average', 'mdi:flower'), TYPE_ALLERGY_TOMORROW: ('Allergy Index: Tomorrow', 'mdi:flower'),
TYPE_ALLERGY_TODAY: ('IndexSensor', 'Allergy Index: Today', 'mdi:flower'), TYPE_ALLERGY_YESTERDAY: ('Allergy Index: Yesterday', 'mdi:flower'),
TYPE_ALLERGY_TOMORROW: ( TYPE_ASTHMA_TODAY: ('Asthma Index: Today', 'mdi:flower'),
'IndexSensor', 'Allergy Index: Tomorrow', 'mdi:flower'), TYPE_ASTHMA_TOMORROW: ('Asthma Index: Tomorrow', 'mdi:flower'),
TYPE_ALLERGY_YESTERDAY: ( TYPE_ASTHMA_YESTERDAY: ('Asthma Index: Yesterday', 'mdi:flower'),
'IndexSensor', 'Allergy Index: Yesterday', 'mdi:flower'), TYPE_ASTHMA_FORECAST: ('Asthma Index: Forecasted Average', 'mdi:flower'),
TYPE_ASTHMA_TODAY: ('IndexSensor', 'Asthma Index: Today', 'mdi:flower'), TYPE_ASTHMA_HISTORIC: ('Asthma Index: Historical Average', 'mdi:flower'),
TYPE_ASTHMA_TOMORROW: ( TYPE_DISEASE_FORECAST: ('Cold & Flu: Forecasted Average', 'mdi:snowflake')
'IndexSensor', 'Asthma Index: Tomorrow', 'mdi:flower'),
TYPE_ASTHMA_YESTERDAY: (
'IndexSensor', 'Asthma 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: (
'ForecastSensor', 'Cold & Flu: Forecasted Average', 'mdi:snowflake')
} }

View file

@ -2,11 +2,15 @@
import logging import logging
from statistics import mean from statistics import mean
import numpy as np
from homeassistant.components.iqvia import ( from homeassistant.components.iqvia import (
DATA_CLIENT, DOMAIN, SENSORS, TYPE_ALLERGY_FORECAST, TYPE_ALLERGY_OUTLOOK, DATA_CLIENT, DOMAIN, SENSORS, TYPE_ALLERGY_FORECAST, TYPE_ALLERGY_HISTORIC,
TYPE_ALLERGY_INDEX, TYPE_ALLERGY_TODAY, TYPE_ALLERGY_TOMORROW, TYPE_ALLERGY_OUTLOOK, TYPE_ALLERGY_INDEX, TYPE_ALLERGY_TODAY,
TYPE_ALLERGY_YESTERDAY, TYPE_ASTHMA_INDEX, TYPE_ASTHMA_TODAY, TYPE_ALLERGY_TOMORROW, TYPE_ALLERGY_YESTERDAY, TYPE_ASTHMA_FORECAST,
TYPE_ASTHMA_TOMORROW, TYPE_ASTHMA_YESTERDAY, IQVIAEntity) TYPE_ASTHMA_HISTORIC, TYPE_ASTHMA_INDEX, TYPE_ASTHMA_TODAY,
TYPE_ASTHMA_TOMORROW, TYPE_ASTHMA_YESTERDAY, TYPE_DISEASE_FORECAST,
IQVIAEntity)
from homeassistant.const import ATTR_STATE from homeassistant.const import ATTR_STATE
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -53,11 +57,25 @@ async def async_setup_platform(
"""Configure the platform and add the sensors.""" """Configure the platform and add the sensors."""
iqvia = hass.data[DOMAIN][DATA_CLIENT] iqvia = hass.data[DOMAIN][DATA_CLIENT]
sensor_class_mapping = {
TYPE_ALLERGY_FORECAST: ForecastSensor,
TYPE_ALLERGY_HISTORIC: HistoricalSensor,
TYPE_ALLERGY_TODAY: IndexSensor,
TYPE_ALLERGY_TOMORROW: IndexSensor,
TYPE_ALLERGY_YESTERDAY: IndexSensor,
TYPE_ASTHMA_FORECAST: ForecastSensor,
TYPE_ASTHMA_HISTORIC: HistoricalSensor,
TYPE_ASTHMA_TODAY: IndexSensor,
TYPE_ASTHMA_TOMORROW: IndexSensor,
TYPE_ASTHMA_YESTERDAY: IndexSensor,
TYPE_DISEASE_FORECAST: ForecastSensor,
}
sensors = [] sensors = []
for kind in iqvia.sensor_types: for sensor_type in iqvia.sensor_types:
sensor_class, name, icon = SENSORS[kind] klass = sensor_class_mapping[sensor_type]
sensors.append( name, icon = SENSORS[sensor_type]
globals()[sensor_class](iqvia, kind, name, icon, iqvia.zip_code)) sensors.append(klass(iqvia, sensor_type, name, icon, iqvia.zip_code))
async_add_entities(sensors, True) async_add_entities(sensors, True)
@ -72,8 +90,6 @@ def calculate_average_rating(indices):
def calculate_trend(indices): def calculate_trend(indices):
"""Calculate the "moving average" of a set of indices.""" """Calculate the "moving average" of a set of indices."""
import numpy as np
def moving_average(data, samples): def moving_average(data, samples):
"""Determine the "moving average" (http://tinyurl.com/yaereb3c).""" """Determine the "moving average" (http://tinyurl.com/yaereb3c)."""
ret = np.cumsum(data, dtype=float) ret = np.cumsum(data, dtype=float)
@ -92,11 +108,10 @@ class ForecastSensor(IQVIAEntity):
async def async_update(self): async def async_update(self):
"""Update the sensor.""" """Update the sensor."""
await self._iqvia.async_update()
if not self._iqvia.data: if not self._iqvia.data:
return return
data = self._iqvia.data[self._kind].get('Location') data = self._iqvia.data[self._type].get('Location')
if not data: if not data:
return return
@ -115,7 +130,7 @@ class ForecastSensor(IQVIAEntity):
ATTR_ZIP_CODE: data['ZIP'] ATTR_ZIP_CODE: data['ZIP']
}) })
if self._kind == TYPE_ALLERGY_FORECAST: if self._type == TYPE_ALLERGY_FORECAST:
outlook = self._iqvia.data[TYPE_ALLERGY_OUTLOOK] outlook = self._iqvia.data[TYPE_ALLERGY_OUTLOOK]
self._attrs[ATTR_OUTLOOK] = outlook.get('Outlook') self._attrs[ATTR_OUTLOOK] = outlook.get('Outlook')
self._attrs[ATTR_SEASON] = outlook.get('Season') self._attrs[ATTR_SEASON] = outlook.get('Season')
@ -128,11 +143,10 @@ class HistoricalSensor(IQVIAEntity):
async def async_update(self): async def async_update(self):
"""Update the sensor.""" """Update the sensor."""
await self._iqvia.async_update()
if not self._iqvia.data: if not self._iqvia.data:
return return
data = self._iqvia.data[self._kind].get('Location') data = self._iqvia.data[self._type].get('Location')
if not data: if not data:
return return
@ -155,22 +169,21 @@ class IndexSensor(IQVIAEntity):
async def async_update(self): async def async_update(self):
"""Update the sensor.""" """Update the sensor."""
await self._iqvia.async_update()
if not self._iqvia.data: if not self._iqvia.data:
return return
data = {} data = {}
if self._kind in (TYPE_ALLERGY_TODAY, TYPE_ALLERGY_TOMORROW, if self._type in (TYPE_ALLERGY_TODAY, TYPE_ALLERGY_TOMORROW,
TYPE_ALLERGY_YESTERDAY): TYPE_ALLERGY_YESTERDAY):
data = self._iqvia.data[TYPE_ALLERGY_INDEX].get('Location') data = self._iqvia.data[TYPE_ALLERGY_INDEX].get('Location')
elif self._kind in (TYPE_ASTHMA_TODAY, TYPE_ASTHMA_TOMORROW, elif self._type in (TYPE_ASTHMA_TODAY, TYPE_ASTHMA_TOMORROW,
TYPE_ASTHMA_YESTERDAY): TYPE_ASTHMA_YESTERDAY):
data = self._iqvia.data[TYPE_ASTHMA_INDEX].get('Location') data = self._iqvia.data[TYPE_ASTHMA_INDEX].get('Location')
if not data: if not data:
return return
key = self._kind.split('_')[-1].title() key = self._type.split('_')[-1].title()
[period] = [p for p in data['periods'] if p['Type'] == key] [period] = [p for p in data['periods'] if p['Type'] == key]
[rating] = [ [rating] = [
i['label'] for i in RATING_MAPPING i['label'] for i in RATING_MAPPING
@ -184,7 +197,7 @@ class IndexSensor(IQVIAEntity):
ATTR_ZIP_CODE: data['ZIP'] ATTR_ZIP_CODE: data['ZIP']
}) })
if self._kind in (TYPE_ALLERGY_TODAY, TYPE_ALLERGY_TOMORROW, if self._type in (TYPE_ALLERGY_TODAY, TYPE_ALLERGY_TOMORROW,
TYPE_ALLERGY_YESTERDAY): TYPE_ALLERGY_YESTERDAY):
for idx, attrs in enumerate(period['Triggers']): for idx, attrs in enumerate(period['Triggers']):
index = idx + 1 index = idx + 1
@ -196,7 +209,7 @@ class IndexSensor(IQVIAEntity):
'{0}_{1}'.format(ATTR_ALLERGEN_TYPE, index): '{0}_{1}'.format(ATTR_ALLERGEN_TYPE, index):
attrs['PlantType'], attrs['PlantType'],
}) })
elif self._kind in (TYPE_ASTHMA_TODAY, TYPE_ASTHMA_TOMORROW, elif self._type in (TYPE_ASTHMA_TODAY, TYPE_ASTHMA_TOMORROW,
TYPE_ASTHMA_YESTERDAY): TYPE_ASTHMA_YESTERDAY):
for idx, attrs in enumerate(period['Triggers']): for idx, attrs in enumerate(period['Triggers']):
index = idx + 1 index = idx + 1