Skip to content

Instantly share code, notes, and snippets.

@tchellomello
Created September 24, 2016 05:22
Show Gist options
  • Save tchellomello/25bd1ecafe26563afc362c252cb23adb to your computer and use it in GitHub Desktop.
Save tchellomello/25bd1ecafe26563afc362c252cb23adb to your computer and use it in GitHub Desktop.
"""
Support for WUnderground weather service.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/sensor.wunderground/
"""
from datetime import timedelta
import logging
import requests
import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.helpers.entity import Entity
import homeassistant.helpers.config_validation as cv
from homeassistant.util import Throttle
from homeassistant.const import (
CONF_MONITORED_CONDITIONS, CONF_API_KEY, TEMP_FAHRENHEIT, TEMP_CELSIUS,
STATE_UNKNOWN)
_RESOURCE = 'http://api.wunderground.com/api/{}/conditions/q/'
_ALERTS = 'http://api.wunderground.com/api/{}/alerts/q/'
_LOGGER = logging.getLogger(__name__)
TEST_JSON = {
"response": {
"version":"0.1",
"termsofService":"http://www.wunderground.com/weather/api/d/terms.html",
"features": {
"alerts": 1
}
}
,"query_zone": "010",
"alerts": [
{
"type": "FLO",
"description": "Areal Flood Warning",
"date": "9:36 PM CDT on September 22, 2016",
"date_epoch": "1474598160",
"expires": "10:00 AM CDT on September 23, 2016",
"expires_epoch": "1474642800",
"tz_short":"CDT",
"tz_long":"America/Chicago",
"message": "\u000AThe National Weather Service in La Crosse has extended the\u000A\u000A* Flood Warning for...\u000A Clayton County in northeastern Iowa...\u000A Fayette County in northeastern Iowa...\u000A Chickasaw County in northeastern Iowa...\u000A Howard County in northeastern Iowa...\u000A Allamakee County in northeastern Iowa...\u000A Mitchell County in north central Iowa...\u000A Winneshiek County in northeastern Iowa...\u000A Floyd County in north central Iowa...\u000A Crawford County in southwestern Wisconsin...\u000A Vernon County in southwestern Wisconsin...\u000A Richland County in southwestern Wisconsin...\u000A\u000A* until 1000 am CDT Friday\u000A\u000A* recent heavy rainfall has caused widespread flooding across\u000A northeast Iowa into southwest Wisconsin. There are washed out\u000A roads...flooded roads...mudslides and Rising River levels.\u000A\u000A* Communities and Road ways in lower areas or near creeks and rivers\u000A will continue to have water related dangers into Friday morning.\u000A Plus some additional rain is expected overnight.\u000A\u000APrecautionary/preparedness actions...\u000A\u000AUse extreme caution if you have travel planned in the region.\u000AThere are many roads closed and be sure to heed all detours. Do not\u000Adrive into high water as some some roadways may be washed out.\u000A\u000A\u000ALat...Lon 4350 9302 4350 9122 4373 9126 4373 9031\u000A 4356 9031 4355 9019 4317 9020 4321 9029\u000A 4321 9055 4299 9106 4299 9115 4275 9107\u000A 4266 9091 4265 9090 4264 9208 4291 9208\u000A 4291 9303\u000A\u000A\u000A04\u000A\u000A\u000A",
"phenomena": "FA",
"significance": "W",
"ZONES": [
{
"state":"IA",
"ZONE":"011"
}
,
{
"state":"IA",
"ZONE":"019"
}
,
{
"state":"IA",
"ZONE":"030"
}
,
{
"state":"IA",
"ZONE":"029"
}
,
{
"state":"IA",
"ZONE":"018"
}
,
{
"state":"IA",
"ZONE":"009"
}
,
{
"state":"IA",
"ZONE":"008"
}
,
{
"state":"IA",
"ZONE":"010"
}
,
{
"state":"WI",
"ZONE":"054"
}
,
{
"state":"WI",
"ZONE":"055"
}
,
{
"state":"WI",
"ZONE":"053"
}
],
"StormBased": {
}
}
,
{
"type": "WAT",
"description": "Flash Flood Watch",
"date": "8:25 PM CDT on September 22, 2016",
"date_epoch": "1474593900",
"expires": "1:00 PM CDT on September 23, 2016",
"expires_epoch": "1474653600",
"tz_short":"CDT",
"tz_long":"America/Chicago",
"message": "\u000A...Flash Flood Watch remains in effect through Friday afternoon...\u000A\u000AThe Flash Flood Watch continues for\u000A\u000A* portions of Iowa and southwest Wisconsin...including the \u000A following areas...in Iowa...Allamakee...Chickasaw...Clayton... \u000A Fayette...Floyd...Howard...Mitchell and Winneshiek. In \u000A southwest Wisconsin...Crawford...Grant and Richland. \u000A\u000A* Through Friday afternoon\u000A\u000A* an additional 1 to 2 inches of rain could fall overnight into\u000A Friday morning.\u000A\u000A* Roads will have water over them and mudslides have occurred. \u000A Drainage areas will be full and could flood again. \u000A\u000APrecautionary/preparedness actions...\u000A\u000AA Flash Flood Watch means that conditions may develop that lead\u000Ato flash flooding. Flash flooding is a very dangerous situation.\u000A\u000AThose living or recreating near waterways should monitor later\u000Aforecasts and be prepared to take action should flash flood\u000Awarnings be issued.\u000A\u000A\u000A\u000A04\u000A\u000A\u000A\u000A",
"phenomena": "FF",
"significance": "A",
"ZONES": [
{
"state":"IA",
"ZONE":"008"
}
,
{
"state":"IA",
"ZONE":"009"
}
,
{
"state":"IA",
"ZONE":"010"
}
,
{
"state":"IA",
"ZONE":"011"
}
,
{
"state":"IA",
"ZONE":"018"
}
,
{
"state":"IA",
"ZONE":"019"
}
,
{
"state":"IA",
"ZONE":"029"
}
,
{
"state":"IA",
"ZONE":"030"
}
,
{
"state":"WI",
"ZONE":"054"
}
,
{
"state":"WI",
"ZONE":"055"
}
,
{
"state":"WI",
"ZONE":"061"
}
],
"StormBased": {
}
}
]
}
TEST_JSON_1 = {
"response": {
"version":"0.1",
"termsofService":"http://www.wunderground.com/weather/api/d/terms.html",
"features": {
"alerts": 1
}
}
,"query_zone": "010",
"alerts": [
{
"type": "WAT",
"description": "Flash Flood Watch",
"date": "8:25 PM CDT on September 22, 2016",
"date_epoch": "1474593900",
"expires": "1:00 PM CDT on September 23, 2016",
"expires_epoch": "1474653600",
"tz_short":"CDT",
"tz_long":"America/Chicago",
"message": "\u000A...Flash Flood Watch remains in effect through Friday afternoon...\u000A\u000AThe Flash Flood Watch continues for\u000A\u000A* portions of Iowa and southwest Wisconsin...including the \u000A following areas...in Iowa...Allamakee...Chickasaw...Clayton... \u000A Fayette...Floyd...Howard...Mitchell and Winneshiek. In \u000A southwest Wisconsin...Crawford...Grant and Richland. \u000A\u000A* Through Friday afternoon\u000A\u000A* an additional 1 to 2 inches of rain could fall overnight into\u000A Friday morning.\u000A\u000A* Roads will have water over them and mudslides have occurred. \u000A Drainage areas will be full and could flood again. \u000A\u000APrecautionary/preparedness actions...\u000A\u000AA Flash Flood Watch means that conditions may develop that lead\u000Ato flash flooding. Flash flooding is a very dangerous situation.\u000A\u000AThose living or recreating near waterways should monitor later\u000Aforecasts and be prepared to take action should flash flood\u000Awarnings be issued.\u000A\u000A\u000A\u000A04\u000A\u000A\u000A\u000A",
"phenomena": "FF",
"significance": "A",
"ZONES": [
{
"state":"IA",
"ZONE":"008"
}
,
{
"state":"IA",
"ZONE":"009"
}
,
{
"state":"IA",
"ZONE":"010"
}
,
{
"state":"IA",
"ZONE":"011"
}
,
{
"state":"IA",
"ZONE":"018"
}
,
{
"state":"IA",
"ZONE":"019"
}
,
{
"state":"IA",
"ZONE":"029"
}
,
{
"state":"IA",
"ZONE":"030"
}
,
{
"state":"WI",
"ZONE":"054"
}
,
{
"state":"WI",
"ZONE":"055"
}
,
{
"state":"WI",
"ZONE":"061"
}
],
"StormBased": {
}
}
]
}
CONF_PWS_ID = 'pws_id'
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=300)
# Sensor types are defined like: Name, units
SENSOR_TYPES = {
'alerts': ['Alerts', None],
'weather': ['Weather Summary', None],
'station_id': ['Station ID', None],
'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],
'dewpoint_c': ['Dewpoint (°C)', TEMP_CELSIUS],
'dewpoint_f': ['Dewpoint (°F)', TEMP_FAHRENHEIT],
'dewpoint_string': ['Dewpoint Summary', None],
'wind_kph': ['Wind Speed', 'kpH'],
'wind_mph': ['Wind Speed', 'mpH'],
'UV': ['UV', None],
'pressure_in': ['Pressure', 'in'],
'pressure_mb': ['Pressure', 'mbar'],
'wind_dir': ['Wind Direction', None],
'wind_string': ['Wind Summary', None],
'temp_c': ['Temperature (°C)', TEMP_CELSIUS],
'temp_f': ['Temperature (°F)', TEMP_FAHRENHEIT],
'relative_humidity': ['Relative Humidity', '%'],
'visibility_mi': ['Visibility (miles)', 'mi'],
'visibility_km': ['Visibility (km)', 'km'],
'precip_today_in': ['Precipation Today', 'in'],
'precip_today_metric': ['Precipitation Today', 'mm'],
'precip_today_string': ['Precipitation today', None],
'solarradiation': ['Solar Radiation', None]
}
#Alert Attributes
ALERTS_ATTRS = [
'date',
'description',
'expires',
#'type',
#'message',
]
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_API_KEY): cv.string,
vol.Optional(CONF_PWS_ID): cv.string,
vol.Required(CONF_MONITORED_CONDITIONS, default=[]):
vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]),
})
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the WUnderground sensor."""
rest = WUndergroundData(hass,
config.get(CONF_API_KEY),
config.get(CONF_PWS_ID, None))
sensors = []
for variable in config[CONF_MONITORED_CONDITIONS]:
sensors.append(WUndergroundSensor(rest, variable))
try:
rest.update()
except ValueError as err:
_LOGGER.error("Received error from WUnderground: %s", err)
return False
add_devices(sensors)
return True
class WUndergroundSensor(Entity):
"""Implementing the WUnderground sensor."""
def __init__(self, rest, condition):
"""Initialize the sensor."""
self.rest = rest
self._condition = condition
@property
def name(self):
"""Return the name of the sensor."""
return "PWS_" + self._condition
@property
def state(self):
"""Return the state of the sensor."""
if self.rest.data and 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]
if self._condition == 'alerts':
return len(self.rest.alerts)
return STATE_UNKNOWN
@property
def device_state_attributes(self):
"""Return the state attributes."""
attrs = {}
if self.rest.alerts and self._condition == 'alerts':
for data in self.rest.alerts:
for alert in ALERTS_ATTRS:
if data[alert]:
if len(self.rest.alerts) > 1:
dkey = alert.capitalize() + '_' + data['type']
else:
dkey = alert.capitalize()
attrs.update({dkey:data[alert]})
return attrs
@property
def entity_picture(self):
"""Return the entity picture."""
if self._condition == 'weather':
return self.rest.data['icon_url']
@property
def unit_of_measurement(self):
"""Return the units of measurement."""
return SENSOR_TYPES[self._condition][1]
def update(self):
"""Update current conditions."""
self.rest.update()
# pylint: disable=too-few-public-methods
class WUndergroundData(object):
"""Get data from WUnderground."""
def __init__(self, hass, api_key, pws_id=None):
"""Initialize the data object."""
self._hass = hass
self._api_key = api_key
self._pws_id = pws_id
self._latitude = hass.config.latitude
self._longitude = hass.config.longitude
self.data = None
self.alerts = None
def _build_url(self, baseurl=_RESOURCE):
url = baseurl.format(self._api_key)
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)
def update(self):
"""Get the latest data from WUnderground."""
try:
result = requests.get(self._build_url(), timeout=10).json()
if "error" in result['response']:
raise ValueError(result['response']["error"]
["description"])
else:
self.data = result["current_observation"]
result = requests.get(self._build_url(_ALERTS), timeout=10).json()
_LOGGER.debug("DEBUGGG: %s", result)
if "error" in result['response']:
raise ValueError(result['response']["error"]
["description"])
else:
self.alerts = result["alerts"]
#self.alerts = TEST_JSON["alerts"] #hardcode test
#self.alerts = TEST_JSON_1["alerts"] #hardcode test
except ValueError as err:
_LOGGER.error("Check WUnderground API %s", err.args)
self.data = None
self.alerts = None
raise
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment