1
mirror of https://github.com/home-assistant/core synced 2024-09-03 08:14:07 +02:00
ha-core/homeassistant/components/iqvia/sensor.py
2022-05-06 12:51:19 +02:00

294 lines
8.6 KiB
Python

"""Support for IQVIA sensors."""
from __future__ import annotations
from statistics import mean
from typing import NamedTuple
import numpy as np
from homeassistant.components.sensor import (
SensorEntity,
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_STATE
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import IQVIAEntity
from .const import (
DOMAIN,
TYPE_ALLERGY_FORECAST,
TYPE_ALLERGY_INDEX,
TYPE_ALLERGY_OUTLOOK,
TYPE_ALLERGY_TODAY,
TYPE_ALLERGY_TOMORROW,
TYPE_ASTHMA_FORECAST,
TYPE_ASTHMA_INDEX,
TYPE_ASTHMA_TODAY,
TYPE_ASTHMA_TOMORROW,
TYPE_DISEASE_FORECAST,
TYPE_DISEASE_INDEX,
TYPE_DISEASE_TODAY,
)
ATTR_ALLERGEN_AMOUNT = "allergen_amount"
ATTR_ALLERGEN_GENUS = "allergen_genus"
ATTR_ALLERGEN_NAME = "allergen_name"
ATTR_ALLERGEN_TYPE = "allergen_type"
ATTR_CITY = "city"
ATTR_OUTLOOK = "outlook"
ATTR_RATING = "rating"
ATTR_SEASON = "season"
ATTR_TREND = "trend"
ATTR_ZIP_CODE = "zip_code"
API_CATEGORY_MAPPING = {
TYPE_ALLERGY_TODAY: TYPE_ALLERGY_INDEX,
TYPE_ALLERGY_TOMORROW: TYPE_ALLERGY_INDEX,
TYPE_ALLERGY_TOMORROW: TYPE_ALLERGY_INDEX,
TYPE_ASTHMA_TODAY: TYPE_ASTHMA_INDEX,
TYPE_ASTHMA_TOMORROW: TYPE_ASTHMA_INDEX,
TYPE_DISEASE_TODAY: TYPE_DISEASE_INDEX,
}
class Rating(NamedTuple):
"""Assign label to value range."""
label: str
minimum: float
maximum: float
RATING_MAPPING: list[Rating] = [
Rating(label="Low", minimum=0.0, maximum=2.4),
Rating(label="Low/Medium", minimum=2.5, maximum=4.8),
Rating(label="Medium", minimum=4.9, maximum=7.2),
Rating(label="Medium/High", minimum=7.3, maximum=9.6),
Rating(label="High", minimum=9.7, maximum=12),
]
TREND_FLAT = "Flat"
TREND_INCREASING = "Increasing"
TREND_SUBSIDING = "Subsiding"
FORECAST_SENSOR_DESCRIPTIONS = (
SensorEntityDescription(
key=TYPE_ALLERGY_FORECAST,
name="Allergy Index: Forecasted Average",
icon="mdi:flower",
),
SensorEntityDescription(
key=TYPE_ASTHMA_FORECAST,
name="Asthma Index: Forecasted Average",
icon="mdi:flower",
),
SensorEntityDescription(
key=TYPE_DISEASE_FORECAST,
name="Cold & Flu: Forecasted Average",
icon="mdi:snowflake",
),
)
INDEX_SENSOR_DESCRIPTIONS = (
SensorEntityDescription(
key=TYPE_ALLERGY_TODAY,
name="Allergy Index: Today",
icon="mdi:flower",
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key=TYPE_ALLERGY_TOMORROW,
name="Allergy Index: Tomorrow",
icon="mdi:flower",
),
SensorEntityDescription(
key=TYPE_ASTHMA_TODAY,
name="Asthma Index: Today",
icon="mdi:flower",
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key=TYPE_ASTHMA_TOMORROW,
name="Asthma Index: Tomorrow",
icon="mdi:flower",
),
SensorEntityDescription(
key=TYPE_DISEASE_TODAY,
name="Cold & Flu Index: Today",
icon="mdi:pill",
state_class=SensorStateClass.MEASUREMENT,
),
)
async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""Set up IQVIA sensors based on a config entry."""
sensors: list[ForecastSensor | IndexSensor] = [
ForecastSensor(
hass.data[DOMAIN][entry.entry_id][
API_CATEGORY_MAPPING.get(description.key, description.key)
],
entry,
description,
)
for description in FORECAST_SENSOR_DESCRIPTIONS
]
sensors.extend(
[
IndexSensor(
hass.data[DOMAIN][entry.entry_id][
API_CATEGORY_MAPPING.get(description.key, description.key)
],
entry,
description,
)
for description in INDEX_SENSOR_DESCRIPTIONS
]
)
async_add_entities(sensors)
@callback
def calculate_trend(indices: list[float]) -> str:
"""Calculate the "moving average" of a set of indices."""
index_range = np.arange(0, len(indices))
index_array = np.array(indices)
linear_fit = np.polyfit(index_range, index_array, 1) # type: ignore[no-untyped-call]
slope = round(linear_fit[0], 2)
if slope > 0:
return TREND_INCREASING
if slope < 0:
return TREND_SUBSIDING
return TREND_FLAT
class ForecastSensor(IQVIAEntity, SensorEntity):
"""Define sensor related to forecast data."""
@callback
def update_from_latest_data(self) -> None:
"""Update the sensor."""
if not self.available:
return
data = self.coordinator.data.get("Location", {})
if not data.get("periods"):
return
indices = [p["Index"] for p in data["periods"]]
average = round(mean(indices), 1)
[rating] = [
i.label for i in RATING_MAPPING if i.minimum <= average <= i.maximum
]
self._attr_native_value = average
self._attr_extra_state_attributes.update(
{
ATTR_CITY: data["City"].title(),
ATTR_RATING: rating,
ATTR_STATE: data["State"],
ATTR_TREND: calculate_trend(indices),
ATTR_ZIP_CODE: data["ZIP"],
}
)
if self.entity_description.key == TYPE_ALLERGY_FORECAST:
outlook_coordinator = self.hass.data[DOMAIN][self._entry.entry_id][
TYPE_ALLERGY_OUTLOOK
]
if not outlook_coordinator.last_update_success:
return
self._attr_extra_state_attributes[
ATTR_OUTLOOK
] = outlook_coordinator.data.get("Outlook")
self._attr_extra_state_attributes[
ATTR_SEASON
] = outlook_coordinator.data.get("Season")
class IndexSensor(IQVIAEntity, SensorEntity):
"""Define sensor related to indices."""
@callback
def update_from_latest_data(self) -> None:
"""Update the sensor."""
if not self.coordinator.last_update_success:
return
try:
if self.entity_description.key in (
TYPE_ALLERGY_TODAY,
TYPE_ALLERGY_TOMORROW,
):
data = self.coordinator.data.get("Location")
elif self.entity_description.key in (
TYPE_ASTHMA_TODAY,
TYPE_ASTHMA_TOMORROW,
):
data = self.coordinator.data.get("Location")
elif self.entity_description.key == TYPE_DISEASE_TODAY:
data = self.coordinator.data.get("Location")
except KeyError:
return
key = self.entity_description.key.split("_")[-1].title()
try:
[period] = [p for p in data["periods"] if p["Type"] == key]
except ValueError:
return
[rating] = [
i.label for i in RATING_MAPPING if i.minimum <= period["Index"] <= i.maximum
]
self._attr_extra_state_attributes.update(
{
ATTR_CITY: data["City"].title(),
ATTR_RATING: rating,
ATTR_STATE: data["State"],
ATTR_ZIP_CODE: data["ZIP"],
}
)
if self.entity_description.key in (TYPE_ALLERGY_TODAY, TYPE_ALLERGY_TOMORROW):
for idx, attrs in enumerate(period["Triggers"]):
index = idx + 1
self._attr_extra_state_attributes.update(
{
f"{ATTR_ALLERGEN_GENUS}_{index}": attrs["Genus"],
f"{ATTR_ALLERGEN_NAME}_{index}": attrs["Name"],
f"{ATTR_ALLERGEN_TYPE}_{index}": attrs["PlantType"],
}
)
elif self.entity_description.key in (TYPE_ASTHMA_TODAY, TYPE_ASTHMA_TOMORROW):
for idx, attrs in enumerate(period["Triggers"]):
index = idx + 1
self._attr_extra_state_attributes.update(
{
f"{ATTR_ALLERGEN_NAME}_{index}": attrs["Name"],
f"{ATTR_ALLERGEN_AMOUNT}_{index}": attrs["PPM"],
}
)
elif self.entity_description.key == TYPE_DISEASE_TODAY:
for attrs in period["Triggers"]:
self._attr_extra_state_attributes[
f"{attrs['Name'].lower()}_index"
] = attrs["Index"]
self._attr_native_value = period["Index"]