"""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]