Skip to content

Commit

Permalink
Add simple api (#11)
Browse files Browse the repository at this point in the history
Add simple class to retrieve current observations with metar fallback for missing values.
  • Loading branch information
MatthewFlamm authored Jul 17, 2019
1 parent 1ae0d6e commit 4a0fb3e
Show file tree
Hide file tree
Showing 16 changed files with 1,001 additions and 10 deletions.
8 changes: 7 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,10 @@ __pycache__/
.tox/
build/
dist/
.coverage
.coverage
bin/
lib/
pyvenv.cfg
lib64
.in
.out
1 change: 1 addition & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ include README.md
include LICENSE
include requirements-test.txt
include .coveragerc
include VERSION
1 change: 1 addition & 0 deletions VERSION
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
0.6.0
3 changes: 2 additions & 1 deletion pynws/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@
asynchronously and organizing the data in an easier to use manner
"""


from .simple_nws import SimpleNWS
from .nws import *
from .const import version
6 changes: 5 additions & 1 deletion pynws/const.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
"""
Constants for pynws
"""
import os

__version__ = '0.6'
file_dir = os.path.join(os.path.dirname(__file__), '..')

with open(os.path.join(file_dir, 'VERSION')) as version_file:
version = version_file.read().strip()

API_URL = 'https://api.weather.gov/'
API_STATIONS = 'points/{},{}/stations'
Expand Down
9 changes: 9 additions & 0 deletions pynws/nws.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ async def forecast_hourly(self):
raise NwsError("Need to set lattitude and longitude")
return await forecast_hourly(*self.latlon, self.session, self.userid)


def get_header(userid):
"""Get header.
Expand All @@ -51,6 +52,7 @@ def get_header(userid):
return {'accept': API_ACCEPT,
'User-Agent': API_USER.format(userid)}


async def get_obs_from_stn(station, websession, userid, limit=5):
"""Get observation response from station"""
if limit == 0:
Expand All @@ -65,11 +67,13 @@ async def get_obs_from_stn(station, websession, userid, limit=5):
obs = await res.json()
return obs


async def observations(station, websession, userid, limit=5):
"""Observations from station"""
res = await get_obs_from_stn(station, websession, userid, limit)
return [o['properties'] for o in res['features']]


async def get_stn_from_pnt(lat, lon, websession, userid):
"""Get list of stations for lat/lon"""

Expand All @@ -80,12 +84,14 @@ async def get_stn_from_pnt(lat, lon, websession, userid):
jres = await res.json()
return jres


async def stations(lat, lon, websession, userid):
"""Returns list of stations for a point."""
res = await get_stn_from_pnt(lat, lon, websession, userid)
return [s['properties']['stationIdentifier']
for s in res['features']]


async def get_forc_from_pnt(lat, lon, websession, userid):
"""update forecast"""

Expand All @@ -96,11 +102,13 @@ async def get_forc_from_pnt(lat, lon, websession, userid):
jres = await res.json()
return jres


async def forecast(lat, lon, websession, userid):
"""Returns forecast as list """
res = await get_forc_from_pnt(lat, lon, websession, userid)
return res['properties']['periods']


async def get_hour_forc_from_pnt(lat, lon, websession, userid):
"""update forecast"""

Expand All @@ -111,6 +119,7 @@ async def get_hour_forc_from_pnt(lat, lon, websession, userid):
jres = await res.json()
return jres


async def forecast_hourly(lat, lon, websession, userid):
"""Returns hourly forecast as list """
res = await get_hour_forc_from_pnt(lat, lon, websession, userid)
Expand Down
169 changes: 169 additions & 0 deletions pynws/simple_nws.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
"""Support for NWS weather service."""
from statistics import mean

from metar.Metar import Metar

from .nws import Nws
from .const import API_WEATHER_CODE

WIND_DIRECTIONS = ['N', 'NNE', 'NE', 'ENE',
'E', 'ESE', 'SE', 'SSE',
'S', 'SSW', 'SW', 'WSW',
'W', 'WNW', 'NW', 'NNW']

WIND = {name: idx * 360 / 16 for idx, name in enumerate(WIND_DIRECTIONS)}

OBSERVATIONS = {
'temperature': ['temp', 'C', None],
'barometricPressure': None,
'seaLevelPressure': ['press', 'HPA', 100],
'relativeHumidity': None,
'windSpeed': ['wind_speed', 'MPS', None],
'windDirection': ['wind_dir', None, None],
'visibility': ['vis', 'M', None],
'elevation': None,
'textDescription': None,
'dewpoint': None,
'windGust': None,
'station': None,
'timestamp': None,
'icon': None,
'maxTemperatureLast24Hours': None,
'minTemperatureLast24Hours': None,
'precipitationLastHour': None,
'precipitationLast3Hours': None,
'precipitationLast6Hours': None,
'windChill': None,
'heatIndex': None,
}

def convert_weather(weather):
"""Convert short code to readable name."""
return [(API_WEATHER_CODE.get(w[0], w[0]), w[1]) for w in weather]


def parse_icon(icon):
"""
Parse icon url to NWS weather codes.
Example:
https://api.weather.gov/icons/land/day/skc/tsra,40?size=medium
Example return:
('day', (('skc', None), ('tsra', 40),))
"""
icon_list = icon.split('/')
time = icon_list[5]
weather = [i.split('?')[0] for i in icon_list[6:]]
code = [w.split(',')[0] for w in weather]
chance = [int(w.split(',')[1]) if len(w.split(',')) == 2 else None
for w in weather]
return time, tuple(zip(code, chance))


class SimpleNWS:
"""
NWS simplified data.
Uses normal api first. If value is None, use metar info.
"""
def __init__(self, lat, lon, api_key, mode, session):
"""Set up simplified NWS class."""
self.lat = lat
self.lon = lon
self.api_key = api_key
self.session = session
self.nws = Nws(session, latlon=(float(lat), float(lon)),
userid=api_key)
self.mode = mode

self._observation = None
self._metar_obs = None
self.station = None
self.stations = None
self._forecast = None

async def set_station(self, station=None):
"""
Set station or retrieve station list.
If no station is supplied, the first retrieved value is set.
"""
if station:
self.nws.station = station
self.station = station
self.stations = [self.station]
else:
self.stations = await self.nws.stations()
self.nws.station = self.stations[0]
self.station = self.stations[0]

async def update_observation(self):
"""Update observation."""

obs = await self.nws.observations(limit=1)
if obs is None:
return None
self._observation = obs[0]
metar_msg = self._observation.get('rawMessage')
if metar_msg:
self._metar_obs = Metar(metar_msg)
else:
self._metar_obs = None

async def update_forecast(self):
"""Update forecast."""
if self.mode == 'daynight':
forecast = await self.nws.forecast()
elif self.mode == 'hourly':
forecast = await self.nws.forecast_hourly()
self._forecast = forecast

@property
def observation(self):
"""Observation dict"""

if self._observation is None:
return None

data = {}
for obs, met in OBSERVATIONS.items():
data[obs] = self._observation[obs]
if isinstance(data[obs], dict):
data[obs] = data[obs].get('value')
if data[obs] is None and (met is not None
and self._metar_obs is not None):
met_prop = getattr(self._metar_obs, met[0])
if met_prop:
if met[1]:
data[obs] = met_prop.value(units=met[1])
else:
data[obs] = met_prop.value()
if met[2] is not None:
data[obs] = data[obs] * met[2]

time, weather = parse_icon(data['icon'])
data['iconTime'] = time
data['iconWeather'] = convert_weather(weather)
return data

@property
def forecast(self):
"""Return forecast."""
forecast = []
for forecast_entry in self._forecast:
# get weather
time, weather = parse_icon(forecast_entry['icon'])
weather = convert_weather(weather)
forecast_entry['iconTime'] = time
forecast_entry['iconWeather'] = weather
forecast_entry['windBearing'] = \
WIND[forecast_entry['windDirection']]

# wind speed reported as '7 mph' or '7 to 10 mph'
# if range, take average
wind_speed = forecast_entry['windSpeed'].split(' ')[0::2]
wind_speed_avg = mean(int(w) for w in wind_speed)
forecast_entry['windSpeedAvg'] = wind_speed_avg

forecast.append(forecast_entry)
return forecast
104 changes: 104 additions & 0 deletions pynws/tests/metar_observation_response.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
METAR_OBSERVATION_RESPONSE = {
'features':[{'properties': {
"@id": "https://api.weather.gov/stations/PHNG/observations/2019-06-12T10:57:00+00:00",
"@type": "wx:ObservationStation",
"elevation": {
"value": 3,
"unitCode": "unit:m"
},
"station": "https://api.weather.gov/stations/PHNG",
"timestamp": "2019-06-12T10:57:00+00:00",
"rawMessage": "PHNG 121057Z 08007KT 10SM FEW023 26/20 A3001 RMK AO2 SLP158 T02560200",
"textDescription": "Mostly Clear",
"icon": "https://api.weather.gov/icons/land/night/few?size=medium",
"presentWeather": [],
"temperature": {
"value": None,
"unitCode": "unit:degC",
"qualityControl": "qc:Z"
},
"dewpoint": {
"value": None,
"unitCode": "unit:degC",
"qualityControl": "qc:Z"
},
"windDirection": {
"value": None,
"unitCode": "unit:degree_(angle)",
"qualityControl": "qc:Z"
},
"windSpeed": {
"value": None,
"unitCode": "unit:m_s-1",
"qualityControl": "qc:Z"
},
"windGust": {
"value": None,
"unitCode": "unit:m_s-1",
"qualityControl": "qc:Z"
},
"barometricPressure": {
"value": None,
"unitCode": "unit:Pa",
"qualityControl": "qc:Z"
},
"seaLevelPressure": {
"value": None,
"unitCode": "unit:Pa",
"qualityControl": "qc:Z"
},
"visibility": {
"value": 16090,
"unitCode": "unit:m",
"qualityControl": "qc:C"
},
"maxTemperatureLast24Hours": {
"value": None,
"unitCode": "unit:degC",
"qualityControl": None
},
"minTemperatureLast24Hours": {
"value": None,
"unitCode": "unit:degC",
"qualityControl": None
},
"precipitationLastHour": {
"value": None,
"unitCode": "unit:m",
"qualityControl": "qc:Z"
},
"precipitationLast3Hours": {
"value": None,
"unitCode": "unit:m",
"qualityControl": "qc:Z"
},
"precipitationLast6Hours": {
"value": None,
"unitCode": "unit:m",
"qualityControl": "qc:Z"
},
"relativeHumidity": {
"value": None,
"unitCode": "unit:percent",
"qualityControl": "qc:C"
},
"windChill": {
"value": None,
"unitCode": "unit:degC",
"qualityControl": "qc:V"
},
"heatIndex": {
"value": None,
"unitCode": "unit:degC",
"qualityControl": "qc:V"
},
"cloudLayers": [
{
"base": {
"value": 700,
"unitCode": "unit:m"
},
"amount": "FEW"
}
]
}}]}
Loading

0 comments on commit 4a0fb3e

Please sign in to comment.