574 lines
18 KiB
Python
574 lines
18 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,
|
|
PrecipitationType,
|
|
WeatherCode,
|
|
)
|
|
|
|
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_ATTR_CLOUD_COVER,
|
|
CC_ATTR_CONDITION,
|
|
CC_ATTR_HUMIDITY,
|
|
CC_ATTR_OZONE,
|
|
CC_ATTR_PRECIPITATION,
|
|
CC_ATTR_PRECIPITATION_PROBABILITY,
|
|
CC_ATTR_PRECIPITATION_TYPE,
|
|
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_GUST,
|
|
CC_ATTR_WIND_SPEED,
|
|
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,
|
|
CONDITIONS_V3,
|
|
CONF_TIMESTEP,
|
|
DEFAULT_FORECAST_TYPE,
|
|
DOMAIN,
|
|
MAX_FORECASTS,
|
|
)
|
|
|
|
|
|
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]
|
|
|
|
api_class = ClimaCellV3WeatherEntity if api_version == 3 else ClimaCellWeatherEntity
|
|
entities = [
|
|
api_class(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 ClimaCellWeatherEntity(BaseClimaCellWeatherEntity):
|
|
"""Entity that talks to ClimaCell v4 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 condition is None:
|
|
return None
|
|
# We won't guard here, instead we will fail hard
|
|
condition = WeatherCode(condition)
|
|
if condition in (WeatherCode.CLEAR, WeatherCode.MOSTLY_CLEAR):
|
|
if sun_is_up:
|
|
return CLEAR_CONDITIONS["day"]
|
|
return CLEAR_CONDITIONS["night"]
|
|
return CONDITIONS[condition]
|
|
|
|
@property
|
|
def temperature(self):
|
|
"""Return the platform temperature."""
|
|
return self._get_current_property(CC_ATTR_TEMPERATURE)
|
|
|
|
@property
|
|
def _pressure(self):
|
|
"""Return the raw pressure."""
|
|
return self._get_current_property(CC_ATTR_PRESSURE)
|
|
|
|
@property
|
|
def humidity(self):
|
|
"""Return the humidity."""
|
|
return self._get_current_property(CC_ATTR_HUMIDITY)
|
|
|
|
@property
|
|
def wind_gust(self):
|
|
"""Return the wind gust speed."""
|
|
return self._get_current_property(CC_ATTR_WIND_GUST)
|
|
|
|
@property
|
|
def cloud_cover(self):
|
|
"""Reteurn the cloud cover."""
|
|
return self._get_current_property(CC_ATTR_CLOUD_COVER)
|
|
|
|
@property
|
|
def precipitation_type(self):
|
|
"""Return precipitation type."""
|
|
precipitation_type = self._get_current_property(CC_ATTR_PRECIPITATION_TYPE)
|
|
if precipitation_type is None:
|
|
return None
|
|
return PrecipitationType(precipitation_type).name.lower()
|
|
|
|
@property
|
|
def _wind_speed(self):
|
|
"""Return the raw wind speed."""
|
|
return self._get_current_property(CC_ATTR_WIND_SPEED)
|
|
|
|
@property
|
|
def wind_bearing(self):
|
|
"""Return the wind bearing."""
|
|
return self._get_current_property(CC_ATTR_WIND_DIRECTION)
|
|
|
|
@property
|
|
def ozone(self):
|
|
"""Return the O3 (ozone) level."""
|
|
return self._get_current_property(CC_ATTR_OZONE)
|
|
|
|
@property
|
|
def condition(self):
|
|
"""Return the condition."""
|
|
return self._translate_condition(
|
|
self._get_current_property(CC_ATTR_CONDITION),
|
|
is_up(self.hass),
|
|
)
|
|
|
|
@property
|
|
def _visibility(self):
|
|
"""Return the raw visibility."""
|
|
return self._get_current_property(CC_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 = []
|
|
max_forecasts = MAX_FORECASTS[self.forecast_type]
|
|
forecast_count = 0
|
|
|
|
# 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(forecast[CC_ATTR_TIMESTAMP])
|
|
|
|
# Throw out past data
|
|
if forecast_dt.date() < dt_util.utcnow().date():
|
|
continue
|
|
|
|
values = forecast["values"]
|
|
use_datetime = True
|
|
|
|
condition = values.get(CC_ATTR_CONDITION)
|
|
precipitation = values.get(CC_ATTR_PRECIPITATION)
|
|
precipitation_probability = values.get(CC_ATTR_PRECIPITATION_PROBABILITY)
|
|
|
|
temp = values.get(CC_ATTR_TEMPERATURE_HIGH)
|
|
temp_low = None
|
|
wind_direction = values.get(CC_ATTR_WIND_DIRECTION)
|
|
wind_speed = values.get(CC_ATTR_WIND_SPEED)
|
|
|
|
if self.forecast_type == DAILY:
|
|
use_datetime = False
|
|
temp_low = values.get(CC_ATTR_TEMPERATURE_LOW)
|
|
if precipitation:
|
|
precipitation = precipitation * 24
|
|
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(
|
|
self._forecast_dict(
|
|
forecast_dt,
|
|
use_datetime,
|
|
condition,
|
|
precipitation,
|
|
precipitation_probability,
|
|
temp,
|
|
temp_low,
|
|
wind_direction,
|
|
wind_speed,
|
|
)
|
|
)
|
|
|
|
forecast_count += 1
|
|
if forecast_count == max_forecasts:
|
|
break
|
|
|
|
return forecasts
|
|
|
|
|
|
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):
|
|
"""Reteurn 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
|