core/homeassistant/components/climacell/weather.py

398 lines
13 KiB
Python

"""Weather component that handles meteorological data for your location."""
from __future__ import annotations
from abc import abstractmethod
from collections.abc import Mapping
from datetime import datetime
from typing import Any, cast
from pyclimacell.const import CURRENT, DAILY, FORECASTS, HOURLY, NOWCAST
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 (
CONF_API_VERSION,
CONF_NAME,
LENGTH_FEET,
LENGTH_KILOMETERS,
LENGTH_METERS,
LENGTH_MILES,
PRESSURE_HPA,
PRESSURE_INHG,
SPEED_KILOMETERS_PER_HOUR,
SPEED_MILES_PER_HOUR,
TEMP_FAHRENHEIT,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.sun import is_up
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 homeassistant.util.speed import convert as speed_convert
from . import ClimaCellDataUpdateCoordinator, ClimaCellEntity
from .const import (
ATTR_CLOUD_COVER,
ATTR_PRECIPITATION_TYPE,
ATTR_WIND_GUST,
CC_V3_ATTR_CLOUD_COVER,
CC_V3_ATTR_CONDITION,
CC_V3_ATTR_HUMIDITY,
CC_V3_ATTR_OZONE,
CC_V3_ATTR_PRECIPITATION,
CC_V3_ATTR_PRECIPITATION_DAILY,
CC_V3_ATTR_PRECIPITATION_PROBABILITY,
CC_V3_ATTR_PRECIPITATION_TYPE,
CC_V3_ATTR_PRESSURE,
CC_V3_ATTR_TEMPERATURE,
CC_V3_ATTR_TEMPERATURE_HIGH,
CC_V3_ATTR_TEMPERATURE_LOW,
CC_V3_ATTR_TIMESTAMP,
CC_V3_ATTR_VISIBILITY,
CC_V3_ATTR_WIND_DIRECTION,
CC_V3_ATTR_WIND_GUST,
CC_V3_ATTR_WIND_SPEED,
CLEAR_CONDITIONS,
CONDITIONS_V3,
CONF_TIMESTEP,
DEFAULT_FORECAST_TYPE,
DOMAIN,
)
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up a config entry."""
coordinator = hass.data[DOMAIN][config_entry.entry_id]
api_version = config_entry.data[CONF_API_VERSION]
entities = [
ClimaCellV3WeatherEntity(config_entry, coordinator, api_version, forecast_type)
for forecast_type in (DAILY, HOURLY, NOWCAST)
]
async_add_entities(entities)
class BaseClimaCellWeatherEntity(ClimaCellEntity, WeatherEntity):
"""Base ClimaCell weather entity."""
def __init__(
self,
config_entry: ConfigEntry,
coordinator: ClimaCellDataUpdateCoordinator,
api_version: int,
forecast_type: str,
) -> None:
"""Initialize ClimaCell Weather Entity."""
super().__init__(config_entry, coordinator, api_version)
self.forecast_type = forecast_type
self._attr_entity_registry_enabled_default = (
forecast_type == DEFAULT_FORECAST_TYPE
)
self._attr_name = f"{config_entry.data[CONF_NAME]} - {forecast_type.title()}"
self._attr_unique_id = f"{config_entry.unique_id}_{forecast_type}"
@staticmethod
@abstractmethod
def _translate_condition(
condition: str | int | None, sun_is_up: bool = True
) -> str | None:
"""Translate ClimaCell condition into an HA condition."""
def _forecast_dict(
self,
forecast_dt: datetime,
use_datetime: bool,
condition: int | str,
precipitation: float | None,
precipitation_probability: float | None,
temp: float | None,
temp_low: float | None,
wind_direction: float | None,
wind_speed: float | None,
) -> dict[str, Any]:
"""Return formatted Forecast dict from ClimaCell forecast data."""
if use_datetime:
translated_condition = self._translate_condition(
condition, is_up(self.hass, forecast_dt)
)
else:
translated_condition = self._translate_condition(condition, True)
if self.hass.config.units.is_metric:
if precipitation:
precipitation = round(
distance_convert(precipitation / 12, LENGTH_FEET, LENGTH_METERS)
* 1000,
4,
)
if wind_speed:
wind_speed = round(
speed_convert(
wind_speed, SPEED_MILES_PER_HOUR, SPEED_KILOMETERS_PER_HOUR
),
4,
)
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}
@property
def extra_state_attributes(self) -> Mapping[str, Any] | None:
"""Return additional state attributes."""
wind_gust = self.wind_gust
if wind_gust and self.hass.config.units.is_metric:
wind_gust = round(
speed_convert(
self.wind_gust, SPEED_MILES_PER_HOUR, SPEED_KILOMETERS_PER_HOUR
),
4,
)
cloud_cover = self.cloud_cover
return {
ATTR_CLOUD_COVER: cloud_cover,
ATTR_WIND_GUST: wind_gust,
ATTR_PRECIPITATION_TYPE: self.precipitation_type,
}
@property
@abstractmethod
def cloud_cover(self):
"""Return cloud cover."""
@property
@abstractmethod
def wind_gust(self):
"""Return wind gust speed."""
@property
@abstractmethod
def precipitation_type(self):
"""Return precipitation type."""
@property
@abstractmethod
def _pressure(self):
"""Return the raw pressure."""
@property
def pressure(self):
"""Return the pressure."""
if self.hass.config.units.is_metric and self._pressure:
return round(
pressure_convert(self._pressure, PRESSURE_INHG, PRESSURE_HPA), 4
)
return self._pressure
@property
@abstractmethod
def _wind_speed(self):
"""Return the raw wind speed."""
@property
def wind_speed(self):
"""Return the wind speed."""
if self.hass.config.units.is_metric and self._wind_speed:
return round(
speed_convert(
self._wind_speed, SPEED_MILES_PER_HOUR, SPEED_KILOMETERS_PER_HOUR
),
4,
)
return self._wind_speed
@property
@abstractmethod
def _visibility(self):
"""Return the raw visibility."""
@property
def visibility(self):
"""Return the visibility."""
if self.hass.config.units.is_metric and self._visibility:
return round(
distance_convert(self._visibility, LENGTH_MILES, LENGTH_KILOMETERS), 4
)
return self._visibility
class ClimaCellV3WeatherEntity(BaseClimaCellWeatherEntity):
"""Entity that talks to ClimaCell v3 API to retrieve weather data."""
_attr_temperature_unit = TEMP_FAHRENHEIT
@staticmethod
def _translate_condition(
condition: int | str | None, sun_is_up: bool = True
) -> str | None:
"""Translate ClimaCell condition into an HA condition."""
if not condition:
return None
condition = cast(str, condition)
if "clear" in condition.lower():
if sun_is_up:
return CLEAR_CONDITIONS["day"]
return CLEAR_CONDITIONS["night"]
return CONDITIONS_V3[condition]
@property
def temperature(self):
"""Return the platform temperature."""
return self._get_cc_value(
self.coordinator.data[CURRENT], CC_V3_ATTR_TEMPERATURE
)
@property
def _pressure(self):
"""Return the raw pressure."""
return self._get_cc_value(self.coordinator.data[CURRENT], CC_V3_ATTR_PRESSURE)
@property
def humidity(self):
"""Return the humidity."""
return self._get_cc_value(self.coordinator.data[CURRENT], CC_V3_ATTR_HUMIDITY)
@property
def wind_gust(self):
"""Return the wind gust speed."""
return self._get_cc_value(self.coordinator.data[CURRENT], CC_V3_ATTR_WIND_GUST)
@property
def cloud_cover(self):
"""Return the cloud cover."""
return self._get_cc_value(
self.coordinator.data[CURRENT], CC_V3_ATTR_CLOUD_COVER
)
@property
def precipitation_type(self):
"""Return precipitation type."""
return self._get_cc_value(
self.coordinator.data[CURRENT], CC_V3_ATTR_PRECIPITATION_TYPE
)
@property
def _wind_speed(self):
"""Return the raw wind speed."""
return self._get_cc_value(self.coordinator.data[CURRENT], CC_V3_ATTR_WIND_SPEED)
@property
def wind_bearing(self):
"""Return the wind bearing."""
return self._get_cc_value(
self.coordinator.data[CURRENT], CC_V3_ATTR_WIND_DIRECTION
)
@property
def ozone(self):
"""Return the O3 (ozone) level."""
return self._get_cc_value(self.coordinator.data[CURRENT], CC_V3_ATTR_OZONE)
@property
def condition(self):
"""Return the condition."""
return self._translate_condition(
self._get_cc_value(self.coordinator.data[CURRENT], CC_V3_ATTR_CONDITION),
is_up(self.hass),
)
@property
def _visibility(self):
"""Return the raw visibility."""
return self._get_cc_value(self.coordinator.data[CURRENT], CC_V3_ATTR_VISIBILITY)
@property
def forecast(self):
"""Return the forecast."""
# Check if forecasts are available
raw_forecasts = self.coordinator.data.get(FORECASTS, {}).get(self.forecast_type)
if not raw_forecasts:
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 raw_forecasts:
forecast_dt = dt_util.parse_datetime(
self._get_cc_value(forecast, CC_V3_ATTR_TIMESTAMP)
)
use_datetime = True
condition = self._get_cc_value(forecast, CC_V3_ATTR_CONDITION)
precipitation = self._get_cc_value(forecast, CC_V3_ATTR_PRECIPITATION)
precipitation_probability = self._get_cc_value(
forecast, CC_V3_ATTR_PRECIPITATION_PROBABILITY
)
temp = self._get_cc_value(forecast, CC_V3_ATTR_TEMPERATURE)
temp_low = None
wind_direction = self._get_cc_value(forecast, CC_V3_ATTR_WIND_DIRECTION)
wind_speed = self._get_cc_value(forecast, CC_V3_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_V3_ATTR_PRECIPITATION_DAILY
)
temp = next(
(
self._get_cc_value(item, CC_V3_ATTR_TEMPERATURE_HIGH)
for item in forecast[CC_V3_ATTR_TEMPERATURE]
if "max" in item
),
temp,
)
temp_low = next(
(
self._get_cc_value(item, CC_V3_ATTR_TEMPERATURE_LOW)
for item in forecast[CC_V3_ATTR_TEMPERATURE]
if "min" in item
),
temp_low,
)
elif self.forecast_type == NOWCAST and precipitation:
# Precipitation is forecasted in CONF_TIMESTEP increments but in a
# per hour rate, so value needs to be converted to an amount.
precipitation = (
precipitation / 60 * self._config_entry.options[CONF_TIMESTEP]
)
forecasts.append(
self._forecast_dict(
forecast_dt,
use_datetime,
condition,
precipitation,
precipitation_probability,
temp,
temp_low,
wind_direction,
wind_speed,
)
)
return forecasts