2016-08-18 16:37:00 +00:00
|
|
|
"""
|
2016-08-18 16:46:04 +00:00
|
|
|
Support for WUnderground weather service.
|
2016-08-18 16:37:00 +00:00
|
|
|
|
|
|
|
For more details about this platform, please refer to the documentation at
|
|
|
|
https://home-assistant.io/components/sensor.wunderground/
|
|
|
|
"""
|
2016-08-17 19:06:12 +00:00
|
|
|
from datetime import timedelta
|
|
|
|
import logging
|
2016-08-18 04:18:37 +00:00
|
|
|
|
2016-11-25 20:03:12 +00:00
|
|
|
import re
|
2016-08-20 22:40:16 +00:00
|
|
|
import requests
|
2016-08-18 04:18:37 +00:00
|
|
|
import voluptuous as vol
|
|
|
|
|
2016-08-20 22:40:16 +00:00
|
|
|
from homeassistant.components.sensor import PLATFORM_SCHEMA
|
|
|
|
from homeassistant.const import (
|
|
|
|
CONF_MONITORED_CONDITIONS, CONF_API_KEY, TEMP_FAHRENHEIT, TEMP_CELSIUS,
|
2016-10-11 07:28:19 +00:00
|
|
|
STATE_UNKNOWN, ATTR_ATTRIBUTION)
|
|
|
|
from homeassistant.helpers.entity import Entity
|
|
|
|
from homeassistant.util import Throttle
|
|
|
|
import homeassistant.helpers.config_validation as cv
|
2016-08-17 19:06:12 +00:00
|
|
|
|
2016-12-11 23:43:42 +00:00
|
|
|
_RESOURCE = 'http://api.wunderground.com/api/{}/conditions/{}/q/'
|
|
|
|
_ALERTS = 'http://api.wunderground.com/api/{}/alerts/{}/q/'
|
2016-08-17 19:06:12 +00:00
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
|
2016-10-11 07:28:19 +00:00
|
|
|
CONF_ATTRIBUTION = "Data provided by the WUnderground weather service"
|
2016-08-20 22:40:16 +00:00
|
|
|
CONF_PWS_ID = 'pws_id'
|
2016-12-11 23:43:42 +00:00
|
|
|
CONF_LANG = 'lang'
|
|
|
|
|
|
|
|
DEFAULT_LANG = 'EN'
|
2016-08-20 22:40:16 +00:00
|
|
|
|
2016-10-27 06:31:49 +00:00
|
|
|
MIN_TIME_BETWEEN_UPDATES_ALERTS = timedelta(minutes=15)
|
|
|
|
MIN_TIME_BETWEEN_UPDATES_OBSERVATION = timedelta(minutes=5)
|
2016-08-17 19:06:12 +00:00
|
|
|
|
|
|
|
# Sensor types are defined like: Name, units
|
|
|
|
SENSOR_TYPES = {
|
2016-10-15 04:35:27 +00:00
|
|
|
'alerts': ['Alerts', None],
|
2016-11-11 07:01:20 +00:00
|
|
|
'dewpoint_c': ['Dewpoint (°C)', TEMP_CELSIUS],
|
|
|
|
'dewpoint_f': ['Dewpoint (°F)', TEMP_FAHRENHEIT],
|
|
|
|
'dewpoint_string': ['Dewpoint Summary', None],
|
2016-08-17 19:06:12 +00:00
|
|
|
'feelslike_c': ['Feels Like (°C)', TEMP_CELSIUS],
|
|
|
|
'feelslike_f': ['Feels Like (°F)', TEMP_FAHRENHEIT],
|
|
|
|
'feelslike_string': ['Feels Like', None],
|
|
|
|
'heat_index_c': ['Dewpoint (°C)', TEMP_CELSIUS],
|
|
|
|
'heat_index_f': ['Dewpoint (°F)', TEMP_FAHRENHEIT],
|
|
|
|
'heat_index_string': ['Heat Index Summary', None],
|
2016-11-11 07:01:20 +00:00
|
|
|
'elevation': ['Elevation', 'ft'],
|
|
|
|
'location': ['Location', None],
|
|
|
|
'observation_time': ['Observation Time', None],
|
|
|
|
'precip_1hr_in': ['Precipation 1hr', 'in'],
|
|
|
|
'precip_1hr_metric': ['Precipation 1hr', 'mm'],
|
|
|
|
'precip_1hr_string': ['Precipation 1hr', None],
|
|
|
|
'precip_today_in': ['Precipation Today', 'in'],
|
|
|
|
'precip_today_metric': ['Precipitation Today', 'mm'],
|
|
|
|
'precip_today_string': ['Precipitation today', None],
|
2016-08-17 19:06:12 +00:00
|
|
|
'pressure_in': ['Pressure', 'in'],
|
2016-11-11 07:01:20 +00:00
|
|
|
'pressure_mb': ['Pressure', 'mb'],
|
|
|
|
'pressure_trend': ['Pressure Trend', None],
|
|
|
|
'relative_humidity': ['Relative Humidity', '%'],
|
|
|
|
'station_id': ['Station ID', None],
|
|
|
|
'solarradiation': ['Solar Radiation', None],
|
|
|
|
'temperature_string': ['Temperature Summary', None],
|
2016-08-17 19:06:12 +00:00
|
|
|
'temp_c': ['Temperature (°C)', TEMP_CELSIUS],
|
|
|
|
'temp_f': ['Temperature (°F)', TEMP_FAHRENHEIT],
|
2016-11-11 07:01:20 +00:00
|
|
|
'UV': ['UV', None],
|
2016-08-17 19:06:12 +00:00
|
|
|
'visibility_km': ['Visibility (km)', 'km'],
|
2016-11-11 07:01:20 +00:00
|
|
|
'visibility_mi': ['Visibility (miles)', 'mi'],
|
|
|
|
'weather': ['Weather Summary', None],
|
|
|
|
'wind_degrees': ['Wind Degrees', None],
|
|
|
|
'wind_dir': ['Wind Direction', None],
|
|
|
|
'wind_gust_kph': ['Wind Gust', 'kpH'],
|
|
|
|
'wind_gust_mph': ['Wind Gust', 'mpH'],
|
|
|
|
'wind_kph': ['Wind Speed', 'kpH'],
|
|
|
|
'wind_mph': ['Wind Speed', 'mpH'],
|
|
|
|
'wind_string': ['Wind Summary', None],
|
2016-08-17 19:06:12 +00:00
|
|
|
}
|
|
|
|
|
2016-10-15 04:35:27 +00:00
|
|
|
# Alert Attributes
|
|
|
|
ALERTS_ATTRS = [
|
|
|
|
'date',
|
|
|
|
'description',
|
|
|
|
'expires',
|
|
|
|
'message',
|
|
|
|
]
|
|
|
|
|
2016-12-11 23:43:42 +00:00
|
|
|
# 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', 'PU', 'RO', 'RU', 'SR', 'SK',
|
|
|
|
'SL', 'SP', 'SI', 'SW', 'CH', 'TL',
|
|
|
|
'TT', 'TH', 'UA', 'UZ', 'VU', 'CY',
|
|
|
|
'SN', 'JI', 'YI',
|
|
|
|
]
|
|
|
|
|
2016-08-18 16:47:52 +00:00
|
|
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
2016-08-18 16:40:28 +00:00
|
|
|
vol.Required(CONF_API_KEY): cv.string,
|
|
|
|
vol.Optional(CONF_PWS_ID): cv.string,
|
2016-12-11 23:43:42 +00:00
|
|
|
vol.Optional(CONF_LANG, default=DEFAULT_LANG):
|
|
|
|
vol.All(vol.In(LANG_CODES)),
|
2016-08-20 22:40:16 +00:00
|
|
|
vol.Required(CONF_MONITORED_CONDITIONS, default=[]):
|
|
|
|
vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]),
|
2016-08-18 04:19:13 +00:00
|
|
|
})
|
|
|
|
|
2016-08-17 19:06:12 +00:00
|
|
|
|
|
|
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
2016-08-18 16:46:04 +00:00
|
|
|
"""Setup the WUnderground sensor."""
|
2016-08-18 04:29:00 +00:00
|
|
|
rest = WUndergroundData(hass,
|
2016-08-17 19:06:12 +00:00
|
|
|
config.get(CONF_API_KEY),
|
2016-12-11 23:43:42 +00:00
|
|
|
config.get(CONF_PWS_ID),
|
|
|
|
config.get(CONF_LANG))
|
2016-08-17 19:06:12 +00:00
|
|
|
sensors = []
|
2016-08-18 16:38:34 +00:00
|
|
|
for variable in config[CONF_MONITORED_CONDITIONS]:
|
2016-08-18 16:32:19 +00:00
|
|
|
sensors.append(WUndergroundSensor(rest, variable))
|
2016-08-18 04:29:25 +00:00
|
|
|
|
|
|
|
try:
|
2016-08-17 19:06:12 +00:00
|
|
|
rest.update()
|
2016-08-18 04:29:25 +00:00
|
|
|
except ValueError as err:
|
|
|
|
_LOGGER.error("Received error from WUnderground: %s", err)
|
|
|
|
return False
|
|
|
|
|
|
|
|
add_devices(sensors)
|
|
|
|
|
|
|
|
return True
|
2016-08-17 19:06:12 +00:00
|
|
|
|
|
|
|
|
|
|
|
class WUndergroundSensor(Entity):
|
2016-08-18 16:46:04 +00:00
|
|
|
"""Implementing the WUnderground sensor."""
|
2016-08-17 19:06:12 +00:00
|
|
|
|
|
|
|
def __init__(self, rest, condition):
|
|
|
|
"""Initialize the sensor."""
|
|
|
|
self.rest = rest
|
|
|
|
self._condition = condition
|
|
|
|
|
|
|
|
@property
|
|
|
|
def name(self):
|
|
|
|
"""Return the name of the sensor."""
|
2016-08-18 04:29:49 +00:00
|
|
|
return "PWS_" + self._condition
|
2016-08-17 19:06:12 +00:00
|
|
|
|
|
|
|
@property
|
|
|
|
def state(self):
|
|
|
|
"""Return the state of the sensor."""
|
2016-11-11 07:01:20 +00:00
|
|
|
if self.rest.data:
|
|
|
|
|
2016-11-18 22:05:03 +00:00
|
|
|
if self._condition == 'elevation' and self._condition in \
|
|
|
|
self.rest.data['observation_location']:
|
2016-11-11 07:01:20 +00:00
|
|
|
return self.rest.data['observation_location'][self._condition]\
|
|
|
|
.split()[0]
|
|
|
|
|
|
|
|
if self._condition == 'location' and \
|
|
|
|
'full' in self.rest.data['display_location']:
|
|
|
|
return self.rest.data['display_location']['full']
|
|
|
|
|
|
|
|
if self._condition in self.rest.data:
|
|
|
|
if self._condition == 'relative_humidity':
|
|
|
|
return int(self.rest.data[self._condition][:-1])
|
|
|
|
else:
|
|
|
|
return self.rest.data[self._condition]
|
2016-10-15 04:35:27 +00:00
|
|
|
|
2016-10-26 05:49:51 +00:00
|
|
|
if self._condition == 'alerts':
|
|
|
|
if self.rest.alerts:
|
|
|
|
return len(self.rest.alerts)
|
|
|
|
else:
|
|
|
|
return 0
|
2016-10-15 04:35:27 +00:00
|
|
|
return STATE_UNKNOWN
|
2016-08-17 19:06:12 +00:00
|
|
|
|
2016-10-11 07:28:19 +00:00
|
|
|
@property
|
|
|
|
def device_state_attributes(self):
|
|
|
|
"""Return the state attributes."""
|
2016-10-15 04:35:27 +00:00
|
|
|
attrs = {}
|
|
|
|
|
|
|
|
attrs[ATTR_ATTRIBUTION] = CONF_ATTRIBUTION
|
|
|
|
|
|
|
|
if not self.rest.alerts or self._condition != 'alerts':
|
|
|
|
return attrs
|
|
|
|
|
|
|
|
multiple_alerts = len(self.rest.alerts) > 1
|
|
|
|
for data in self.rest.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
|
2016-10-11 07:28:19 +00:00
|
|
|
|
2016-08-17 19:06:12 +00:00
|
|
|
@property
|
|
|
|
def entity_picture(self):
|
|
|
|
"""Return the entity picture."""
|
Fixes issues #4844 to avoid traceback when self.rest.data is None (#4886)
6-12-09 18:12:30 homeassistant.core: Error doing job: Task exception was never retrieved
Traceback (most recent call last):
File "/usr/lib/python3.4/asyncio/tasks.py", line 237, in _step
result = next(coro)
File "/srv/hass/hass_venv/lib/python3.4/site-packages/homeassistant/helpers/entity_component.py", line 386, in _update_entity_states
yield from update_coro
File "/srv/hass/hass_venv/lib/python3.4/site-packages/homeassistant/helpers/entity.py", line 240, in async_update_ha_state
self._attr_setter('entity_picture', str, ATTR_ENTITY_PICTURE, attr)
File "/srv/hass/hass_venv/lib/python3.4/site-packages/homeassistant/helpers/entity.py", line 307, in _attr_setter
value = getattr(self, name)
File "/srv/hass/hass_venv/lib/python3.4/site-packages/homeassistant/components/sensor/wunderground.py", line 176, in entity_picture
url = self.rest.data['icon_url']
TypeError: 'NoneType' object is not subscriptable
2016-12-14 07:01:14 +00:00
|
|
|
if self.rest.data and self._condition == 'weather':
|
2016-11-25 20:03:12 +00:00
|
|
|
url = self.rest.data['icon_url']
|
|
|
|
return re.sub(r'^http://', 'https://', url, flags=re.IGNORECASE)
|
2016-08-17 19:06:12 +00:00
|
|
|
|
|
|
|
@property
|
|
|
|
def unit_of_measurement(self):
|
|
|
|
"""Return the units of measurement."""
|
|
|
|
return SENSOR_TYPES[self._condition][1]
|
|
|
|
|
|
|
|
def update(self):
|
|
|
|
"""Update current conditions."""
|
2016-10-15 04:35:27 +00:00
|
|
|
if self._condition == 'alerts':
|
|
|
|
self.rest.update_alerts()
|
|
|
|
else:
|
|
|
|
self.rest.update()
|
2016-08-17 19:06:12 +00:00
|
|
|
|
|
|
|
|
|
|
|
class WUndergroundData(object):
|
2016-08-18 16:46:04 +00:00
|
|
|
"""Get data from WUnderground."""
|
2016-08-17 19:06:12 +00:00
|
|
|
|
2016-12-11 23:43:42 +00:00
|
|
|
def __init__(self, hass, api_key, pws_id, lang):
|
2016-08-17 19:06:12 +00:00
|
|
|
"""Initialize the data object."""
|
2016-08-18 04:28:05 +00:00
|
|
|
self._hass = hass
|
2016-08-17 19:06:12 +00:00
|
|
|
self._api_key = api_key
|
|
|
|
self._pws_id = pws_id
|
2016-12-11 23:43:42 +00:00
|
|
|
self._lang = 'lang:{}'.format(lang)
|
2016-08-18 04:28:05 +00:00
|
|
|
self._latitude = hass.config.latitude
|
|
|
|
self._longitude = hass.config.longitude
|
2016-08-17 19:06:12 +00:00
|
|
|
self.data = None
|
2016-10-15 04:35:27 +00:00
|
|
|
self.alerts = None
|
2016-08-17 19:06:12 +00:00
|
|
|
|
2016-10-15 04:35:27 +00:00
|
|
|
def _build_url(self, baseurl=_RESOURCE):
|
2016-12-11 23:43:42 +00:00
|
|
|
url = baseurl.format(self._api_key, self._lang)
|
2016-08-18 04:22:11 +00:00
|
|
|
if self._pws_id:
|
2016-08-18 16:46:24 +00:00
|
|
|
url = url + 'pws:{}'.format(self._pws_id)
|
2016-08-18 04:22:11 +00:00
|
|
|
else:
|
|
|
|
url = url + '{},{}'.format(self._latitude, self._longitude)
|
|
|
|
|
|
|
|
return url + '.json'
|
|
|
|
|
2016-10-27 06:31:49 +00:00
|
|
|
@Throttle(MIN_TIME_BETWEEN_UPDATES_OBSERVATION)
|
2016-08-17 19:06:12 +00:00
|
|
|
def update(self):
|
2016-08-18 16:46:04 +00:00
|
|
|
"""Get the latest data from WUnderground."""
|
2016-08-17 19:06:12 +00:00
|
|
|
try:
|
2016-08-18 04:32:19 +00:00
|
|
|
result = requests.get(self._build_url(), timeout=10).json()
|
2016-08-18 04:32:42 +00:00
|
|
|
if "error" in result['response']:
|
|
|
|
raise ValueError(result['response']["error"]
|
2016-08-17 19:06:12 +00:00
|
|
|
["description"])
|
|
|
|
else:
|
2016-08-18 04:32:42 +00:00
|
|
|
self.data = result["current_observation"]
|
2016-08-17 19:06:12 +00:00
|
|
|
except ValueError as err:
|
2016-08-18 16:46:04 +00:00
|
|
|
_LOGGER.error("Check WUnderground API %s", err.args)
|
2016-08-17 19:06:12 +00:00
|
|
|
self.data = None
|
2016-10-15 04:35:27 +00:00
|
|
|
|
2016-10-27 06:31:49 +00:00
|
|
|
@Throttle(MIN_TIME_BETWEEN_UPDATES_ALERTS)
|
2016-10-15 04:35:27 +00:00
|
|
|
def update_alerts(self):
|
|
|
|
"""Get the latest alerts data from WUnderground."""
|
|
|
|
try:
|
|
|
|
result = requests.get(self._build_url(_ALERTS), timeout=10).json()
|
|
|
|
if "error" in result['response']:
|
|
|
|
raise ValueError(result['response']["error"]
|
|
|
|
["description"])
|
|
|
|
else:
|
|
|
|
self.alerts = result["alerts"]
|
|
|
|
except ValueError as err:
|
|
|
|
_LOGGER.error("Check WUnderground API %s", err.args)
|
|
|
|
self.alerts = None
|