mirror of
https://github.com/home-assistant/core.git
synced 2025-07-22 12:47:08 +00:00
Make WUnderground async (#12385)
* 🐎 Async WUnderground * ∞ Them lines be too long * Fix pylint warnings * Changes according to comments * Remove STATE_UNKNOWN * 🔬 Fix tests * Improve tests
This commit is contained in:
parent
b3a47722f0
commit
fe5626b927
@ -4,21 +4,24 @@ 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 requests
|
||||
|
||||
import aiohttp
|
||||
import async_timeout
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.helpers.typing import HomeAssistantType
|
||||
from homeassistant.helpers.typing import HomeAssistantType, ConfigType
|
||||
from homeassistant.components.sensor import PLATFORM_SCHEMA, ENTITY_ID_FORMAT
|
||||
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, STATE_UNKNOWN, ATTR_ATTRIBUTION)
|
||||
LENGTH_MILES, LENGTH_FEET, ATTR_ATTRIBUTION)
|
||||
from homeassistant.exceptions import PlatformNotReady
|
||||
from homeassistant.helpers.entity import Entity, generate_entity_id
|
||||
from homeassistant.helpers.entity import Entity, async_generate_entity_id
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.util import Throttle
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
@ -627,7 +630,9 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass: HomeAssistantType, config: ConfigType,
|
||||
async_add_devices, discovery_info=None):
|
||||
"""Set up the WUnderground sensor."""
|
||||
latitude = config.get(CONF_LATITUDE, hass.config.latitude)
|
||||
longitude = config.get(CONF_LONGITUDE, hass.config.longitude)
|
||||
@ -639,13 +644,11 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
for variable in config[CONF_MONITORED_CONDITIONS]:
|
||||
sensors.append(WUndergroundSensor(hass, rest, variable))
|
||||
|
||||
rest.update()
|
||||
yield from rest.async_update()
|
||||
if not rest.data:
|
||||
raise PlatformNotReady
|
||||
|
||||
add_devices(sensors)
|
||||
|
||||
return True
|
||||
async_add_devices(sensors, True)
|
||||
|
||||
|
||||
class WUndergroundSensor(Entity):
|
||||
@ -663,7 +666,7 @@ class WUndergroundSensor(Entity):
|
||||
self._entity_picture = None
|
||||
self._unit_of_measurement = self._cfg_expand("unit_of_measurement")
|
||||
self.rest.request_feature(SENSOR_TYPES[condition].feature)
|
||||
self.entity_id = generate_entity_id(
|
||||
self.entity_id = async_generate_entity_id(
|
||||
ENTITY_ID_FORMAT, "pws_" + condition, hass=hass)
|
||||
|
||||
def _cfg_expand(self, what, default=None):
|
||||
@ -727,15 +730,16 @@ class WUndergroundSensor(Entity):
|
||||
"""Return the units of measurement."""
|
||||
return self._unit_of_measurement
|
||||
|
||||
def update(self):
|
||||
@asyncio.coroutine
|
||||
def async_update(self):
|
||||
"""Update current conditions."""
|
||||
self.rest.update()
|
||||
yield from self.rest.async_update()
|
||||
|
||||
if not self.rest.data:
|
||||
# no data, return
|
||||
return
|
||||
|
||||
self._state = self._cfg_expand("value", STATE_UNKNOWN)
|
||||
self._state = self._cfg_expand("value")
|
||||
self._update_attrs()
|
||||
self._icon = self._cfg_expand("icon", super().icon)
|
||||
url = self._cfg_expand("entity_picture")
|
||||
@ -757,6 +761,7 @@ class WUndergroundData(object):
|
||||
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."""
|
||||
@ -764,7 +769,7 @@ class WUndergroundData(object):
|
||||
|
||||
def _build_url(self, baseurl=_RESOURCE):
|
||||
url = baseurl.format(
|
||||
self._api_key, "/".join(self._features), self._lang)
|
||||
self._api_key, '/'.join(sorted(self._features)), self._lang)
|
||||
if self._pws_id:
|
||||
url = url + 'pws:{}'.format(self._pws_id)
|
||||
else:
|
||||
@ -772,20 +777,20 @@ class WUndergroundData(object):
|
||||
|
||||
return url + '.json'
|
||||
|
||||
@asyncio.coroutine
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def update(self):
|
||||
def async_update(self):
|
||||
"""Get the latest data from WUnderground."""
|
||||
try:
|
||||
result = requests.get(self._build_url(), timeout=10).json()
|
||||
with async_timeout.timeout(10, loop=self._hass.loop):
|
||||
response = yield from self._session.get(self._build_url())
|
||||
result = yield from response.json()
|
||||
if "error" in result['response']:
|
||||
raise ValueError(result['response']["error"]
|
||||
["description"])
|
||||
else:
|
||||
self.data = result
|
||||
return True
|
||||
raise ValueError(result['response']["error"]["description"])
|
||||
self.data = result
|
||||
except ValueError as err:
|
||||
_LOGGER.error("Check WUnderground API %s", err.args)
|
||||
self.data = None
|
||||
except requests.RequestException as err:
|
||||
except (asyncio.TimeoutError, aiohttp.ClientError) as err:
|
||||
_LOGGER.error("Error fetching WUnderground data: %s", repr(err))
|
||||
self.data = None
|
||||
|
@ -1,13 +1,14 @@
|
||||
"""The tests for the WUnderground platform."""
|
||||
import unittest
|
||||
import asyncio
|
||||
import aiohttp
|
||||
|
||||
from pytest import raises
|
||||
|
||||
from homeassistant.components.sensor import wunderground
|
||||
from homeassistant.const import TEMP_CELSIUS, LENGTH_INCHES, STATE_UNKNOWN
|
||||
from homeassistant.exceptions import PlatformNotReady
|
||||
|
||||
from requests.exceptions import ConnectionError
|
||||
|
||||
from tests.common import get_test_home_assistant
|
||||
from homeassistant.setup import async_setup_component
|
||||
from tests.common import load_fixture, assert_setup_component
|
||||
|
||||
VALID_CONFIG_PWS = {
|
||||
'platform': 'wunderground',
|
||||
@ -21,6 +22,7 @@ VALID_CONFIG_PWS = {
|
||||
VALID_CONFIG = {
|
||||
'platform': 'wunderground',
|
||||
'api_key': 'foo',
|
||||
'lang': 'EN',
|
||||
'monitored_conditions': [
|
||||
'weather', 'feelslike_c', 'alerts', 'elevation', 'location',
|
||||
'weather_1d_metric', 'precip_1d_in'
|
||||
@ -37,268 +39,107 @@ INVALID_CONFIG = {
|
||||
]
|
||||
}
|
||||
|
||||
FEELS_LIKE = '40'
|
||||
WEATHER = 'Clear'
|
||||
HTTPS_ICON_URL = 'https://icons.wxug.com/i/c/k/clear.gif'
|
||||
ALERT_MESSAGE = 'This is a test alert message'
|
||||
ALERT_ICON = 'mdi:alert-circle-outline'
|
||||
FORECAST_TEXT = 'Mostly Cloudy. Fog overnight.'
|
||||
PRECIP_IN = 0.03
|
||||
URL = 'http://api.wunderground.com/api/foo/alerts/conditions/forecast/lang' \
|
||||
':EN/q/32.87336,-117.22743.json'
|
||||
PWS_URL = 'http://api.wunderground.com/api/foo/alerts/conditions/' \
|
||||
'lang:EN/q/pws:bar.json'
|
||||
INVALID_URL = 'http://api.wunderground.com/api/BOB/alerts/conditions/' \
|
||||
'lang:foo/q/pws:bar.json'
|
||||
|
||||
|
||||
def mocked_requests_get(*args, **kwargs):
|
||||
"""Mock requests.get invocations."""
|
||||
class MockResponse:
|
||||
"""Class to represent a mocked response."""
|
||||
@asyncio.coroutine
|
||||
def test_setup(hass, aioclient_mock):
|
||||
"""Test that the component is loaded."""
|
||||
aioclient_mock.get(URL, text=load_fixture('wunderground-valid.json'))
|
||||
|
||||
def __init__(self, json_data, status_code):
|
||||
"""Initialize the mock response class."""
|
||||
self.json_data = json_data
|
||||
self.status_code = status_code
|
||||
|
||||
def json(self):
|
||||
"""Return the json of the response."""
|
||||
return self.json_data
|
||||
|
||||
if str(args[0]).startswith('http://api.wunderground.com/api/foo/'):
|
||||
return MockResponse({
|
||||
"response": {
|
||||
"version": "0.1",
|
||||
"termsofService":
|
||||
"http://www.wunderground.com/weather/api/d/terms.html",
|
||||
"features": {
|
||||
"conditions": 1,
|
||||
"alerts": 1,
|
||||
"forecast": 1,
|
||||
}
|
||||
}, "current_observation": {
|
||||
"image": {
|
||||
"url":
|
||||
'http://icons.wxug.com/graphics/wu2/logo_130x80.png',
|
||||
"title": "Weather Underground",
|
||||
"link": "http://www.wunderground.com"
|
||||
},
|
||||
"feelslike_c": FEELS_LIKE,
|
||||
"weather": WEATHER,
|
||||
"icon_url": 'http://icons.wxug.com/i/c/k/clear.gif',
|
||||
"display_location": {
|
||||
"city": "Holly Springs",
|
||||
"country": "US",
|
||||
"full": "Holly Springs, NC"
|
||||
},
|
||||
"observation_location": {
|
||||
"elevation": "413 ft",
|
||||
"full": "Twin Lake, Holly Springs, North Carolina"
|
||||
},
|
||||
}, "alerts": [
|
||||
{
|
||||
"type": 'FLO',
|
||||
"description": "Areal Flood Warning",
|
||||
"date": "9:36 PM CDT on September 22, 2016",
|
||||
"expires": "10:00 AM CDT on September 23, 2016",
|
||||
"message": ALERT_MESSAGE,
|
||||
},
|
||||
|
||||
], "forecast": {
|
||||
"txt_forecast": {
|
||||
"date": "22:35 CEST",
|
||||
"forecastday": [
|
||||
{
|
||||
"period": 0,
|
||||
"icon_url":
|
||||
"http://icons.wxug.com/i/c/k/clear.gif",
|
||||
"title": "Tuesday",
|
||||
"fcttext": FORECAST_TEXT,
|
||||
"fcttext_metric": FORECAST_TEXT,
|
||||
"pop": "0"
|
||||
},
|
||||
],
|
||||
}, "simpleforecast": {
|
||||
"forecastday": [
|
||||
{
|
||||
"date": {
|
||||
"pretty": "19:00 CEST 4. Duben 2017",
|
||||
},
|
||||
"period": 1,
|
||||
"high": {
|
||||
"fahrenheit": "56",
|
||||
"celsius": "13",
|
||||
},
|
||||
"low": {
|
||||
"fahrenheit": "43",
|
||||
"celsius": "6",
|
||||
},
|
||||
"conditions": "Možnost deště",
|
||||
"icon_url":
|
||||
"http://icons.wxug.com/i/c/k/chancerain.gif",
|
||||
"qpf_allday": {
|
||||
"in": PRECIP_IN,
|
||||
"mm": 1,
|
||||
},
|
||||
"maxwind": {
|
||||
"mph": 0,
|
||||
"kph": 0,
|
||||
"dir": "",
|
||||
"degrees": 0,
|
||||
},
|
||||
"avewind": {
|
||||
"mph": 0,
|
||||
"kph": 0,
|
||||
"dir": "severní",
|
||||
"degrees": 0
|
||||
}
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}, 200)
|
||||
else:
|
||||
return MockResponse({
|
||||
"response": {
|
||||
"version": "0.1",
|
||||
"termsofService":
|
||||
"http://www.wunderground.com/weather/api/d/terms.html",
|
||||
"features": {},
|
||||
"error": {
|
||||
"type": "keynotfound",
|
||||
"description": "this key does not exist"
|
||||
}
|
||||
}
|
||||
}, 200)
|
||||
with assert_setup_component(1, 'sensor'):
|
||||
yield from async_setup_component(hass, 'sensor',
|
||||
{'sensor': VALID_CONFIG})
|
||||
|
||||
|
||||
def mocked_requests_get_invalid(*args, **kwargs):
|
||||
"""Mock requests.get invocations invalid data."""
|
||||
class MockResponse:
|
||||
"""Class to represent a mocked response."""
|
||||
@asyncio.coroutine
|
||||
def test_setup_pws(hass, aioclient_mock):
|
||||
"""Test that the component is loaded with PWS id."""
|
||||
aioclient_mock.get(PWS_URL, text=load_fixture('wunderground-valid.json'))
|
||||
|
||||
def __init__(self, json_data, status_code):
|
||||
"""Initialize the mock response class."""
|
||||
self.json_data = json_data
|
||||
self.status_code = status_code
|
||||
|
||||
def json(self):
|
||||
"""Return the json of the response."""
|
||||
return self.json_data
|
||||
|
||||
return MockResponse({
|
||||
"response": {
|
||||
"version": "0.1",
|
||||
"termsofService":
|
||||
"http://www.wunderground.com/weather/api/d/terms.html",
|
||||
"features": {
|
||||
"conditions": 1,
|
||||
"alerts": 1,
|
||||
"forecast": 1,
|
||||
}
|
||||
}, "current_observation": {
|
||||
"image": {
|
||||
"url":
|
||||
'http://icons.wxug.com/graphics/wu2/logo_130x80.png',
|
||||
"title": "Weather Underground",
|
||||
"link": "http://www.wunderground.com"
|
||||
},
|
||||
},
|
||||
}, 200)
|
||||
with assert_setup_component(1, 'sensor'):
|
||||
yield from async_setup_component(hass, 'sensor',
|
||||
{'sensor': VALID_CONFIG_PWS})
|
||||
|
||||
|
||||
class TestWundergroundSetup(unittest.TestCase):
|
||||
"""Test the WUnderground platform."""
|
||||
@asyncio.coroutine
|
||||
def test_setup_invalid(hass, aioclient_mock):
|
||||
"""Test that the component is not loaded with invalid config."""
|
||||
aioclient_mock.get(INVALID_URL,
|
||||
text=load_fixture('wunderground-error.json'))
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
DEVICES = []
|
||||
with assert_setup_component(0, 'sensor'):
|
||||
yield from async_setup_component(hass, 'sensor',
|
||||
{'sensor': INVALID_CONFIG})
|
||||
|
||||
def add_devices(self, devices):
|
||||
"""Mock add devices."""
|
||||
for device in devices:
|
||||
self.DEVICES.append(device)
|
||||
|
||||
def setUp(self):
|
||||
"""Initialize values for this testcase class."""
|
||||
self.DEVICES = []
|
||||
self.hass = get_test_home_assistant()
|
||||
self.key = 'foo'
|
||||
self.config = VALID_CONFIG_PWS
|
||||
self.lat = 37.8267
|
||||
self.lon = -122.423
|
||||
self.hass.config.latitude = self.lat
|
||||
self.hass.config.longitude = self.lon
|
||||
@asyncio.coroutine
|
||||
def test_sensor(hass, aioclient_mock):
|
||||
"""Test the WUnderground sensor class and methods."""
|
||||
aioclient_mock.get(URL, text=load_fixture('wunderground-valid.json'))
|
||||
|
||||
def tearDown(self): # pylint: disable=invalid-name
|
||||
"""Stop everything that was started."""
|
||||
self.hass.stop()
|
||||
yield from async_setup_component(hass, 'sensor', {'sensor': VALID_CONFIG})
|
||||
|
||||
@unittest.mock.patch('requests.get', side_effect=mocked_requests_get)
|
||||
def test_setup(self, req_mock):
|
||||
"""Test that the component is loaded if passed in PWS Id."""
|
||||
self.assertTrue(
|
||||
wunderground.setup_platform(self.hass, VALID_CONFIG_PWS,
|
||||
self.add_devices, None))
|
||||
self.assertTrue(
|
||||
wunderground.setup_platform(self.hass, VALID_CONFIG,
|
||||
self.add_devices, None))
|
||||
state = hass.states.get('sensor.pws_weather')
|
||||
assert state.state == 'Clear'
|
||||
assert state.name == "Weather Summary"
|
||||
assert 'unit_of_measurement' not in state.attributes
|
||||
assert state.attributes['entity_picture'] == \
|
||||
'https://icons.wxug.com/i/c/k/clear.gif'
|
||||
|
||||
with self.assertRaises(PlatformNotReady):
|
||||
wunderground.setup_platform(self.hass, INVALID_CONFIG,
|
||||
self.add_devices, None)
|
||||
state = hass.states.get('sensor.pws_alerts')
|
||||
assert state.state == '1'
|
||||
assert state.name == 'Alerts'
|
||||
assert state.attributes['Message'] == \
|
||||
"This is a test alert message"
|
||||
assert state.attributes['icon'] == 'mdi:alert-circle-outline'
|
||||
assert 'entity_picture' not in state.attributes
|
||||
|
||||
@unittest.mock.patch('requests.get', side_effect=mocked_requests_get)
|
||||
def test_sensor(self, req_mock):
|
||||
"""Test the WUnderground sensor class and methods."""
|
||||
wunderground.setup_platform(self.hass, VALID_CONFIG, self.add_devices,
|
||||
None)
|
||||
for device in self.DEVICES:
|
||||
device.update()
|
||||
entity_id = device.entity_id
|
||||
friendly_name = device.name
|
||||
self.assertTrue(entity_id.startswith('sensor.pws_'))
|
||||
if entity_id == 'sensor.pws_weather':
|
||||
self.assertEqual(HTTPS_ICON_URL, device.entity_picture)
|
||||
self.assertEqual(WEATHER, device.state)
|
||||
self.assertIsNone(device.unit_of_measurement)
|
||||
self.assertEqual("Weather Summary", friendly_name)
|
||||
elif entity_id == 'sensor.pws_alerts':
|
||||
self.assertEqual(1, device.state)
|
||||
self.assertEqual(ALERT_MESSAGE,
|
||||
device.device_state_attributes['Message'])
|
||||
self.assertEqual(ALERT_ICON, device.icon)
|
||||
self.assertIsNone(device.entity_picture)
|
||||
self.assertEqual('Alerts', friendly_name)
|
||||
elif entity_id == 'sensor.pws_location':
|
||||
self.assertEqual('Holly Springs, NC', device.state)
|
||||
self.assertEqual('Location', friendly_name)
|
||||
elif entity_id == 'sensor.pws_elevation':
|
||||
self.assertEqual('413', device.state)
|
||||
self.assertEqual('Elevation', friendly_name)
|
||||
elif entity_id == 'sensor.pws_feelslike_c':
|
||||
self.assertIsNone(device.entity_picture)
|
||||
self.assertEqual(FEELS_LIKE, device.state)
|
||||
self.assertEqual(TEMP_CELSIUS, device.unit_of_measurement)
|
||||
self.assertEqual("Feels Like", friendly_name)
|
||||
elif entity_id == 'sensor.pws_weather_1d_metric':
|
||||
self.assertEqual(FORECAST_TEXT, device.state)
|
||||
self.assertEqual('Tuesday', friendly_name)
|
||||
else:
|
||||
self.assertEqual(entity_id, 'sensor.pws_precip_1d_in')
|
||||
self.assertEqual(PRECIP_IN, device.state)
|
||||
self.assertEqual(LENGTH_INCHES, device.unit_of_measurement)
|
||||
self.assertEqual('Precipitation Intensity Today',
|
||||
friendly_name)
|
||||
state = hass.states.get('sensor.pws_location')
|
||||
assert state.state == "Holly Springs, NC"
|
||||
assert state.name == 'Location'
|
||||
|
||||
@unittest.mock.patch('requests.get',
|
||||
side_effect=ConnectionError('test exception'))
|
||||
def test_connect_failed(self, req_mock):
|
||||
"""Test the WUnderground connection error."""
|
||||
with self.assertRaises(PlatformNotReady):
|
||||
wunderground.setup_platform(self.hass, VALID_CONFIG,
|
||||
self.add_devices, None)
|
||||
state = hass.states.get('sensor.pws_elevation')
|
||||
assert state.state == '413'
|
||||
assert state.name == 'Elevation'
|
||||
|
||||
@unittest.mock.patch('requests.get',
|
||||
side_effect=mocked_requests_get_invalid)
|
||||
def test_invalid_data(self, req_mock):
|
||||
"""Test the WUnderground invalid data."""
|
||||
wunderground.setup_platform(self.hass, VALID_CONFIG_PWS,
|
||||
self.add_devices, None)
|
||||
for device in self.DEVICES:
|
||||
device.update()
|
||||
self.assertEqual(STATE_UNKNOWN, device.state)
|
||||
state = hass.states.get('sensor.pws_feelslike_c')
|
||||
assert state.state == '40'
|
||||
assert state.name == "Feels Like"
|
||||
assert 'entity_picture' not in state.attributes
|
||||
assert state.attributes['unit_of_measurement'] == TEMP_CELSIUS
|
||||
|
||||
state = hass.states.get('sensor.pws_weather_1d_metric')
|
||||
assert state.state == "Mostly Cloudy. Fog overnight."
|
||||
assert state.name == 'Tuesday'
|
||||
|
||||
state = hass.states.get('sensor.pws_precip_1d_in')
|
||||
assert state.state == '0.03'
|
||||
assert state.name == "Precipitation Intensity Today"
|
||||
assert state.attributes['unit_of_measurement'] == LENGTH_INCHES
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def test_connect_failed(hass, aioclient_mock):
|
||||
"""Test the WUnderground connection error."""
|
||||
aioclient_mock.get(URL, exc=aiohttp.ClientError())
|
||||
with raises(PlatformNotReady):
|
||||
yield from wunderground.async_setup_platform(hass, VALID_CONFIG,
|
||||
lambda _: None)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def test_invalid_data(hass, aioclient_mock):
|
||||
"""Test the WUnderground invalid data."""
|
||||
aioclient_mock.get(URL, text=load_fixture('wunderground-invalid.json'))
|
||||
|
||||
yield from async_setup_component(hass, 'sensor', {'sensor': VALID_CONFIG})
|
||||
|
||||
for condition in VALID_CONFIG['monitored_conditions']:
|
||||
state = hass.states.get('sensor.pws_' + condition)
|
||||
assert state.state == STATE_UNKNOWN
|
||||
|
11
tests/fixtures/wunderground-error.json
vendored
Normal file
11
tests/fixtures/wunderground-error.json
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"response": {
|
||||
"version": "0.1",
|
||||
"termsofService": "http://www.wunderground.com/weather/api/d/terms.html",
|
||||
"features": {},
|
||||
"error": {
|
||||
"type": "keynotfound",
|
||||
"description": "this key does not exist"
|
||||
}
|
||||
}
|
||||
}
|
18
tests/fixtures/wunderground-invalid.json
vendored
Normal file
18
tests/fixtures/wunderground-invalid.json
vendored
Normal file
@ -0,0 +1,18 @@
|
||||
{
|
||||
"response": {
|
||||
"version": "0.1",
|
||||
"termsofService": "http://www.wunderground.com/weather/api/d/terms.html",
|
||||
"features": {
|
||||
"conditions": 1,
|
||||
"alerts": 1,
|
||||
"forecast": 1
|
||||
}
|
||||
},
|
||||
"current_observation": {
|
||||
"image": {
|
||||
"url": "http://icons.wxug.com/graphics/wu2/logo_130x80.png",
|
||||
"title": "Weather Underground",
|
||||
"link": "http://www.wunderground.com"
|
||||
}
|
||||
}
|
||||
}
|
90
tests/fixtures/wunderground-valid.json
vendored
Normal file
90
tests/fixtures/wunderground-valid.json
vendored
Normal file
@ -0,0 +1,90 @@
|
||||
{
|
||||
"response": {
|
||||
"version": "0.1",
|
||||
"termsofService": "http://www.wunderground.com/weather/api/d/terms.html",
|
||||
"features": {
|
||||
"conditions": 1,
|
||||
"alerts": 1,
|
||||
"forecast": 1
|
||||
}
|
||||
},
|
||||
"current_observation": {
|
||||
"image": {
|
||||
"url": "http://icons.wxug.com/graphics/wu2/logo_130x80.png",
|
||||
"title": "Weather Underground",
|
||||
"link": "http://www.wunderground.com"
|
||||
},
|
||||
"feelslike_c": "40",
|
||||
"weather": "Clear",
|
||||
"icon_url": "http://icons.wxug.com/i/c/k/clear.gif",
|
||||
"display_location": {
|
||||
"city": "Holly Springs",
|
||||
"country": "US",
|
||||
"full": "Holly Springs, NC"
|
||||
},
|
||||
"observation_location": {
|
||||
"elevation": "413 ft",
|
||||
"full": "Twin Lake, Holly Springs, North Carolina"
|
||||
}
|
||||
},
|
||||
"alerts": [
|
||||
{
|
||||
"type": "FLO",
|
||||
"description": "Areal Flood Warning",
|
||||
"date": "9:36 PM CDT on September 22, 2016",
|
||||
"expires": "10:00 AM CDT on September 23, 2016",
|
||||
"message": "This is a test alert message"
|
||||
}
|
||||
],
|
||||
"forecast": {
|
||||
"txt_forecast": {
|
||||
"date": "22:35 CEST",
|
||||
"forecastday": [
|
||||
{
|
||||
"period": 0,
|
||||
"icon_url": "http://icons.wxug.com/i/c/k/clear.gif",
|
||||
"title": "Tuesday",
|
||||
"fcttext": "Mostly Cloudy. Fog overnight.",
|
||||
"fcttext_metric": "Mostly Cloudy. Fog overnight.",
|
||||
"pop": "0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"simpleforecast": {
|
||||
"forecastday": [
|
||||
{
|
||||
"date": {
|
||||
"pretty": "19:00 CEST 4. Duben 2017"
|
||||
},
|
||||
"period": 1,
|
||||
"high": {
|
||||
"fahrenheit": "56",
|
||||
"celsius": "13"
|
||||
},
|
||||
"low": {
|
||||
"fahrenheit": "43",
|
||||
"celsius": "6"
|
||||
},
|
||||
"conditions": "Mo\u017enost de\u0161t\u011b",
|
||||
"icon_url": "http://icons.wxug.com/i/c/k/chancerain.gif",
|
||||
"qpf_allday": {
|
||||
"in": 0.03,
|
||||
"mm": 1
|
||||
},
|
||||
"maxwind": {
|
||||
"mph": 0,
|
||||
"kph": 0,
|
||||
"dir": "",
|
||||
"degrees": 0
|
||||
},
|
||||
"avewind": {
|
||||
"mph": 0,
|
||||
"kph": 0,
|
||||
"dir": "severn\u00ed",
|
||||
"degrees": 0
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user