"""Support for Dark Sky weather service.""" from __future__ import annotations from dataclasses import dataclass, field from datetime import timedelta import logging from typing import Literal, NamedTuple import forecastio from requests.exceptions import ConnectionError as ConnectError, HTTPError, Timeout import voluptuous as vol from homeassistant.components.sensor import ( PLATFORM_SCHEMA, SensorDeviceClass, SensorEntity, SensorEntityDescription, SensorStateClass, ) from homeassistant.const import ( CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_MONITORED_CONDITIONS, CONF_NAME, CONF_SCAN_INTERVAL, DEGREE, PERCENTAGE, UV_INDEX, UnitOfLength, UnitOfPrecipitationDepth, UnitOfPressure, UnitOfSpeed, UnitOfTemperature, UnitOfVolumetricFlux, ) from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util import Throttle from homeassistant.util.unit_system import METRIC_SYSTEM _LOGGER = logging.getLogger(__name__) CONF_FORECAST = "forecast" CONF_HOURLY_FORECAST = "hourly_forecast" CONF_LANGUAGE = "language" CONF_UNITS = "units" DEFAULT_LANGUAGE = "en" DEFAULT_NAME = "Dark Sky" SCAN_INTERVAL = timedelta(seconds=300) DEPRECATED_SENSOR_TYPES = { "apparent_temperature_max", "apparent_temperature_min", "temperature_max", "temperature_min", } MAP_UNIT_SYSTEM: dict[ Literal["si", "us", "ca", "uk", "uk2"], Literal["si_unit", "us_unit", "ca_unit", "uk_unit", "uk2_unit"], ] = { "si": "si_unit", "us": "us_unit", "ca": "ca_unit", "uk": "uk_unit", "uk2": "uk2_unit", } @dataclass class DarkskySensorEntityDescription(SensorEntityDescription): """Describes Darksky sensor entity.""" si_unit: str | None = None us_unit: str | None = None ca_unit: str | None = None uk_unit: str | None = None uk2_unit: str | None = None forecast_mode: list[str] = field(default_factory=list) SENSOR_TYPES: dict[str, DarkskySensorEntityDescription] = { "summary": DarkskySensorEntityDescription( key="summary", name="Summary", forecast_mode=["currently", "hourly", "daily"], ), "minutely_summary": DarkskySensorEntityDescription( key="minutely_summary", name="Minutely Summary", forecast_mode=[], ), "hourly_summary": DarkskySensorEntityDescription( key="hourly_summary", name="Hourly Summary", forecast_mode=[], ), "daily_summary": DarkskySensorEntityDescription( key="daily_summary", name="Daily Summary", forecast_mode=[], ), "icon": DarkskySensorEntityDescription( key="icon", name="Icon", forecast_mode=["currently", "hourly", "daily"], ), "nearest_storm_distance": DarkskySensorEntityDescription( key="nearest_storm_distance", name="Nearest Storm Distance", si_unit=UnitOfLength.KILOMETERS, us_unit=UnitOfLength.MILES, ca_unit=UnitOfLength.KILOMETERS, uk_unit=UnitOfLength.KILOMETERS, uk2_unit=UnitOfLength.MILES, icon="mdi:weather-lightning", forecast_mode=["currently"], ), "nearest_storm_bearing": DarkskySensorEntityDescription( key="nearest_storm_bearing", name="Nearest Storm Bearing", si_unit=DEGREE, us_unit=DEGREE, ca_unit=DEGREE, uk_unit=DEGREE, uk2_unit=DEGREE, icon="mdi:weather-lightning", forecast_mode=["currently"], ), "precip_type": DarkskySensorEntityDescription( key="precip_type", name="Precip", icon="mdi:weather-pouring", forecast_mode=["currently", "minutely", "hourly", "daily"], ), "precip_intensity": DarkskySensorEntityDescription( key="precip_intensity", name="Precip Intensity", si_unit=UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR, us_unit=UnitOfVolumetricFlux.INCHES_PER_HOUR, ca_unit=UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR, uk_unit=UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR, uk2_unit=UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR, icon="mdi:weather-rainy", forecast_mode=["currently", "minutely", "hourly", "daily"], ), "precip_probability": DarkskySensorEntityDescription( key="precip_probability", name="Precip Probability", si_unit=PERCENTAGE, us_unit=PERCENTAGE, ca_unit=PERCENTAGE, uk_unit=PERCENTAGE, uk2_unit=PERCENTAGE, icon="mdi:water-percent", forecast_mode=["currently", "minutely", "hourly", "daily"], ), "precip_accumulation": DarkskySensorEntityDescription( key="precip_accumulation", name="Precip Accumulation", device_class=SensorDeviceClass.PRECIPITATION, si_unit=UnitOfPrecipitationDepth.CENTIMETERS, us_unit=UnitOfPrecipitationDepth.INCHES, ca_unit=UnitOfPrecipitationDepth.CENTIMETERS, uk_unit=UnitOfPrecipitationDepth.CENTIMETERS, uk2_unit=UnitOfPrecipitationDepth.CENTIMETERS, icon="mdi:weather-snowy", forecast_mode=["hourly", "daily"], ), "temperature": DarkskySensorEntityDescription( key="temperature", name="Temperature", device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, si_unit=UnitOfTemperature.CELSIUS, us_unit=UnitOfTemperature.FAHRENHEIT, ca_unit=UnitOfTemperature.CELSIUS, uk_unit=UnitOfTemperature.CELSIUS, uk2_unit=UnitOfTemperature.CELSIUS, forecast_mode=["currently", "hourly"], ), "apparent_temperature": DarkskySensorEntityDescription( key="apparent_temperature", name="Apparent Temperature", device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, si_unit=UnitOfTemperature.CELSIUS, us_unit=UnitOfTemperature.FAHRENHEIT, ca_unit=UnitOfTemperature.CELSIUS, uk_unit=UnitOfTemperature.CELSIUS, uk2_unit=UnitOfTemperature.CELSIUS, forecast_mode=["currently", "hourly"], ), "dew_point": DarkskySensorEntityDescription( key="dew_point", name="Dew Point", device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, si_unit=UnitOfTemperature.CELSIUS, us_unit=UnitOfTemperature.FAHRENHEIT, ca_unit=UnitOfTemperature.CELSIUS, uk_unit=UnitOfTemperature.CELSIUS, uk2_unit=UnitOfTemperature.CELSIUS, forecast_mode=["currently", "hourly", "daily"], ), "wind_speed": DarkskySensorEntityDescription( key="wind_speed", name="Wind Speed", device_class=SensorDeviceClass.WIND_SPEED, si_unit=UnitOfSpeed.METERS_PER_SECOND, us_unit=UnitOfSpeed.MILES_PER_HOUR, ca_unit=UnitOfSpeed.KILOMETERS_PER_HOUR, uk_unit=UnitOfSpeed.MILES_PER_HOUR, uk2_unit=UnitOfSpeed.MILES_PER_HOUR, forecast_mode=["currently", "hourly", "daily"], ), "wind_bearing": DarkskySensorEntityDescription( key="wind_bearing", name="Wind Bearing", si_unit=DEGREE, us_unit=DEGREE, ca_unit=DEGREE, uk_unit=DEGREE, uk2_unit=DEGREE, icon="mdi:compass", forecast_mode=["currently", "hourly", "daily"], ), "wind_gust": DarkskySensorEntityDescription( key="wind_gust", name="Wind Gust", device_class=SensorDeviceClass.WIND_SPEED, si_unit=UnitOfSpeed.METERS_PER_SECOND, us_unit=UnitOfSpeed.MILES_PER_HOUR, ca_unit=UnitOfSpeed.KILOMETERS_PER_HOUR, uk_unit=UnitOfSpeed.MILES_PER_HOUR, uk2_unit=UnitOfSpeed.MILES_PER_HOUR, icon="mdi:weather-windy-variant", forecast_mode=["currently", "hourly", "daily"], ), "cloud_cover": DarkskySensorEntityDescription( key="cloud_cover", name="Cloud Coverage", si_unit=PERCENTAGE, us_unit=PERCENTAGE, ca_unit=PERCENTAGE, uk_unit=PERCENTAGE, uk2_unit=PERCENTAGE, icon="mdi:weather-partly-cloudy", forecast_mode=["currently", "hourly", "daily"], ), "humidity": DarkskySensorEntityDescription( key="humidity", name="Humidity", device_class=SensorDeviceClass.HUMIDITY, state_class=SensorStateClass.MEASUREMENT, si_unit=PERCENTAGE, us_unit=PERCENTAGE, ca_unit=PERCENTAGE, uk_unit=PERCENTAGE, uk2_unit=PERCENTAGE, forecast_mode=["currently", "hourly", "daily"], ), "pressure": DarkskySensorEntityDescription( key="pressure", name="Pressure", device_class=SensorDeviceClass.PRESSURE, si_unit=UnitOfPressure.MBAR, us_unit=UnitOfPressure.MBAR, ca_unit=UnitOfPressure.MBAR, uk_unit=UnitOfPressure.MBAR, uk2_unit=UnitOfPressure.MBAR, forecast_mode=["currently", "hourly", "daily"], ), "visibility": DarkskySensorEntityDescription( key="visibility", name="Visibility", si_unit=UnitOfLength.KILOMETERS, us_unit=UnitOfLength.MILES, ca_unit=UnitOfLength.KILOMETERS, uk_unit=UnitOfLength.KILOMETERS, uk2_unit=UnitOfLength.MILES, icon="mdi:eye", forecast_mode=["currently", "hourly", "daily"], ), "ozone": DarkskySensorEntityDescription( key="ozone", name="Ozone", device_class=SensorDeviceClass.OZONE, si_unit="DU", us_unit="DU", ca_unit="DU", uk_unit="DU", uk2_unit="DU", forecast_mode=["currently", "hourly", "daily"], ), "apparent_temperature_max": DarkskySensorEntityDescription( key="apparent_temperature_max", name="Daily High Apparent Temperature", device_class=SensorDeviceClass.TEMPERATURE, si_unit=UnitOfTemperature.CELSIUS, us_unit=UnitOfTemperature.FAHRENHEIT, ca_unit=UnitOfTemperature.CELSIUS, uk_unit=UnitOfTemperature.CELSIUS, uk2_unit=UnitOfTemperature.CELSIUS, forecast_mode=["daily"], ), "apparent_temperature_high": DarkskySensorEntityDescription( key="apparent_temperature_high", name="Daytime High Apparent Temperature", device_class=SensorDeviceClass.TEMPERATURE, si_unit=UnitOfTemperature.CELSIUS, us_unit=UnitOfTemperature.FAHRENHEIT, ca_unit=UnitOfTemperature.CELSIUS, uk_unit=UnitOfTemperature.CELSIUS, uk2_unit=UnitOfTemperature.CELSIUS, forecast_mode=["daily"], ), "apparent_temperature_min": DarkskySensorEntityDescription( key="apparent_temperature_min", name="Daily Low Apparent Temperature", device_class=SensorDeviceClass.TEMPERATURE, si_unit=UnitOfTemperature.CELSIUS, us_unit=UnitOfTemperature.FAHRENHEIT, ca_unit=UnitOfTemperature.CELSIUS, uk_unit=UnitOfTemperature.CELSIUS, uk2_unit=UnitOfTemperature.CELSIUS, forecast_mode=["daily"], ), "apparent_temperature_low": DarkskySensorEntityDescription( key="apparent_temperature_low", name="Overnight Low Apparent Temperature", device_class=SensorDeviceClass.TEMPERATURE, si_unit=UnitOfTemperature.CELSIUS, us_unit=UnitOfTemperature.FAHRENHEIT, ca_unit=UnitOfTemperature.CELSIUS, uk_unit=UnitOfTemperature.CELSIUS, uk2_unit=UnitOfTemperature.CELSIUS, forecast_mode=["daily"], ), "temperature_max": DarkskySensorEntityDescription( key="temperature_max", name="Daily High Temperature", device_class=SensorDeviceClass.TEMPERATURE, si_unit=UnitOfTemperature.CELSIUS, us_unit=UnitOfTemperature.FAHRENHEIT, ca_unit=UnitOfTemperature.CELSIUS, uk_unit=UnitOfTemperature.CELSIUS, uk2_unit=UnitOfTemperature.CELSIUS, forecast_mode=["daily"], ), "temperature_high": DarkskySensorEntityDescription( key="temperature_high", name="Daytime High Temperature", device_class=SensorDeviceClass.TEMPERATURE, si_unit=UnitOfTemperature.CELSIUS, us_unit=UnitOfTemperature.FAHRENHEIT, ca_unit=UnitOfTemperature.CELSIUS, uk_unit=UnitOfTemperature.CELSIUS, uk2_unit=UnitOfTemperature.CELSIUS, forecast_mode=["daily"], ), "temperature_min": DarkskySensorEntityDescription( key="temperature_min", name="Daily Low Temperature", device_class=SensorDeviceClass.TEMPERATURE, si_unit=UnitOfTemperature.CELSIUS, us_unit=UnitOfTemperature.FAHRENHEIT, ca_unit=UnitOfTemperature.CELSIUS, uk_unit=UnitOfTemperature.CELSIUS, uk2_unit=UnitOfTemperature.CELSIUS, forecast_mode=["daily"], ), "temperature_low": DarkskySensorEntityDescription( key="temperature_low", name="Overnight Low Temperature", device_class=SensorDeviceClass.TEMPERATURE, si_unit=UnitOfTemperature.CELSIUS, us_unit=UnitOfTemperature.FAHRENHEIT, ca_unit=UnitOfTemperature.CELSIUS, uk_unit=UnitOfTemperature.CELSIUS, uk2_unit=UnitOfTemperature.CELSIUS, forecast_mode=["daily"], ), "precip_intensity_max": DarkskySensorEntityDescription( key="precip_intensity_max", name="Daily Max Precip Intensity", si_unit=UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR, us_unit=UnitOfVolumetricFlux.INCHES_PER_HOUR, ca_unit=UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR, uk_unit=UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR, uk2_unit=UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR, icon="mdi:thermometer", forecast_mode=["daily"], ), "uv_index": DarkskySensorEntityDescription( key="uv_index", name="UV Index", si_unit=UV_INDEX, us_unit=UV_INDEX, ca_unit=UV_INDEX, uk_unit=UV_INDEX, uk2_unit=UV_INDEX, icon="mdi:weather-sunny", forecast_mode=["currently", "hourly", "daily"], ), "moon_phase": DarkskySensorEntityDescription( key="moon_phase", name="Moon Phase", icon="mdi:weather-night", forecast_mode=["daily"], ), "sunrise_time": DarkskySensorEntityDescription( key="sunrise_time", name="Sunrise", icon="mdi:white-balance-sunny", forecast_mode=["daily"], ), "sunset_time": DarkskySensorEntityDescription( key="sunset_time", name="Sunset", icon="mdi:weather-night", forecast_mode=["daily"], ), "alerts": DarkskySensorEntityDescription( key="alerts", name="Alerts", icon="mdi:alert-circle-outline", forecast_mode=[], ), } class ConditionPicture(NamedTuple): """Entity picture and icon for condition.""" entity_picture: str icon: str CONDITION_PICTURES: dict[str, ConditionPicture] = { "clear-day": ConditionPicture( entity_picture="/static/images/darksky/weather-sunny.svg", icon="mdi:weather-sunny", ), "clear-night": ConditionPicture( entity_picture="/static/images/darksky/weather-night.svg", icon="mdi:weather-night", ), "rain": ConditionPicture( entity_picture="/static/images/darksky/weather-pouring.svg", icon="mdi:weather-pouring", ), "snow": ConditionPicture( entity_picture="/static/images/darksky/weather-snowy.svg", icon="mdi:weather-snowy", ), "sleet": ConditionPicture( entity_picture="/static/images/darksky/weather-hail.svg", icon="mdi:weather-snowy-rainy", ), "wind": ConditionPicture( entity_picture="/static/images/darksky/weather-windy.svg", icon="mdi:weather-windy", ), "fog": ConditionPicture( entity_picture="/static/images/darksky/weather-fog.svg", icon="mdi:weather-fog", ), "cloudy": ConditionPicture( entity_picture="/static/images/darksky/weather-cloudy.svg", icon="mdi:weather-cloudy", ), "partly-cloudy-day": ConditionPicture( entity_picture="/static/images/darksky/weather-partlycloudy.svg", icon="mdi:weather-partly-cloudy", ), "partly-cloudy-night": ConditionPicture( entity_picture="/static/images/darksky/weather-cloudy.svg", icon="mdi:weather-night-partly-cloudy", ), } # Language Supported Codes LANGUAGE_CODES = [ "ar", "az", "be", "bg", "bn", "bs", "ca", "cs", "da", "de", "el", "en", "ja", "ka", "kn", "ko", "eo", "es", "et", "fi", "fr", "he", "hi", "hr", "hu", "id", "is", "it", "kw", "lv", "ml", "mr", "nb", "nl", "pa", "pl", "pt", "ro", "ru", "sk", "sl", "sr", "sv", "ta", "te", "tet", "tr", "uk", "ur", "x-pig-latin", "zh", "zh-tw", ] ALLOWED_UNITS = ["auto", "si", "us", "ca", "uk", "uk2"] ALERTS_ATTRS = ["time", "description", "expires", "severity", "uri", "regions", "title"] PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_MONITORED_CONDITIONS): vol.All( cv.ensure_list, [vol.In(SENSOR_TYPES)] ), vol.Required(CONF_API_KEY): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_UNITS): vol.In(ALLOWED_UNITS), vol.Optional(CONF_LANGUAGE, default=DEFAULT_LANGUAGE): vol.In(LANGUAGE_CODES), vol.Inclusive( CONF_LATITUDE, "coordinates", "Latitude and longitude must exist together" ): cv.latitude, vol.Inclusive( CONF_LONGITUDE, "coordinates", "Latitude and longitude must exist together" ): cv.longitude, vol.Optional(CONF_FORECAST): vol.All(cv.ensure_list, [vol.Range(min=0, max=7)]), vol.Optional(CONF_HOURLY_FORECAST): vol.All( cv.ensure_list, [vol.Range(min=0, max=48)] ), } ) def setup_platform( hass: HomeAssistant, config: ConfigType, add_entities: AddEntitiesCallback, discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the Dark Sky sensor.""" latitude = config.get(CONF_LATITUDE, hass.config.latitude) longitude = config.get(CONF_LONGITUDE, hass.config.longitude) language = config.get(CONF_LANGUAGE) interval = config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL) if CONF_UNITS in config: units = config[CONF_UNITS] elif hass.config.units is METRIC_SYSTEM: units = "si" else: units = "us" forecast_data = DarkSkyData( api_key=config.get(CONF_API_KEY), latitude=latitude, longitude=longitude, units=units, language=language, interval=interval, ) forecast_data.update() forecast_data.update_currently() # If connection failed don't setup platform. if forecast_data.data is None: return name = config.get(CONF_NAME) forecast = config.get(CONF_FORECAST) forecast_hour = config.get(CONF_HOURLY_FORECAST) sensors: list[SensorEntity] = [] for variable in config[CONF_MONITORED_CONDITIONS]: if variable in DEPRECATED_SENSOR_TYPES: _LOGGER.warning("Monitored condition %s is deprecated", variable) description = SENSOR_TYPES[variable] if not description.forecast_mode or "currently" in description.forecast_mode: if variable == "alerts": sensors.append(DarkSkyAlertSensor(forecast_data, description, name)) else: sensors.append(DarkSkySensor(forecast_data, description, name)) if forecast is not None and "daily" in description.forecast_mode: sensors.extend( [ DarkSkySensor( forecast_data, description, name, forecast_day=forecast_day ) for forecast_day in forecast ] ) if forecast_hour is not None and "hourly" in description.forecast_mode: sensors.extend( [ DarkSkySensor( forecast_data, description, name, forecast_hour=forecast_h ) for forecast_h in forecast_hour ] ) add_entities(sensors, True) class DarkSkySensor(SensorEntity): """Implementation of a Dark Sky sensor.""" _attr_attribution = "Powered by Dark Sky" entity_description: DarkskySensorEntityDescription def __init__( self, forecast_data, description: DarkskySensorEntityDescription, name, forecast_day=None, forecast_hour=None, ): """Initialize the sensor.""" self.entity_description = description self.forecast_data = forecast_data self.forecast_day = forecast_day self.forecast_hour = forecast_hour self._icon: str | None = None if forecast_day is not None: self._attr_name = f"{name} {description.name} {forecast_day}d" elif forecast_hour is not None: self._attr_name = f"{name} {description.name} {forecast_hour}h" else: self._attr_name = f"{name} {description.name}" @property def unit_system(self): """Return the unit system of this entity.""" return self.forecast_data.unit_system @property def entity_picture(self) -> str | None: """Return the entity picture to use in the frontend, if any.""" if self._icon is None or "summary" not in self.entity_description.key: return None if self._icon in CONDITION_PICTURES: return CONDITION_PICTURES[self._icon].entity_picture return None def update_unit_of_measurement(self) -> None: """Update units based on unit system.""" unit_key = MAP_UNIT_SYSTEM.get(self.unit_system, "si_unit") self._attr_native_unit_of_measurement = getattr( self.entity_description, unit_key ) @property def icon(self) -> str | None: """Icon to use in the frontend, if any.""" if ( "summary" in self.entity_description.key and self._icon in CONDITION_PICTURES ): return CONDITION_PICTURES[self._icon].icon return self.entity_description.icon def update(self) -> None: """Get the latest data from Dark Sky and updates the states.""" # Call the API for new forecast data. Each sensor will re-trigger this # same exact call, but that's fine. We cache results for a short period # of time to prevent hitting API limits. Note that Dark Sky will # charge users for too many calls in 1 day, so take care when updating. self.forecast_data.update() self.update_unit_of_measurement() sensor_type = self.entity_description.key if sensor_type == "minutely_summary": self.forecast_data.update_minutely() minutely = self.forecast_data.data_minutely self._attr_native_value = getattr(minutely, "summary", "") self._icon = getattr(minutely, "icon", "") elif sensor_type == "hourly_summary": self.forecast_data.update_hourly() hourly = self.forecast_data.data_hourly self._attr_native_value = getattr(hourly, "summary", "") self._icon = getattr(hourly, "icon", "") elif self.forecast_hour is not None: self.forecast_data.update_hourly() hourly = self.forecast_data.data_hourly if hasattr(hourly, "data"): self._attr_native_value = self.get_state( hourly.data[self.forecast_hour] ) else: self._attr_native_value = 0 elif sensor_type == "daily_summary": self.forecast_data.update_daily() daily = self.forecast_data.data_daily self._attr_native_value = getattr(daily, "summary", "") self._icon = getattr(daily, "icon", "") elif self.forecast_day is not None: self.forecast_data.update_daily() daily = self.forecast_data.data_daily if hasattr(daily, "data"): self._attr_native_value = self.get_state(daily.data[self.forecast_day]) else: self._attr_native_value = 0 else: self.forecast_data.update_currently() currently = self.forecast_data.data_currently self._attr_native_value = self.get_state(currently) def get_state(self, data): """Return a new state based on the type. If the sensor type is unknown, the current state is returned. """ sensor_type = self.entity_description.key lookup_type = convert_to_camel(sensor_type) if (state := getattr(data, lookup_type, None)) is None: return None if "summary" in sensor_type: self._icon = getattr(data, "icon", "") # Some state data needs to be rounded to whole values or converted to # percentages if sensor_type in {"precip_probability", "cloud_cover", "humidity"}: return round(state * 100, 1) if sensor_type in { "dew_point", "temperature", "apparent_temperature", "temperature_low", "apparent_temperature_low", "temperature_min", "apparent_temperature_min", "temperature_high", "apparent_temperature_high", "temperature_max", "apparent_temperature_max", "precip_accumulation", "pressure", "ozone", "uvIndex", }: return round(state, 1) return state class DarkSkyAlertSensor(SensorEntity): """Implementation of a Dark Sky sensor.""" entity_description: DarkskySensorEntityDescription _attr_native_value: int | None def __init__( self, forecast_data, description: DarkskySensorEntityDescription, name ): """Initialize the sensor.""" self.entity_description = description self.forecast_data = forecast_data self._alerts = None self._attr_name = f"{name} {description.name}" @property def icon(self): """Icon to use in the frontend, if any.""" if self._attr_native_value is not None and self._attr_native_value > 0: return "mdi:alert-circle" return "mdi:alert-circle-outline" @property def extra_state_attributes(self): """Return the state attributes.""" return self._alerts def update(self) -> None: """Get the latest data from Dark Sky and updates the states.""" # Call the API for new forecast data. Each sensor will re-trigger this # same exact call, but that's fine. We cache results for a short period # of time to prevent hitting API limits. Note that Dark Sky will # charge users for too many calls in 1 day, so take care when updating. self.forecast_data.update() self.forecast_data.update_alerts() alerts = self.forecast_data.data_alerts self._attr_native_value = self.get_state(alerts) def get_state(self, data): """Return a new state based on the type. If the sensor type is unknown, the current state is returned. """ alerts = {} if data is None: self._alerts = alerts return data multiple_alerts = len(data) > 1 for i, alert in enumerate(data): for attr in ALERTS_ATTRS: if multiple_alerts: dkey = f"{attr}_{i!s}" else: dkey = attr alerts[dkey] = getattr(alert, attr) self._alerts = alerts return len(data) def convert_to_camel(data): """Convert snake case (foo_bar_bat) to camel case (fooBarBat). This is not pythonic, but needed for certain situations. """ components = data.split("_") capital_components = "".join(x.title() for x in components[1:]) return f"{components[0]}{capital_components}" class DarkSkyData: """Get the latest data from Darksky.""" def __init__(self, api_key, latitude, longitude, units, language, interval): """Initialize the data object.""" self._api_key = api_key self.latitude = latitude self.longitude = longitude self.units = units self.language = language self._connect_error = False self.data = None self.unit_system = None self.data_currently = None self.data_minutely = None self.data_hourly = None self.data_daily = None self.data_alerts = None # Apply throttling to methods using configured interval self.update = Throttle(interval)(self._update) self.update_currently = Throttle(interval)(self._update_currently) self.update_minutely = Throttle(interval)(self._update_minutely) self.update_hourly = Throttle(interval)(self._update_hourly) self.update_daily = Throttle(interval)(self._update_daily) self.update_alerts = Throttle(interval)(self._update_alerts) def _update(self): """Get the latest data from Dark Sky.""" try: self.data = forecastio.load_forecast( self._api_key, self.latitude, self.longitude, units=self.units, lang=self.language, ) if self._connect_error: self._connect_error = False _LOGGER.info("Reconnected to Dark Sky") except (ConnectError, HTTPError, Timeout, ValueError) as error: if not self._connect_error: self._connect_error = True _LOGGER.error("Unable to connect to Dark Sky: %s", error) self.data = None self.unit_system = self.data and self.data.json["flags"]["units"] def _update_currently(self): """Update currently data.""" self.data_currently = self.data and self.data.currently() def _update_minutely(self): """Update minutely data.""" self.data_minutely = self.data and self.data.minutely() def _update_hourly(self): """Update hourly data.""" self.data_hourly = self.data and self.data.hourly() def _update_daily(self): """Update daily data.""" self.data_daily = self.data and self.data.daily() def _update_alerts(self): """Update alerts data.""" self.data_alerts = self.data and self.data.alerts()