"""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"]