core/homeassistant/components/weatherkit/weather.py

259 lines
8.7 KiB
Python

"""Weather entity for Apple WeatherKit integration."""
from typing import Any, cast
from apple_weatherkit import DataSetType
from homeassistant.components.weather import (
ATTR_CONDITION_CLOUDY,
ATTR_CONDITION_EXCEPTIONAL,
ATTR_CONDITION_FOG,
ATTR_CONDITION_HAIL,
ATTR_CONDITION_LIGHTNING,
ATTR_CONDITION_PARTLYCLOUDY,
ATTR_CONDITION_POURING,
ATTR_CONDITION_RAINY,
ATTR_CONDITION_SNOWY,
ATTR_CONDITION_SNOWY_RAINY,
ATTR_CONDITION_SUNNY,
ATTR_CONDITION_WINDY,
Forecast,
SingleCoordinatorWeatherEntity,
WeatherEntityFeature,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
UnitOfLength,
UnitOfPressure,
UnitOfSpeed,
UnitOfTemperature,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import (
ATTR_CURRENT_WEATHER,
ATTR_FORECAST_DAILY,
ATTR_FORECAST_HOURLY,
ATTRIBUTION,
DOMAIN,
)
from .coordinator import WeatherKitDataUpdateCoordinator
from .entity import WeatherKitEntity
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Add a weather entity from a config_entry."""
coordinator: WeatherKitDataUpdateCoordinator = hass.data[DOMAIN][
config_entry.entry_id
]
async_add_entities([WeatherKitWeather(coordinator)])
condition_code_to_hass = {
"BlowingDust": ATTR_CONDITION_WINDY,
"Clear": ATTR_CONDITION_SUNNY,
"Cloudy": ATTR_CONDITION_CLOUDY,
"Foggy": ATTR_CONDITION_FOG,
"Haze": ATTR_CONDITION_FOG,
"MostlyClear": ATTR_CONDITION_SUNNY,
"MostlyCloudy": ATTR_CONDITION_CLOUDY,
"PartlyCloudy": ATTR_CONDITION_PARTLYCLOUDY,
"Smoky": ATTR_CONDITION_FOG,
"Breezy": ATTR_CONDITION_WINDY,
"Windy": ATTR_CONDITION_WINDY,
"Drizzle": ATTR_CONDITION_RAINY,
"HeavyRain": ATTR_CONDITION_POURING,
"IsolatedThunderstorms": ATTR_CONDITION_LIGHTNING,
"Rain": ATTR_CONDITION_RAINY,
"SunShowers": ATTR_CONDITION_RAINY,
"ScatteredThunderstorms": ATTR_CONDITION_LIGHTNING,
"StrongStorms": ATTR_CONDITION_LIGHTNING,
"Thunderstorms": ATTR_CONDITION_LIGHTNING,
"Frigid": ATTR_CONDITION_SNOWY,
"Hail": ATTR_CONDITION_HAIL,
"Hot": ATTR_CONDITION_SUNNY,
"Flurries": ATTR_CONDITION_SNOWY,
"Sleet": ATTR_CONDITION_SNOWY,
"Snow": ATTR_CONDITION_SNOWY,
"SunFlurries": ATTR_CONDITION_SNOWY,
"WintryMix": ATTR_CONDITION_SNOWY,
"Blizzard": ATTR_CONDITION_SNOWY,
"BlowingSnow": ATTR_CONDITION_SNOWY,
"FreezingDrizzle": ATTR_CONDITION_SNOWY_RAINY,
"FreezingRain": ATTR_CONDITION_SNOWY_RAINY,
"HeavySnow": ATTR_CONDITION_SNOWY,
"Hurricane": ATTR_CONDITION_EXCEPTIONAL,
"TropicalStorm": ATTR_CONDITION_EXCEPTIONAL,
}
def _map_daily_forecast(forecast: dict[str, Any]) -> Forecast:
return {
"datetime": forecast["forecastStart"],
"condition": condition_code_to_hass[forecast["conditionCode"]],
"native_temperature": forecast["temperatureMax"],
"native_templow": forecast["temperatureMin"],
"native_precipitation": forecast["precipitationAmount"],
"precipitation_probability": forecast["precipitationChance"] * 100,
"uv_index": forecast["maxUvIndex"],
}
def _map_hourly_forecast(forecast: dict[str, Any]) -> Forecast:
return {
"datetime": forecast["forecastStart"],
"condition": condition_code_to_hass[forecast["conditionCode"]],
"native_temperature": forecast["temperature"],
"native_apparent_temperature": forecast["temperatureApparent"],
"native_dew_point": forecast.get("temperatureDewPoint"),
"native_pressure": forecast["pressure"],
"native_wind_gust_speed": forecast.get("windGust"),
"native_wind_speed": forecast["windSpeed"],
"wind_bearing": forecast.get("windDirection"),
"humidity": forecast["humidity"] * 100,
"native_precipitation": forecast.get("precipitationAmount"),
"precipitation_probability": forecast["precipitationChance"] * 100,
"cloud_coverage": forecast["cloudCover"] * 100,
"uv_index": forecast["uvIndex"],
}
class WeatherKitWeather(
SingleCoordinatorWeatherEntity[WeatherKitDataUpdateCoordinator], WeatherKitEntity
):
"""Weather entity for Apple WeatherKit integration."""
_attr_attribution = ATTRIBUTION
_attr_name = None
_attr_native_temperature_unit = UnitOfTemperature.CELSIUS
_attr_native_pressure_unit = UnitOfPressure.MBAR
_attr_native_visibility_unit = UnitOfLength.KILOMETERS
_attr_native_wind_speed_unit = UnitOfSpeed.KILOMETERS_PER_HOUR
_attr_native_precipitation_unit = UnitOfLength.MILLIMETERS
def __init__(
self,
coordinator: WeatherKitDataUpdateCoordinator,
) -> None:
"""Initialize the platform with a coordinator."""
super().__init__(coordinator)
WeatherKitEntity.__init__(self, coordinator, unique_id_suffix=None)
@property
def supported_features(self) -> WeatherEntityFeature:
"""Determine supported features based on available data sets reported by WeatherKit."""
features = WeatherEntityFeature(0)
if not self.coordinator.supported_data_sets:
return features
if DataSetType.DAILY_FORECAST in self.coordinator.supported_data_sets:
features |= WeatherEntityFeature.FORECAST_DAILY
if DataSetType.HOURLY_FORECAST in self.coordinator.supported_data_sets:
features |= WeatherEntityFeature.FORECAST_HOURLY
return features
@property
def data(self) -> dict[str, Any]:
"""Return coordinator data."""
return self.coordinator.data
@property
def current_weather(self) -> dict[str, Any]:
"""Return current weather data."""
return self.data[ATTR_CURRENT_WEATHER]
@property
def condition(self) -> str | None:
"""Return the current condition."""
condition_code = cast(str, self.current_weather.get("conditionCode"))
condition = condition_code_to_hass[condition_code]
if condition == "sunny" and self.current_weather.get("daylight") is False:
condition = "clear-night"
return condition
@property
def native_temperature(self) -> float | None:
"""Return the current temperature."""
return self.current_weather.get("temperature")
@property
def native_apparent_temperature(self) -> float | None:
"""Return the current apparent_temperature."""
return self.current_weather.get("temperatureApparent")
@property
def native_dew_point(self) -> float | None:
"""Return the current dew_point."""
return self.current_weather.get("temperatureDewPoint")
@property
def native_pressure(self) -> float | None:
"""Return the current pressure."""
return self.current_weather.get("pressure")
@property
def humidity(self) -> float | None:
"""Return the current humidity."""
return cast(float, self.current_weather.get("humidity")) * 100
@property
def cloud_coverage(self) -> float | None:
"""Return the current cloud_coverage."""
return cast(float, self.current_weather.get("cloudCover")) * 100
@property
def uv_index(self) -> float | None:
"""Return the current uv_index."""
return self.current_weather.get("uvIndex")
@property
def native_visibility(self) -> float | None:
"""Return the current visibility."""
return cast(float, self.current_weather.get("visibility")) / 1000
@property
def native_wind_gust_speed(self) -> float | None:
"""Return the current wind_gust_speed."""
return self.current_weather.get("windGust")
@property
def native_wind_speed(self) -> float | None:
"""Return the current wind_speed."""
return self.current_weather.get("windSpeed")
@property
def wind_bearing(self) -> float | None:
"""Return the current wind_bearing."""
return self.current_weather.get("windDirection")
@callback
def _async_forecast_daily(self) -> list[Forecast] | None:
"""Return the daily forecast."""
daily_forecast = self.data.get(ATTR_FORECAST_DAILY)
if not daily_forecast:
return None
forecast = daily_forecast.get("days")
return [_map_daily_forecast(f) for f in forecast]
@callback
def _async_forecast_hourly(self) -> list[Forecast] | None:
"""Return the hourly forecast."""
hourly_forecast = self.data.get(ATTR_FORECAST_HOURLY)
if not hourly_forecast:
return None
forecast = hourly_forecast.get("hours")
return [_map_hourly_forecast(f) for f in forecast]