hass-core/homeassistant/components/wunderground/sensor.py
Penny Wood f195ecca4b Consolidate all platforms that have tests (#22109)
* 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.
2019-03-18 23:07:39 -07:00

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))