core/homeassistant/components/climacell/weather.py

301 lines
9.7 KiB
Python

"""Weather component that handles meteorological data for your location."""
from datetime import datetime
import logging
from typing import Any, Callable, Dict, List, Optional
from homeassistant.components.weather import (
ATTR_FORECAST_CONDITION,
ATTR_FORECAST_PRECIPITATION,
ATTR_FORECAST_PRECIPITATION_PROBABILITY,
ATTR_FORECAST_TEMP,
ATTR_FORECAST_TEMP_LOW,
ATTR_FORECAST_TIME,
ATTR_FORECAST_WIND_BEARING,
ATTR_FORECAST_WIND_SPEED,
WeatherEntity,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
LENGTH_FEET,
LENGTH_KILOMETERS,
LENGTH_METERS,
LENGTH_MILES,
PRESSURE_HPA,
PRESSURE_INHG,
TEMP_FAHRENHEIT,
)
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.sun import is_up
from homeassistant.helpers.typing import HomeAssistantType
from homeassistant.util import dt as dt_util
from homeassistant.util.distance import convert as distance_convert
from homeassistant.util.pressure import convert as pressure_convert
from . import ClimaCellDataUpdateCoordinator, ClimaCellEntity
from .const import (
CC_ATTR_CONDITION,
CC_ATTR_HUMIDITY,
CC_ATTR_OZONE,
CC_ATTR_PRECIPITATION,
CC_ATTR_PRECIPITATION_DAILY,
CC_ATTR_PRECIPITATION_PROBABILITY,
CC_ATTR_PRESSURE,
CC_ATTR_TEMPERATURE,
CC_ATTR_TEMPERATURE_HIGH,
CC_ATTR_TEMPERATURE_LOW,
CC_ATTR_TIMESTAMP,
CC_ATTR_VISIBILITY,
CC_ATTR_WIND_DIRECTION,
CC_ATTR_WIND_SPEED,
CLEAR_CONDITIONS,
CONDITIONS,
CONF_TIMESTEP,
CURRENT,
DAILY,
DEFAULT_FORECAST_TYPE,
DOMAIN,
FORECASTS,
HOURLY,
NOWCAST,
)
# mypy: allow-untyped-defs, no-check-untyped-defs
_LOGGER = logging.getLogger(__name__)
def _translate_condition(
condition: Optional[str], sun_is_up: bool = True
) -> Optional[str]:
"""Translate ClimaCell condition into an HA condition."""
if not condition:
return None
if "clear" in condition.lower():
if sun_is_up:
return CLEAR_CONDITIONS["day"]
return CLEAR_CONDITIONS["night"]
return CONDITIONS[condition]
def _forecast_dict(
hass: HomeAssistantType,
forecast_dt: datetime,
use_datetime: bool,
condition: str,
precipitation: Optional[float],
precipitation_probability: Optional[float],
temp: Optional[float],
temp_low: Optional[float],
wind_direction: Optional[float],
wind_speed: Optional[float],
) -> Dict[str, Any]:
"""Return formatted Forecast dict from ClimaCell forecast data."""
if use_datetime:
translated_condition = _translate_condition(condition, is_up(hass, forecast_dt))
else:
translated_condition = _translate_condition(condition, True)
if hass.config.units.is_metric:
if precipitation:
precipitation = (
distance_convert(precipitation / 12, LENGTH_FEET, LENGTH_METERS) * 1000
)
if wind_speed:
wind_speed = distance_convert(wind_speed, LENGTH_MILES, LENGTH_KILOMETERS)
data = {
ATTR_FORECAST_TIME: forecast_dt.isoformat(),
ATTR_FORECAST_CONDITION: translated_condition,
ATTR_FORECAST_PRECIPITATION: precipitation,
ATTR_FORECAST_PRECIPITATION_PROBABILITY: precipitation_probability,
ATTR_FORECAST_TEMP: temp,
ATTR_FORECAST_TEMP_LOW: temp_low,
ATTR_FORECAST_WIND_BEARING: wind_direction,
ATTR_FORECAST_WIND_SPEED: wind_speed,
}
return {k: v for k, v in data.items() if v is not None}
async def async_setup_entry(
hass: HomeAssistantType,
config_entry: ConfigEntry,
async_add_entities: Callable[[List[Entity], bool], None],
) -> None:
"""Set up a config entry."""
coordinator = hass.data[DOMAIN][config_entry.entry_id]
entities = [
ClimaCellWeatherEntity(config_entry, coordinator, forecast_type)
for forecast_type in [DAILY, HOURLY, NOWCAST]
]
async_add_entities(entities)
class ClimaCellWeatherEntity(ClimaCellEntity, WeatherEntity):
"""Entity that talks to ClimaCell API to retrieve weather data."""
def __init__(
self,
config_entry: ConfigEntry,
coordinator: ClimaCellDataUpdateCoordinator,
forecast_type: str,
) -> None:
"""Initialize ClimaCell weather entity."""
super().__init__(config_entry, coordinator)
self.forecast_type = forecast_type
@property
def entity_registry_enabled_default(self) -> bool:
"""Return if the entity should be enabled when first added to the entity registry."""
if self.forecast_type == DEFAULT_FORECAST_TYPE:
return True
return False
@property
def name(self) -> str:
"""Return the name of the entity."""
return f"{super().name} - {self.forecast_type.title()}"
@property
def unique_id(self) -> str:
"""Return the unique id of the entity."""
return f"{super().unique_id}_{self.forecast_type}"
@property
def temperature(self):
"""Return the platform temperature."""
return self._get_cc_value(self.coordinator.data[CURRENT], CC_ATTR_TEMPERATURE)
@property
def temperature_unit(self):
"""Return the unit of measurement."""
return TEMP_FAHRENHEIT
@property
def pressure(self):
"""Return the pressure."""
pressure = self._get_cc_value(self.coordinator.data[CURRENT], CC_ATTR_PRESSURE)
if self.hass.config.units.is_metric and pressure:
return pressure_convert(pressure, PRESSURE_INHG, PRESSURE_HPA)
return pressure
@property
def humidity(self):
"""Return the humidity."""
return self._get_cc_value(self.coordinator.data[CURRENT], CC_ATTR_HUMIDITY)
@property
def wind_speed(self):
"""Return the wind speed."""
wind_speed = self._get_cc_value(
self.coordinator.data[CURRENT], CC_ATTR_WIND_SPEED
)
if self.hass.config.units.is_metric and wind_speed:
return distance_convert(wind_speed, LENGTH_MILES, LENGTH_KILOMETERS)
return wind_speed
@property
def wind_bearing(self):
"""Return the wind bearing."""
return self._get_cc_value(
self.coordinator.data[CURRENT], CC_ATTR_WIND_DIRECTION
)
@property
def ozone(self):
"""Return the O3 (ozone) level."""
return self._get_cc_value(self.coordinator.data[CURRENT], CC_ATTR_OZONE)
@property
def condition(self):
"""Return the condition."""
return _translate_condition(
self._get_cc_value(self.coordinator.data[CURRENT], CC_ATTR_CONDITION),
is_up(self.hass),
)
@property
def visibility(self):
"""Return the visibility."""
visibility = self._get_cc_value(
self.coordinator.data[CURRENT], CC_ATTR_VISIBILITY
)
if self.hass.config.units.is_metric and visibility:
return distance_convert(visibility, LENGTH_MILES, LENGTH_KILOMETERS)
return visibility
@property
def forecast(self):
"""Return the forecast."""
# Check if forecasts are available
if not self.coordinator.data[FORECASTS].get(self.forecast_type):
return None
forecasts = []
# Set default values (in cases where keys don't exist), None will be
# returned. Override properties per forecast type as needed
for forecast in self.coordinator.data[FORECASTS][self.forecast_type]:
forecast_dt = dt_util.parse_datetime(
self._get_cc_value(forecast, CC_ATTR_TIMESTAMP)
)
use_datetime = True
condition = self._get_cc_value(forecast, CC_ATTR_CONDITION)
precipitation = self._get_cc_value(forecast, CC_ATTR_PRECIPITATION)
precipitation_probability = self._get_cc_value(
forecast, CC_ATTR_PRECIPITATION_PROBABILITY
)
temp = self._get_cc_value(forecast, CC_ATTR_TEMPERATURE)
temp_low = None
wind_direction = self._get_cc_value(forecast, CC_ATTR_WIND_DIRECTION)
wind_speed = self._get_cc_value(forecast, CC_ATTR_WIND_SPEED)
if self.forecast_type == DAILY:
use_datetime = False
forecast_dt = dt_util.start_of_local_day(forecast_dt)
precipitation = self._get_cc_value(
forecast, CC_ATTR_PRECIPITATION_DAILY
)
temp = next(
(
self._get_cc_value(item, CC_ATTR_TEMPERATURE_HIGH)
for item in forecast[CC_ATTR_TEMPERATURE]
if "max" in item
),
temp,
)
temp_low = next(
(
self._get_cc_value(item, CC_ATTR_TEMPERATURE_LOW)
for item in forecast[CC_ATTR_TEMPERATURE]
if "min" in item
),
temp_low,
)
elif self.forecast_type == NOWCAST:
# Precipitation is forecasted in CONF_TIMESTEP increments but in a
# per hour rate, so value needs to be converted to an amount.
if precipitation:
precipitation = (
precipitation / 60 * self._config_entry.options[CONF_TIMESTEP]
)
forecasts.append(
_forecast_dict(
self.hass,
forecast_dt,
use_datetime,
condition,
precipitation,
precipitation_probability,
temp,
temp_low,
wind_direction,
wind_speed,
)
)
return forecasts