ha-core/homeassistant/components/nws/weather.py

330 lines
10 KiB
Python

"""Support for NWS weather service."""
from __future__ import annotations
from types import MappingProxyType
from typing import TYPE_CHECKING, Any
from homeassistant.components.weather import (
ATTR_CONDITION_CLEAR_NIGHT,
ATTR_CONDITION_SUNNY,
ATTR_FORECAST_CONDITION,
ATTR_FORECAST_NATIVE_TEMP,
ATTR_FORECAST_NATIVE_WIND_SPEED,
ATTR_FORECAST_PRECIPITATION_PROBABILITY,
ATTR_FORECAST_TIME,
ATTR_FORECAST_WIND_BEARING,
Forecast,
WeatherEntity,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
CONF_LATITUDE,
CONF_LONGITUDE,
UnitOfLength,
UnitOfPressure,
UnitOfSpeed,
UnitOfTemperature,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.util.dt import utcnow
from homeassistant.util.unit_conversion import SpeedConverter, TemperatureConverter
from homeassistant.util.unit_system import UnitSystem
from . import base_unique_id, device_info
from .const import (
ATTR_FORECAST_DAYTIME,
ATTR_FORECAST_DETAILED_DESCRIPTION,
ATTRIBUTION,
CONDITION_CLASSES,
COORDINATOR_FORECAST,
COORDINATOR_FORECAST_HOURLY,
COORDINATOR_OBSERVATION,
DAYNIGHT,
DOMAIN,
FORECAST_VALID_TIME,
HOURLY,
NWS_DATA,
OBSERVATION_VALID_TIME,
)
PARALLEL_UPDATES = 0
def convert_condition(
time: str, weather: tuple[tuple[str, int | None], ...]
) -> tuple[str, int | None]:
"""Convert NWS codes to HA condition.
Choose first condition in CONDITION_CLASSES that exists in weather code.
If no match is found, return first condition from NWS
"""
conditions: list[str] = [w[0] for w in weather]
prec_probs = [w[1] or 0 for w in weather]
# Choose condition with highest priority.
cond = next(
(
key
for key, value in CONDITION_CLASSES.items()
if any(condition in value for condition in conditions)
),
conditions[0],
)
if cond == "clear":
if time == "day":
return ATTR_CONDITION_SUNNY, max(prec_probs)
if time == "night":
return ATTR_CONDITION_CLEAR_NIGHT, max(prec_probs)
return cond, max(prec_probs)
async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""Set up the NWS weather platform."""
hass_data = hass.data[DOMAIN][entry.entry_id]
async_add_entities(
[
NWSWeather(entry.data, hass_data, DAYNIGHT, hass.config.units),
NWSWeather(entry.data, hass_data, HOURLY, hass.config.units),
],
False,
)
if TYPE_CHECKING:
class NWSForecast(Forecast):
"""Forecast with extra fields needed for NWS."""
detailed_description: str | None
daytime: bool | None
class NWSWeather(WeatherEntity):
"""Representation of a weather condition."""
_attr_should_poll = False
def __init__(
self,
entry_data: MappingProxyType[str, Any],
hass_data: dict[str, Any],
mode: str,
units: UnitSystem,
) -> None:
"""Initialise the platform with a data instance and station name."""
self.nws = hass_data[NWS_DATA]
self.latitude = entry_data[CONF_LATITUDE]
self.longitude = entry_data[CONF_LONGITUDE]
self.coordinator_observation = hass_data[COORDINATOR_OBSERVATION]
if mode == DAYNIGHT:
self.coordinator_forecast = hass_data[COORDINATOR_FORECAST]
else:
self.coordinator_forecast = hass_data[COORDINATOR_FORECAST_HOURLY]
self.station = self.nws.station
self.mode = mode
self.observation = None
self._forecast = None
async def async_added_to_hass(self) -> None:
"""Set up a listener and load data."""
self.async_on_remove(
self.coordinator_observation.async_add_listener(self._update_callback)
)
self.async_on_remove(
self.coordinator_forecast.async_add_listener(self._update_callback)
)
self._update_callback()
@callback
def _update_callback(self) -> None:
"""Load data from integration."""
self.observation = self.nws.observation
if self.mode == DAYNIGHT:
self._forecast = self.nws.forecast
else:
self._forecast = self.nws.forecast_hourly
self.async_write_ha_state()
@property
def attribution(self) -> str:
"""Return the attribution."""
return ATTRIBUTION
@property
def name(self) -> str:
"""Return the name of the station."""
return f"{self.station} {self.mode.title()}"
@property
def native_temperature(self) -> float | None:
"""Return the current temperature."""
if self.observation:
return self.observation.get("temperature")
return None
@property
def native_temperature_unit(self) -> str:
"""Return the current temperature unit."""
return UnitOfTemperature.CELSIUS
@property
def native_pressure(self) -> int | None:
"""Return the current pressure."""
if self.observation:
return self.observation.get("seaLevelPressure")
return None
@property
def native_pressure_unit(self) -> str:
"""Return the current pressure unit."""
return UnitOfPressure.PA
@property
def humidity(self) -> float | None:
"""Return the name of the sensor."""
if self.observation:
return self.observation.get("relativeHumidity")
return None
@property
def native_wind_speed(self) -> float | None:
"""Return the current windspeed."""
if self.observation:
return self.observation.get("windSpeed")
return None
@property
def native_wind_speed_unit(self) -> str:
"""Return the current windspeed."""
return UnitOfSpeed.KILOMETERS_PER_HOUR
@property
def wind_bearing(self) -> int | None:
"""Return the current wind bearing (degrees)."""
if self.observation:
return self.observation.get("windDirection")
return None
@property
def condition(self) -> str | None:
"""Return current condition."""
weather = None
if self.observation:
weather = self.observation.get("iconWeather")
time = self.observation.get("iconTime")
if weather:
cond, _ = convert_condition(time, weather)
return cond
return None
@property
def native_visibility(self) -> int | None:
"""Return visibility."""
if self.observation:
return self.observation.get("visibility")
return None
@property
def native_visibility_unit(self) -> str:
"""Return visibility unit."""
return UnitOfLength.METERS
@property
def forecast(self) -> list[Forecast] | None:
"""Return forecast."""
if self._forecast is None:
return None
forecast: list[NWSForecast] = []
for forecast_entry in self._forecast:
data = {
ATTR_FORECAST_DETAILED_DESCRIPTION: forecast_entry.get(
"detailedForecast"
),
ATTR_FORECAST_TIME: forecast_entry.get("startTime"),
}
if (temp := forecast_entry.get("temperature")) is not None:
data[ATTR_FORECAST_NATIVE_TEMP] = TemperatureConverter.convert(
temp, UnitOfTemperature.FAHRENHEIT, UnitOfTemperature.CELSIUS
)
else:
data[ATTR_FORECAST_NATIVE_TEMP] = None
if self.mode == DAYNIGHT:
data[ATTR_FORECAST_DAYTIME] = forecast_entry.get("isDaytime")
time = forecast_entry.get("iconTime")
weather = forecast_entry.get("iconWeather")
if time and weather:
cond, precip = convert_condition(time, weather)
else:
cond, precip = None, None
data[ATTR_FORECAST_CONDITION] = cond
data[ATTR_FORECAST_PRECIPITATION_PROBABILITY] = precip
data[ATTR_FORECAST_WIND_BEARING] = forecast_entry.get("windBearing")
wind_speed = forecast_entry.get("windSpeedAvg")
if wind_speed is not None:
data[ATTR_FORECAST_NATIVE_WIND_SPEED] = SpeedConverter.convert(
wind_speed,
UnitOfSpeed.MILES_PER_HOUR,
UnitOfSpeed.KILOMETERS_PER_HOUR,
)
else:
data[ATTR_FORECAST_NATIVE_WIND_SPEED] = None
forecast.append(data)
return forecast
@property
def unique_id(self) -> str:
"""Return a unique_id for this entity."""
return f"{base_unique_id(self.latitude, self.longitude)}_{self.mode}"
@property
def available(self) -> bool:
"""Return if state is available."""
last_success = (
self.coordinator_observation.last_update_success
and self.coordinator_forecast.last_update_success
)
if (
self.coordinator_observation.last_update_success_time
and self.coordinator_forecast.last_update_success_time
):
last_success_time = (
utcnow() - self.coordinator_observation.last_update_success_time
< OBSERVATION_VALID_TIME
and utcnow() - self.coordinator_forecast.last_update_success_time
< FORECAST_VALID_TIME
)
else:
last_success_time = False
return last_success or last_success_time
async def async_update(self) -> None:
"""Update the entity.
Only used by the generic entity update service.
"""
await self.coordinator_observation.async_request_refresh()
await self.coordinator_forecast.async_request_refresh()
@property
def entity_registry_enabled_default(self) -> bool:
"""Return if the entity should be enabled when first added to the entity registry."""
return self.mode == DAYNIGHT
@property
def device_info(self) -> DeviceInfo:
"""Return device info."""
return device_info(self.latitude, self.longitude)