diff --git a/CODEOWNERS b/CODEOWNERS index 71520e11acf..3ede39518c1 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -190,6 +190,7 @@ homeassistant/components/notify/* @home-assistant/core homeassistant/components/notion/* @bachya homeassistant/components/nsw_fuel_station/* @nickw444 homeassistant/components/nuki/* @pschmitt +homeassistant/components/nws/* @MatthewFlamm homeassistant/components/ohmconnect/* @robbiet480 homeassistant/components/onboarding/* @home-assistant/core homeassistant/components/opentherm_gw/* @mvn23 diff --git a/homeassistant/components/nws/__init__.py b/homeassistant/components/nws/__init__.py new file mode 100644 index 00000000000..dde2f6dee11 --- /dev/null +++ b/homeassistant/components/nws/__init__.py @@ -0,0 +1 @@ +"""NWS Integration.""" diff --git a/homeassistant/components/nws/manifest.json b/homeassistant/components/nws/manifest.json new file mode 100644 index 00000000000..b0e5fdb2088 --- /dev/null +++ b/homeassistant/components/nws/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "nws", + "name": "National Weather Service", + "documentation": "https://www.home-assistant.io/components/nws", + "dependencies": [], + "codeowners": ["@MatthewFlamm"], + "requirements": ["pynws==0.7.4"] +} diff --git a/homeassistant/components/nws/weather.py b/homeassistant/components/nws/weather.py new file mode 100644 index 00000000000..23cf84411a3 --- /dev/null +++ b/homeassistant/components/nws/weather.py @@ -0,0 +1,378 @@ +"""Support for NWS weather service.""" +from collections import OrderedDict +from datetime import timedelta +from json import JSONDecodeError +import logging + +import aiohttp +from pynws import SimpleNWS +import voluptuous as vol + +from homeassistant.components.weather import ( + WeatherEntity, + PLATFORM_SCHEMA, + ATTR_FORECAST_CONDITION, + ATTR_FORECAST_TEMP, + ATTR_FORECAST_TIME, + ATTR_FORECAST_WIND_SPEED, + ATTR_FORECAST_WIND_BEARING, +) +from homeassistant.const import ( + CONF_API_KEY, + CONF_NAME, + CONF_LATITUDE, + CONF_LONGITUDE, + CONF_MODE, + LENGTH_KILOMETERS, + LENGTH_METERS, + LENGTH_MILES, + PRESSURE_HPA, + PRESSURE_PA, + PRESSURE_INHG, + TEMP_CELSIUS, + TEMP_FAHRENHEIT, +) +from homeassistant.exceptions import PlatformNotReady +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers import config_validation as cv +from homeassistant.util import Throttle +from homeassistant.util.distance import convert as convert_distance +from homeassistant.util.pressure import convert as convert_pressure +from homeassistant.util.temperature import convert as convert_temperature + +_LOGGER = logging.getLogger(__name__) + +ATTRIBUTION = "Data from National Weather Service/NOAA" + +SCAN_INTERVAL = timedelta(minutes=15) +MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=5) + +CONF_STATION = "station" + +ATTR_FORECAST_DETAIL_DESCRIPTION = "detailed_description" +ATTR_FORECAST_PRECIP_PROB = "precipitation_probability" +ATTR_FORECAST_DAYTIME = "daytime" + +# Ordered so that a single condition can be chosen from multiple weather codes. +# Catalog of NWS icon weather codes listed at: +# https://api.weather.gov/icons +CONDITION_CLASSES = OrderedDict( + [ + ( + "exceptional", + [ + "Tornado", + "Hurricane conditions", + "Tropical storm conditions", + "Dust", + "Smoke", + "Haze", + "Hot", + "Cold", + ], + ), + ("snowy", ["Snow", "Sleet", "Blizzard"]), + ( + "snowy-rainy", + [ + "Rain/snow", + "Rain/sleet", + "Freezing rain/snow", + "Freezing rain", + "Rain/freezing rain", + ], + ), + ("hail", []), + ( + "lightning-rainy", + [ + "Thunderstorm (high cloud cover)", + "Thunderstorm (medium cloud cover)", + "Thunderstorm (low cloud cover)", + ], + ), + ("lightning", []), + ("pouring", []), + ( + "rainy", + [ + "Rain", + "Rain showers (high cloud cover)", + "Rain showers (low cloud cover)", + ], + ), + ("windy-variant", ["Mostly cloudy and windy", "Overcast and windy"]), + ( + "windy", + [ + "Fair/clear and windy", + "A few clouds and windy", + "Partly cloudy and windy", + ], + ), + ("fog", ["Fog/mist"]), + ("clear", ["Fair/clear"]), # sunny and clear-night + ("cloudy", ["Mostly cloudy", "Overcast"]), + ("partlycloudy", ["A few clouds", "Partly cloudy"]), + ] +) + +ERRORS = (aiohttp.ClientError, JSONDecodeError) + +FORECAST_MODE = ["daynight", "hourly"] + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Optional(CONF_NAME): cv.string, + 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.Optional(CONF_MODE, default="daynight"): vol.In(FORECAST_MODE), + vol.Optional(CONF_STATION): cv.string, + vol.Required(CONF_API_KEY): cv.string, + } +) + + +def convert_condition(time, weather): + """ + Convert NWS codes to HA condition. + + Choose first condition in CONDITION_CLASSES that exists in weather code. + If no match is found, return first condition from NWS + """ + conditions = [w[0] for w in weather] + prec_probs = [w[1] or 0 for w in weather] + + # Choose condition with highest priority. + cond = next( + ( + key + for key, value in CONDITION_CLASSES.items() + if any(condition in value for condition in conditions) + ), + conditions[0], + ) + + if cond == "clear": + if time == "day": + return "sunny", max(prec_probs) + if time == "night": + return "clear-night", max(prec_probs) + return cond, max(prec_probs) + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up the NWS weather platform.""" + + latitude = config.get(CONF_LATITUDE, hass.config.latitude) + longitude = config.get(CONF_LONGITUDE, hass.config.longitude) + station = config.get(CONF_STATION) + api_key = config[CONF_API_KEY] + mode = config[CONF_MODE] + + websession = async_get_clientsession(hass) + # ID request as being from HA, pynws prepends the api_key in addition + api_key_ha = f"{api_key} homeassistant" + nws = SimpleNWS(latitude, longitude, api_key_ha, mode, websession) + + _LOGGER.debug("Setting up station: %s", station) + try: + await nws.set_station(station) + except ERRORS as status: + _LOGGER.error( + "Error getting station list for %s: %s", (latitude, longitude), status + ) + raise PlatformNotReady + + _LOGGER.debug("Station list: %s", nws.stations) + _LOGGER.debug( + "Initialized for coordinates %s, %s -> station %s", + latitude, + longitude, + nws.station, + ) + + async_add_entities([NWSWeather(nws, mode, hass.config.units, config)], True) + + +class NWSWeather(WeatherEntity): + """Representation of a weather condition.""" + + def __init__(self, nws, mode, units, config): + """Initialise the platform with a data instance and station name.""" + self.nws = nws + self.station_name = config.get(CONF_NAME, self.nws.station) + self.is_metric = units.is_metric + self.mode = mode + + self.observation = None + self._forecast = None + + @Throttle(MIN_TIME_BETWEEN_UPDATES) + async def async_update(self): + """Update Condition.""" + _LOGGER.debug("Updating station observations %s", self.nws.station) + try: + await self.nws.update_observation() + except ERRORS as status: + _LOGGER.error( + "Error updating observation from station %s: %s", + self.nws.station, + status, + ) + else: + self.observation = self.nws.observation + _LOGGER.debug("Updating forecast") + try: + await self.nws.update_forecast() + except ERRORS as status: + _LOGGER.error( + "Error updating forecast from station %s: %s", self.nws.station, status + ) + return + self._forecast = self.nws.forecast + + @property + def attribution(self): + """Return the attribution.""" + return ATTRIBUTION + + @property + def name(self): + """Return the name of the station.""" + return self.station_name + + @property + def temperature(self): + """Return the current temperature.""" + temp_c = None + if self.observation: + temp_c = self.observation.get("temperature") + if temp_c: + return convert_temperature(temp_c, TEMP_CELSIUS, TEMP_FAHRENHEIT) + return None + + @property + def pressure(self): + """Return the current pressure.""" + pressure_pa = None + if self.observation: + pressure_pa = self.observation.get("seaLevelPressure") + if pressure_pa is None: + return None + if self.is_metric: + pressure = convert_pressure(pressure_pa, PRESSURE_PA, PRESSURE_HPA) + pressure = round(pressure) + else: + pressure = convert_pressure(pressure_pa, PRESSURE_PA, PRESSURE_INHG) + pressure = round(pressure, 2) + return pressure + + @property + def humidity(self): + """Return the name of the sensor.""" + humidity = None + if self.observation: + humidity = self.observation.get("relativeHumidity") + return humidity + + @property + def wind_speed(self): + """Return the current windspeed.""" + wind_m_s = None + if self.observation: + wind_m_s = self.observation.get("windSpeed") + if wind_m_s is None: + return None + wind_m_hr = wind_m_s * 3600 + + if self.is_metric: + wind = convert_distance(wind_m_hr, LENGTH_METERS, LENGTH_KILOMETERS) + else: + wind = convert_distance(wind_m_hr, LENGTH_METERS, LENGTH_MILES) + return round(wind) + + @property + def wind_bearing(self): + """Return the current wind bearing (degrees).""" + wind_bearing = None + if self.observation: + wind_bearing = self.observation.get("windDirection") + return wind_bearing + + @property + def temperature_unit(self): + """Return the unit of measurement.""" + return TEMP_FAHRENHEIT + + @property + def condition(self): + """Return current condition.""" + weather = None + if self.observation: + weather = self.observation.get("iconWeather") + time = self.observation.get("iconTime") + + if weather: + cond, _ = convert_condition(time, weather) + return cond + return None + + @property + def visibility(self): + """Return visibility.""" + vis_m = None + if self.observation: + vis_m = self.observation.get("visibility") + if vis_m is None: + return None + + if self.is_metric: + vis = convert_distance(vis_m, LENGTH_METERS, LENGTH_KILOMETERS) + else: + vis = convert_distance(vis_m, LENGTH_METERS, LENGTH_MILES) + return round(vis, 0) + + @property + def forecast(self): + """Return forecast.""" + if self._forecast is None: + return None + forecast = [] + for forecast_entry in self._forecast: + data = { + ATTR_FORECAST_DETAIL_DESCRIPTION: forecast_entry.get( + "detailedForecast" + ), + ATTR_FORECAST_TEMP: forecast_entry.get("temperature"), + ATTR_FORECAST_TIME: forecast_entry.get("startTime"), + } + + if self.mode == "daynight": + data[ATTR_FORECAST_DAYTIME] = forecast_entry.get("isDaytime") + time = forecast_entry.get("iconTime") + weather = forecast_entry.get("iconWeather") + if time and weather: + cond, precip = convert_condition(time, weather) + else: + cond, precip = None, None + data[ATTR_FORECAST_CONDITION] = cond + data[ATTR_FORECAST_PRECIP_PROB] = precip + + data[ATTR_FORECAST_WIND_BEARING] = forecast_entry.get("windBearing") + wind_speed = forecast_entry.get("windSpeedAvg") + if wind_speed: + if self.is_metric: + data[ATTR_FORECAST_WIND_SPEED] = round( + convert_distance(wind_speed, LENGTH_MILES, LENGTH_KILOMETERS) + ) + else: + data[ATTR_FORECAST_WIND_SPEED] = round(wind_speed) + else: + data[ATTR_FORECAST_WIND_SPEED] = None + forecast.append(data) + return forecast diff --git a/homeassistant/components/weather/__init__.py b/homeassistant/components/weather/__init__.py index 8f276279ee5..fd122f66ac2 100644 --- a/homeassistant/components/weather/__init__.py +++ b/homeassistant/components/weather/__init__.py @@ -125,11 +125,11 @@ class WeatherEntity(Entity): @property def state_attributes(self): """Return the state attributes.""" - data = { - ATTR_WEATHER_TEMPERATURE: show_temp( + data = {} + if self.temperature is not None: + data[ATTR_WEATHER_TEMPERATURE] = show_temp( self.hass, self.temperature, self.temperature_unit, self.precision ) - } humidity = self.humidity if humidity is not None: diff --git a/requirements_all.txt b/requirements_all.txt index d3d60e6a43e..c8e79616e17 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1308,6 +1308,9 @@ pynuki==1.3.3 # homeassistant.components.nut pynut2==2.1.2 +# homeassistant.components.nws +pynws==0.7.4 + # homeassistant.components.nx584 pynx584==0.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b5d139719ef..c90ad27554e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -294,6 +294,9 @@ pymfy==0.5.2 # homeassistant.components.monoprice pymonoprice==0.3 +# homeassistant.components.nws +pynws==0.7.4 + # homeassistant.components.nx584 pynx584==0.4 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 6643fcf7aa9..dd36771994b 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -120,6 +120,7 @@ TEST_REQUIREMENTS = ( "pylitejet", "pymfy", "pymonoprice", + "pynws", "pynx584", "pyopenuv", "pyotp", diff --git a/tests/components/nws/test_weather.py b/tests/components/nws/test_weather.py new file mode 100644 index 00000000000..436d25750fc --- /dev/null +++ b/tests/components/nws/test_weather.py @@ -0,0 +1,274 @@ +"""Tests for the NWS weather component.""" +from homeassistant.components.nws.weather import ATTR_FORECAST_PRECIP_PROB +from homeassistant.components.weather import ( + ATTR_WEATHER_HUMIDITY, + ATTR_WEATHER_PRESSURE, + ATTR_WEATHER_TEMPERATURE, + ATTR_WEATHER_VISIBILITY, + ATTR_WEATHER_WIND_BEARING, + ATTR_WEATHER_WIND_SPEED, +) +from homeassistant.components.weather import ( + ATTR_FORECAST, + ATTR_FORECAST_CONDITION, + ATTR_FORECAST_TEMP, + ATTR_FORECAST_TIME, + ATTR_FORECAST_WIND_BEARING, + ATTR_FORECAST_WIND_SPEED, +) + +from homeassistant.const import ( + LENGTH_KILOMETERS, + LENGTH_METERS, + LENGTH_MILES, + PRESSURE_INHG, + PRESSURE_PA, + PRESSURE_HPA, + TEMP_CELSIUS, + TEMP_FAHRENHEIT, +) +from homeassistant.util.pressure import convert as convert_pressure +from homeassistant.util.distance import convert as convert_distance +from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM +from homeassistant.util.temperature import convert as convert_temperature +from homeassistant.setup import async_setup_component + +from tests.common import load_fixture, assert_setup_component + +EXP_OBS_IMP = { + ATTR_WEATHER_TEMPERATURE: round( + convert_temperature(26.7, TEMP_CELSIUS, TEMP_FAHRENHEIT) + ), + ATTR_WEATHER_WIND_BEARING: 190, + ATTR_WEATHER_WIND_SPEED: round( + convert_distance(2.6, LENGTH_METERS, LENGTH_MILES) * 3600 + ), + ATTR_WEATHER_PRESSURE: round( + convert_pressure(101040, PRESSURE_PA, PRESSURE_INHG), 2 + ), + ATTR_WEATHER_VISIBILITY: round( + convert_distance(16090, LENGTH_METERS, LENGTH_MILES) + ), + ATTR_WEATHER_HUMIDITY: 64, +} + +EXP_OBS_METR = { + ATTR_WEATHER_TEMPERATURE: round(26.7), + ATTR_WEATHER_WIND_BEARING: 190, + ATTR_WEATHER_WIND_SPEED: round( + convert_distance(2.6, LENGTH_METERS, LENGTH_KILOMETERS) * 3600 + ), + ATTR_WEATHER_PRESSURE: round(convert_pressure(101040, PRESSURE_PA, PRESSURE_HPA)), + ATTR_WEATHER_VISIBILITY: round( + convert_distance(16090, LENGTH_METERS, LENGTH_KILOMETERS) + ), + ATTR_WEATHER_HUMIDITY: 64, +} + +EXP_FORE_IMP = { + ATTR_FORECAST_CONDITION: "lightning-rainy", + ATTR_FORECAST_TIME: "2019-08-12T20:00:00-04:00", + ATTR_FORECAST_TEMP: 70, + ATTR_FORECAST_WIND_SPEED: 10, + ATTR_FORECAST_WIND_BEARING: 180, + ATTR_FORECAST_PRECIP_PROB: 90, +} + +EXP_FORE_METR = { + ATTR_FORECAST_CONDITION: "lightning-rainy", + ATTR_FORECAST_TIME: "2019-08-12T20:00:00-04:00", + ATTR_FORECAST_TEMP: round(convert_temperature(70, TEMP_FAHRENHEIT, TEMP_CELSIUS)), + ATTR_FORECAST_WIND_SPEED: round( + convert_distance(10, LENGTH_MILES, LENGTH_KILOMETERS) + ), + ATTR_FORECAST_WIND_BEARING: 180, + ATTR_FORECAST_PRECIP_PROB: 90, +} + + +MINIMAL_CONFIG = { + "weather": { + "platform": "nws", + "api_key": "x@example.com", + "latitude": 40.0, + "longitude": -85.0, + } +} + +INVALID_CONFIG = { + "weather": {"platform": "nws", "api_key": "x@example.com", "latitude": 40.0} +} + +STAURL = "https://api.weather.gov/points/{},{}/stations" +OBSURL = "https://api.weather.gov/stations/{}/observations/" +FORCURL = "https://api.weather.gov/points/{},{}/forecast" + + +async def test_imperial(hass, aioclient_mock): + """Test with imperial units.""" + aioclient_mock.get( + STAURL.format(40.0, -85.0), text=load_fixture("nws-weather-sta-valid.json") + ) + aioclient_mock.get( + OBSURL.format("KMIE"), + text=load_fixture("nws-weather-obs-valid.json"), + params={"limit": 1}, + ) + aioclient_mock.get( + FORCURL.format(40.0, -85.0), text=load_fixture("nws-weather-fore-valid.json") + ) + + hass.config.units = IMPERIAL_SYSTEM + + with assert_setup_component(1, "weather"): + await async_setup_component(hass, "weather", MINIMAL_CONFIG) + + state = hass.states.get("weather.kmie") + assert state + assert state.state == "sunny" + + data = state.attributes + for key, value in EXP_OBS_IMP.items(): + assert data.get(key) == value + assert state.attributes.get("friendly_name") == "KMIE" + forecast = data.get(ATTR_FORECAST) + for key, value in EXP_FORE_IMP.items(): + assert forecast[0].get(key) == value + + +async def test_metric(hass, aioclient_mock): + """Test with metric units.""" + aioclient_mock.get( + STAURL.format(40.0, -85.0), text=load_fixture("nws-weather-sta-valid.json") + ) + aioclient_mock.get( + OBSURL.format("KMIE"), + text=load_fixture("nws-weather-obs-valid.json"), + params={"limit": 1}, + ) + aioclient_mock.get( + FORCURL.format(40.0, -85.0), text=load_fixture("nws-weather-fore-valid.json") + ) + + hass.config.units = METRIC_SYSTEM + + with assert_setup_component(1, "weather"): + await async_setup_component(hass, "weather", MINIMAL_CONFIG) + + state = hass.states.get("weather.kmie") + assert state + assert state.state == "sunny" + + data = state.attributes + for key, value in EXP_OBS_METR.items(): + assert data.get(key) == value + assert state.attributes.get("friendly_name") == "KMIE" + forecast = data.get(ATTR_FORECAST) + for key, value in EXP_FORE_METR.items(): + assert forecast[0].get(key) == value + + +async def test_none(hass, aioclient_mock): + """Test with imperial units.""" + aioclient_mock.get( + STAURL.format(40.0, -85.0), text=load_fixture("nws-weather-sta-valid.json") + ) + aioclient_mock.get( + OBSURL.format("KMIE"), + text=load_fixture("nws-weather-obs-null.json"), + params={"limit": 1}, + ) + aioclient_mock.get( + FORCURL.format(40.0, -85.0), text=load_fixture("nws-weather-fore-null.json") + ) + + hass.config.units = IMPERIAL_SYSTEM + + with assert_setup_component(1, "weather"): + await async_setup_component(hass, "weather", MINIMAL_CONFIG) + + state = hass.states.get("weather.kmie") + assert state + assert state.state == "unknown" + + data = state.attributes + for key in EXP_OBS_IMP: + assert data.get(key) is None + assert state.attributes.get("friendly_name") == "KMIE" + forecast = data.get(ATTR_FORECAST) + for key in EXP_FORE_IMP: + assert forecast[0].get(key) is None + + +async def test_fail_obs(hass, aioclient_mock): + """Test failing observation/forecast update.""" + aioclient_mock.get( + STAURL.format(40.0, -85.0), text=load_fixture("nws-weather-sta-valid.json") + ) + aioclient_mock.get( + OBSURL.format("KMIE"), + text=load_fixture("nws-weather-obs-valid.json"), + params={"limit": 1}, + status=400, + ) + aioclient_mock.get( + FORCURL.format(40.0, -85.0), + text=load_fixture("nws-weather-fore-valid.json"), + status=400, + ) + + hass.config.units = IMPERIAL_SYSTEM + + with assert_setup_component(1, "weather"): + await async_setup_component(hass, "weather", MINIMAL_CONFIG) + + state = hass.states.get("weather.kmie") + assert state + + +async def test_fail_stn(hass, aioclient_mock): + """Test failing station update.""" + aioclient_mock.get( + STAURL.format(40.0, -85.0), + text=load_fixture("nws-weather-sta-valid.json"), + status=400, + ) + aioclient_mock.get( + OBSURL.format("KMIE"), + text=load_fixture("nws-weather-obs-valid.json"), + params={"limit": 1}, + ) + aioclient_mock.get( + FORCURL.format(40.0, -85.0), text=load_fixture("nws-weather-fore-valid.json") + ) + + hass.config.units = IMPERIAL_SYSTEM + + with assert_setup_component(1, "weather"): + await async_setup_component(hass, "weather", MINIMAL_CONFIG) + + state = hass.states.get("weather.kmie") + assert state is None + + +async def test_invalid_config(hass, aioclient_mock): + """Test invalid config..""" + aioclient_mock.get( + STAURL.format(40.0, -85.0), text=load_fixture("nws-weather-sta-valid.json") + ) + aioclient_mock.get( + OBSURL.format("KMIE"), + text=load_fixture("nws-weather-obs-valid.json"), + params={"limit": 1}, + ) + aioclient_mock.get( + FORCURL.format(40.0, -85.0), text=load_fixture("nws-weather-fore-valid.json") + ) + + hass.config.units = IMPERIAL_SYSTEM + + with assert_setup_component(0, "weather"): + await async_setup_component(hass, "weather", INVALID_CONFIG) + + state = hass.states.get("weather.kmie") + assert state is None diff --git a/tests/fixtures/nws-weather-fore-null.json b/tests/fixtures/nws-weather-fore-null.json new file mode 100644 index 00000000000..6085bcdada9 --- /dev/null +++ b/tests/fixtures/nws-weather-fore-null.json @@ -0,0 +1,80 @@ +{ + "@context": [ + "https://raw.githubusercontent.com/geojson/geojson-ld/master/contexts/geojson-base.jsonld", + { + "wx": "https://api.weather.gov/ontology#", + "geo": "http://www.opengis.net/ont/geosparql#", + "unit": "http://codes.wmo.int/common/unit/", + "@vocab": "https://api.weather.gov/ontology#" + } + ], + "type": "Feature", + "geometry": { + "type": "GeometryCollection", + "geometries": [ + { + "type": "Point", + "coordinates": [ + -85.014692800000006, + 39.993574700000003 + ] + }, + { + "type": "Polygon", + "coordinates": [ + [ + [ + -85.027968599999994, + 40.005368300000001 + ], + [ + -85.0300814, + 39.983399599999998 + ], + [ + -85.001420100000004, + 39.981779299999999 + ], + [ + -84.999301200000005, + 40.0037479 + ], + [ + -85.027968599999994, + 40.005368300000001 + ] + ] + ] + } + ] + }, + "properties": { + "updated": "2019-08-12T23:17:40+00:00", + "units": "us", + "forecastGenerator": "BaselineForecastGenerator", + "generatedAt": "2019-08-13T00:33:19+00:00", + "updateTime": "2019-08-12T23:17:40+00:00", + "validTimes": "2019-08-12T17:00:00+00:00/P8DT6H", + "elevation": { + "value": 366.06479999999999, + "unitCode": "unit:m" + }, + "periods": [ + { + "number": null, + "name": null, + "startTime": null, + "endTime": null, + "isDaytime": null, + "temperature": null, + "temperatureUnit": null, + "temperatureTrend": null, + "windSpeed": null, + "windDirection": null, + "icon": null, + "shortForecast": null, + "detailedForecast": null + } + ] + } +} diff --git a/tests/fixtures/nws-weather-fore-valid.json b/tests/fixtures/nws-weather-fore-valid.json new file mode 100644 index 00000000000..b3f4f4ccea8 --- /dev/null +++ b/tests/fixtures/nws-weather-fore-valid.json @@ -0,0 +1,80 @@ +{ + "@context": [ + "https://raw.githubusercontent.com/geojson/geojson-ld/master/contexts/geojson-base.jsonld", + { + "wx": "https://api.weather.gov/ontology#", + "geo": "http://www.opengis.net/ont/geosparql#", + "unit": "http://codes.wmo.int/common/unit/", + "@vocab": "https://api.weather.gov/ontology#" + } + ], + "type": "Feature", + "geometry": { + "type": "GeometryCollection", + "geometries": [ + { + "type": "Point", + "coordinates": [ + -85.014692800000006, + 39.993574700000003 + ] + }, + { + "type": "Polygon", + "coordinates": [ + [ + [ + -85.027968599999994, + 40.005368300000001 + ], + [ + -85.0300814, + 39.983399599999998 + ], + [ + -85.001420100000004, + 39.981779299999999 + ], + [ + -84.999301200000005, + 40.0037479 + ], + [ + -85.027968599999994, + 40.005368300000001 + ] + ] + ] + } + ] + }, + "properties": { + "updated": "2019-08-12T23:17:40+00:00", + "units": "us", + "forecastGenerator": "BaselineForecastGenerator", + "generatedAt": "2019-08-13T00:33:19+00:00", + "updateTime": "2019-08-12T23:17:40+00:00", + "validTimes": "2019-08-12T17:00:00+00:00/P8DT6H", + "elevation": { + "value": 366.06479999999999, + "unitCode": "unit:m" + }, + "periods": [ + { + "number": 1, + "name": "Tonight", + "startTime": "2019-08-12T20:00:00-04:00", + "endTime": "2019-08-13T06:00:00-04:00", + "isDaytime": false, + "temperature": 70, + "temperatureUnit": "F", + "temperatureTrend": null, + "windSpeed": "7 to 13 mph", + "windDirection": "S", + "icon": "https://api.weather.gov/icons/land/night/tsra,40/tsra,90?size=medium", + "shortForecast": "Showers And Thunderstorms", + "detailedForecast": "A detailed forecast." + } + ] + } +} diff --git a/tests/fixtures/nws-weather-obs-null.json b/tests/fixtures/nws-weather-obs-null.json new file mode 100644 index 00000000000..36ae66283e5 --- /dev/null +++ b/tests/fixtures/nws-weather-obs-null.json @@ -0,0 +1,161 @@ +{ + "@context": [ + "https://raw.githubusercontent.com/geojson/geojson-ld/master/contexts/geojson-base.jsonld", + { + "wx": "https://api.weather.gov/ontology#", + "s": "https://schema.org/", + "geo": "http://www.opengis.net/ont/geosparql#", + "unit": "http://codes.wmo.int/common/unit/", + "@vocab": "https://api.weather.gov/ontology#", + "geometry": { + "@id": "s:GeoCoordinates", + "@type": "geo:wktLiteral" + }, + "city": "s:addressLocality", + "state": "s:addressRegion", + "distance": { + "@id": "s:Distance", + "@type": "s:QuantitativeValue" + }, + "bearing": { + "@type": "s:QuantitativeValue" + }, + "value": { + "@id": "s:value" + }, + "unitCode": { + "@id": "s:unitCode", + "@type": "@id" + }, + "forecastOffice": { + "@type": "@id" + }, + "forecastGridData": { + "@type": "@id" + }, + "publicZone": { + "@type": "@id" + }, + "county": { + "@type": "@id" + } + } + ], + "type": "FeatureCollection", + "features": [ + { + "id": "https://api.weather.gov/stations/KMIE/observations/2019-08-12T23:53:00+00:00", + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -85.400000000000006, + 40.25 + ] + }, + "properties": { + "@id": "https://api.weather.gov/stations/KMIE/observations/2019-08-12T23:53:00+00:00", + "@type": "wx:ObservationStation", + "elevation": { + "value": 286, + "unitCode": "unit:m" + }, + "station": "https://api.weather.gov/stations/KMIE", + "timestamp": "2019-08-12T23:53:00+00:00", + "rawMessage": null, + "textDescription": "Clear", + "icon": null, + "presentWeather": [], + "temperature": { + "value": null, + "unitCode": "unit:degC", + "qualityControl": "qc:V" + }, + "dewpoint": { + "value": null, + "unitCode": "unit:degC", + "qualityControl": "qc:V" + }, + "windDirection": { + "value": null, + "unitCode": "unit:degree_(angle)", + "qualityControl": "qc:V" + }, + "windSpeed": { + "value": null, + "unitCode": "unit:m_s-1", + "qualityControl": "qc:V" + }, + "windGust": { + "value": null, + "unitCode": "unit:m_s-1", + "qualityControl": "qc:Z" + }, + "barometricPressure": { + "value": null, + "unitCode": "unit:Pa", + "qualityControl": "qc:V" + }, + "seaLevelPressure": { + "value": null, + "unitCode": "unit:Pa", + "qualityControl": "qc:V" + }, + "visibility": { + "value": null, + "unitCode": "unit:m", + "qualityControl": "qc:C" + }, + "maxTemperatureLast24Hours": { + "value": null, + "unitCode": "unit:degC", + "qualityControl": null + }, + "minTemperatureLast24Hours": { + "value": null, + "unitCode": "unit:degC", + "qualityControl": null + }, + "precipitationLastHour": { + "value": null, + "unitCode": "unit:m", + "qualityControl": "qc:Z" + }, + "precipitationLast3Hours": { + "value": null, + "unitCode": "unit:m", + "qualityControl": "qc:Z" + }, + "precipitationLast6Hours": { + "value": 0, + "unitCode": "unit:m", + "qualityControl": "qc:C" + }, + "relativeHumidity": { + "value": null, + "unitCode": "unit:percent", + "qualityControl": "qc:C" + }, + "windChill": { + "value": null, + "unitCode": "unit:degC", + "qualityControl": "qc:V" + }, + "heatIndex": { + "value": null, + "unitCode": "unit:degC", + "qualityControl": "qc:V" + }, + "cloudLayers": [ + { + "base": { + "value": null, + "unitCode": "unit:m" + }, + "amount": "CLR" + } + ] + } + } + ] +} diff --git a/tests/fixtures/nws-weather-obs-valid.json b/tests/fixtures/nws-weather-obs-valid.json new file mode 100644 index 00000000000..a6d307fc9b1 --- /dev/null +++ b/tests/fixtures/nws-weather-obs-valid.json @@ -0,0 +1,161 @@ +{ + "@context": [ + "https://raw.githubusercontent.com/geojson/geojson-ld/master/contexts/geojson-base.jsonld", + { + "wx": "https://api.weather.gov/ontology#", + "s": "https://schema.org/", + "geo": "http://www.opengis.net/ont/geosparql#", + "unit": "http://codes.wmo.int/common/unit/", + "@vocab": "https://api.weather.gov/ontology#", + "geometry": { + "@id": "s:GeoCoordinates", + "@type": "geo:wktLiteral" + }, + "city": "s:addressLocality", + "state": "s:addressRegion", + "distance": { + "@id": "s:Distance", + "@type": "s:QuantitativeValue" + }, + "bearing": { + "@type": "s:QuantitativeValue" + }, + "value": { + "@id": "s:value" + }, + "unitCode": { + "@id": "s:unitCode", + "@type": "@id" + }, + "forecastOffice": { + "@type": "@id" + }, + "forecastGridData": { + "@type": "@id" + }, + "publicZone": { + "@type": "@id" + }, + "county": { + "@type": "@id" + } + } + ], + "type": "FeatureCollection", + "features": [ + { + "id": "https://api.weather.gov/stations/KMIE/observations/2019-08-12T23:53:00+00:00", + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -85.400000000000006, + 40.25 + ] + }, + "properties": { + "@id": "https://api.weather.gov/stations/KMIE/observations/2019-08-12T23:53:00+00:00", + "@type": "wx:ObservationStation", + "elevation": { + "value": 286, + "unitCode": "unit:m" + }, + "station": "https://api.weather.gov/stations/KMIE", + "timestamp": "2019-08-12T23:53:00+00:00", + "rawMessage": "KMIE 122353Z 19005KT 10SM CLR 27/19 A2987 RMK AO2 SLP104 60000 T02670194 10272 20250 58002", + "textDescription": "Clear", + "icon": "https://api.weather.gov/icons/land/day/skc?size=medium", + "presentWeather": [], + "temperature": { + "value": 26.700000000000045, + "unitCode": "unit:degC", + "qualityControl": "qc:V" + }, + "dewpoint": { + "value": 19.400000000000034, + "unitCode": "unit:degC", + "qualityControl": "qc:V" + }, + "windDirection": { + "value": 190, + "unitCode": "unit:degree_(angle)", + "qualityControl": "qc:V" + }, + "windSpeed": { + "value": 2.6000000000000001, + "unitCode": "unit:m_s-1", + "qualityControl": "qc:V" + }, + "windGust": { + "value": null, + "unitCode": "unit:m_s-1", + "qualityControl": "qc:Z" + }, + "barometricPressure": { + "value": 101150, + "unitCode": "unit:Pa", + "qualityControl": "qc:V" + }, + "seaLevelPressure": { + "value": 101040, + "unitCode": "unit:Pa", + "qualityControl": "qc:V" + }, + "visibility": { + "value": 16090, + "unitCode": "unit:m", + "qualityControl": "qc:C" + }, + "maxTemperatureLast24Hours": { + "value": null, + "unitCode": "unit:degC", + "qualityControl": null + }, + "minTemperatureLast24Hours": { + "value": null, + "unitCode": "unit:degC", + "qualityControl": null + }, + "precipitationLastHour": { + "value": null, + "unitCode": "unit:m", + "qualityControl": "qc:Z" + }, + "precipitationLast3Hours": { + "value": null, + "unitCode": "unit:m", + "qualityControl": "qc:Z" + }, + "precipitationLast6Hours": { + "value": 0, + "unitCode": "unit:m", + "qualityControl": "qc:C" + }, + "relativeHumidity": { + "value": 64.292485914891955, + "unitCode": "unit:percent", + "qualityControl": "qc:C" + }, + "windChill": { + "value": null, + "unitCode": "unit:degC", + "qualityControl": "qc:V" + }, + "heatIndex": { + "value": 27.981288713580284, + "unitCode": "unit:degC", + "qualityControl": "qc:V" + }, + "cloudLayers": [ + { + "base": { + "value": null, + "unitCode": "unit:m" + }, + "amount": "CLR" + } + ] + } + } + ] +} \ No newline at end of file diff --git a/tests/fixtures/nws-weather-sta-valid.json b/tests/fixtures/nws-weather-sta-valid.json new file mode 100644 index 00000000000..b4fe086366c --- /dev/null +++ b/tests/fixtures/nws-weather-sta-valid.json @@ -0,0 +1,996 @@ +{ + "@context": [ + "https://raw.githubusercontent.com/geojson/geojson-ld/master/contexts/geojson-base.jsonld", + { + "wx": "https://api.weather.gov/ontology#", + "s": "https://schema.org/", + "geo": "http://www.opengis.net/ont/geosparql#", + "unit": "http://codes.wmo.int/common/unit/", + "@vocab": "https://api.weather.gov/ontology#", + "geometry": { + "@id": "s:GeoCoordinates", + "@type": "geo:wktLiteral" + }, + "city": "s:addressLocality", + "state": "s:addressRegion", + "distance": { + "@id": "s:Distance", + "@type": "s:QuantitativeValue" + }, + "bearing": { + "@type": "s:QuantitativeValue" + }, + "value": { + "@id": "s:value" + }, + "unitCode": { + "@id": "s:unitCode", + "@type": "@id" + }, + "forecastOffice": { + "@type": "@id" + }, + "forecastGridData": { + "@type": "@id" + }, + "publicZone": { + "@type": "@id" + }, + "county": { + "@type": "@id" + }, + "observationStations": { + "@container": "@list", + "@type": "@id" + } + } + ], + "type": "FeatureCollection", + "features": [ + { + "id": "https://api.weather.gov/stations/KMIE", + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -85.393609999999995, + 40.234169999999999 + ] + }, + "properties": { + "@id": "https://api.weather.gov/stations/KMIE", + "@type": "wx:ObservationStation", + "elevation": { + "value": 284.988, + "unitCode": "unit:m" + }, + "stationIdentifier": "KMIE", + "name": "Muncie, Delaware County-Johnson Field", + "timeZone": "America/Indiana/Indianapolis" + } + }, + { + "id": "https://api.weather.gov/stations/KVES", + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -84.531899899999999, + 40.2044 + ] + }, + "properties": { + "@id": "https://api.weather.gov/stations/KVES", + "@type": "wx:ObservationStation", + "elevation": { + "value": 306.93360000000001, + "unitCode": "unit:m" + }, + "stationIdentifier": "KVES", + "name": "Versailles Darke County Airport", + "timeZone": "America/New_York" + } + }, + { + "id": "https://api.weather.gov/stations/KAID", + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -85.609769999999997, + 40.106119999999997 + ] + }, + "properties": { + "@id": "https://api.weather.gov/stations/KAID", + "@type": "wx:ObservationStation", + "elevation": { + "value": 276.14879999999999, + "unitCode": "unit:m" + }, + "stationIdentifier": "KAID", + "name": "Anderson Municipal Airport", + "timeZone": "America/Indiana/Indianapolis" + } + }, + { + "id": "https://api.weather.gov/stations/KDAY", + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -84.218609999999998, + 39.906109999999998 + ] + }, + "properties": { + "@id": "https://api.weather.gov/stations/KDAY", + "@type": "wx:ObservationStation", + "elevation": { + "value": 306.93360000000001, + "unitCode": "unit:m" + }, + "stationIdentifier": "KDAY", + "name": "Dayton, Cox Dayton International Airport", + "timeZone": "America/New_York" + } + }, + { + "id": "https://api.weather.gov/stations/KGEZ", + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -85.799819999999997, + 39.585459999999998 + ] + }, + "properties": { + "@id": "https://api.weather.gov/stations/KGEZ", + "@type": "wx:ObservationStation", + "elevation": { + "value": 244.1448, + "unitCode": "unit:m" + }, + "stationIdentifier": "KGEZ", + "name": "Shelbyville Municipal Airport", + "timeZone": "America/Indiana/Indianapolis" + } + }, + { + "id": "https://api.weather.gov/stations/KMGY", + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -84.224720000000005, + 39.588889999999999 + ] + }, + "properties": { + "@id": "https://api.weather.gov/stations/KMGY", + "@type": "wx:ObservationStation", + "elevation": { + "value": 291.9984, + "unitCode": "unit:m" + }, + "stationIdentifier": "KMGY", + "name": "Dayton, Dayton-Wright Brothers Airport", + "timeZone": "America/New_York" + } + }, + { + "id": "https://api.weather.gov/stations/KHAO", + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -84.520610000000005, + 39.36121 + ] + }, + "properties": { + "@id": "https://api.weather.gov/stations/KHAO", + "@type": "wx:ObservationStation", + "elevation": { + "value": 185.0136, + "unitCode": "unit:m" + }, + "stationIdentifier": "KHAO", + "name": "Butler County Regional Airport", + "timeZone": "America/New_York" + } + }, + { + "id": "https://api.weather.gov/stations/KFFO", + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -84.049999999999997, + 39.833329900000003 + ] + }, + "properties": { + "@id": "https://api.weather.gov/stations/KFFO", + "@type": "wx:ObservationStation", + "elevation": { + "value": 250.85040000000001, + "unitCode": "unit:m" + }, + "stationIdentifier": "KFFO", + "name": "Dayton / Wright-Patterson Air Force Base", + "timeZone": "America/New_York" + } + }, + { + "id": "https://api.weather.gov/stations/KCVG", + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -84.672290000000004, + 39.044559999999997 + ] + }, + "properties": { + "@id": "https://api.weather.gov/stations/KCVG", + "@type": "wx:ObservationStation", + "elevation": { + "value": 262.12799999999999, + "unitCode": "unit:m" + }, + "stationIdentifier": "KCVG", + "name": "Cincinnati/Northern Kentucky International Airport", + "timeZone": "America/New_York" + } + }, + { + "id": "https://api.weather.gov/stations/KEDJ", + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -83.819199999999995, + 40.372300000000003 + ] + }, + "properties": { + "@id": "https://api.weather.gov/stations/KEDJ", + "@type": "wx:ObservationStation", + "elevation": { + "value": 341.98560000000003, + "unitCode": "unit:m" + }, + "stationIdentifier": "KEDJ", + "name": "Bellefontaine Regional Airport", + "timeZone": "America/New_York" + } + }, + { + "id": "https://api.weather.gov/stations/KFWA", + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -85.206370000000007, + 40.97251 + ] + }, + "properties": { + "@id": "https://api.weather.gov/stations/KFWA", + "@type": "wx:ObservationStation", + "elevation": { + "value": 242.9256, + "unitCode": "unit:m" + }, + "stationIdentifier": "KFWA", + "name": "Fort Wayne International Airport", + "timeZone": "America/Indiana/Indianapolis" + } + }, + { + "id": "https://api.weather.gov/stations/KBAK", + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -85.900000000000006, + 39.266669999999998 + ] + }, + "properties": { + "@id": "https://api.weather.gov/stations/KBAK", + "@type": "wx:ObservationStation", + "elevation": { + "value": 199.94880000000001, + "unitCode": "unit:m" + }, + "stationIdentifier": "KBAK", + "name": "Columbus / Bakalar", + "timeZone": "America/Indiana/Indianapolis" + } + }, + { + "id": "https://api.weather.gov/stations/KEYE", + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -86.295829999999995, + 39.825000000000003 + ] + }, + "properties": { + "@id": "https://api.weather.gov/stations/KEYE", + "@type": "wx:ObservationStation", + "elevation": { + "value": 249.93600000000001, + "unitCode": "unit:m" + }, + "stationIdentifier": "KEYE", + "name": "Indianapolis, Eagle Creek Airpark", + "timeZone": "America/Indiana/Indianapolis" + } + }, + { + "id": "https://api.weather.gov/stations/KLUK", + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -84.41583, + 39.105829999999997 + ] + }, + "properties": { + "@id": "https://api.weather.gov/stations/KLUK", + "@type": "wx:ObservationStation", + "elevation": { + "value": 146.9136, + "unitCode": "unit:m" + }, + "stationIdentifier": "KLUK", + "name": "Cincinnati, Cincinnati Municipal Airport Lunken Field", + "timeZone": "America/New_York" + } + }, + { + "id": "https://api.weather.gov/stations/KIND", + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -86.281599999999997, + 39.725180000000002 + ] + }, + "properties": { + "@id": "https://api.weather.gov/stations/KIND", + "@type": "wx:ObservationStation", + "elevation": { + "value": 240.792, + "unitCode": "unit:m" + }, + "stationIdentifier": "KIND", + "name": "Indianapolis International Airport", + "timeZone": "America/Indiana/Indianapolis" + } + }, + { + "id": "https://api.weather.gov/stations/KAOH", + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -84.021389999999997, + 40.708060000000003 + ] + }, + "properties": { + "@id": "https://api.weather.gov/stations/KAOH", + "@type": "wx:ObservationStation", + "elevation": { + "value": 296.87520000000001, + "unitCode": "unit:m" + }, + "stationIdentifier": "KAOH", + "name": "Lima, Lima Allen County Airport", + "timeZone": "America/New_York" + } + }, + { + "id": "https://api.weather.gov/stations/KI69", + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -84.2102, + 39.078400000000002 + ] + }, + "properties": { + "@id": "https://api.weather.gov/stations/KI69", + "@type": "wx:ObservationStation", + "elevation": { + "value": 256.94640000000004, + "unitCode": "unit:m" + }, + "stationIdentifier": "KI69", + "name": "Batavia Clermont County Airport", + "timeZone": "America/New_York" + } + }, + { + "id": "https://api.weather.gov/stations/KILN", + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -83.779169899999999, + 39.428330000000003 + ] + }, + "properties": { + "@id": "https://api.weather.gov/stations/KILN", + "@type": "wx:ObservationStation", + "elevation": { + "value": 327.96480000000003, + "unitCode": "unit:m" + }, + "stationIdentifier": "KILN", + "name": "Wilmington, Airborne Airpark Airport", + "timeZone": "America/New_York" + } + }, + { + "id": "https://api.weather.gov/stations/KMRT", + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -83.351600000000005, + 40.224699999999999 + ] + }, + "properties": { + "@id": "https://api.weather.gov/stations/KMRT", + "@type": "wx:ObservationStation", + "elevation": { + "value": 311.20080000000002, + "unitCode": "unit:m" + }, + "stationIdentifier": "KMRT", + "name": "Marysville Union County Airport", + "timeZone": "America/New_York" + } + }, + { + "id": "https://api.weather.gov/stations/KTZR", + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -83.137219999999999, + 39.900829999999999 + ] + }, + "properties": { + "@id": "https://api.weather.gov/stations/KTZR", + "@type": "wx:ObservationStation", + "elevation": { + "value": 276.14879999999999, + "unitCode": "unit:m" + }, + "stationIdentifier": "KTZR", + "name": "Columbus, Bolton Field Airport", + "timeZone": "America/New_York" + } + }, + { + "id": "https://api.weather.gov/stations/KFDY", + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -83.668610000000001, + 41.01361 + ] + }, + "properties": { + "@id": "https://api.weather.gov/stations/KFDY", + "@type": "wx:ObservationStation", + "elevation": { + "value": 248.10720000000001, + "unitCode": "unit:m" + }, + "stationIdentifier": "KFDY", + "name": "Findlay, Findlay Airport", + "timeZone": "America/New_York" + } + }, + { + "id": "https://api.weather.gov/stations/KDLZ", + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -83.114800000000002, + 40.279699999999998 + ] + }, + "properties": { + "@id": "https://api.weather.gov/stations/KDLZ", + "@type": "wx:ObservationStation", + "elevation": { + "value": 288.036, + "unitCode": "unit:m" + }, + "stationIdentifier": "KDLZ", + "name": "Delaware Municipal Airport", + "timeZone": "America/New_York" + } + }, + { + "id": "https://api.weather.gov/stations/KOSU", + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -83.0780599, + 40.078060000000001 + ] + }, + "properties": { + "@id": "https://api.weather.gov/stations/KOSU", + "@type": "wx:ObservationStation", + "elevation": { + "value": 274.92959999999999, + "unitCode": "unit:m" + }, + "stationIdentifier": "KOSU", + "name": "Columbus, Ohio State University Airport", + "timeZone": "America/New_York" + } + }, + { + "id": "https://api.weather.gov/stations/KLCK", + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -82.933329999999998, + 39.816670000000002 + ] + }, + "properties": { + "@id": "https://api.weather.gov/stations/KLCK", + "@type": "wx:ObservationStation", + "elevation": { + "value": 227.07600000000002, + "unitCode": "unit:m" + }, + "stationIdentifier": "KLCK", + "name": "Rickenbacker Air National Guard Base", + "timeZone": "America/New_York" + } + }, + { + "id": "https://api.weather.gov/stations/KMNN", + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -83.068330000000003, + 40.616669999999999 + ] + }, + "properties": { + "@id": "https://api.weather.gov/stations/KMNN", + "@type": "wx:ObservationStation", + "elevation": { + "value": 302.97120000000001, + "unitCode": "unit:m" + }, + "stationIdentifier": "KMNN", + "name": "Marion, Marion Municipal Airport", + "timeZone": "America/New_York" + } + }, + { + "id": "https://api.weather.gov/stations/KCMH", + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -82.876390000000001, + 39.994999999999997 + ] + }, + "properties": { + "@id": "https://api.weather.gov/stations/KCMH", + "@type": "wx:ObservationStation", + "elevation": { + "value": 248.10720000000001, + "unitCode": "unit:m" + }, + "stationIdentifier": "KCMH", + "name": "Columbus - John Glenn Columbus International Airport", + "timeZone": "America/New_York" + } + }, + { + "id": "https://api.weather.gov/stations/KFGX", + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -83.743399999999994, + 38.541800000000002 + ] + }, + "properties": { + "@id": "https://api.weather.gov/stations/KFGX", + "@type": "wx:ObservationStation", + "elevation": { + "value": 277.9776, + "unitCode": "unit:m" + }, + "stationIdentifier": "KFGX", + "name": "Flemingsburg Fleming-Mason Airport", + "timeZone": "America/New_York" + } + }, + { + "id": "https://api.weather.gov/stations/KFFT", + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -84.903329999999997, + 38.184719999999999 + ] + }, + "properties": { + "@id": "https://api.weather.gov/stations/KFFT", + "@type": "wx:ObservationStation", + "elevation": { + "value": 245.0592, + "unitCode": "unit:m" + }, + "stationIdentifier": "KFFT", + "name": "Frankfort, Capital City Airport", + "timeZone": "America/New_York" + } + }, + { + "id": "https://api.weather.gov/stations/KLHQ", + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -82.663330000000002, + 39.757219900000003 + ] + }, + "properties": { + "@id": "https://api.weather.gov/stations/KLHQ", + "@type": "wx:ObservationStation", + "elevation": { + "value": 263.95679999999999, + "unitCode": "unit:m" + }, + "stationIdentifier": "KLHQ", + "name": "Lancaster, Fairfield County Airport", + "timeZone": "America/New_York" + } + }, + { + "id": "https://api.weather.gov/stations/KLOU", + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -85.663610000000006, + 38.227780000000003 + ] + }, + "properties": { + "@id": "https://api.weather.gov/stations/KLOU", + "@type": "wx:ObservationStation", + "elevation": { + "value": 166.11600000000001, + "unitCode": "unit:m" + }, + "stationIdentifier": "KLOU", + "name": "Louisville, Bowman Field Airport", + "timeZone": "America/New_York" + } + }, + { + "id": "https://api.weather.gov/stations/KSDF", + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -85.72972, + 38.177219999999998 + ] + }, + "properties": { + "@id": "https://api.weather.gov/stations/KSDF", + "@type": "wx:ObservationStation", + "elevation": { + "value": 150.876, + "unitCode": "unit:m" + }, + "stationIdentifier": "KSDF", + "name": "Louisville, Standiford Field", + "timeZone": "America/New_York" + } + }, + { + "id": "https://api.weather.gov/stations/KVTA", + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -82.462500000000006, + 40.022779999999997 + ] + }, + "properties": { + "@id": "https://api.weather.gov/stations/KVTA", + "@type": "wx:ObservationStation", + "elevation": { + "value": 269.13839999999999, + "unitCode": "unit:m" + }, + "stationIdentifier": "KVTA", + "name": "Newark, Newark Heath Airport", + "timeZone": "America/New_York" + } + }, + { + "id": "https://api.weather.gov/stations/KLEX", + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -84.6114599, + 38.033900000000003 + ] + }, + "properties": { + "@id": "https://api.weather.gov/stations/KLEX", + "@type": "wx:ObservationStation", + "elevation": { + "value": 291.084, + "unitCode": "unit:m" + }, + "stationIdentifier": "KLEX", + "name": "Lexington Blue Grass Airport", + "timeZone": "America/New_York" + } + }, + { + "id": "https://api.weather.gov/stations/KMFD", + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -82.517780000000002, + 40.820279900000003 + ] + }, + "properties": { + "@id": "https://api.weather.gov/stations/KMFD", + "@type": "wx:ObservationStation", + "elevation": { + "value": 395.02080000000001, + "unitCode": "unit:m" + }, + "stationIdentifier": "KMFD", + "name": "Mansfield - Mansfield Lahm Regional Airport", + "timeZone": "America/New_York" + } + }, + { + "id": "https://api.weather.gov/stations/KZZV", + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -81.892219999999995, + 39.94444 + ] + }, + "properties": { + "@id": "https://api.weather.gov/stations/KZZV", + "@type": "wx:ObservationStation", + "elevation": { + "value": 274.01519999999999, + "unitCode": "unit:m" + }, + "stationIdentifier": "KZZV", + "name": "Zanesville, Zanesville Municipal Airport", + "timeZone": "America/New_York" + } + }, + { + "id": "https://api.weather.gov/stations/KHTS", + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -82.555000000000007, + 38.365000000000002 + ] + }, + "properties": { + "@id": "https://api.weather.gov/stations/KHTS", + "@type": "wx:ObservationStation", + "elevation": { + "value": 252.06960000000001, + "unitCode": "unit:m" + }, + "stationIdentifier": "KHTS", + "name": "Huntington, Tri-State Airport", + "timeZone": "America/New_York" + } + }, + { + "id": "https://api.weather.gov/stations/KBJJ", + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -81.886669999999995, + 40.873060000000002 + ] + }, + "properties": { + "@id": "https://api.weather.gov/stations/KBJJ", + "@type": "wx:ObservationStation", + "elevation": { + "value": 345.94800000000004, + "unitCode": "unit:m" + }, + "stationIdentifier": "KBJJ", + "name": "Wooster, Wayne County Airport", + "timeZone": "America/New_York" + } + }, + { + "id": "https://api.weather.gov/stations/KPHD", + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -81.423609999999996, + 40.471939900000002 + ] + }, + "properties": { + "@id": "https://api.weather.gov/stations/KPHD", + "@type": "wx:ObservationStation", + "elevation": { + "value": 271.88159999999999, + "unitCode": "unit:m" + }, + "stationIdentifier": "KPHD", + "name": "New Philadelphia, Harry Clever Field", + "timeZone": "America/New_York" + } + }, + { + "id": "https://api.weather.gov/stations/KPKB", + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -81.439170000000004, + 39.344999999999999 + ] + }, + "properties": { + "@id": "https://api.weather.gov/stations/KPKB", + "@type": "wx:ObservationStation", + "elevation": { + "value": 262.12799999999999, + "unitCode": "unit:m" + }, + "stationIdentifier": "KPKB", + "name": "Parkersburg, Mid-Ohio Valley Regional Airport", + "timeZone": "America/New_York" + } + }, + { + "id": "https://api.weather.gov/stations/KCAK", + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -81.443430000000006, + 40.918109999999999 + ] + }, + "properties": { + "@id": "https://api.weather.gov/stations/KCAK", + "@type": "wx:ObservationStation", + "elevation": { + "value": 369.11279999999999, + "unitCode": "unit:m" + }, + "stationIdentifier": "KCAK", + "name": "Akron Canton Regional Airport", + "timeZone": "America/New_York" + } + }, + { + "id": "https://api.weather.gov/stations/KCRW", + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -81.591390000000004, + 38.379440000000002 + ] + }, + "properties": { + "@id": "https://api.weather.gov/stations/KCRW", + "@type": "wx:ObservationStation", + "elevation": { + "value": 299.00880000000001, + "unitCode": "unit:m" + }, + "stationIdentifier": "KCRW", + "name": "Charleston, Yeager Airport", + "timeZone": "America/New_York" + } + } + ], + "observationStations": [ + "https://api.weather.gov/stations/KMIE", + "https://api.weather.gov/stations/KVES", + "https://api.weather.gov/stations/KAID", + "https://api.weather.gov/stations/KDAY", + "https://api.weather.gov/stations/KGEZ", + "https://api.weather.gov/stations/KMGY", + "https://api.weather.gov/stations/KHAO", + "https://api.weather.gov/stations/KFFO", + "https://api.weather.gov/stations/KCVG", + "https://api.weather.gov/stations/KEDJ", + "https://api.weather.gov/stations/KFWA", + "https://api.weather.gov/stations/KBAK", + "https://api.weather.gov/stations/KEYE", + "https://api.weather.gov/stations/KLUK", + "https://api.weather.gov/stations/KIND", + "https://api.weather.gov/stations/KAOH", + "https://api.weather.gov/stations/KI69", + "https://api.weather.gov/stations/KILN", + "https://api.weather.gov/stations/KMRT", + "https://api.weather.gov/stations/KTZR", + "https://api.weather.gov/stations/KFDY", + "https://api.weather.gov/stations/KDLZ", + "https://api.weather.gov/stations/KOSU", + "https://api.weather.gov/stations/KLCK", + "https://api.weather.gov/stations/KMNN", + "https://api.weather.gov/stations/KCMH", + "https://api.weather.gov/stations/KFGX", + "https://api.weather.gov/stations/KFFT", + "https://api.weather.gov/stations/KLHQ", + "https://api.weather.gov/stations/KLOU", + "https://api.weather.gov/stations/KSDF", + "https://api.weather.gov/stations/KVTA", + "https://api.weather.gov/stations/KLEX", + "https://api.weather.gov/stations/KMFD", + "https://api.weather.gov/stations/KZZV", + "https://api.weather.gov/stations/KHTS", + "https://api.weather.gov/stations/KBJJ", + "https://api.weather.gov/stations/KPHD", + "https://api.weather.gov/stations/KPKB", + "https://api.weather.gov/stations/KCAK", + "https://api.weather.gov/stations/KCRW" + ] +} \ No newline at end of file