"""Pogoda component.""" from __future__ import annotations from collections.abc import Callable from datetime import datetime, timezone import logging from homeassistant.components.weather import ( Forecast, WeatherEntity, WeatherEntityFeature, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( STATE_UNAVAILABLE, STATE_UNKNOWN, UnitOfSpeed, UnitOfTemperature, ) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ( ATTR_API_CONDITION, ATTR_API_FEELS_LIKE_TEMPERATURE, ATTR_API_IMAGE, ATTR_API_TEMPERATURE, ATTR_API_WIND_BEARING, ATTR_API_WIND_GUST, ATTR_API_WIND_SPEED, ATTR_API_YA_CONDITION, ATTR_FORECAST_DATA, ATTR_FORECAST_HOURLY, ATTR_FORECAST_HOURLY_ICONS, ATTR_FORECAST_TWICE_DAILY, ATTR_FORECAST_TWICE_DAILY_ICONS, ATTRIBUTION, DOMAIN, ENTRY_NAME, TEMPERATURE_CONVERTER, UPDATER, WIND_SPEED_CONVERTER, convert_unit_value, ) from .device_trigger import TRIGGERS from .updater import WeatherUpdater _LOGGER = logging.getLogger(__name__) def _get_converter( converter: Callable[[float, str, str], float], unit_from: str, unit_to: str ) -> Callable[[float], float | None]: def wrap(value: float | None) -> float | None: return convert_unit_value(converter, value, unit_from, unit_to) return wrap async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up weather "Yandex.Pogoda" weather entry.""" domain_data = hass.data[DOMAIN][config_entry.entry_id] name = domain_data[ENTRY_NAME] updater = domain_data[UPDATER] async_add_entities([YandexWeather(name, config_entry, updater, hass)], False) class YandexWeather(WeatherEntity, CoordinatorEntity, RestoreEntity): """Yandex.Pogoda entry.""" _attr_attribution = ATTRIBUTION _attr_native_wind_speed_unit = UnitOfSpeed.METERS_PER_SECOND _attr_native_temperature_unit = UnitOfTemperature.CELSIUS _hourly_forecast: list[Forecast] | None _twice_daily_forecast: list[Forecast] | None _attr_supported_features = ( WeatherEntityFeature.FORECAST_HOURLY | WeatherEntityFeature.FORECAST_TWICE_DAILY ) _unrecorded_attributes = frozenset( { "forecast_hourly", "forecast_twice_daily", ATTR_FORECAST_HOURLY_ICONS, ATTR_FORECAST_TWICE_DAILY_ICONS, } ) coordinator: WeatherUpdater def __init__( self, name, config_entry: ConfigEntry, updater: WeatherUpdater, hass: HomeAssistant, ) -> None: """Initialize entry.""" WeatherEntity.__init__(self) CoordinatorEntity.__init__(self, coordinator=updater) RestoreEntity.__init__(self) self.hass = hass self._attr_name = name self._attr_condition = None self._attr_unique_id = config_entry.unique_id self._hourly_forecast = [] self._twice_daily_forecast = [] self._attr_device_info = self.coordinator.device_info async def async_added_to_hass(self) -> None: """When entity is added to hass.""" await RestoreEntity.async_added_to_hass(self) await CoordinatorEntity.async_added_to_hass(self) state = await self.async_get_last_state() if not state: _LOGGER.debug("Have no state for restore!") await self.coordinator.async_config_entry_first_refresh() return if state.state in (STATE_UNAVAILABLE, STATE_UNKNOWN): self._attr_available = False await self.coordinator.async_config_entry_first_refresh() else: _LOGGER.debug(f"state for restore: {state}") self._attr_available = True self._attr_condition = state.state temp_converter = _get_converter( TEMPERATURE_CONVERTER, state.attributes.get("temperature_unit") or self._attr_native_temperature_unit, self._attr_native_temperature_unit, ) ws_converter = _get_converter( WIND_SPEED_CONVERTER, state.attributes.get("wind_speed_unit") or self._attr_native_wind_speed_unit, self._attr_native_wind_speed_unit, ) self._attr_native_temperature = temp_converter( state.attributes.get("temperature") ) self._attr_native_wind_speed = ws_converter( state.attributes.get("wind_speed") ) self._attr_native_wind_gust_speed = ws_converter( state.attributes.get("wind_gust_speed") ) self._attr_native_apparent_temperature = temp_converter( state.attributes.get("apparent_temperature") ) self._attr_wind_bearing = state.attributes.get("wind_bearing") self._attr_entity_picture = state.attributes.get("entity_picture") self._attr_extra_state_attributes = {} f_data = { ATTR_FORECAST_HOURLY: state.attributes.get("forecast_hourly"), ATTR_FORECAST_TWICE_DAILY: state.attributes.get("forecast_twice_daily"), } self._update_forecast_data(f_data) for attr in ( ATTR_API_YA_CONDITION, ATTR_FORECAST_HOURLY_ICONS, ATTR_FORECAST_TWICE_DAILY_ICONS, ): if value := state.attributes.get(attr): self._attr_extra_state_attributes[attr] = value # last_updated is last call of self.async_write_ha_state(), not a real last update since_last_update = datetime.now(timezone.utc) - state.last_updated.replace( tzinfo=timezone.utc ) _LOGGER.debug( f"Time since last update: {since_last_update} ({state.last_updated}), " f"update interval is {self.coordinator.update_interval}" ) if since_last_update > self.coordinator.update_interval: await self.coordinator.async_config_entry_first_refresh() else: self.coordinator.schedule_refresh( offset=self.coordinator.update_interval - since_last_update ) self.async_write_ha_state() def _update_forecast_data(self, forecast_data: dict): self._hourly_forecast = forecast_data.get(ATTR_FORECAST_HOURLY, []) self._twice_daily_forecast = forecast_data.get(ATTR_FORECAST_TWICE_DAILY, []) self._attr_extra_state_attributes["forecast_hourly"] = self._hourly_forecast self._attr_extra_state_attributes["forecast_twice_daily"] = ( self._twice_daily_forecast ) def _handle_coordinator_update(self) -> None: self._attr_available = True self.update_condition_and_fire_event( new_condition=self.coordinator.data.get(ATTR_API_CONDITION) ) self._attr_entity_picture = self.coordinator.data.get(ATTR_API_IMAGE) self._attr_native_temperature = self.coordinator.data.get(ATTR_API_TEMPERATURE) self._attr_native_wind_speed = self.coordinator.data.get(ATTR_API_WIND_SPEED) self._attr_wind_bearing = self.coordinator.data.get(ATTR_API_WIND_BEARING) self._attr_native_wind_gust_speed = self.coordinator.data.get( ATTR_API_WIND_GUST ) self._attr_native_apparent_temperature = self.coordinator.data.get( ATTR_API_FEELS_LIKE_TEMPERATURE ) _LOGGER.debug(f"_handle_coordinator_update: {self._hourly_forecast=}") self._attr_extra_state_attributes = { ATTR_API_YA_CONDITION: self.coordinator.data.get(ATTR_API_YA_CONDITION), ATTR_FORECAST_HOURLY_ICONS: self.coordinator.data.get( ATTR_FORECAST_HOURLY_ICONS ), ATTR_FORECAST_TWICE_DAILY_ICONS: self.coordinator.data.get( ATTR_FORECAST_TWICE_DAILY_ICONS ), } self._update_forecast_data(self.coordinator.data.get(ATTR_FORECAST_DATA, {})) self.async_write_ha_state() def update_condition_and_fire_event(self, new_condition: str): """Set new condition and fire event on change.""" if ( new_condition != self._attr_condition and self.hass is not None and new_condition in TRIGGERS ): self.hass.bus.async_fire( DOMAIN + "_event", { "device_id": self.coordinator.device_id, "type": new_condition, }, ) self._attr_condition = new_condition async def async_forecast_hourly(self) -> list[Forecast] | None: """Return the hourly forecast in native units. Only implement this method if `WeatherEntityFeature.FORECAST_HOURLY` is set """ _LOGGER.debug(f"async_forecast_hourly: {self._hourly_forecast=}") return self._hourly_forecast if self._hourly_forecast else [] async def async_forecast_twice_daily(self) -> list[Forecast] | None: """Return the daily forecast in native units.""" _LOGGER.debug(f"async_forecast_twice_daily: {self._twice_daily_forecast=}") return self._twice_daily_forecast if self._twice_daily_forecast else []