mirror of
https://github.com/home-assistant/core.git
synced 2025-08-02 12:15:08 +02:00
refactor to new pynws
This commit is contained in:
@@ -30,7 +30,7 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTRIBUTION = 'Data from National Weather Service/NOAA'
|
||||
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=30)
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=15)
|
||||
|
||||
CONF_STATION = 'station'
|
||||
|
||||
@@ -42,20 +42,34 @@ ATTR_FORECAST_DAYTIME = 'daytime'
|
||||
# Catalog of NWS icon weather codes listed at:
|
||||
# https://api.weather.gov/icons
|
||||
CONDITION_CLASSES = OrderedDict([
|
||||
('snowy', ['snow', 'snow_sleet', 'sleet', 'blizzard']),
|
||||
('snowy-rainy', ['rain_snow', 'rain_sleet', 'fzra',
|
||||
'rain_fzra', 'snow_fzra']),
|
||||
('snowy', ['Snow',
|
||||
'Sleet',
|
||||
'Blizzard']),
|
||||
('snowy-rainy', ['Rain/snow',
|
||||
'Rain/sleet',
|
||||
'Freezing rain/snow',
|
||||
'Freezing rain',
|
||||
'Rain/freezing rain']),
|
||||
('hail', []),
|
||||
('lightning-rainy', ['tsra', 'tsra_sct', 'tsra_hi']),
|
||||
('lightning-rainy', ['Thunderstorm (high cloud cover)',
|
||||
'Thunderstorm (medium cloud cover)',
|
||||
'Thunderstorm (low cloud cover)']),
|
||||
('lightning', []),
|
||||
('pouring', []),
|
||||
('rainy', ['rain', 'rain_showers', 'rain_showers_hi']),
|
||||
('windy-variant', ['wind_bkn', 'wind_ovc']),
|
||||
('windy', ['wind_skc', 'wind_few', 'wind_sct']),
|
||||
('fog', ['fog']),
|
||||
('clear', ['skc']), # sunny and clear-night
|
||||
('cloudy', ['bkn', 'ovc']),
|
||||
('partlycloudy', ['few', 'sct'])
|
||||
('rainy', ['Rain',
|
||||
'Rain showers (high cloud cover)',
|
||||
'Rain showers (low cloud cover)']),
|
||||
('windy-variant', ['Mostly cloudy and windy',
|
||||
'Overcast and windy']),
|
||||
('windy', ['Fair/clear and windy',
|
||||
'A few clouds and windy',
|
||||
'Partly cloudy and windy']),
|
||||
('fog', ['Fog/mist']),
|
||||
('clear', ['Fair/clear']), # sunny and clear-night
|
||||
('cloudy', ['Mostly cloudy',
|
||||
'Overcast']),
|
||||
('partlycloudy', ['A few clouds',
|
||||
'Partly cloudy']),
|
||||
])
|
||||
|
||||
ERRORS = (aiohttp.ClientError, JSONDecodeError, asyncio.CancelledError)
|
||||
@@ -68,13 +82,6 @@ FORECAST_CLASSES = {
|
||||
|
||||
FORECAST_MODE = ['daynight', 'hourly']
|
||||
|
||||
WIND_DIRECTIONS = ['N', 'NNE', 'NE', 'ENE',
|
||||
'E', 'ESE', 'SE', 'SSE',
|
||||
'S', 'SSW', 'SW', 'WSW',
|
||||
'W', 'WNW', 'NW', 'NNW']
|
||||
|
||||
WIND = {name: idx * 360 / 16 for idx, name in enumerate(WIND_DIRECTIONS)}
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_NAME): cv.string,
|
||||
vol.Optional(CONF_LATITUDE): cv.latitude,
|
||||
@@ -85,34 +92,15 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
})
|
||||
|
||||
|
||||
def parse_icon(icon):
|
||||
"""
|
||||
Parse icon url to NWS weather codes.
|
||||
|
||||
Example:
|
||||
https://api.weather.gov/icons/land/day/skc/tsra,40?size=medium
|
||||
|
||||
Example return:
|
||||
('day', (('skc', 0), ('tsra', 40),))
|
||||
"""
|
||||
icon_list = icon.split('/')
|
||||
time = icon_list[5]
|
||||
weather = [i.split('?')[0] for i in icon_list[6:]]
|
||||
code = [w.split(',')[0] for w in weather]
|
||||
chance = [int(w.split(',')[1]) if len(w.split(',')) == 2 else 0
|
||||
for w in weather]
|
||||
return time, tuple(zip(code, chance))
|
||||
|
||||
|
||||
def convert_condition(time, weather):
|
||||
"""
|
||||
Convert NWS codes to HA condition.
|
||||
|
||||
Choose first condition in CONDITION_CLASSES that exists in weather code.
|
||||
If no match is found, return fitst condition from NWS
|
||||
If no match is found, return first condition from NWS
|
||||
"""
|
||||
conditions = [w[0] for w in weather]
|
||||
prec_prob = [w[1] 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()
|
||||
@@ -123,19 +111,19 @@ def convert_condition(time, weather):
|
||||
if time == 'day':
|
||||
return 'sunny', max(prec_prob)
|
||||
if time == 'night':
|
||||
return 'clear-night', max(prec_prob)
|
||||
return 'clear-night', max(prec_prob)
|
||||
return cond, max(prec_prob)
|
||||
|
||||
|
||||
async def async_setup_platform(hass, config, async_add_entities,
|
||||
discovery_info=None):
|
||||
"""Set up the NWS weather platform."""
|
||||
from pynws import Nws
|
||||
from metar import Metar
|
||||
from pynws import SimpleNws
|
||||
latitude = config.get(CONF_LATITUDE, hass.config.latitude)
|
||||
longitude = config.get(CONF_LONGITUDE, hass.config.longitude)
|
||||
station = config.get(CONF_STATION)
|
||||
api_key = config[CONF_API_KEY]
|
||||
mode = config[CONF_MODE]
|
||||
|
||||
if None in (latitude, longitude):
|
||||
_LOGGER.error("Latitude/longitude not set in Home Assistant config")
|
||||
@@ -143,77 +131,54 @@ async def async_setup_platform(hass, config, async_add_entities,
|
||||
|
||||
websession = async_get_clientsession(hass)
|
||||
# ID request as being from HA, pynws prepends the api_key in addition
|
||||
api_key_ha = '{} {}'.format(api_key, 'homeassistant')
|
||||
nws = Nws(websession, latlon=(float(latitude), float(longitude)),
|
||||
userid=api_key_ha)
|
||||
api_key_ha = f"{api_key} homeassistant"
|
||||
nws = simple_nws(lat, lon, userid=api_key_ha, mode, websession)
|
||||
|
||||
_LOGGER.debug("Setting up station: %s", station)
|
||||
if station is None:
|
||||
try:
|
||||
with async_timeout.timeout(10, loop=hass.loop):
|
||||
stations = await nws.stations()
|
||||
except ERRORS as status:
|
||||
_LOGGER.error("Error getting station list for %s: %s",
|
||||
(latitude, longitude), status)
|
||||
raise PlatformNotReady
|
||||
_LOGGER.debug("Station list: %s", stations)
|
||||
nws.station = stations[0]
|
||||
_LOGGER.debug("Initialized for coordinates %s, %s -> station %s",
|
||||
latitude, longitude, stations[0])
|
||||
else:
|
||||
nws.station = station
|
||||
_LOGGER.debug("Initialized station %s", station[0])
|
||||
try:
|
||||
await nws.set_station(station)
|
||||
except ERRORS as status:
|
||||
_LOGGER.error("Error getting station list for %s: %s",
|
||||
(latitude, longitude), status)
|
||||
raise PlatformNotReady
|
||||
|
||||
_LOGGER.debug("Station list: %s", nws.stations)
|
||||
_LOGGER.debug("Initialized for coordinates %s, %s -> station %s",
|
||||
latitude, longitude, nws.station)
|
||||
|
||||
async_add_entities(
|
||||
[NWSWeather(nws, Metar.Metar, hass.config.units, config)],
|
||||
[NWSWeather(nws, hass.config.units, config)],
|
||||
True)
|
||||
|
||||
|
||||
class NWSWeather(WeatherEntity):
|
||||
"""Representation of a weather condition."""
|
||||
|
||||
def __init__(self, nws, metar, units, config):
|
||||
def __init__(self, nws, units, config):
|
||||
"""Initialise the platform with a data instance and station name."""
|
||||
self._nws = nws
|
||||
self._metar = metar
|
||||
self._station_name = config.get(CONF_NAME, self._nws.station)
|
||||
|
||||
self._metar_obs = None
|
||||
self._observation = None
|
||||
self._forecast = None
|
||||
self._description = None
|
||||
self._is_metric = units.is_metric
|
||||
self._mode = config[CONF_MODE]
|
||||
self.nws = nws
|
||||
self.station_name = config.get(CONF_NAME, self._nws.station)
|
||||
self.is_metric = units.is_metric
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
async def async_update(self):
|
||||
"""Update Condition."""
|
||||
_LOGGER.debug("Updating station observations %s", self._nws.station)
|
||||
_LOGGER.debug("Updating station observations %s", self.nws.station)
|
||||
try:
|
||||
with async_timeout.timeout(10, loop=self.hass.loop):
|
||||
obs = await self._nws.observations(limit=1)
|
||||
await self.nws.update_observations()
|
||||
except ERRORS as status:
|
||||
_LOGGER.error("Error updating observation from station %s: %s",
|
||||
self._nws.station, status)
|
||||
self.nws.station, status)
|
||||
else:
|
||||
self._observation = obs[0]
|
||||
metar_msg = self._observation.get('rawMessage')
|
||||
if metar_msg:
|
||||
self._metar_obs = self._metar(metar_msg)
|
||||
else:
|
||||
self._metar_obs = None
|
||||
_LOGGER.debug("Observations: %s", self._observation)
|
||||
self.observation = self.nws.observation
|
||||
_LOGGER.debug("Updating forecast")
|
||||
try:
|
||||
if self._mode == 'daynight':
|
||||
self._forecast = await self._nws.forecast()
|
||||
elif self._mode == 'hourly':
|
||||
self._forecast = await self._nws.forecast_hourly()
|
||||
await self.nws.update_forecast()
|
||||
except ERRORS as status:
|
||||
_LOGGER.error("Error updating forecast from station %s: %s",
|
||||
self._nws.station, status)
|
||||
self.nws.station, status)
|
||||
else:
|
||||
_LOGGER.debug("Forecasts: %s", self._forecast)
|
||||
self.forecast = self.nws.forecast
|
||||
return
|
||||
|
||||
@property
|
||||
@@ -224,17 +189,15 @@ class NWSWeather(WeatherEntity):
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the station."""
|
||||
return self._station_name
|
||||
return self.station_name
|
||||
|
||||
@property
|
||||
def temperature(self):
|
||||
"""Return the current temperature."""
|
||||
temp_c = None
|
||||
if self._observation:
|
||||
temp_c = self._observation.get('temperature', {}).get('value')
|
||||
if temp_c is None and self._metar_obs and self._metar_obs.temp:
|
||||
temp_c = self._metar_obs.temp.value(units='C')
|
||||
if temp_c is not None:
|
||||
if self.observation:
|
||||
temp_c = self.observation.get('temperature')
|
||||
if temp_c:
|
||||
return convert_temperature(temp_c, TEMP_CELSIUS, TEMP_FAHRENHEIT)
|
||||
return None
|
||||
|
||||
@@ -243,18 +206,10 @@ class NWSWeather(WeatherEntity):
|
||||
"""Return the current pressure."""
|
||||
pressure_pa = None
|
||||
if self._observation:
|
||||
pressure_pa = self._observation.get('seaLevelPressure',
|
||||
{}).get('value')
|
||||
|
||||
if pressure_pa is None and self._metar_obs and self._metar_obs.press:
|
||||
pressure_hpa = self._metar_obs.press.value(units='HPA')
|
||||
if pressure_hpa is None:
|
||||
return None
|
||||
pressure_pa = convert_pressure(pressure_hpa, PRESSURE_HPA,
|
||||
PRESSURE_PA)
|
||||
pressure_pa = self.observation.get('seaLevelPressure')
|
||||
if pressure_pa is None:
|
||||
return None
|
||||
if self._is_metric:
|
||||
if self.is_metric:
|
||||
pressure = convert_pressure(pressure_pa,
|
||||
PRESSURE_PA, PRESSURE_HPA)
|
||||
pressure = round(pressure)
|
||||
@@ -268,19 +223,16 @@ class NWSWeather(WeatherEntity):
|
||||
def humidity(self):
|
||||
"""Return the name of the sensor."""
|
||||
humidity = None
|
||||
if self._observation:
|
||||
humidity = self._observation.get('relativeHumidity',
|
||||
{}).get('value')
|
||||
if self.observation:
|
||||
humidity = self.observation.get('relativeHumidity')
|
||||
return humidity
|
||||
|
||||
@property
|
||||
def wind_speed(self):
|
||||
"""Return the current windspeed."""
|
||||
wind_m_s = None
|
||||
if self._observation:
|
||||
wind_m_s = self._observation.get('windSpeed', {}).get('value')
|
||||
if wind_m_s is None and self._metar_obs and self._metar_obs.wind_speed:
|
||||
wind_m_s = self._metar_obs.wind_speed.value(units='MPS')
|
||||
if self.observation:
|
||||
wind_m_s = self.observation.get('windSpeed')
|
||||
if wind_m_s is None:
|
||||
return None
|
||||
wind_m_hr = wind_m_s * 3600
|
||||
@@ -296,12 +248,8 @@ class NWSWeather(WeatherEntity):
|
||||
def wind_bearing(self):
|
||||
"""Return the current wind bearing (degrees)."""
|
||||
wind_bearing = None
|
||||
if self._observation:
|
||||
wind_bearing = self._observation.get('windDirection',
|
||||
{}).get('value')
|
||||
if wind_bearing is None and (self._metar_obs
|
||||
and self._metar_obs.wind_dir):
|
||||
wind_bearing = self._metar_obs.wind_dir.value()
|
||||
if self.observation:
|
||||
wind_bearing = self.observation.get('windBearing')
|
||||
return wind_bearing
|
||||
|
||||
@property
|
||||
@@ -312,23 +260,22 @@ class NWSWeather(WeatherEntity):
|
||||
@property
|
||||
def condition(self):
|
||||
"""Return current condition."""
|
||||
icon = None
|
||||
if self._observation:
|
||||
icon = self._observation.get('icon')
|
||||
if icon:
|
||||
time, weather = parse_icon(self._observation['icon'])
|
||||
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
|
||||
return None
|
||||
|
||||
@property
|
||||
def visibility(self):
|
||||
"""Return visibility."""
|
||||
vis_m = None
|
||||
if self._observation:
|
||||
vis_m = self._observation.get('visibility', {}).get('value')
|
||||
if vis_m is None and self._metar_obs and self._metar_obs.vis:
|
||||
vis_m = self._metar_obs.vis.value(units='M')
|
||||
if self.observation:
|
||||
vis_m = self._observation.get('visibility')
|
||||
if vis_m is None:
|
||||
return None
|
||||
|
||||
@@ -343,27 +290,26 @@ class NWSWeather(WeatherEntity):
|
||||
"""Return forecast."""
|
||||
forecast = []
|
||||
for forecast_entry in self._forecast:
|
||||
data = {attr: forecast_entry[name]
|
||||
for attr, name in FORECAST_CLASSES.items()}
|
||||
data = {
|
||||
ATTR_FORECAST_DETAIL_DESCRIPTION: forecast_entry.get('detailedForecast'),
|
||||
ATTR_FORECAST_TEMP: forecast_entry.get('temperature'),
|
||||
ATTR_FORECAST_TIME: forecast_entry.get('startTime'),
|
||||
}
|
||||
|
||||
if self._mode == 'daynight':
|
||||
data[ATTR_FORECAST_DAYTIME] = forecast_entry['isDaytime']
|
||||
time, weather = parse_icon(forecast_entry['icon'])
|
||||
data[ATTR_FORECAST_DAYTIME] = forecast_entry.get('isDaytime')
|
||||
time = forecast_entry.get('iconTime')
|
||||
weather = forecast_entry.get('iconWeather')
|
||||
cond, precip = convert_condition(time, weather)
|
||||
data[ATTR_FORECAST_CONDITION] = cond
|
||||
if precip > 0:
|
||||
data[ATTR_FORECAST_PRECIP_PROB] = precip
|
||||
else:
|
||||
data[ATTR_FORECAST_PRECIP_PROB] = None
|
||||
data[ATTR_FORECAST_PRECIP_PROB] = precip
|
||||
|
||||
data[ATTR_FORECAST_WIND_BEARING] = \
|
||||
WIND[forecast_entry['windDirection']]
|
||||
|
||||
# wind speed reported as '7 mph' or '7 to 10 mph'
|
||||
# if range, take average
|
||||
wind_speed = forecast_entry['windSpeed'].split(' ')[0::2]
|
||||
wind_speed_avg = mean(int(w) for w in wind_speed)
|
||||
wind_speed = forecast_entry.get('windSpeedAvg')
|
||||
if self._is_metric:
|
||||
data[ATTR_FORECAST_WIND_SPEED] = round(
|
||||
convert_distance(wind_speed_avg,
|
||||
convert_distance(wind_speed,
|
||||
LENGTH_MILES, LENGTH_KILOMETERS))
|
||||
else:
|
||||
data[ATTR_FORECAST_WIND_SPEED] = round(wind_speed_avg)
|
||||
|
Reference in New Issue
Block a user