core/homeassistant/components/wunderground/sensor.py

1256 lines
39 KiB
Python
Raw Normal View History

"""Support for WUnderground weather service."""
import asyncio
from datetime import timedelta
import logging
import re
from typing import Any, Callable, Optional, Union
import aiohttp
import async_timeout
2016-08-18 04:18:37 +00:00
import voluptuous as vol
from homeassistant.components import sensor
from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.const import (
ATTR_ATTRIBUTION,
2019-07-31 19:25:30 +00:00
CONF_API_KEY,
CONF_LATITUDE,
CONF_LONGITUDE,
CONF_MONITORED_CONDITIONS,
IRRADIATION_WATTS_PER_SQUARE_METER,
LENGTH_FEET,
2019-07-31 19:25:30 +00:00
LENGTH_INCHES,
LENGTH_KILOMETERS,
LENGTH_MILES,
SPEED_KILOMETERS_PER_HOUR,
SPEED_MILES_PER_HOUR,
TEMP_CELSIUS,
TEMP_FAHRENHEIT,
UNIT_PERCENTAGE,
2019-07-31 19:25:30 +00:00
)
from homeassistant.exceptions import PlatformNotReady
from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.typing import ConfigType, HomeAssistantType
from homeassistant.util import Throttle
2019-07-31 19:25:30 +00:00
_RESOURCE = "http://api.wunderground.com/api/{}/{}/{}/q/"
_LOGGER = logging.getLogger(__name__)
ATTRIBUTION = "Data provided by the WUnderground weather service"
2019-07-31 19:25:30 +00:00
CONF_PWS_ID = "pws_id"
CONF_LANG = "lang"
2019-07-31 19:25:30 +00:00
DEFAULT_LANG = "EN"
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=5)
# Helper classes for declaring sensor configurations
2019-07-31 19:25:30 +00:00
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.
"""
2019-07-31 19:25:30 +00:00
def __init__(
self,
friendly_name: Union[str, Callable],
feature: str,
value: Callable[["WUndergroundData"], Any],
unit_of_measurement: Optional[str] = None,
2019-07-31 19:25:30 +00:00
entity_picture=None,
icon: str = "mdi:gauge",
2019-07-31 19:25:30 +00:00
device_state_attributes=None,
device_class=None,
):
"""Initialize sensor configuration.
:param friendly_name: Friendly name
:param feature: WU feature. See:
https://www.wunderground.com/weather/api/d/docs?d=data/index
:param value: callback that extracts desired value from WUndergroundData object
:param unit_of_measurement: unit of measurement
:param entity_picture: value or callback returning URL of entity picture
:param icon: icon name or URL
:param device_state_attributes: 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."""
2019-07-31 19:25:30 +00:00
def __init__(
self,
friendly_name: Union[str, Callable],
field: str,
icon: Optional[str] = "mdi:gauge",
unit_of_measurement: Optional[str] = None,
2019-07-31 19:25:30 +00:00
device_class=None,
):
"""Initialize current conditions sensor configuration.
:param friendly_name: Friendly name of sensor
:field: Field name in the "current_observation" dictionary.
:icon: icon name or URL, if None sensor will use current weather symbol
:unit_of_measurement: unit of measurement
"""
super().__init__(
friendly_name,
"conditions",
2019-07-31 19:25:30 +00:00
value=lambda wu: wu.data["current_observation"][field],
icon=icon,
unit_of_measurement=unit_of_measurement,
2019-07-31 19:25:30 +00:00
entity_picture=lambda wu: wu.data["current_observation"]["icon_url"]
if icon is None
else None,
device_state_attributes={
2019-07-31 19:25:30 +00:00
"date": lambda wu: wu.data["current_observation"]["observation_time"]
},
2019-07-31 19:25:30 +00:00
device_class=device_class,
)
class WUDailyTextForecastSensorConfig(WUSensorConfig):
"""Helper for defining sensor configurations for daily text forecasts."""
def __init__(
self, period: int, field: str, unit_of_measurement: Optional[str] = None
):
"""Initialize daily text forecast sensor configuration.
:param period: forecast period number
:param field: field name to use as value
:param unit_of_measurement: unit of measurement
"""
super().__init__(
2019-07-31 19:25:30 +00:00
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={
2019-07-31 19:25:30 +00:00
"date": lambda wu: wu.data["forecast"]["txt_forecast"]["date"]
},
)
class WUDailySimpleForecastSensorConfig(WUSensorConfig):
"""Helper for defining sensor configurations for daily simpleforecasts."""
2019-07-31 19:25:30 +00:00
def __init__(
self,
friendly_name: str,
period: int,
field: str,
wu_unit: Optional[str] = None,
ha_unit: Optional[str] = None,
2019-07-31 19:25:30 +00:00
icon=None,
device_class=None,
):
"""Initialize daily simple forecast sensor configuration.
:param friendly_name: friendly_name of the sensor
:param period: forecast period number
:param field: field name to use as value
:param wu_unit: "fahrenheit", "celsius", "degrees" etc. see the example json at:
https://www.wunderground.com/weather/api/d/docs?d=data/forecast&MR=1
:param ha_unit: corresponding unit in Home Assistant
"""
super().__init__(
friendly_name=friendly_name,
2019-07-31 19:25:30 +00:00
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,
2019-07-31 19:25:30 +00:00
entity_picture=lambda wu: wu.data["forecast"]["simpleforecast"][
"forecastday"
][period]["icon_url"]
if not icon
else None,
icon=icon,
device_state_attributes={
2019-07-31 19:25:30 +00:00
"date": lambda wu: wu.data["forecast"]["simpleforecast"]["forecastday"][
period
]["date"]["pretty"]
},
2019-07-31 19:25:30 +00:00
device_class=device_class,
)
class WUHourlyForecastSensorConfig(WUSensorConfig):
"""Helper for defining sensor configurations for hourly text forecasts."""
def __init__(self, period: int, field: int):
"""Initialize hourly forecast sensor configuration.
:param period: forecast period number
:param field: field name to use as value
"""
super().__init__(
friendly_name=lambda wu: (
f"{wu.data['hourly_forecast'][period]['FCTTIME']['weekday_name_abbrev']} "
f"{wu.data['hourly_forecast'][period]['FCTTIME']['civil']}"
2019-07-31 19:25:30 +00:00
),
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={
2019-07-31 19:25:30 +00:00
"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."""
2019-07-31 19:25:30 +00:00
def __init__(
self,
friendly_name: Union[str, Callable],
field: str,
value_type: str,
wu_unit: str,
unit_of_measurement: str,
icon: str,
2019-07-31 19:25:30 +00:00
device_class=None,
):
"""Initialize almanac sensor configuration.
:param friendly_name: Friendly name
:param field: value name returned in 'almanac' dict as returned by the WU API
:param value_type: "record" or "normal"
:param wu_unit: unit name in WU API
:param unit_of_measurement: unit of measurement
:param icon: icon name or URL
"""
super().__init__(
friendly_name=friendly_name,
feature="almanac",
2019-07-31 19:25:30 +00:00
value=lambda wu: wu.data["almanac"][field][value_type][wu_unit],
unit_of_measurement=unit_of_measurement,
icon=icon,
2019-07-31 19:25:30 +00:00
device_class="temperature",
)
class WUAlertsSensorConfig(WUSensorConfig):
"""Helper for defining field configuration for alerts."""
def __init__(self, friendly_name: Union[str, Callable]):
"""Initialiize alerts sensor configuration.
:param friendly_name: Friendly name
"""
super().__init__(
friendly_name=friendly_name,
feature="alerts",
2019-07-31 19:25:30 +00:00
value=lambda wu: len(wu.data["alerts"]),
icon=lambda wu: "mdi:alert-circle-outline"
2019-07-31 19:25:30 +00:00
if wu.data["alerts"]
else "mdi:check-circle-outline",
device_state_attributes=self._get_attributes,
)
@staticmethod
def _get_attributes(rest):
attrs = {}
2019-07-31 19:25:30 +00:00
if "alerts" not in rest.data:
return attrs
2019-07-31 19:25:30 +00:00
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 = f"{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 = {
2019-07-31 19:25:30 +00:00
"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,
2019-07-31 19:25:30 +00:00
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=UNIT_PERCENTAGE,
icon="mdi:water-percent",
2019-07-31 19:25:30 +00:00
device_class="humidity",
),
"station_id": WUCurrentConditionsSensorConfig(
"Station ID", "station_id", "mdi:home"
),
"solarradiation": WUCurrentConditionsSensorConfig(
"Solar Radiation",
"solarradiation",
"mdi:weather-sunny",
IRRADIATION_WATTS_PER_SQUARE_METER,
2019-07-31 19:25:30 +00:00
),
"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", SPEED_KILOMETERS_PER_HOUR
2019-07-31 19:25:30 +00:00
),
"wind_gust_mph": WUCurrentConditionsSensorConfig(
"Wind Gust", "wind_gust_mph", "mdi:weather-windy", SPEED_MILES_PER_HOUR
2019-07-31 19:25:30 +00:00
),
"wind_kph": WUCurrentConditionsSensorConfig(
"Wind Speed", "wind_kph", "mdi:weather-windy", SPEED_KILOMETERS_PER_HOUR
2019-07-31 19:25:30 +00:00
),
"wind_mph": WUCurrentConditionsSensorConfig(
"Wind Speed", "wind_mph", "mdi:weather-windy", SPEED_MILES_PER_HOUR
2019-07-31 19:25:30 +00:00
),
"wind_string": WUCurrentConditionsSensorConfig(
"Wind Summary", "wind_string", "mdi:weather-windy"
),
"temp_high_record_c": WUAlmanacSensorConfig(
lambda wu: (
f"High Temperature Record "
f"({wu.data['almanac']['temp_high']['recordyear']})"
2019-07-31 19:25:30 +00:00
),
"temp_high",
"record",
"C",
TEMP_CELSIUS,
"mdi:thermometer",
),
"temp_high_record_f": WUAlmanacSensorConfig(
lambda wu: (
f"High Temperature Record "
f"({wu.data['almanac']['temp_high']['recordyear']})"
2019-07-31 19:25:30 +00:00
),
"temp_high",
"record",
"F",
TEMP_FAHRENHEIT,
"mdi:thermometer",
),
"temp_low_record_c": WUAlmanacSensorConfig(
lambda wu: (
f"Low Temperature Record "
f"({wu.data['almanac']['temp_low']['recordyear']})"
2019-07-31 19:25:30 +00:00
),
"temp_low",
"record",
"C",
TEMP_CELSIUS,
"mdi:thermometer",
),
"temp_low_record_f": WUAlmanacSensorConfig(
lambda wu: (
f"Low Temperature Record "
f"({wu.data['almanac']['temp_low']['recordyear']})"
2019-07-31 19:25:30 +00:00
),
"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",
SPEED_KILOMETERS_PER_HOUR,
SPEED_KILOMETERS_PER_HOUR,
"mdi:weather-windy",
2019-07-31 19:25:30 +00:00
),
"wind_gust_2d_kph": WUDailySimpleForecastSensorConfig(
"Max. Wind Tomorrow",
1,
"maxwind",
SPEED_KILOMETERS_PER_HOUR,
SPEED_KILOMETERS_PER_HOUR,
"mdi:weather-windy",
2019-07-31 19:25:30 +00:00
),
"wind_gust_3d_kph": WUDailySimpleForecastSensorConfig(
"Max. Wind in 3 Days",
2,
"maxwind",
SPEED_KILOMETERS_PER_HOUR,
SPEED_KILOMETERS_PER_HOUR,
"mdi:weather-windy",
2019-07-31 19:25:30 +00:00
),
"wind_gust_4d_kph": WUDailySimpleForecastSensorConfig(
"Max. Wind in 4 Days",
3,
"maxwind",
SPEED_KILOMETERS_PER_HOUR,
SPEED_KILOMETERS_PER_HOUR,
"mdi:weather-windy",
2019-07-31 19:25:30 +00:00
),
"wind_gust_1d_mph": WUDailySimpleForecastSensorConfig(
"Max. Wind Today",
0,
"maxwind",
SPEED_MILES_PER_HOUR,
SPEED_MILES_PER_HOUR,
"mdi:weather-windy",
2019-07-31 19:25:30 +00:00
),
"wind_gust_2d_mph": WUDailySimpleForecastSensorConfig(
"Max. Wind Tomorrow",
1,
"maxwind",
SPEED_MILES_PER_HOUR,
SPEED_MILES_PER_HOUR,
"mdi:weather-windy",
2019-07-31 19:25:30 +00:00
),
"wind_gust_3d_mph": WUDailySimpleForecastSensorConfig(
"Max. Wind in 3 Days",
2,
"maxwind",
SPEED_MILES_PER_HOUR,
SPEED_MILES_PER_HOUR,
"mdi:weather-windy",
2019-07-31 19:25:30 +00:00
),
"wind_gust_4d_mph": WUDailySimpleForecastSensorConfig(
"Max. Wind in 4 Days",
3,
"maxwind",
SPEED_MILES_PER_HOUR,
SPEED_MILES_PER_HOUR,
"mdi:weather-windy",
2019-07-31 19:25:30 +00:00
),
"wind_1d_kph": WUDailySimpleForecastSensorConfig(
"Avg. Wind Today",
0,
"avewind",
SPEED_KILOMETERS_PER_HOUR,
SPEED_KILOMETERS_PER_HOUR,
"mdi:weather-windy",
2019-07-31 19:25:30 +00:00
),
"wind_2d_kph": WUDailySimpleForecastSensorConfig(
"Avg. Wind Tomorrow",
1,
"avewind",
SPEED_KILOMETERS_PER_HOUR,
SPEED_KILOMETERS_PER_HOUR,
"mdi:weather-windy",
2019-07-31 19:25:30 +00:00
),
"wind_3d_kph": WUDailySimpleForecastSensorConfig(
"Avg. Wind in 3 Days",
2,
"avewind",
SPEED_KILOMETERS_PER_HOUR,
SPEED_KILOMETERS_PER_HOUR,
"mdi:weather-windy",
2019-07-31 19:25:30 +00:00
),
"wind_4d_kph": WUDailySimpleForecastSensorConfig(
"Avg. Wind in 4 Days",
3,
"avewind",
SPEED_KILOMETERS_PER_HOUR,
SPEED_KILOMETERS_PER_HOUR,
"mdi:weather-windy",
2019-07-31 19:25:30 +00:00
),
"wind_1d_mph": WUDailySimpleForecastSensorConfig(
"Avg. Wind Today",
0,
"avewind",
SPEED_MILES_PER_HOUR,
SPEED_MILES_PER_HOUR,
"mdi:weather-windy",
2019-07-31 19:25:30 +00:00
),
"wind_2d_mph": WUDailySimpleForecastSensorConfig(
"Avg. Wind Tomorrow",
1,
"avewind",
SPEED_MILES_PER_HOUR,
SPEED_MILES_PER_HOUR,
"mdi:weather-windy",
2019-07-31 19:25:30 +00:00
),
"wind_3d_mph": WUDailySimpleForecastSensorConfig(
"Avg. Wind in 3 Days",
2,
"avewind",
SPEED_MILES_PER_HOUR,
SPEED_MILES_PER_HOUR,
"mdi:weather-windy",
2019-07-31 19:25:30 +00:00
),
"wind_4d_mph": WUDailySimpleForecastSensorConfig(
"Avg. Wind in 4 Days",
3,
"avewind",
SPEED_MILES_PER_HOUR,
SPEED_MILES_PER_HOUR,
"mdi:weather-windy",
2019-07-31 19:25:30 +00:00
),
"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,
UNIT_PERCENTAGE,
"mdi:umbrella",
2019-07-31 19:25:30 +00:00
),
"precip_2d": WUDailySimpleForecastSensorConfig(
"Precipitation Probability Tomorrow",
1,
"pop",
None,
UNIT_PERCENTAGE,
"mdi:umbrella",
2019-07-31 19:25:30 +00:00
),
"precip_3d": WUDailySimpleForecastSensorConfig(
"Precipitation Probability in 3 Days",
2,
"pop",
None,
UNIT_PERCENTAGE,
"mdi:umbrella",
2019-07-31 19:25:30 +00:00
),
"precip_4d": WUDailySimpleForecastSensorConfig(
"Precipitation Probability in 4 Days",
3,
"pop",
None,
UNIT_PERCENTAGE,
"mdi:umbrella",
2019-07-31 19:25:30 +00:00
),
}
# Alert Attributes
2019-07-31 19:25:30 +00:00
ALERTS_ATTRS = ["date", "description", "expires", "message"]
# Language Supported Codes
LANG_CODES = [
2019-07-31 19:25:30 +00:00
"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",
]
2019-07-31 19:25:30 +00:00
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(
2019-07-31 19:25:30 +00:00
hass,
config.get(CONF_API_KEY),
pws_id,
config.get(CONF_LANG),
latitude,
longitude,
)
if pws_id is None:
unique_id_base = f"@{longitude:06f},{latitude:06f}"
else:
# Manually specified weather station, use that for unique_id
unique_id_base = pws_id
sensors = []
2016-08-18 16:38:34 +00:00
for variable in config[CONF_MONITORED_CONDITIONS]:
2019-07-31 19:25:30 +00:00
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):
2016-08-18 16:46:04 +00:00
"""Implementing the WUnderground sensor."""
2019-07-31 19:25:30 +00:00
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(f"pws_{condition}")
self._unique_id = f"{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:
2019-07-31 19:25:30 +00:00
_LOGGER.warning(
"Failed to expand cfg from WU API. Condition: %s Attr: %s Error: %s",
2019-07-31 19:25:30 +00:00
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:
2019-07-31 19:25:30 +00:00
_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
2018-10-01 06:55:43 +00:00
async def async_update(self):
"""Update current conditions."""
2018-10-01 06:55:43 +00:00
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):
2019-07-31 19:25:30 +00:00
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:
2016-08-18 16:46:04 +00:00
"""Get data from WUnderground."""
def __init__(self, hass, api_key, pws_id, lang, latitude, longitude):
"""Initialize the data object."""
2016-08-18 04:28:05 +00:00
self._hass = hass
self._api_key = api_key
self._pws_id = pws_id
self._lang = f"lang:{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(
2019-07-31 19:25:30 +00:00
self._api_key, "/".join(sorted(self._features)), self._lang
)
2016-08-18 04:22:11 +00:00
if self._pws_id:
url = f"{url}pws:{self._pws_id}"
2016-08-18 04:22:11 +00:00
else:
url = f"{url}{self._latitude},{self._longitude}"
2016-08-18 04:22:11 +00:00
return f"{url}.json"
2016-08-18 04:22:11 +00:00
@Throttle(MIN_TIME_BETWEEN_UPDATES)
async def async_update(self):
2016-08-18 16:46:04 +00:00
"""Get the latest data from WUnderground."""
try:
with async_timeout.timeout(10):
response = await self._session.get(self._build_url())
result = await response.json()
2019-07-31 19:25:30 +00:00
if "error" in result["response"]:
raise ValueError(result["response"]["error"]["description"])
self.data = result
except ValueError as err:
2016-08-18 16:46:04 +00:00
_LOGGER.error("Check WUnderground API %s", err.args)
except (asyncio.TimeoutError, aiohttp.ClientError) as err:
_LOGGER.error("Error fetching WUnderground data: %s", repr(err))