* Moved climate components with tests into platform dirs. * Updated tests from climate component. * Moved binary_sensor components with tests into platform dirs. * Updated tests from binary_sensor component. * Moved calendar components with tests into platform dirs. * Updated tests from calendar component. * Moved camera components with tests into platform dirs. * Updated tests from camera component. * Moved cover components with tests into platform dirs. * Updated tests from cover component. * Moved device_tracker components with tests into platform dirs. * Updated tests from device_tracker component. * Moved fan components with tests into platform dirs. * Updated tests from fan component. * Moved geo_location components with tests into platform dirs. * Updated tests from geo_location component. * Moved image_processing components with tests into platform dirs. * Updated tests from image_processing component. * Moved light components with tests into platform dirs. * Updated tests from light component. * Moved lock components with tests into platform dirs. * Moved media_player components with tests into platform dirs. * Updated tests from media_player component. * Moved scene components with tests into platform dirs. * Moved sensor components with tests into platform dirs. * Updated tests from sensor component. * Moved switch components with tests into platform dirs. * Updated tests from sensor component. * Moved vacuum components with tests into platform dirs. * Updated tests from vacuum component. * Moved weather components with tests into platform dirs. * Fixed __init__.py files * Fixes for stuff moved as part of this branch. * Fix stuff needed to merge with balloob's branch. * Formatting issues. * Missing __init__.py files. * Fix-ups * Fixup * Regenerated requirements. * Linting errors fixed. * Fixed more broken tests. * Missing init files. * Fix broken tests. * More broken tests * There seems to be a thread race condition. I suspect the logger stuff is running in another thread, which means waiting until the aio loop is done is missing the log messages. Used sleep instead because that allows the logger thread to run. I think the api_streams sensor might not be thread safe. * Disabled tests, will remove sensor in #22147 * Updated coverage and codeowners.
824 lines
36 KiB
Python
824 lines
36 KiB
Python
"""
|
|
Support for WUnderground weather service.
|
|
|
|
For more details about this platform, please refer to the documentation at
|
|
https://home-assistant.io/components/sensor.wunderground/
|
|
"""
|
|
import asyncio
|
|
from datetime import timedelta
|
|
import logging
|
|
import re
|
|
|
|
import aiohttp
|
|
import async_timeout
|
|
import voluptuous as vol
|
|
|
|
from homeassistant.helpers.typing import HomeAssistantType, ConfigType
|
|
from homeassistant.components import sensor
|
|
from homeassistant.components.sensor import PLATFORM_SCHEMA
|
|
from homeassistant.const import (
|
|
CONF_MONITORED_CONDITIONS, CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE,
|
|
TEMP_FAHRENHEIT, TEMP_CELSIUS, LENGTH_INCHES, LENGTH_KILOMETERS,
|
|
LENGTH_MILES, LENGTH_FEET, ATTR_ATTRIBUTION)
|
|
from homeassistant.exceptions import PlatformNotReady
|
|
from homeassistant.helpers.entity import Entity
|
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
|
from homeassistant.util import Throttle
|
|
import homeassistant.helpers.config_validation as cv
|
|
|
|
_RESOURCE = 'http://api.wunderground.com/api/{}/{}/{}/q/'
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
ATTRIBUTION = "Data provided by the WUnderground weather service"
|
|
|
|
CONF_PWS_ID = 'pws_id'
|
|
CONF_LANG = 'lang'
|
|
|
|
DEFAULT_LANG = 'EN'
|
|
|
|
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=5)
|
|
|
|
|
|
# Helper classes for declaring sensor configurations
|
|
|
|
class WUSensorConfig:
|
|
"""WU Sensor Configuration.
|
|
|
|
defines basic HA properties of the weather sensor and
|
|
stores callbacks that can parse sensor values out of
|
|
the json data received by WU API.
|
|
"""
|
|
|
|
def __init__(self, friendly_name, feature, value,
|
|
unit_of_measurement=None, entity_picture=None,
|
|
icon="mdi:gauge", device_state_attributes=None,
|
|
device_class=None):
|
|
"""Constructor.
|
|
|
|
Args:
|
|
friendly_name (string|func): Friendly name
|
|
feature (string): WU feature. See:
|
|
https://www.wunderground.com/weather/api/d/docs?d=data/index
|
|
value (function(WUndergroundData)): callback that
|
|
extracts desired value from WUndergroundData object
|
|
unit_of_measurement (string): unit of measurement
|
|
entity_picture (string): value or callback returning
|
|
URL of entity picture
|
|
icon (string): icon name or URL
|
|
device_state_attributes (dict): dictionary of attributes,
|
|
or callable that returns it
|
|
"""
|
|
self.friendly_name = friendly_name
|
|
self.unit_of_measurement = unit_of_measurement
|
|
self.feature = feature
|
|
self.value = value
|
|
self.entity_picture = entity_picture
|
|
self.icon = icon
|
|
self.device_state_attributes = device_state_attributes or {}
|
|
self.device_class = device_class
|
|
|
|
|
|
class WUCurrentConditionsSensorConfig(WUSensorConfig):
|
|
"""Helper for defining sensor configurations for current conditions."""
|
|
|
|
def __init__(self, friendly_name, field, icon="mdi:gauge",
|
|
unit_of_measurement=None, device_class=None):
|
|
"""Constructor.
|
|
|
|
Args:
|
|
friendly_name (string|func): Friendly name of sensor
|
|
field (string): Field name in the "current_observation"
|
|
dictionary.
|
|
icon (string): icon name or URL, if None sensor
|
|
will use current weather symbol
|
|
unit_of_measurement (string): unit of measurement
|
|
"""
|
|
super().__init__(
|
|
friendly_name,
|
|
"conditions",
|
|
value=lambda wu: wu.data['current_observation'][field],
|
|
icon=icon,
|
|
unit_of_measurement=unit_of_measurement,
|
|
entity_picture=lambda wu: wu.data['current_observation'][
|
|
'icon_url'] if icon is None else None,
|
|
device_state_attributes={
|
|
'date': lambda wu: wu.data['current_observation'][
|
|
'observation_time']
|
|
},
|
|
device_class=device_class
|
|
)
|
|
|
|
|
|
class WUDailyTextForecastSensorConfig(WUSensorConfig):
|
|
"""Helper for defining sensor configurations for daily text forecasts."""
|
|
|
|
def __init__(self, period, field, unit_of_measurement=None):
|
|
"""Constructor.
|
|
|
|
Args:
|
|
period (int): forecast period number
|
|
field (string): field name to use as value
|
|
unit_of_measurement(string): unit of measurement
|
|
"""
|
|
super().__init__(
|
|
friendly_name=lambda wu: wu.data['forecast']['txt_forecast'][
|
|
'forecastday'][period]['title'],
|
|
feature='forecast',
|
|
value=lambda wu: wu.data['forecast']['txt_forecast'][
|
|
'forecastday'][period][field],
|
|
entity_picture=lambda wu: wu.data['forecast']['txt_forecast'][
|
|
'forecastday'][period]['icon_url'],
|
|
unit_of_measurement=unit_of_measurement,
|
|
device_state_attributes={
|
|
'date': lambda wu: wu.data['forecast']['txt_forecast']['date']
|
|
}
|
|
)
|
|
|
|
|
|
class WUDailySimpleForecastSensorConfig(WUSensorConfig):
|
|
"""Helper for defining sensor configurations for daily simpleforecasts."""
|
|
|
|
def __init__(self, friendly_name, period, field, wu_unit=None,
|
|
ha_unit=None, icon=None, device_class=None):
|
|
"""Constructor.
|
|
|
|
Args:
|
|
period (int): forecast period number
|
|
field (string): field name to use as value
|
|
wu_unit (string): "fahrenheit", "celsius", "degrees" etc.
|
|
see the example json at:
|
|
https://www.wunderground.com/weather/api/d/docs?d=data/forecast&MR=1
|
|
ha_unit (string): corresponding unit in home assistant
|
|
title (string): friendly_name of the sensor
|
|
"""
|
|
super().__init__(
|
|
friendly_name=friendly_name,
|
|
feature='forecast',
|
|
value=(lambda wu: wu.data['forecast']['simpleforecast'][
|
|
'forecastday'][period][field][wu_unit])
|
|
if wu_unit else
|
|
(lambda wu: wu.data['forecast']['simpleforecast'][
|
|
'forecastday'][period][field]),
|
|
unit_of_measurement=ha_unit,
|
|
entity_picture=lambda wu: wu.data['forecast']['simpleforecast'][
|
|
'forecastday'][period]['icon_url'] if not icon else None,
|
|
icon=icon,
|
|
device_state_attributes={
|
|
'date': lambda wu: wu.data['forecast']['simpleforecast'][
|
|
'forecastday'][period]['date']['pretty']
|
|
},
|
|
device_class=device_class
|
|
)
|
|
|
|
|
|
class WUHourlyForecastSensorConfig(WUSensorConfig):
|
|
"""Helper for defining sensor configurations for hourly text forecasts."""
|
|
|
|
def __init__(self, period, field):
|
|
"""Constructor.
|
|
|
|
Args:
|
|
period (int): forecast period number
|
|
field (int): field name to use as value
|
|
"""
|
|
super().__init__(
|
|
friendly_name=lambda wu: "{} {}".format(
|
|
wu.data['hourly_forecast'][period]['FCTTIME'][
|
|
'weekday_name_abbrev'],
|
|
wu.data['hourly_forecast'][period]['FCTTIME'][
|
|
'civil']),
|
|
feature='hourly',
|
|
value=lambda wu: wu.data['hourly_forecast'][period][
|
|
field],
|
|
entity_picture=lambda wu: wu.data['hourly_forecast'][
|
|
period]["icon_url"],
|
|
device_state_attributes={
|
|
'temp_c': lambda wu: wu.data['hourly_forecast'][
|
|
period]['temp']['metric'],
|
|
'temp_f': lambda wu: wu.data['hourly_forecast'][
|
|
period]['temp']['english'],
|
|
'dewpoint_c': lambda wu: wu.data['hourly_forecast'][
|
|
period]['dewpoint']['metric'],
|
|
'dewpoint_f': lambda wu: wu.data['hourly_forecast'][
|
|
period]['dewpoint']['english'],
|
|
'precip_prop': lambda wu: wu.data['hourly_forecast'][
|
|
period]['pop'],
|
|
'sky': lambda wu: wu.data['hourly_forecast'][
|
|
period]['sky'],
|
|
'precip_mm': lambda wu: wu.data['hourly_forecast'][
|
|
period]['qpf']['metric'],
|
|
'precip_in': lambda wu: wu.data['hourly_forecast'][
|
|
period]['qpf']['english'],
|
|
'humidity': lambda wu: wu.data['hourly_forecast'][
|
|
period]['humidity'],
|
|
'wind_kph': lambda wu: wu.data['hourly_forecast'][
|
|
period]['wspd']['metric'],
|
|
'wind_mph': lambda wu: wu.data['hourly_forecast'][
|
|
period]['wspd']['english'],
|
|
'pressure_mb': lambda wu: wu.data['hourly_forecast'][
|
|
period]['mslp']['metric'],
|
|
'pressure_inHg': lambda wu: wu.data['hourly_forecast'][
|
|
period]['mslp']['english'],
|
|
'date': lambda wu: wu.data['hourly_forecast'][
|
|
period]['FCTTIME']['pretty'],
|
|
}
|
|
)
|
|
|
|
|
|
class WUAlmanacSensorConfig(WUSensorConfig):
|
|
"""Helper for defining field configurations for almanac sensors."""
|
|
|
|
def __init__(self, friendly_name, field, value_type, wu_unit,
|
|
unit_of_measurement, icon, device_class=None):
|
|
"""Constructor.
|
|
|
|
Args:
|
|
friendly_name (string|func): Friendly name
|
|
field (string): value name returned in 'almanac' dict
|
|
as returned by the WU API
|
|
value_type (string): "record" or "normal"
|
|
wu_unit (string): unit name in WU API
|
|
icon (string): icon name or URL
|
|
unit_of_measurement (string): unit of measurement
|
|
"""
|
|
super().__init__(
|
|
friendly_name=friendly_name,
|
|
feature="almanac",
|
|
value=lambda wu: wu.data['almanac'][field][value_type][wu_unit],
|
|
unit_of_measurement=unit_of_measurement,
|
|
icon=icon,
|
|
device_class="temperature"
|
|
)
|
|
|
|
|
|
class WUAlertsSensorConfig(WUSensorConfig):
|
|
"""Helper for defining field configuration for alerts."""
|
|
|
|
def __init__(self, friendly_name):
|
|
"""Constructor.
|
|
|
|
Args:
|
|
friendly_name (string|func): Friendly name
|
|
"""
|
|
super().__init__(
|
|
friendly_name=friendly_name,
|
|
feature="alerts",
|
|
value=lambda wu: len(wu.data['alerts']),
|
|
icon=lambda wu: "mdi:alert-circle-outline"
|
|
if wu.data['alerts'] else "mdi:check-circle-outline",
|
|
device_state_attributes=self._get_attributes
|
|
)
|
|
|
|
@staticmethod
|
|
def _get_attributes(rest):
|
|
|
|
attrs = {}
|
|
|
|
if 'alerts' not in rest.data:
|
|
return attrs
|
|
|
|
alerts = rest.data['alerts']
|
|
multiple_alerts = len(alerts) > 1
|
|
for data in alerts:
|
|
for alert in ALERTS_ATTRS:
|
|
if data[alert]:
|
|
if multiple_alerts:
|
|
dkey = alert.capitalize() + '_' + data['type']
|
|
else:
|
|
dkey = alert.capitalize()
|
|
attrs[dkey] = data[alert]
|
|
return attrs
|
|
|
|
|
|
# Declaration of supported WU sensors
|
|
# (see above helper classes for argument explanation)
|
|
|
|
SENSOR_TYPES = {
|
|
'alerts': WUAlertsSensorConfig('Alerts'),
|
|
'dewpoint_c': WUCurrentConditionsSensorConfig(
|
|
'Dewpoint', 'dewpoint_c', 'mdi:water', TEMP_CELSIUS),
|
|
'dewpoint_f': WUCurrentConditionsSensorConfig(
|
|
'Dewpoint', 'dewpoint_f', 'mdi:water', TEMP_FAHRENHEIT),
|
|
'dewpoint_string': WUCurrentConditionsSensorConfig(
|
|
'Dewpoint Summary', 'dewpoint_string', 'mdi:water'),
|
|
'feelslike_c': WUCurrentConditionsSensorConfig(
|
|
'Feels Like', 'feelslike_c', 'mdi:thermometer', TEMP_CELSIUS),
|
|
'feelslike_f': WUCurrentConditionsSensorConfig(
|
|
'Feels Like', 'feelslike_f', 'mdi:thermometer', TEMP_FAHRENHEIT),
|
|
'feelslike_string': WUCurrentConditionsSensorConfig(
|
|
'Feels Like', 'feelslike_string', "mdi:thermometer"),
|
|
'heat_index_c': WUCurrentConditionsSensorConfig(
|
|
'Heat index', 'heat_index_c', "mdi:thermometer", TEMP_CELSIUS),
|
|
'heat_index_f': WUCurrentConditionsSensorConfig(
|
|
'Heat index', 'heat_index_f', "mdi:thermometer", TEMP_FAHRENHEIT),
|
|
'heat_index_string': WUCurrentConditionsSensorConfig(
|
|
'Heat Index Summary', 'heat_index_string', "mdi:thermometer"),
|
|
'elevation': WUSensorConfig(
|
|
'Elevation',
|
|
'conditions',
|
|
value=lambda wu: wu.data['current_observation'][
|
|
'observation_location']['elevation'].split()[0],
|
|
unit_of_measurement=LENGTH_FEET,
|
|
icon="mdi:elevation-rise"),
|
|
'location': WUSensorConfig(
|
|
'Location',
|
|
'conditions',
|
|
value=lambda wu: wu.data['current_observation'][
|
|
'display_location']['full'],
|
|
icon="mdi:map-marker"),
|
|
'observation_time': WUCurrentConditionsSensorConfig(
|
|
'Observation Time', 'observation_time', "mdi:clock"),
|
|
'precip_1hr_in': WUCurrentConditionsSensorConfig(
|
|
'Precipitation 1hr', 'precip_1hr_in', "mdi:umbrella", LENGTH_INCHES),
|
|
'precip_1hr_metric': WUCurrentConditionsSensorConfig(
|
|
'Precipitation 1hr', 'precip_1hr_metric', "mdi:umbrella", 'mm'),
|
|
'precip_1hr_string': WUCurrentConditionsSensorConfig(
|
|
'Precipitation 1hr', 'precip_1hr_string', "mdi:umbrella"),
|
|
'precip_today_in': WUCurrentConditionsSensorConfig(
|
|
'Precipitation Today', 'precip_today_in', "mdi:umbrella",
|
|
LENGTH_INCHES),
|
|
'precip_today_metric': WUCurrentConditionsSensorConfig(
|
|
'Precipitation Today', 'precip_today_metric', "mdi:umbrella", 'mm'),
|
|
'precip_today_string': WUCurrentConditionsSensorConfig(
|
|
'Precipitation Today', 'precip_today_string', "mdi:umbrella"),
|
|
'pressure_in': WUCurrentConditionsSensorConfig(
|
|
'Pressure', 'pressure_in', "mdi:gauge", 'inHg',
|
|
device_class="pressure"),
|
|
'pressure_mb': WUCurrentConditionsSensorConfig(
|
|
'Pressure', 'pressure_mb', "mdi:gauge", 'mb',
|
|
device_class="pressure"),
|
|
'pressure_trend': WUCurrentConditionsSensorConfig(
|
|
'Pressure Trend', 'pressure_trend', "mdi:gauge",
|
|
device_class="pressure"),
|
|
'relative_humidity': WUSensorConfig(
|
|
'Relative Humidity',
|
|
'conditions',
|
|
value=lambda wu: int(wu.data['current_observation'][
|
|
'relative_humidity'][:-1]),
|
|
unit_of_measurement='%',
|
|
icon="mdi:water-percent",
|
|
device_class="humidity"),
|
|
'station_id': WUCurrentConditionsSensorConfig(
|
|
'Station ID', 'station_id', "mdi:home"),
|
|
'solarradiation': WUCurrentConditionsSensorConfig(
|
|
'Solar Radiation', 'solarradiation', "mdi:weather-sunny", "w/m2"),
|
|
'temperature_string': WUCurrentConditionsSensorConfig(
|
|
'Temperature Summary', 'temperature_string', "mdi:thermometer"),
|
|
'temp_c': WUCurrentConditionsSensorConfig(
|
|
'Temperature', 'temp_c', "mdi:thermometer", TEMP_CELSIUS,
|
|
device_class="temperature"),
|
|
'temp_f': WUCurrentConditionsSensorConfig(
|
|
'Temperature', 'temp_f', "mdi:thermometer", TEMP_FAHRENHEIT,
|
|
device_class="temperature"),
|
|
'UV': WUCurrentConditionsSensorConfig(
|
|
'UV', 'UV', "mdi:sunglasses"),
|
|
'visibility_km': WUCurrentConditionsSensorConfig(
|
|
'Visibility (km)', 'visibility_km', "mdi:eye", LENGTH_KILOMETERS),
|
|
'visibility_mi': WUCurrentConditionsSensorConfig(
|
|
'Visibility (miles)', 'visibility_mi', "mdi:eye", LENGTH_MILES),
|
|
'weather': WUCurrentConditionsSensorConfig(
|
|
'Weather Summary', 'weather', None),
|
|
'wind_degrees': WUCurrentConditionsSensorConfig(
|
|
'Wind Degrees', 'wind_degrees', "mdi:weather-windy", "°"),
|
|
'wind_dir': WUCurrentConditionsSensorConfig(
|
|
'Wind Direction', 'wind_dir', "mdi:weather-windy"),
|
|
'wind_gust_kph': WUCurrentConditionsSensorConfig(
|
|
'Wind Gust', 'wind_gust_kph', "mdi:weather-windy", 'kph'),
|
|
'wind_gust_mph': WUCurrentConditionsSensorConfig(
|
|
'Wind Gust', 'wind_gust_mph', "mdi:weather-windy", 'mph'),
|
|
'wind_kph': WUCurrentConditionsSensorConfig(
|
|
'Wind Speed', 'wind_kph', "mdi:weather-windy", 'kph'),
|
|
'wind_mph': WUCurrentConditionsSensorConfig(
|
|
'Wind Speed', 'wind_mph', "mdi:weather-windy", 'mph'),
|
|
'wind_string': WUCurrentConditionsSensorConfig(
|
|
'Wind Summary', 'wind_string', "mdi:weather-windy"),
|
|
'temp_high_record_c': WUAlmanacSensorConfig(
|
|
lambda wu: 'High Temperature Record ({})'.format(
|
|
wu.data['almanac']['temp_high']['recordyear']),
|
|
'temp_high', 'record', 'C', TEMP_CELSIUS, 'mdi:thermometer'),
|
|
'temp_high_record_f': WUAlmanacSensorConfig(
|
|
lambda wu: 'High Temperature Record ({})'.format(
|
|
wu.data['almanac']['temp_high']['recordyear']),
|
|
'temp_high', 'record', 'F', TEMP_FAHRENHEIT, 'mdi:thermometer'),
|
|
'temp_low_record_c': WUAlmanacSensorConfig(
|
|
lambda wu: 'Low Temperature Record ({})'.format(
|
|
wu.data['almanac']['temp_low']['recordyear']),
|
|
'temp_low', 'record', 'C', TEMP_CELSIUS, 'mdi:thermometer'),
|
|
'temp_low_record_f': WUAlmanacSensorConfig(
|
|
lambda wu: 'Low Temperature Record ({})'.format(
|
|
wu.data['almanac']['temp_low']['recordyear']),
|
|
'temp_low', 'record', 'F', TEMP_FAHRENHEIT, 'mdi:thermometer'),
|
|
'temp_low_avg_c': WUAlmanacSensorConfig(
|
|
'Historic Average of Low Temperatures for Today',
|
|
'temp_low', 'normal', 'C', TEMP_CELSIUS, 'mdi:thermometer'),
|
|
'temp_low_avg_f': WUAlmanacSensorConfig(
|
|
'Historic Average of Low Temperatures for Today',
|
|
'temp_low', 'normal', 'F', TEMP_FAHRENHEIT, 'mdi:thermometer'),
|
|
'temp_high_avg_c': WUAlmanacSensorConfig(
|
|
'Historic Average of High Temperatures for Today',
|
|
'temp_high', 'normal', 'C', TEMP_CELSIUS, "mdi:thermometer"),
|
|
'temp_high_avg_f': WUAlmanacSensorConfig(
|
|
'Historic Average of High Temperatures for Today',
|
|
'temp_high', 'normal', 'F', TEMP_FAHRENHEIT, "mdi:thermometer"),
|
|
'weather_1d': WUDailyTextForecastSensorConfig(0, "fcttext"),
|
|
'weather_1d_metric': WUDailyTextForecastSensorConfig(0, "fcttext_metric"),
|
|
'weather_1n': WUDailyTextForecastSensorConfig(1, "fcttext"),
|
|
'weather_1n_metric': WUDailyTextForecastSensorConfig(1, "fcttext_metric"),
|
|
'weather_2d': WUDailyTextForecastSensorConfig(2, "fcttext"),
|
|
'weather_2d_metric': WUDailyTextForecastSensorConfig(2, "fcttext_metric"),
|
|
'weather_2n': WUDailyTextForecastSensorConfig(3, "fcttext"),
|
|
'weather_2n_metric': WUDailyTextForecastSensorConfig(3, "fcttext_metric"),
|
|
'weather_3d': WUDailyTextForecastSensorConfig(4, "fcttext"),
|
|
'weather_3d_metric': WUDailyTextForecastSensorConfig(4, "fcttext_metric"),
|
|
'weather_3n': WUDailyTextForecastSensorConfig(5, "fcttext"),
|
|
'weather_3n_metric': WUDailyTextForecastSensorConfig(5, "fcttext_metric"),
|
|
'weather_4d': WUDailyTextForecastSensorConfig(6, "fcttext"),
|
|
'weather_4d_metric': WUDailyTextForecastSensorConfig(6, "fcttext_metric"),
|
|
'weather_4n': WUDailyTextForecastSensorConfig(7, "fcttext"),
|
|
'weather_4n_metric': WUDailyTextForecastSensorConfig(7, "fcttext_metric"),
|
|
'weather_1h': WUHourlyForecastSensorConfig(0, "condition"),
|
|
'weather_2h': WUHourlyForecastSensorConfig(1, "condition"),
|
|
'weather_3h': WUHourlyForecastSensorConfig(2, "condition"),
|
|
'weather_4h': WUHourlyForecastSensorConfig(3, "condition"),
|
|
'weather_5h': WUHourlyForecastSensorConfig(4, "condition"),
|
|
'weather_6h': WUHourlyForecastSensorConfig(5, "condition"),
|
|
'weather_7h': WUHourlyForecastSensorConfig(6, "condition"),
|
|
'weather_8h': WUHourlyForecastSensorConfig(7, "condition"),
|
|
'weather_9h': WUHourlyForecastSensorConfig(8, "condition"),
|
|
'weather_10h': WUHourlyForecastSensorConfig(9, "condition"),
|
|
'weather_11h': WUHourlyForecastSensorConfig(10, "condition"),
|
|
'weather_12h': WUHourlyForecastSensorConfig(11, "condition"),
|
|
'weather_13h': WUHourlyForecastSensorConfig(12, "condition"),
|
|
'weather_14h': WUHourlyForecastSensorConfig(13, "condition"),
|
|
'weather_15h': WUHourlyForecastSensorConfig(14, "condition"),
|
|
'weather_16h': WUHourlyForecastSensorConfig(15, "condition"),
|
|
'weather_17h': WUHourlyForecastSensorConfig(16, "condition"),
|
|
'weather_18h': WUHourlyForecastSensorConfig(17, "condition"),
|
|
'weather_19h': WUHourlyForecastSensorConfig(18, "condition"),
|
|
'weather_20h': WUHourlyForecastSensorConfig(19, "condition"),
|
|
'weather_21h': WUHourlyForecastSensorConfig(20, "condition"),
|
|
'weather_22h': WUHourlyForecastSensorConfig(21, "condition"),
|
|
'weather_23h': WUHourlyForecastSensorConfig(22, "condition"),
|
|
'weather_24h': WUHourlyForecastSensorConfig(23, "condition"),
|
|
'weather_25h': WUHourlyForecastSensorConfig(24, "condition"),
|
|
'weather_26h': WUHourlyForecastSensorConfig(25, "condition"),
|
|
'weather_27h': WUHourlyForecastSensorConfig(26, "condition"),
|
|
'weather_28h': WUHourlyForecastSensorConfig(27, "condition"),
|
|
'weather_29h': WUHourlyForecastSensorConfig(28, "condition"),
|
|
'weather_30h': WUHourlyForecastSensorConfig(29, "condition"),
|
|
'weather_31h': WUHourlyForecastSensorConfig(30, "condition"),
|
|
'weather_32h': WUHourlyForecastSensorConfig(31, "condition"),
|
|
'weather_33h': WUHourlyForecastSensorConfig(32, "condition"),
|
|
'weather_34h': WUHourlyForecastSensorConfig(33, "condition"),
|
|
'weather_35h': WUHourlyForecastSensorConfig(34, "condition"),
|
|
'weather_36h': WUHourlyForecastSensorConfig(35, "condition"),
|
|
'temp_high_1d_c': WUDailySimpleForecastSensorConfig(
|
|
"High Temperature Today", 0, "high", "celsius", TEMP_CELSIUS,
|
|
"mdi:thermometer", device_class="temperature"),
|
|
'temp_high_2d_c': WUDailySimpleForecastSensorConfig(
|
|
"High Temperature Tomorrow", 1, "high", "celsius", TEMP_CELSIUS,
|
|
"mdi:thermometer", device_class="temperature"),
|
|
'temp_high_3d_c': WUDailySimpleForecastSensorConfig(
|
|
"High Temperature in 3 Days", 2, "high", "celsius", TEMP_CELSIUS,
|
|
"mdi:thermometer", device_class="temperature"),
|
|
'temp_high_4d_c': WUDailySimpleForecastSensorConfig(
|
|
"High Temperature in 4 Days", 3, "high", "celsius", TEMP_CELSIUS,
|
|
"mdi:thermometer", device_class="temperature"),
|
|
'temp_high_1d_f': WUDailySimpleForecastSensorConfig(
|
|
"High Temperature Today", 0, "high", "fahrenheit", TEMP_FAHRENHEIT,
|
|
"mdi:thermometer", device_class="temperature"),
|
|
'temp_high_2d_f': WUDailySimpleForecastSensorConfig(
|
|
"High Temperature Tomorrow", 1, "high", "fahrenheit", TEMP_FAHRENHEIT,
|
|
"mdi:thermometer", device_class="temperature"),
|
|
'temp_high_3d_f': WUDailySimpleForecastSensorConfig(
|
|
"High Temperature in 3 Days", 2, "high", "fahrenheit", TEMP_FAHRENHEIT,
|
|
"mdi:thermometer", device_class="temperature"),
|
|
'temp_high_4d_f': WUDailySimpleForecastSensorConfig(
|
|
"High Temperature in 4 Days", 3, "high", "fahrenheit", TEMP_FAHRENHEIT,
|
|
"mdi:thermometer", device_class="temperature"),
|
|
'temp_low_1d_c': WUDailySimpleForecastSensorConfig(
|
|
"Low Temperature Today", 0, "low", "celsius", TEMP_CELSIUS,
|
|
"mdi:thermometer", device_class="temperature"),
|
|
'temp_low_2d_c': WUDailySimpleForecastSensorConfig(
|
|
"Low Temperature Tomorrow", 1, "low", "celsius", TEMP_CELSIUS,
|
|
"mdi:thermometer", device_class="temperature"),
|
|
'temp_low_3d_c': WUDailySimpleForecastSensorConfig(
|
|
"Low Temperature in 3 Days", 2, "low", "celsius", TEMP_CELSIUS,
|
|
"mdi:thermometer", device_class="temperature"),
|
|
'temp_low_4d_c': WUDailySimpleForecastSensorConfig(
|
|
"Low Temperature in 4 Days", 3, "low", "celsius", TEMP_CELSIUS,
|
|
"mdi:thermometer", device_class="temperature"),
|
|
'temp_low_1d_f': WUDailySimpleForecastSensorConfig(
|
|
"Low Temperature Today", 0, "low", "fahrenheit", TEMP_FAHRENHEIT,
|
|
"mdi:thermometer", device_class="temperature"),
|
|
'temp_low_2d_f': WUDailySimpleForecastSensorConfig(
|
|
"Low Temperature Tomorrow", 1, "low", "fahrenheit", TEMP_FAHRENHEIT,
|
|
"mdi:thermometer", device_class="temperature"),
|
|
'temp_low_3d_f': WUDailySimpleForecastSensorConfig(
|
|
"Low Temperature in 3 Days", 2, "low", "fahrenheit", TEMP_FAHRENHEIT,
|
|
"mdi:thermometer", device_class="temperature"),
|
|
'temp_low_4d_f': WUDailySimpleForecastSensorConfig(
|
|
"Low Temperature in 4 Days", 3, "low", "fahrenheit", TEMP_FAHRENHEIT,
|
|
"mdi:thermometer", device_class="temperature"),
|
|
'wind_gust_1d_kph': WUDailySimpleForecastSensorConfig(
|
|
"Max. Wind Today", 0, "maxwind", "kph", "kph", "mdi:weather-windy"),
|
|
'wind_gust_2d_kph': WUDailySimpleForecastSensorConfig(
|
|
"Max. Wind Tomorrow", 1, "maxwind", "kph", "kph", "mdi:weather-windy"),
|
|
'wind_gust_3d_kph': WUDailySimpleForecastSensorConfig(
|
|
"Max. Wind in 3 Days", 2, "maxwind", "kph", "kph",
|
|
"mdi:weather-windy"),
|
|
'wind_gust_4d_kph': WUDailySimpleForecastSensorConfig(
|
|
"Max. Wind in 4 Days", 3, "maxwind", "kph", "kph",
|
|
"mdi:weather-windy"),
|
|
'wind_gust_1d_mph': WUDailySimpleForecastSensorConfig(
|
|
"Max. Wind Today", 0, "maxwind", "mph", "mph",
|
|
"mdi:weather-windy"),
|
|
'wind_gust_2d_mph': WUDailySimpleForecastSensorConfig(
|
|
"Max. Wind Tomorrow", 1, "maxwind", "mph", "mph",
|
|
"mdi:weather-windy"),
|
|
'wind_gust_3d_mph': WUDailySimpleForecastSensorConfig(
|
|
"Max. Wind in 3 Days", 2, "maxwind", "mph", "mph",
|
|
"mdi:weather-windy"),
|
|
'wind_gust_4d_mph': WUDailySimpleForecastSensorConfig(
|
|
"Max. Wind in 4 Days", 3, "maxwind", "mph", "mph",
|
|
"mdi:weather-windy"),
|
|
'wind_1d_kph': WUDailySimpleForecastSensorConfig(
|
|
"Avg. Wind Today", 0, "avewind", "kph", "kph",
|
|
"mdi:weather-windy"),
|
|
'wind_2d_kph': WUDailySimpleForecastSensorConfig(
|
|
"Avg. Wind Tomorrow", 1, "avewind", "kph", "kph",
|
|
"mdi:weather-windy"),
|
|
'wind_3d_kph': WUDailySimpleForecastSensorConfig(
|
|
"Avg. Wind in 3 Days", 2, "avewind", "kph", "kph",
|
|
"mdi:weather-windy"),
|
|
'wind_4d_kph': WUDailySimpleForecastSensorConfig(
|
|
"Avg. Wind in 4 Days", 3, "avewind", "kph", "kph",
|
|
"mdi:weather-windy"),
|
|
'wind_1d_mph': WUDailySimpleForecastSensorConfig(
|
|
"Avg. Wind Today", 0, "avewind", "mph", "mph",
|
|
"mdi:weather-windy"),
|
|
'wind_2d_mph': WUDailySimpleForecastSensorConfig(
|
|
"Avg. Wind Tomorrow", 1, "avewind", "mph", "mph",
|
|
"mdi:weather-windy"),
|
|
'wind_3d_mph': WUDailySimpleForecastSensorConfig(
|
|
"Avg. Wind in 3 Days", 2, "avewind", "mph", "mph",
|
|
"mdi:weather-windy"),
|
|
'wind_4d_mph': WUDailySimpleForecastSensorConfig(
|
|
"Avg. Wind in 4 Days", 3, "avewind", "mph", "mph",
|
|
"mdi:weather-windy"),
|
|
'precip_1d_mm': WUDailySimpleForecastSensorConfig(
|
|
"Precipitation Intensity Today", 0, 'qpf_allday', 'mm', 'mm',
|
|
"mdi:umbrella"),
|
|
'precip_2d_mm': WUDailySimpleForecastSensorConfig(
|
|
"Precipitation Intensity Tomorrow", 1, 'qpf_allday', 'mm', 'mm',
|
|
"mdi:umbrella"),
|
|
'precip_3d_mm': WUDailySimpleForecastSensorConfig(
|
|
"Precipitation Intensity in 3 Days", 2, 'qpf_allday', 'mm', 'mm',
|
|
"mdi:umbrella"),
|
|
'precip_4d_mm': WUDailySimpleForecastSensorConfig(
|
|
"Precipitation Intensity in 4 Days", 3, 'qpf_allday', 'mm', 'mm',
|
|
"mdi:umbrella"),
|
|
'precip_1d_in': WUDailySimpleForecastSensorConfig(
|
|
"Precipitation Intensity Today", 0, 'qpf_allday', 'in',
|
|
LENGTH_INCHES, "mdi:umbrella"),
|
|
'precip_2d_in': WUDailySimpleForecastSensorConfig(
|
|
"Precipitation Intensity Tomorrow", 1, 'qpf_allday', 'in',
|
|
LENGTH_INCHES, "mdi:umbrella"),
|
|
'precip_3d_in': WUDailySimpleForecastSensorConfig(
|
|
"Precipitation Intensity in 3 Days", 2, 'qpf_allday', 'in',
|
|
LENGTH_INCHES, "mdi:umbrella"),
|
|
'precip_4d_in': WUDailySimpleForecastSensorConfig(
|
|
"Precipitation Intensity in 4 Days", 3, 'qpf_allday', 'in',
|
|
LENGTH_INCHES, "mdi:umbrella"),
|
|
'precip_1d': WUDailySimpleForecastSensorConfig(
|
|
"Precipitation Probability Today", 0, "pop", None, "%",
|
|
"mdi:umbrella"),
|
|
'precip_2d': WUDailySimpleForecastSensorConfig(
|
|
"Precipitation Probability Tomorrow", 1, "pop", None, "%",
|
|
"mdi:umbrella"),
|
|
'precip_3d': WUDailySimpleForecastSensorConfig(
|
|
"Precipitation Probability in 3 Days", 2, "pop", None, "%",
|
|
"mdi:umbrella"),
|
|
'precip_4d': WUDailySimpleForecastSensorConfig(
|
|
"Precipitation Probability in 4 Days", 3, "pop", None, "%",
|
|
"mdi:umbrella"),
|
|
}
|
|
|
|
# Alert Attributes
|
|
ALERTS_ATTRS = [
|
|
'date',
|
|
'description',
|
|
'expires',
|
|
'message',
|
|
]
|
|
|
|
# Language Supported Codes
|
|
LANG_CODES = [
|
|
'AF', 'AL', 'AR', 'HY', 'AZ', 'EU',
|
|
'BY', 'BU', 'LI', 'MY', 'CA', 'CN',
|
|
'TW', 'CR', 'CZ', 'DK', 'DV', 'NL',
|
|
'EN', 'EO', 'ET', 'FA', 'FI', 'FR',
|
|
'FC', 'GZ', 'DL', 'KA', 'GR', 'GU',
|
|
'HT', 'IL', 'HI', 'HU', 'IS', 'IO',
|
|
'ID', 'IR', 'IT', 'JP', 'JW', 'KM',
|
|
'KR', 'KU', 'LA', 'LV', 'LT', 'ND',
|
|
'MK', 'MT', 'GM', 'MI', 'MR', 'MN',
|
|
'NO', 'OC', 'PS', 'GN', 'PL', 'BR',
|
|
'PA', 'RO', 'RU', 'SR', 'SK', 'SL',
|
|
'SP', 'SI', 'SW', 'CH', 'TL', 'TT',
|
|
'TH', 'TR', 'TK', 'UA', 'UZ', 'VU',
|
|
'CY', 'SN', 'JI', 'YI',
|
|
]
|
|
|
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
|
vol.Required(CONF_API_KEY): cv.string,
|
|
vol.Optional(CONF_PWS_ID): cv.string,
|
|
vol.Optional(CONF_LANG, default=DEFAULT_LANG): vol.All(vol.In(LANG_CODES)),
|
|
vol.Inclusive(CONF_LATITUDE, 'coordinates',
|
|
'Latitude and longitude must exist together'): cv.latitude,
|
|
vol.Inclusive(CONF_LONGITUDE, 'coordinates',
|
|
'Latitude and longitude must exist together'): cv.longitude,
|
|
vol.Required(CONF_MONITORED_CONDITIONS):
|
|
vol.All(cv.ensure_list, vol.Length(min=1), [vol.In(SENSOR_TYPES)])
|
|
})
|
|
|
|
|
|
async def async_setup_platform(hass: HomeAssistantType, config: ConfigType,
|
|
async_add_entities, discovery_info=None):
|
|
"""Set up the WUnderground sensor."""
|
|
latitude = config.get(CONF_LATITUDE, hass.config.latitude)
|
|
longitude = config.get(CONF_LONGITUDE, hass.config.longitude)
|
|
pws_id = config.get(CONF_PWS_ID)
|
|
|
|
rest = WUndergroundData(
|
|
hass, config.get(CONF_API_KEY), pws_id,
|
|
config.get(CONF_LANG), latitude, longitude)
|
|
|
|
if pws_id is None:
|
|
unique_id_base = "@{:06f},{:06f}".format(longitude, latitude)
|
|
else:
|
|
# Manually specified weather station, use that for unique_id
|
|
unique_id_base = pws_id
|
|
sensors = []
|
|
for variable in config[CONF_MONITORED_CONDITIONS]:
|
|
sensors.append(WUndergroundSensor(hass, rest, variable,
|
|
unique_id_base))
|
|
|
|
await rest.async_update()
|
|
if not rest.data:
|
|
raise PlatformNotReady
|
|
|
|
async_add_entities(sensors, True)
|
|
|
|
|
|
class WUndergroundSensor(Entity):
|
|
"""Implementing the WUnderground sensor."""
|
|
|
|
def __init__(self, hass: HomeAssistantType, rest, condition,
|
|
unique_id_base: str):
|
|
"""Initialize the sensor."""
|
|
self.rest = rest
|
|
self._condition = condition
|
|
self._state = None
|
|
self._attributes = {ATTR_ATTRIBUTION: ATTRIBUTION}
|
|
self._icon = None
|
|
self._entity_picture = None
|
|
self._unit_of_measurement = self._cfg_expand("unit_of_measurement")
|
|
self.rest.request_feature(SENSOR_TYPES[condition].feature)
|
|
# This is only the suggested entity id, it might get changed by
|
|
# the entity registry later.
|
|
self.entity_id = sensor.ENTITY_ID_FORMAT.format('pws_' + condition)
|
|
self._unique_id = "{},{}".format(unique_id_base, condition)
|
|
self._device_class = self._cfg_expand("device_class")
|
|
|
|
def _cfg_expand(self, what, default=None):
|
|
"""Parse and return sensor data."""
|
|
cfg = SENSOR_TYPES[self._condition]
|
|
val = getattr(cfg, what)
|
|
if not callable(val):
|
|
return val
|
|
try:
|
|
val = val(self.rest)
|
|
except (KeyError, IndexError, TypeError, ValueError) as err:
|
|
_LOGGER.warning("Failed to expand cfg from WU API."
|
|
" Condition: %s Attr: %s Error: %s",
|
|
self._condition, what, repr(err))
|
|
val = default
|
|
|
|
return val
|
|
|
|
def _update_attrs(self):
|
|
"""Parse and update device state attributes."""
|
|
attrs = self._cfg_expand("device_state_attributes", {})
|
|
|
|
for (attr, callback) in attrs.items():
|
|
if callable(callback):
|
|
try:
|
|
self._attributes[attr] = callback(self.rest)
|
|
except (KeyError, IndexError, TypeError, ValueError) as err:
|
|
_LOGGER.warning("Failed to update attrs from WU API."
|
|
" Condition: %s Attr: %s Error: %s",
|
|
self._condition, attr, repr(err))
|
|
else:
|
|
self._attributes[attr] = callback
|
|
|
|
@property
|
|
def name(self):
|
|
"""Return the name of the sensor."""
|
|
return self._cfg_expand("friendly_name")
|
|
|
|
@property
|
|
def state(self):
|
|
"""Return the state of the sensor."""
|
|
return self._state
|
|
|
|
@property
|
|
def device_state_attributes(self):
|
|
"""Return the state attributes."""
|
|
return self._attributes
|
|
|
|
@property
|
|
def icon(self):
|
|
"""Return icon."""
|
|
return self._icon
|
|
|
|
@property
|
|
def entity_picture(self):
|
|
"""Return the entity picture."""
|
|
return self._entity_picture
|
|
|
|
@property
|
|
def unit_of_measurement(self):
|
|
"""Return the units of measurement."""
|
|
return self._unit_of_measurement
|
|
|
|
@property
|
|
def device_class(self):
|
|
"""Return the units of measurement."""
|
|
return self._device_class
|
|
|
|
async def async_update(self):
|
|
"""Update current conditions."""
|
|
await self.rest.async_update()
|
|
|
|
if not self.rest.data:
|
|
# no data, return
|
|
return
|
|
|
|
self._state = self._cfg_expand("value")
|
|
self._update_attrs()
|
|
self._icon = self._cfg_expand("icon", super().icon)
|
|
url = self._cfg_expand("entity_picture")
|
|
if isinstance(url, str):
|
|
self._entity_picture = re.sub(r'^http://', 'https://',
|
|
url, flags=re.IGNORECASE)
|
|
|
|
@property
|
|
def unique_id(self) -> str:
|
|
"""Return a unique ID."""
|
|
return self._unique_id
|
|
|
|
|
|
class WUndergroundData:
|
|
"""Get data from WUnderground."""
|
|
|
|
def __init__(self, hass, api_key, pws_id, lang, latitude, longitude):
|
|
"""Initialize the data object."""
|
|
self._hass = hass
|
|
self._api_key = api_key
|
|
self._pws_id = pws_id
|
|
self._lang = 'lang:{}'.format(lang)
|
|
self._latitude = latitude
|
|
self._longitude = longitude
|
|
self._features = set()
|
|
self.data = None
|
|
self._session = async_get_clientsession(self._hass)
|
|
|
|
def request_feature(self, feature):
|
|
"""Register feature to be fetched from WU API."""
|
|
self._features.add(feature)
|
|
|
|
def _build_url(self, baseurl=_RESOURCE):
|
|
url = baseurl.format(
|
|
self._api_key, '/'.join(sorted(self._features)), self._lang)
|
|
if self._pws_id:
|
|
url = url + 'pws:{}'.format(self._pws_id)
|
|
else:
|
|
url = url + '{},{}'.format(self._latitude, self._longitude)
|
|
|
|
return url + '.json'
|
|
|
|
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
|
async def async_update(self):
|
|
"""Get the latest data from WUnderground."""
|
|
try:
|
|
with async_timeout.timeout(10, loop=self._hass.loop):
|
|
response = await self._session.get(self._build_url())
|
|
result = await response.json()
|
|
if "error" in result['response']:
|
|
raise ValueError(result['response']["error"]["description"])
|
|
self.data = result
|
|
except ValueError as err:
|
|
_LOGGER.error("Check WUnderground API %s", err.args)
|
|
except (asyncio.TimeoutError, aiohttp.ClientError) as err:
|
|
_LOGGER.error("Error fetching WUnderground data: %s", repr(err))
|