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 commentspull/23572/head
parent
606dbb85d2
commit
8fe95f4bab
|
@ -1,18 +1,22 @@
|
|||
"""Support for IQVIA."""
|
||||
import asyncio
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from pyiqvia import Client
|
||||
from pyiqvia.errors import IQVIAError, InvalidZipError
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import (
|
||||
ATTR_ATTRIBUTION, CONF_MONITORED_CONDITIONS, CONF_SCAN_INTERVAL)
|
||||
from homeassistant.const import ATTR_ATTRIBUTION, CONF_MONITORED_CONDITIONS
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers import (
|
||||
aiohttp_client, config_validation as cv, discovery)
|
||||
from homeassistant.helpers import aiohttp_client, config_validation as cv
|
||||
from homeassistant.helpers.discovery import async_load_platform
|
||||
from homeassistant.helpers.dispatcher import (
|
||||
async_dispatcher_connect, async_dispatcher_send)
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.event import async_track_time_interval
|
||||
from homeassistant.util.decorator import Registry
|
||||
|
||||
from .const import (
|
||||
DATA_CLIENT, DATA_LISTENER, DOMAIN, SENSORS, TOPIC_DATA_UPDATE,
|
||||
|
@ -24,6 +28,7 @@ from .const import (
|
|||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
CONF_ZIP_CODE = 'zip_code'
|
||||
|
||||
DATA_CONFIG = 'config'
|
||||
|
@ -31,8 +36,18 @@ DATA_CONFIG = 'config'
|
|||
DEFAULT_ATTRIBUTION = 'Data provided by IQVIA™'
|
||||
DEFAULT_SCAN_INTERVAL = timedelta(minutes=30)
|
||||
|
||||
NOTIFICATION_ID = 'iqvia_setup'
|
||||
NOTIFICATION_TITLE = 'IQVIA Setup'
|
||||
FETCHER_MAPPING = {
|
||||
(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({
|
||||
DOMAIN: vol.Schema({
|
||||
|
@ -45,16 +60,10 @@ CONFIG_SCHEMA = vol.Schema({
|
|||
|
||||
async def async_setup(hass, config):
|
||||
"""Set up the IQVIA component."""
|
||||
from pyiqvia import Client
|
||||
from pyiqvia.errors import IQVIAError
|
||||
|
||||
hass.data[DOMAIN] = {}
|
||||
hass.data[DOMAIN][DATA_CLIENT] = {}
|
||||
hass.data[DOMAIN][DATA_LISTENER] = {}
|
||||
|
||||
if DOMAIN not in config:
|
||||
return True
|
||||
|
||||
conf = config[DOMAIN]
|
||||
|
||||
websession = aiohttp_client.async_get_clientsession(hass)
|
||||
|
@ -66,17 +75,12 @@ async def async_setup(hass, config):
|
|||
await iqvia.async_update()
|
||||
except IQVIAError as 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
|
||||
|
||||
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):
|
||||
"""Refresh IQVIA data."""
|
||||
|
@ -86,9 +90,7 @@ async def async_setup(hass, config):
|
|||
|
||||
hass.data[DOMAIN][DATA_LISTENER] = async_track_time_interval(
|
||||
hass, refresh,
|
||||
timedelta(
|
||||
seconds=conf.get(
|
||||
CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL.seconds)))
|
||||
DEFAULT_SCAN_INTERVAL)
|
||||
|
||||
return True
|
||||
|
||||
|
@ -103,94 +105,81 @@ class IQVIAData:
|
|||
self.sensor_types = sensor_types
|
||||
self.zip_code = client.zip_code
|
||||
|
||||
async def _get_data(self, method, key):
|
||||
"""Return API data from a specific call."""
|
||||
from pyiqvia.errors import IQVIAError
|
||||
|
||||
try:
|
||||
data = await method()
|
||||
self.data[key] = data
|
||||
except IQVIAError as err:
|
||||
_LOGGER.error('Unable to get "%s" data: %s', key, err)
|
||||
self.data[key] = {}
|
||||
self.fetchers = Registry()
|
||||
self.fetchers.register(TYPE_ALLERGY_FORECAST)(
|
||||
self._client.allergens.extended)
|
||||
self.fetchers.register(TYPE_ALLERGY_HISTORIC)(
|
||||
self._client.allergens.historic)
|
||||
self.fetchers.register(TYPE_ALLERGY_OUTLOOK)(
|
||||
self._client.allergens.outlook)
|
||||
self.fetchers.register(TYPE_ALLERGY_INDEX)(
|
||||
self._client.allergens.current)
|
||||
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):
|
||||
"""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
|
||||
# it sometimes has parts (but not the whole thing) go down:
|
||||
#
|
||||
# 1. If `InvalidZipError` is thrown, quit everything immediately.
|
||||
# 2. If an individual request throws any other error, try the others.
|
||||
try:
|
||||
if TYPE_ALLERGY_FORECAST in self.sensor_types:
|
||||
await self._get_data(
|
||||
self._client.allergens.extended, TYPE_ALLERGY_FORECAST)
|
||||
await self._get_data(
|
||||
self._client.allergens.outlook, TYPE_ALLERGY_OUTLOOK)
|
||||
# they sometimes have parts (but not the whole thing) go down:
|
||||
# 1. If `InvalidZipError` is thrown, quit everything immediately.
|
||||
# 2. If a single request throws any other error, try the others.
|
||||
for key, result in zip(tasks, results):
|
||||
if isinstance(result, InvalidZipError):
|
||||
_LOGGER.error("No data for ZIP: %s", self._client.zip_code)
|
||||
self.data = {}
|
||||
return
|
||||
|
||||
if TYPE_ALLERGY_HISTORIC in self.sensor_types:
|
||||
await self._get_data(
|
||||
self._client.allergens.historic, TYPE_ALLERGY_HISTORIC)
|
||||
if isinstance(result, IQVIAError):
|
||||
_LOGGER.error('Unable to get %s data: %s', key, result)
|
||||
self.data[key] = {}
|
||||
continue
|
||||
|
||||
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 = {}
|
||||
_LOGGER.debug('Loaded new %s data', key)
|
||||
self.data[key] = result
|
||||
|
||||
|
||||
class IQVIAEntity(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."""
|
||||
self._async_unsub_dispatcher_connect = None
|
||||
self._attrs = {ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION}
|
||||
self._icon = icon
|
||||
self._iqvia = iqvia
|
||||
self._kind = kind
|
||||
self._name = name
|
||||
self._state = None
|
||||
self._type = sensor_type
|
||||
self._zip_code = zip_code
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
"""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):
|
||||
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):
|
||||
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
|
||||
def device_state_attributes(self):
|
||||
|
@ -215,7 +204,7 @@ class IQVIAEntity(Entity):
|
|||
@property
|
||||
def unique_id(self):
|
||||
"""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
|
||||
def unit_of_measurement(self):
|
||||
|
|
|
@ -22,24 +22,15 @@ TYPE_ASTHMA_YESTERDAY = 'asthma_index_yesterday'
|
|||
TYPE_DISEASE_FORECAST = 'disease_average_forecasted'
|
||||
|
||||
SENSORS = {
|
||||
TYPE_ALLERGY_FORECAST: (
|
||||
'ForecastSensor', 'Allergy Index: Forecasted Average', 'mdi:flower'),
|
||||
TYPE_ALLERGY_HISTORIC: (
|
||||
'HistoricalSensor', 'Allergy Index: Historical Average', 'mdi:flower'),
|
||||
TYPE_ALLERGY_TODAY: ('IndexSensor', 'Allergy Index: Today', 'mdi:flower'),
|
||||
TYPE_ALLERGY_TOMORROW: (
|
||||
'IndexSensor', 'Allergy Index: Tomorrow', 'mdi:flower'),
|
||||
TYPE_ALLERGY_YESTERDAY: (
|
||||
'IndexSensor', 'Allergy Index: Yesterday', 'mdi:flower'),
|
||||
TYPE_ASTHMA_TODAY: ('IndexSensor', 'Asthma Index: Today', 'mdi:flower'),
|
||||
TYPE_ASTHMA_TOMORROW: (
|
||||
'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')
|
||||
TYPE_ALLERGY_FORECAST: ('Allergy Index: Forecasted Average', 'mdi:flower'),
|
||||
TYPE_ALLERGY_HISTORIC: ('Allergy Index: Historical Average', 'mdi:flower'),
|
||||
TYPE_ALLERGY_TODAY: ('Allergy Index: Today', 'mdi:flower'),
|
||||
TYPE_ALLERGY_TOMORROW: ('Allergy Index: Tomorrow', 'mdi:flower'),
|
||||
TYPE_ALLERGY_YESTERDAY: ('Allergy Index: Yesterday', 'mdi:flower'),
|
||||
TYPE_ASTHMA_TODAY: ('Asthma Index: Today', 'mdi:flower'),
|
||||
TYPE_ASTHMA_TOMORROW: ('Asthma Index: Tomorrow', 'mdi:flower'),
|
||||
TYPE_ASTHMA_YESTERDAY: ('Asthma Index: Yesterday', 'mdi:flower'),
|
||||
TYPE_ASTHMA_FORECAST: ('Asthma Index: Forecasted Average', 'mdi:flower'),
|
||||
TYPE_ASTHMA_HISTORIC: ('Asthma Index: Historical Average', 'mdi:flower'),
|
||||
TYPE_DISEASE_FORECAST: ('Cold & Flu: Forecasted Average', 'mdi:snowflake')
|
||||
}
|
||||
|
|
|
@ -2,11 +2,15 @@
|
|||
import logging
|
||||
from statistics import mean
|
||||
|
||||
import numpy as np
|
||||
|
||||
from homeassistant.components.iqvia import (
|
||||
DATA_CLIENT, DOMAIN, SENSORS, TYPE_ALLERGY_FORECAST, TYPE_ALLERGY_OUTLOOK,
|
||||
TYPE_ALLERGY_INDEX, TYPE_ALLERGY_TODAY, TYPE_ALLERGY_TOMORROW,
|
||||
TYPE_ALLERGY_YESTERDAY, TYPE_ASTHMA_INDEX, TYPE_ASTHMA_TODAY,
|
||||
TYPE_ASTHMA_TOMORROW, TYPE_ASTHMA_YESTERDAY, IQVIAEntity)
|
||||
DATA_CLIENT, DOMAIN, SENSORS, TYPE_ALLERGY_FORECAST, TYPE_ALLERGY_HISTORIC,
|
||||
TYPE_ALLERGY_OUTLOOK, TYPE_ALLERGY_INDEX, TYPE_ALLERGY_TODAY,
|
||||
TYPE_ALLERGY_TOMORROW, TYPE_ALLERGY_YESTERDAY, TYPE_ASTHMA_FORECAST,
|
||||
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
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
@ -53,11 +57,25 @@ async def async_setup_platform(
|
|||
"""Configure the platform and add the sensors."""
|
||||
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 = []
|
||||
for kind in iqvia.sensor_types:
|
||||
sensor_class, name, icon = SENSORS[kind]
|
||||
sensors.append(
|
||||
globals()[sensor_class](iqvia, kind, name, icon, iqvia.zip_code))
|
||||
for sensor_type in iqvia.sensor_types:
|
||||
klass = sensor_class_mapping[sensor_type]
|
||||
name, icon = SENSORS[sensor_type]
|
||||
sensors.append(klass(iqvia, sensor_type, name, icon, iqvia.zip_code))
|
||||
|
||||
async_add_entities(sensors, True)
|
||||
|
||||
|
@ -72,8 +90,6 @@ def calculate_average_rating(indices):
|
|||
|
||||
def calculate_trend(indices):
|
||||
"""Calculate the "moving average" of a set of indices."""
|
||||
import numpy as np
|
||||
|
||||
def moving_average(data, samples):
|
||||
"""Determine the "moving average" (http://tinyurl.com/yaereb3c)."""
|
||||
ret = np.cumsum(data, dtype=float)
|
||||
|
@ -92,11 +108,10 @@ class ForecastSensor(IQVIAEntity):
|
|||
|
||||
async def async_update(self):
|
||||
"""Update the sensor."""
|
||||
await self._iqvia.async_update()
|
||||
if not self._iqvia.data:
|
||||
return
|
||||
|
||||
data = self._iqvia.data[self._kind].get('Location')
|
||||
data = self._iqvia.data[self._type].get('Location')
|
||||
if not data:
|
||||
return
|
||||
|
||||
|
@ -115,7 +130,7 @@ class ForecastSensor(IQVIAEntity):
|
|||
ATTR_ZIP_CODE: data['ZIP']
|
||||
})
|
||||
|
||||
if self._kind == TYPE_ALLERGY_FORECAST:
|
||||
if self._type == TYPE_ALLERGY_FORECAST:
|
||||
outlook = self._iqvia.data[TYPE_ALLERGY_OUTLOOK]
|
||||
self._attrs[ATTR_OUTLOOK] = outlook.get('Outlook')
|
||||
self._attrs[ATTR_SEASON] = outlook.get('Season')
|
||||
|
@ -128,11 +143,10 @@ class HistoricalSensor(IQVIAEntity):
|
|||
|
||||
async def async_update(self):
|
||||
"""Update the sensor."""
|
||||
await self._iqvia.async_update()
|
||||
if not self._iqvia.data:
|
||||
return
|
||||
|
||||
data = self._iqvia.data[self._kind].get('Location')
|
||||
data = self._iqvia.data[self._type].get('Location')
|
||||
if not data:
|
||||
return
|
||||
|
||||
|
@ -155,22 +169,21 @@ class IndexSensor(IQVIAEntity):
|
|||
|
||||
async def async_update(self):
|
||||
"""Update the sensor."""
|
||||
await self._iqvia.async_update()
|
||||
if not self._iqvia.data:
|
||||
return
|
||||
|
||||
data = {}
|
||||
if self._kind in (TYPE_ALLERGY_TODAY, TYPE_ALLERGY_TOMORROW,
|
||||
if self._type in (TYPE_ALLERGY_TODAY, TYPE_ALLERGY_TOMORROW,
|
||||
TYPE_ALLERGY_YESTERDAY):
|
||||
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):
|
||||
data = self._iqvia.data[TYPE_ASTHMA_INDEX].get('Location')
|
||||
|
||||
if not data:
|
||||
return
|
||||
|
||||
key = self._kind.split('_')[-1].title()
|
||||
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
|
||||
|
@ -184,7 +197,7 @@ class IndexSensor(IQVIAEntity):
|
|||
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):
|
||||
for idx, attrs in enumerate(period['Triggers']):
|
||||
index = idx + 1
|
||||
|
@ -196,7 +209,7 @@ class IndexSensor(IQVIAEntity):
|
|||
'{0}_{1}'.format(ATTR_ALLERGEN_TYPE, index):
|
||||
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):
|
||||
for idx, attrs in enumerate(period['Triggers']):
|
||||
index = idx + 1
|
||||
|
|
Loading…
Reference in New Issue