330 lines
10 KiB
Python
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)
|