add new metar parser and update weather module
parent
7622bd3376
commit
83518a8dbc
|
@ -0,0 +1,280 @@
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
INTENSITY = {
|
||||||
|
"-": "light",
|
||||||
|
"+": "heavy",
|
||||||
|
"VC": "in the vicinity:",
|
||||||
|
}
|
||||||
|
|
||||||
|
DESCRIPTOR = {
|
||||||
|
"MI": "shallow",
|
||||||
|
"PR": "partial",
|
||||||
|
"BC": "patches",
|
||||||
|
"DR": "low drifting",
|
||||||
|
"BL": "blowing",
|
||||||
|
"SH": "showers",
|
||||||
|
"TS": "thunderstorm",
|
||||||
|
"FZ": "freezing",
|
||||||
|
}
|
||||||
|
|
||||||
|
PRECIPITATION = {
|
||||||
|
"DZ": "drizzle",
|
||||||
|
"RA": "rain",
|
||||||
|
"SN": "snow",
|
||||||
|
"SG": "snow grains",
|
||||||
|
"IC": "ice crystals",
|
||||||
|
"PL": "ice pellets",
|
||||||
|
"GR": "hail",
|
||||||
|
"GS": "small hail",
|
||||||
|
"UP": "unknown precipitation",
|
||||||
|
}
|
||||||
|
|
||||||
|
OBSCURATION = {
|
||||||
|
"BR": "mist",
|
||||||
|
"FG": "fog",
|
||||||
|
"VA": "volcanic ash",
|
||||||
|
"DU": "widespread dust",
|
||||||
|
"SA": "sand",
|
||||||
|
"HZ": "haze",
|
||||||
|
"PY": "spray",
|
||||||
|
}
|
||||||
|
|
||||||
|
CLOUD_COVER = {
|
||||||
|
"SKC": "clear",
|
||||||
|
"CLR": "clear",
|
||||||
|
"NSC": "clear",
|
||||||
|
"FEW": "a few clouds",
|
||||||
|
"SCT": "scattered clouds",
|
||||||
|
"BKN": "broken clouds",
|
||||||
|
"OVC": "overcast",
|
||||||
|
"VV": "indefinite ceiling",
|
||||||
|
}
|
||||||
|
|
||||||
|
OTHER = {
|
||||||
|
"PO": "whirls",
|
||||||
|
"SQ": "squals",
|
||||||
|
"FC": "tornado",
|
||||||
|
"SS": "sandstorm",
|
||||||
|
"DS": "duststorm",
|
||||||
|
}
|
||||||
|
|
||||||
|
import re
|
||||||
|
|
||||||
|
|
||||||
|
class Weather(object):
|
||||||
|
cover = None
|
||||||
|
height = None
|
||||||
|
wind_speed = None
|
||||||
|
wind_direction = None
|
||||||
|
intensity = None
|
||||||
|
descriptor = None
|
||||||
|
precipitation = None
|
||||||
|
obscuration = None
|
||||||
|
other = None
|
||||||
|
conditions = None
|
||||||
|
|
||||||
|
def describe_wind(self):
|
||||||
|
if self.wind_speed is not None:
|
||||||
|
if self.wind_speed < 1:
|
||||||
|
return "calm"
|
||||||
|
elif self.wind_speed < 4:
|
||||||
|
return "light air"
|
||||||
|
elif self.wind_speed < 7:
|
||||||
|
return "light breeze"
|
||||||
|
elif self.wind_speed < 11:
|
||||||
|
return "gentle breeze"
|
||||||
|
elif self.wind_speed < 16:
|
||||||
|
return "moderate breeze"
|
||||||
|
elif self.wind_speed < 22:
|
||||||
|
return "fresh breeze"
|
||||||
|
elif self.wind_speed < 28:
|
||||||
|
return "strong breeze"
|
||||||
|
elif self.wind_speed < 34:
|
||||||
|
return "near gale"
|
||||||
|
elif self.wind_speed < 41:
|
||||||
|
return "gale"
|
||||||
|
elif self.wind_speed < 56:
|
||||||
|
return "storm"
|
||||||
|
elif self.wind_speed < 64:
|
||||||
|
return "violent storm"
|
||||||
|
else:
|
||||||
|
return "hurricane"
|
||||||
|
else:
|
||||||
|
return 'unknown'
|
||||||
|
|
||||||
|
def windsock(self):
|
||||||
|
if self.wind_direction is not None:
|
||||||
|
if (self.wind_speed <= 22.5) or (self.wind_speed > 337.5):
|
||||||
|
return '\u2191'
|
||||||
|
elif (self.wind_speed > 22.5) and (self.wind_speed <= 67.5):
|
||||||
|
return '\u2197'
|
||||||
|
elif (self.wind_speed > 67.5) and (self.wind_speed <= 112.5):
|
||||||
|
return '\u2192'
|
||||||
|
elif (self.wind_speed > 112.5) and (self.wind_speed <= 157.5):
|
||||||
|
return '\u2198'
|
||||||
|
elif (self.wind_speed > 157.5) and (self.wind_speed <= 202.5):
|
||||||
|
return '\u2193'
|
||||||
|
elif (self.wind_speed > 202.5) and (self.wind_speed <= 247.5):
|
||||||
|
return '\u2199'
|
||||||
|
elif (self.wind_speed > 247.5) and (self.wind_speed <= 292.5):
|
||||||
|
return '\u2190'
|
||||||
|
elif (self.wind_speed > 292.5) and (self.wind_speed <= 337.5):
|
||||||
|
return '\u2196'
|
||||||
|
else:
|
||||||
|
return '?'
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
if self.conditions:
|
||||||
|
ret = "{cover}, {temperature}°C, {pressure} hPa, {conditions}, "\
|
||||||
|
"{windnote} {wind} m/s ({windsock}) - {station} {time}"
|
||||||
|
else:
|
||||||
|
ret = "{cover}, {temperature}°C, {pressure} hPa, "\
|
||||||
|
"{windnote} {wind} m/s ({windsock}) - {station} {time}"
|
||||||
|
|
||||||
|
wind = self.wind_speed if self.wind_speed is not None else '?'
|
||||||
|
|
||||||
|
return ret.format(cover=self.cover, temperature=self.temperature,
|
||||||
|
pressure=self.pressure, conditions=self.conditions,
|
||||||
|
wind=wind, windnote=self.describe_wind(),
|
||||||
|
windsock=self.windsock(), station=self.station,
|
||||||
|
time=self.time.strftime("%H:%MZ"))
|
||||||
|
|
||||||
|
|
||||||
|
def build_regex(key, classifier):
|
||||||
|
ret = "|".join([re.escape(x) for x in classifier.keys()])
|
||||||
|
return r"(?P<{key}>{regex})".format(key=re.escape(key), regex=ret)
|
||||||
|
|
||||||
|
|
||||||
|
def weather_regex():
|
||||||
|
ret = r'\s'
|
||||||
|
ret += build_regex('intensity', INTENSITY) + r'?'
|
||||||
|
ret += build_regex('descriptor', DESCRIPTOR) + r'?'
|
||||||
|
ret += build_regex('precipitation', PRECIPITATION) + r'?'
|
||||||
|
ret += build_regex('obscuration', OBSCURATION) + r'?'
|
||||||
|
ret += build_regex('other', OTHER) + r'?'
|
||||||
|
ret += r'\s'
|
||||||
|
return re.compile(ret)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_temp(t):
|
||||||
|
if t[0] == 'M':
|
||||||
|
return -int(t[1:])
|
||||||
|
return int(t)
|
||||||
|
|
||||||
|
|
||||||
|
def parse(data):
|
||||||
|
w = Weather()
|
||||||
|
|
||||||
|
data = data.splitlines()
|
||||||
|
metar = data[1].split()
|
||||||
|
|
||||||
|
w.metar = data[1]
|
||||||
|
w.station = metar[0]
|
||||||
|
metar = metar[1:]
|
||||||
|
|
||||||
|
# time
|
||||||
|
time_re = re.compile(r"\d{2}(?P<hour>\d{2})(?P<min>\d{2})Z")
|
||||||
|
m = time_re.search(w.metar)
|
||||||
|
if m:
|
||||||
|
w.time = datetime.time(hour=int(m.group('hour')),
|
||||||
|
minute=int(m.group('min')))
|
||||||
|
|
||||||
|
# mode
|
||||||
|
#if metar[0] == "AUTO":
|
||||||
|
# metar = metar[1:]
|
||||||
|
|
||||||
|
# wind speed
|
||||||
|
wind_re = re.compile(r"(?P<direction>\d{3})(?P<speed>\d+)(G(?P<gust>\d+))?(?P<unit>KT|MPS)")
|
||||||
|
m = wind_re.search(w.metar)
|
||||||
|
if m:
|
||||||
|
w.wind_direction = int(m.group('direction'))
|
||||||
|
|
||||||
|
if m.group('unit') == "KT":
|
||||||
|
# convert knots to m/s
|
||||||
|
w.wind_speed = round(int(m.group('speed')) * 1852 / 3600)
|
||||||
|
if m.group('gust'):
|
||||||
|
w.wind_gust = round(int(m.group('speed')) * 1852 / 3600)
|
||||||
|
else:
|
||||||
|
w.wind_gust = None
|
||||||
|
else:
|
||||||
|
w.wind_speed = int(m.group('speed'))
|
||||||
|
if m.group('gust'):
|
||||||
|
w.wind_gust = int(m.group('gust'))
|
||||||
|
else:
|
||||||
|
w.wind_gust = None
|
||||||
|
metar = metar[1:]
|
||||||
|
|
||||||
|
# visibility
|
||||||
|
# 0800N?
|
||||||
|
visibility_re = re.compile(r"(?P<vis>(?P<dist>\d+)SM|(?P<disti>\d{4})\s|CAVOK)")
|
||||||
|
m = visibility_re.search(w.metar)
|
||||||
|
if m:
|
||||||
|
if m.group('dist'):
|
||||||
|
w.visibility = m.group('dist')
|
||||||
|
elif m.group('disti'):
|
||||||
|
w.visibility = m.group('disti')
|
||||||
|
elif m.group('vis') == 'CAVOK':
|
||||||
|
w.cover = "clear"
|
||||||
|
w.visibility = m.group('vis')
|
||||||
|
else:
|
||||||
|
w.visibility = None
|
||||||
|
|
||||||
|
# runway visibility range
|
||||||
|
|
||||||
|
# conditions
|
||||||
|
matches = weather_regex().finditer(w.metar)
|
||||||
|
for m in matches:
|
||||||
|
if not m:
|
||||||
|
continue
|
||||||
|
|
||||||
|
weather = []
|
||||||
|
if m.group('intensity'):
|
||||||
|
w.intensity = INTENSITY[m.group('intensity')]
|
||||||
|
weather.append(w.intensity)
|
||||||
|
if m.group('descriptor'):
|
||||||
|
w.descriptor = DESCRIPTOR[m.group('descriptor')]
|
||||||
|
weather.append(w.descriptor)
|
||||||
|
if m.group('precipitation'):
|
||||||
|
w.precipitation = PRECIPITATION[m.group('precipitation')]
|
||||||
|
weather.append(w.precipitation)
|
||||||
|
if m.group('obscuration'):
|
||||||
|
w.obscuration = OBSCURATION[m.group('obscuration')]
|
||||||
|
weather.append(w.obscuration)
|
||||||
|
if m.group('other'):
|
||||||
|
w.other = OTHER[m.group('other')]
|
||||||
|
weather.append(w.other)
|
||||||
|
if len(weather) > 0:
|
||||||
|
w.conditions = " ".join(weather)
|
||||||
|
|
||||||
|
# cloud cover
|
||||||
|
cover_re = re.compile(build_regex('cover', CLOUD_COVER) +\
|
||||||
|
r"(?P<height>\d*)")
|
||||||
|
matches = cover_re.finditer(w.metar)
|
||||||
|
for m in matches:
|
||||||
|
w.cover = CLOUD_COVER[m.group('cover')]
|
||||||
|
w.height = m.group('height')
|
||||||
|
|
||||||
|
# temperature
|
||||||
|
temp_re = re.compile(r"(?P<temp>[M\d]+)\/(?P<dewpoint>[M\d]+)")
|
||||||
|
m = temp_re.search(w.metar)
|
||||||
|
if m:
|
||||||
|
w.temperature = parse_temp(m.group('temp'))
|
||||||
|
w.dewpoint = parse_temp(m.group('dewpoint'))
|
||||||
|
|
||||||
|
# pressure
|
||||||
|
pressure_re = re.compile(r"([QA])(\d+)")
|
||||||
|
m = pressure_re.search(w.metar)
|
||||||
|
if m.group(1) == 'A':
|
||||||
|
# convert inHg to hPa
|
||||||
|
w.pressure = round(int(m.group(2)) / 100 * 3.386389)
|
||||||
|
else:
|
||||||
|
w.pressure = int(m.group(2))
|
||||||
|
|
||||||
|
return w
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import glob
|
||||||
|
for station in glob.glob('test/metar/*.TXT'):
|
||||||
|
with open(station) as f:
|
||||||
|
print(parse(f.read()))
|
|
@ -27,27 +27,21 @@ class TestWeather(unittest.TestCase):
|
||||||
self.assertEqual(icao, 'KIAD')
|
self.assertEqual(icao, 'KIAD')
|
||||||
|
|
||||||
def test_airport(self):
|
def test_airport(self):
|
||||||
input = Mock(
|
input = Mock(group=lambda x: 'KIAD')
|
||||||
match=Mock(group=lambda x: 'KIAD'),
|
|
||||||
sender='#phenny', nick='phenny_test')
|
|
||||||
f_weather(self.phenny, input)
|
f_weather(self.phenny, input)
|
||||||
|
|
||||||
assert self.phenny.msg.called is True
|
assert self.phenny.say.called is True
|
||||||
|
|
||||||
def test_place(self):
|
def test_place(self):
|
||||||
input = Mock(
|
input = Mock(group=lambda x: 'Blacksburg')
|
||||||
match=Mock(group=lambda x: 'Blacksburg'),
|
|
||||||
sender='#phenny', nick='phenny_test')
|
|
||||||
f_weather(self.phenny, input)
|
f_weather(self.phenny, input)
|
||||||
|
|
||||||
assert self.phenny.msg.called is True
|
assert self.phenny.say.called is True
|
||||||
|
|
||||||
def test_notfound(self):
|
def test_notfound(self):
|
||||||
input = Mock(
|
input = Mock(group=lambda x: 'Hell')
|
||||||
match=Mock(group=lambda x: 'Hell'),
|
|
||||||
sender='#phenny', nick='phenny_test')
|
|
||||||
f_weather(self.phenny, input)
|
f_weather(self.phenny, input)
|
||||||
|
|
||||||
self.phenny.msg.called_once_with('#phenny',
|
self.phenny.say.called_once_with('#phenny',
|
||||||
"No NOAA data available for that location.")
|
"No NOAA data available for that location.")
|
||||||
|
|
||||||
|
|
|
@ -8,15 +8,16 @@ http://inamidst.com/phenny/
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import re, urllib.request, urllib.parse, urllib.error
|
import re, urllib.request, urllib.parse, urllib.error
|
||||||
|
import metar
|
||||||
import web
|
import web
|
||||||
from tools import deprecated, GrumbleError
|
from tools import deprecated, GrumbleError
|
||||||
|
|
||||||
r_from = re.compile(r'(?i)([+-]\d+):00 from')
|
r_from = re.compile(r'(?i)([+-]\d+):00 from')
|
||||||
|
|
||||||
def location(name):
|
def location(name):
|
||||||
name = urllib.parse.quote(name)
|
name = urllib.parse.quote(name)
|
||||||
uri = 'http://ws.geonames.org/searchJSON?q=%s&maxRows=1' % name
|
uri = 'http://ws.geonames.org/searchJSON?q=%s&maxRows=1' % name
|
||||||
for i in range(10):
|
for i in range(10):
|
||||||
bytes = web.get(uri)
|
bytes = web.get(uri)
|
||||||
if bytes is not None: break
|
if bytes is not None: break
|
||||||
|
|
||||||
|
@ -29,14 +30,14 @@ def location(name):
|
||||||
lng = results['geonames'][0]['lng']
|
lng = results['geonames'][0]['lng']
|
||||||
return name, countryName, lat, lng
|
return name, countryName, lat, lng
|
||||||
|
|
||||||
def local(icao, hour, minute):
|
def local(icao, hour, minute):
|
||||||
uri = ('http://www.flightstats.com/' +
|
uri = ('http://www.flightstats.com/' +
|
||||||
'go/Airport/airportDetails.do?airportCode=%s')
|
'go/Airport/airportDetails.do?airportCode=%s')
|
||||||
try: bytes = web.get(uri % icao)
|
try: bytes = web.get(uri % icao)
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
raise GrumbleError('A WEBSITE HAS GONE DOWN WTF STUPID WEB')
|
raise GrumbleError('A WEBSITE HAS GONE DOWN WTF STUPID WEB')
|
||||||
m = r_from.search(bytes)
|
m = r_from.search(bytes)
|
||||||
if m:
|
if m:
|
||||||
offset = m.group(1)
|
offset = m.group(1)
|
||||||
lhour = int(hour) + int(offset)
|
lhour = int(hour) + int(offset)
|
||||||
lhour = lhour % 24
|
lhour = lhour % 24
|
||||||
|
@ -46,7 +47,7 @@ def local(icao, hour, minute):
|
||||||
# ':' + str(minute) + 'Z)')
|
# ':' + str(minute) + 'Z)')
|
||||||
return str(hour) + ':' + str(minute) + 'Z'
|
return str(hour) + ':' + str(minute) + 'Z'
|
||||||
|
|
||||||
def code(phenny, search):
|
def code(phenny, search):
|
||||||
from icao import data
|
from icao import data
|
||||||
|
|
||||||
if search.upper() in [loc[0] for loc in data]:
|
if search.upper() in [loc[0] for loc in data]:
|
||||||
|
@ -55,358 +56,41 @@ def code(phenny, search):
|
||||||
name, country, latitude, longitude = location(search)
|
name, country, latitude, longitude = location(search)
|
||||||
if name == '?': return False
|
if name == '?': return False
|
||||||
sumOfSquares = (99999999999999999999999999999, 'ICAO')
|
sumOfSquares = (99999999999999999999999999999, 'ICAO')
|
||||||
for icao_code, lat, lon in data:
|
for icao_code, lat, lon in data:
|
||||||
latDiff = abs(latitude - lat)
|
latDiff = abs(latitude - lat)
|
||||||
lonDiff = abs(longitude - lon)
|
lonDiff = abs(longitude - lon)
|
||||||
diff = (latDiff * latDiff) + (lonDiff * lonDiff)
|
diff = (latDiff * latDiff) + (lonDiff * lonDiff)
|
||||||
if diff < sumOfSquares[0]:
|
if diff < sumOfSquares[0]:
|
||||||
sumOfSquares = (diff, icao_code)
|
sumOfSquares = (diff, icao_code)
|
||||||
return sumOfSquares[1]
|
return sumOfSquares[1]
|
||||||
|
|
||||||
@deprecated
|
def f_weather(phenny, input):
|
||||||
def f_weather(self, origin, match, args):
|
|
||||||
""".weather <ICAO> - Show the weather at airport with the code <ICAO>."""
|
""".weather <ICAO> - Show the weather at airport with the code <ICAO>."""
|
||||||
if origin.sender == '#talis':
|
icao_code = input.group(2)
|
||||||
if args[0].startswith('.weather '): return
|
if not icao_code:
|
||||||
|
return phenny.say("Try .weather London, for example?")
|
||||||
|
|
||||||
icao_code = match.group(2)
|
icao_code = code(phenny, icao_code)
|
||||||
if not icao_code:
|
|
||||||
return self.msg(origin.sender, 'Try .weather London, for example?')
|
|
||||||
|
|
||||||
icao_code = code(self, icao_code)
|
if not icao_code:
|
||||||
|
phenny.say("No ICAO code found, sorry")
|
||||||
if not icao_code:
|
|
||||||
self.msg(origin.sender, 'No ICAO code found, sorry')
|
|
||||||
return
|
return
|
||||||
|
|
||||||
uri = 'http://weather.noaa.gov/pub/data/observations/metar/stations/%s.TXT'
|
uri = 'http://weather.noaa.gov/pub/data/observations/metar/stations/%s.TXT'
|
||||||
try: bytes = web.get(uri % icao_code)
|
try:
|
||||||
except AttributeError:
|
bytes = web.get(uri % icao_code)
|
||||||
|
except AttributeError:
|
||||||
raise GrumbleError('OH CRAP NOAA HAS GONE DOWN THE WEB IS BROKEN')
|
raise GrumbleError('OH CRAP NOAA HAS GONE DOWN THE WEB IS BROKEN')
|
||||||
except urllib.error.HTTPError:
|
except urllib.error.HTTPError:
|
||||||
self.msg(origin.sender, "No NOAA data available for that location.")
|
phenny.say("No NOAA data available for that location.")
|
||||||
return
|
return
|
||||||
|
|
||||||
if 'Not Found' in bytes:
|
if 'Not Found' in bytes:
|
||||||
self.msg(origin.sender, icao_code+': no such ICAO code, or no NOAA data')
|
phenny.say(icao_code + ": no such ICAO code, or no NOAA data")
|
||||||
return
|
return
|
||||||
|
|
||||||
metar = bytes.splitlines().pop()
|
phenny.say(str(metar.parse(bytes)))
|
||||||
metar = metar.split(' ')
|
|
||||||
|
|
||||||
if len(metar[0]) == 4:
|
|
||||||
metar = metar[1:]
|
|
||||||
|
|
||||||
if metar[0].endswith('Z'):
|
|
||||||
time = metar[0]
|
|
||||||
metar = metar[1:]
|
|
||||||
else: time = None
|
|
||||||
|
|
||||||
if metar[0] == 'AUTO':
|
|
||||||
metar = metar[1:]
|
|
||||||
if metar[0] == 'VCU':
|
|
||||||
self.msg(origin.sender, icao_code + ': no data provided')
|
|
||||||
return
|
|
||||||
|
|
||||||
if metar[0].endswith('KT'):
|
|
||||||
wind = metar[0]
|
|
||||||
metar = metar[1:]
|
|
||||||
else: wind = None
|
|
||||||
|
|
||||||
if ('V' in metar[0]) and (metar[0] != 'CAVOK'):
|
|
||||||
vari = metar[0]
|
|
||||||
metar = metar[1:]
|
|
||||||
else: vari = None
|
|
||||||
|
|
||||||
if ((len(metar[0]) == 4) or
|
|
||||||
metar[0].endswith('SM')):
|
|
||||||
visibility = metar[0]
|
|
||||||
metar = metar[1:]
|
|
||||||
else: visibility = None
|
|
||||||
|
|
||||||
while metar[0].startswith('R') and (metar[0].endswith('L')
|
|
||||||
or 'L/' in metar[0]):
|
|
||||||
metar = metar[1:]
|
|
||||||
|
|
||||||
if len(metar[0]) == 6 and (metar[0].endswith('N') or
|
|
||||||
metar[0].endswith('E') or
|
|
||||||
metar[0].endswith('S') or
|
|
||||||
metar[0].endswith('W')):
|
|
||||||
metar = metar[1:] # 7000SE?
|
|
||||||
|
|
||||||
cond = []
|
|
||||||
while (((len(metar[0]) < 5) or
|
|
||||||
metar[0].startswith('+') or
|
|
||||||
metar[0].startswith('-')) and (not (metar[0].startswith('VV') or
|
|
||||||
metar[0].startswith('SKC') or metar[0].startswith('CLR') or
|
|
||||||
metar[0].startswith('FEW') or metar[0].startswith('SCT') or
|
|
||||||
metar[0].startswith('BKN') or metar[0].startswith('OVC')))):
|
|
||||||
cond.append(metar[0])
|
|
||||||
metar = metar[1:]
|
|
||||||
|
|
||||||
while '/P' in metar[0]:
|
|
||||||
metar = metar[1:]
|
|
||||||
|
|
||||||
if not metar:
|
|
||||||
self.msg(origin.sender, icao_code + ': no data provided')
|
|
||||||
return
|
|
||||||
|
|
||||||
cover = []
|
|
||||||
while (metar[0].startswith('VV') or metar[0].startswith('SKC') or
|
|
||||||
metar[0].startswith('CLR') or metar[0].startswith('FEW') or
|
|
||||||
metar[0].startswith('SCT') or metar[0].startswith('BKN') or
|
|
||||||
metar[0].startswith('OVC')):
|
|
||||||
cover.append(metar[0])
|
|
||||||
metar = metar[1:]
|
|
||||||
if not metar:
|
|
||||||
self.msg(origin.sender, icao_code + ': no data provided')
|
|
||||||
return
|
|
||||||
|
|
||||||
if metar[0] == 'CAVOK':
|
|
||||||
cover.append('CLR')
|
|
||||||
metar = metar[1:]
|
|
||||||
|
|
||||||
if metar[0] == 'PRFG':
|
|
||||||
cover.append('CLR') # @@?
|
|
||||||
metar = metar[1:]
|
|
||||||
|
|
||||||
if metar[0] == 'NSC':
|
|
||||||
cover.append('CLR')
|
|
||||||
metar = metar[1:]
|
|
||||||
|
|
||||||
if ('/' in metar[0]) or (len(metar[0]) == 5 and metar[0][2] == '.'):
|
|
||||||
temp = metar[0]
|
|
||||||
metar = metar[1:]
|
|
||||||
else: temp = None
|
|
||||||
|
|
||||||
if metar[0].startswith('QFE'):
|
|
||||||
metar = metar[1:]
|
|
||||||
|
|
||||||
if metar[0].startswith('Q') or metar[0].startswith('A'):
|
|
||||||
pressure = metar[0]
|
|
||||||
metar = metar[1:]
|
|
||||||
else: pressure = None
|
|
||||||
|
|
||||||
if time:
|
|
||||||
hour = time[2:4]
|
|
||||||
minute = time[4:6]
|
|
||||||
time = local(icao_code, hour, minute)
|
|
||||||
else: time = '(time unknown)'
|
|
||||||
|
|
||||||
if wind:
|
|
||||||
speed = int(wind[3:5])
|
|
||||||
if speed < 1:
|
|
||||||
description = 'Calm'
|
|
||||||
elif speed < 4:
|
|
||||||
description = 'Light air'
|
|
||||||
elif speed < 7:
|
|
||||||
description = 'Light breeze'
|
|
||||||
elif speed < 11:
|
|
||||||
description = 'Gentle breeze'
|
|
||||||
elif speed < 16:
|
|
||||||
description = 'Moderate breeze'
|
|
||||||
elif speed < 22:
|
|
||||||
description = 'Fresh breeze'
|
|
||||||
elif speed < 28:
|
|
||||||
description = 'Strong breeze'
|
|
||||||
elif speed < 34:
|
|
||||||
description = 'Near gale'
|
|
||||||
elif speed < 41:
|
|
||||||
description = 'Gale'
|
|
||||||
elif speed < 48:
|
|
||||||
description = 'Strong gale'
|
|
||||||
elif speed < 56:
|
|
||||||
description = 'Storm'
|
|
||||||
elif speed < 64:
|
|
||||||
description = 'Violent storm'
|
|
||||||
else: description = 'Hurricane'
|
|
||||||
|
|
||||||
if wind[0:3] == 'VRB':
|
|
||||||
degrees = '\u21BB'
|
|
||||||
else:
|
|
||||||
degrees = float(wind[0:3])
|
|
||||||
if (degrees <= 22.5) or (degrees > 337.5):
|
|
||||||
degrees = '\u2191'
|
|
||||||
elif (degrees > 22.5) and (degrees <= 67.5):
|
|
||||||
degrees = '\u2197'
|
|
||||||
elif (degrees > 67.5) and (degrees <= 112.5):
|
|
||||||
degrees = '\u2192'
|
|
||||||
elif (degrees > 112.5) and (degrees <= 157.5):
|
|
||||||
degrees = '\u2198'
|
|
||||||
elif (degrees > 157.5) and (degrees <= 202.5):
|
|
||||||
degrees = '\u2193'
|
|
||||||
elif (degrees > 202.5) and (degrees <= 247.5):
|
|
||||||
degrees = '\u2199'
|
|
||||||
elif (degrees > 247.5) and (degrees <= 292.5):
|
|
||||||
degrees = '\u2190'
|
|
||||||
elif (degrees > 292.5) and (degrees <= 337.5):
|
|
||||||
degrees = '\u2196'
|
|
||||||
|
|
||||||
if not icao_code.startswith('EN') and not icao_code.startswith('ED'):
|
|
||||||
wind = '%s %skt (%s)' % (description, speed, degrees)
|
|
||||||
elif icao_code.startswith('ED'):
|
|
||||||
kmh = int(round(speed * 1.852, 0))
|
|
||||||
wind = '%s %skm/h (%skt) (%s)' % (description, kmh, speed, degrees)
|
|
||||||
elif icao_code.startswith('EN'):
|
|
||||||
ms = int(round(speed * 0.514444444, 0))
|
|
||||||
wind = '%s %sm/s (%skt) (%s)' % (description, ms, speed, degrees)
|
|
||||||
else: wind = '(wind unknown)'
|
|
||||||
|
|
||||||
if visibility:
|
|
||||||
visibility = visibility + 'm'
|
|
||||||
else: visibility = '(visibility unknown)'
|
|
||||||
|
|
||||||
if cover:
|
|
||||||
level = None
|
|
||||||
for c in cover:
|
|
||||||
if c.startswith('OVC') or c.startswith('VV'):
|
|
||||||
if (level is None) or (level < 8):
|
|
||||||
level = 8
|
|
||||||
elif c.startswith('BKN'):
|
|
||||||
if (level is None) or (level < 5):
|
|
||||||
level = 5
|
|
||||||
elif c.startswith('SCT'):
|
|
||||||
if (level is None) or (level < 3):
|
|
||||||
level = 3
|
|
||||||
elif c.startswith('FEW'):
|
|
||||||
if (level is None) or (level < 1):
|
|
||||||
level = 1
|
|
||||||
elif c.startswith('SKC') or c.startswith('CLR'):
|
|
||||||
if level is None:
|
|
||||||
level = 0
|
|
||||||
|
|
||||||
if level == 8:
|
|
||||||
cover = 'Overcast \u2601'
|
|
||||||
elif level == 5:
|
|
||||||
cover = 'Cloudy'
|
|
||||||
elif level == 3:
|
|
||||||
cover = 'Scattered'
|
|
||||||
elif (level == 1) or (level == 0):
|
|
||||||
cover = 'Clear \u263C'
|
|
||||||
else: cover = 'Cover Unknown'
|
|
||||||
else: cover = 'Cover Unknown'
|
|
||||||
|
|
||||||
if temp:
|
|
||||||
if '/' in temp:
|
|
||||||
temp = temp.split('/')[0]
|
|
||||||
else: temp = temp.split('.')[0]
|
|
||||||
if temp.startswith('M'):
|
|
||||||
temp = '-' + temp[1:]
|
|
||||||
try: temp = int(temp)
|
|
||||||
except ValueError: temp = '?'
|
|
||||||
else: temp = '?'
|
|
||||||
|
|
||||||
if pressure:
|
|
||||||
if pressure.startswith('Q'):
|
|
||||||
pressure = pressure.lstrip('Q')
|
|
||||||
if pressure != 'NIL':
|
|
||||||
pressure = str(int(pressure)) + 'mb'
|
|
||||||
else: pressure = '?mb'
|
|
||||||
elif pressure.startswith('A'):
|
|
||||||
pressure = pressure.lstrip('A')
|
|
||||||
if pressure != 'NIL':
|
|
||||||
inches = pressure[:2] + '.' + pressure[2:]
|
|
||||||
mb = int(float(inches) * 33.7685)
|
|
||||||
pressure = '%sin (%smb)' % (inches, mb)
|
|
||||||
else: pressure = '?mb'
|
|
||||||
|
|
||||||
if isinstance(temp, int):
|
|
||||||
f = round((temp * 1.8) + 32, 2)
|
|
||||||
temp = '%s\u2109 (%s\u2103)' % (f, temp)
|
|
||||||
else: pressure = '?mb'
|
|
||||||
if isinstance(temp, int):
|
|
||||||
temp = '%s\u2103' % temp
|
|
||||||
|
|
||||||
if cond:
|
|
||||||
conds = cond
|
|
||||||
cond = ''
|
|
||||||
|
|
||||||
intensities = {
|
|
||||||
'-': 'Light',
|
|
||||||
'+': 'Heavy'
|
|
||||||
}
|
|
||||||
|
|
||||||
descriptors = {
|
|
||||||
'MI': 'Shallow',
|
|
||||||
'PR': 'Partial',
|
|
||||||
'BC': 'Patches',
|
|
||||||
'DR': 'Drifting',
|
|
||||||
'BL': 'Blowing',
|
|
||||||
'SH': 'Showers of',
|
|
||||||
'TS': 'Thundery',
|
|
||||||
'FZ': 'Freezing',
|
|
||||||
'VC': 'In the vicinity:'
|
|
||||||
}
|
|
||||||
|
|
||||||
phenomena = {
|
|
||||||
'DZ': 'Drizzle',
|
|
||||||
'RA': 'Rain',
|
|
||||||
'SN': 'Snow',
|
|
||||||
'SG': 'Snow Grains',
|
|
||||||
'IC': 'Ice Crystals',
|
|
||||||
'PL': 'Ice Pellets',
|
|
||||||
'GR': 'Hail',
|
|
||||||
'GS': 'Small Hail',
|
|
||||||
'UP': 'Unknown Precipitation',
|
|
||||||
'BR': 'Mist',
|
|
||||||
'FG': 'Fog',
|
|
||||||
'FU': 'Smoke',
|
|
||||||
'VA': 'Volcanic Ash',
|
|
||||||
'DU': 'Dust',
|
|
||||||
'SA': 'Sand',
|
|
||||||
'HZ': 'Haze',
|
|
||||||
'PY': 'Spray',
|
|
||||||
'PO': 'Whirls',
|
|
||||||
'SQ': 'Squalls',
|
|
||||||
'FC': 'Tornado',
|
|
||||||
'SS': 'Sandstorm',
|
|
||||||
'DS': 'Duststorm',
|
|
||||||
# ? Cf. http://swhack.com/logs/2007-10-05#T07-58-56
|
|
||||||
'TS': 'Thunderstorm',
|
|
||||||
'SH': 'Showers'
|
|
||||||
}
|
|
||||||
|
|
||||||
for c in conds:
|
|
||||||
if c.endswith('//'):
|
|
||||||
if cond: cond += ', '
|
|
||||||
cond += 'Some Precipitation'
|
|
||||||
elif len(c) == 5:
|
|
||||||
intensity = intensities[c[0]]
|
|
||||||
descriptor = descriptors[c[1:3]]
|
|
||||||
phenomenon = phenomena.get(c[3:], c[3:])
|
|
||||||
if cond: cond += ', '
|
|
||||||
cond += intensity + ' ' + descriptor + ' ' + phenomenon
|
|
||||||
elif len(c) == 4:
|
|
||||||
descriptor = descriptors.get(c[:2], c[:2])
|
|
||||||
phenomenon = phenomena.get(c[2:], c[2:])
|
|
||||||
if cond: cond += ', '
|
|
||||||
cond += descriptor + ' ' + phenomenon
|
|
||||||
elif len(c) == 3:
|
|
||||||
intensity = intensities.get(c[0], c[0])
|
|
||||||
phenomenon = phenomena.get(c[1:], c[1:])
|
|
||||||
if cond: cond += ', '
|
|
||||||
cond += intensity + ' ' + phenomenon
|
|
||||||
elif len(c) == 2:
|
|
||||||
phenomenon = phenomena.get(c, c)
|
|
||||||
if cond: cond += ', '
|
|
||||||
cond += phenomenon
|
|
||||||
|
|
||||||
# if not cond:
|
|
||||||
# format = u'%s at %s: %s, %s, %s, %s'
|
|
||||||
# args = (icao, time, cover, temp, pressure, wind)
|
|
||||||
# else:
|
|
||||||
# format = u'%s at %s: %s, %s, %s, %s, %s'
|
|
||||||
# args = (icao, time, cover, temp, pressure, cond, wind)
|
|
||||||
|
|
||||||
if not cond:
|
|
||||||
format = '%s, %s, %s, %s - %s %s'
|
|
||||||
args = (cover, temp, pressure, wind, str(icao_code), time)
|
|
||||||
else:
|
|
||||||
format = '%s, %s, %s, %s, %s - %s, %s'
|
|
||||||
args = (cover, temp, pressure, cond, wind, str(icao_code), time)
|
|
||||||
|
|
||||||
self.msg(origin.sender, format % args)
|
|
||||||
f_weather.rule = (['weather'], r'(.*)')
|
f_weather.rule = (['weather'], r'(.*)')
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
print(__doc__.strip())
|
print(__doc__.strip())
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
2013/01/11 00:28
|
||||||
|
CYUX 110028Z AUTO 25010KT 6SM -SN BKN043 OVC065 M28/M31 A2975 RMK SLP077
|
|
@ -0,0 +1,2 @@
|
||||||
|
2012/03/31 08:00
|
||||||
|
DNIM 310800Z 17005KT 9999 NSC 27/24 Q1013
|
|
@ -0,0 +1,2 @@
|
||||||
|
2010/06/18 06:00
|
||||||
|
DXLK 180600Z 28002KT 9999 FEW016 SCT120 BKN260 24/23 Q1013
|
|
@ -0,0 +1,2 @@
|
||||||
|
2013/01/11 00:20
|
||||||
|
EDDF 110020Z 21005KT 9999 SCT020 BKN040 BKN070 02/01 Q1010 NOSIG
|
|
@ -0,0 +1,2 @@
|
||||||
|
2013/01/11 00:20
|
||||||
|
EDDH 110020Z 32008KT 9999 FEW018 BKN050 M00/M02 Q1012 TEMPO 3500 SN BKN010
|
|
@ -0,0 +1,2 @@
|
||||||
|
2013/01/11 00:20
|
||||||
|
EDDM 110020Z 26008KT 9999 FEW012 SCT033 BKN045 02/01 Q1009 NOSIG
|
|
@ -0,0 +1,2 @@
|
||||||
|
2013/01/10 21:50
|
||||||
|
EDDT 102150Z 32007KT 9999 FEW014 BKN036 M00/M03 Q1008 TEMPO BKN012
|
|
@ -0,0 +1,2 @@
|
||||||
|
2013/01/10 18:20
|
||||||
|
ENSO 101820Z VRB01KT 9999 FEW025 00/M01 Q1017
|
|
@ -0,0 +1,2 @@
|
||||||
|
2013/01/11 01:00
|
||||||
|
HEGN 110100Z 31008KT CAVOK 09/00 Q1025 NOSIG
|
|
@ -0,0 +1,2 @@
|
||||||
|
2013/01/10 21:53
|
||||||
|
KAXN 102153Z AUTO 16019G24KT 10SM BKN021 03/M06 A2989 RMK AO2 SLP137 T00331056
|
|
@ -0,0 +1,2 @@
|
||||||
|
2013/01/10 20:15
|
||||||
|
KBCB 102015Z AUTO 07004KT 10SM CLR 14/01 A3046 RMK AO2
|
|
@ -0,0 +1,2 @@
|
||||||
|
2013/01/11 00:20
|
||||||
|
KBIL 110020Z 05017KT 10SM FEW019 OVC030 M03/M07 A2957 RMK AO2 T10281072
|
|
@ -0,0 +1,2 @@
|
||||||
|
2013/01/10 21:52
|
||||||
|
KCID 102152Z 11013KT 3SM -RA BR OVC015 01/00 A2998 RMK AO2 RAB12 SLP162 P0001 T00110000
|
|
@ -0,0 +1,2 @@
|
||||||
|
2013/01/11 00:35
|
||||||
|
KCXP 110035Z AUTO 31006KT 10SM FEW043 M02/M13 A2990 RMK AO2
|
|
@ -0,0 +1,2 @@
|
||||||
|
2013/01/10 21:53
|
||||||
|
KDEN 102153Z 00000KT 10SM SCT150 OVC200 02/M07 A2959 RMK AO2 SLP007 T00221067
|
|
@ -0,0 +1,2 @@
|
||||||
|
2013/01/10 18:52
|
||||||
|
KIAD 101852Z 35008KT 10SM FEW250 12/M04 A3052 RMK AO2 SLP335 T01221039
|
|
@ -0,0 +1,2 @@
|
||||||
|
2013/01/10 23:53
|
||||||
|
KLAX 102353Z 28025G31KT 10SM FEW070 SCT110 13/02 A2995 RMK AO2 PK WND 28033/2337 SLP141 T01280022 10150 20122 50002
|
|
@ -0,0 +1,2 @@
|
||||||
|
2013/01/10 23:51
|
||||||
|
KLGA 102351Z 31007KT 10SM SCT250 07/M04 A3052 RMK AO2 SLP335 T00721039 10089 20072 53014
|
|
@ -0,0 +1,2 @@
|
||||||
|
2013/01/11 00:53
|
||||||
|
KMCO 110053Z 10005KT 10SM FEW250 20/18 A3029 RMK AO2 SLP255 T02000178
|
|
@ -0,0 +1,2 @@
|
||||||
|
2013/01/10 23:54
|
||||||
|
KMGJ 102354Z AUTO 00000KT 10SM CLR 00/M04 A3051 RMK AO2 SLP336 T00001044 10078 21006 53014 TSNO
|
|
@ -0,0 +1,2 @@
|
||||||
|
2013/01/11 00:53
|
||||||
|
KMIA 110053Z 08011KT 10SM FEW060 SCT250 24/19 A3026 RMK AO2 SLP246 T02390194
|
|
@ -0,0 +1,2 @@
|
||||||
|
2013/01/10 21:51
|
||||||
|
KSAN 102151Z 30017G22KT 10SM SCT040 BKN060 BKN180 13/01 A3003 RMK AO2 PK WND 28027/2140 SLP167 T01330011
|
|
@ -0,0 +1,2 @@
|
||||||
|
2013/01/11 00:56
|
||||||
|
KSFO 110056Z 29017KT 10SM FEW033 SCT049 09/02 A3012 RMK AO2 SLP199 T00940017
|
|
@ -0,0 +1,2 @@
|
||||||
|
2013/01/10 18:30
|
||||||
|
LRAR 101830Z 11003KT 2000 0800N R27/1000VP2000D BCFG SCT005 M02/M02 Q1011 09890392
|
|
@ -0,0 +1,2 @@
|
||||||
|
2013/01/10 23:44
|
||||||
|
MMUN 102344Z 11007KT 7SM SCT015TCU SCT080 26/23 A3006 RMK SLP178 52010 906 8/230 HZY AS W
|
|
@ -0,0 +1,2 @@
|
||||||
|
2010/07/15 11:00
|
||||||
|
OEMM 151100Z 02009KT CAVOK 48/01 Q0997
|
|
@ -0,0 +1,2 @@
|
||||||
|
2011/06/09 09:00
|
||||||
|
TBOB 090900Z 11006KT 9999 SCT014 SCT038 28/25 Q1014 NOSIG
|
|
@ -0,0 +1,2 @@
|
||||||
|
2013/01/10 16:00
|
||||||
|
UUOK 101600Z 16004MPS 9999 OVC013 M13/M16 Q1014 NOSIG RMK 12CLRD60
|
|
@ -0,0 +1,2 @@
|
||||||
|
2013/01/11 00:30
|
||||||
|
YSSY 110030Z 05007KT 350V080 CAVOK 28/17 Q1007 NOSIG
|
|
@ -0,0 +1,2 @@
|
||||||
|
2008/03/23 23:00
|
||||||
|
ZBDT 232300Z 333004MPS CAVOK M04/M14 Q1020 NOSIG
|
|
@ -0,0 +1,2 @@
|
||||||
|
2012/09/28 04:00
|
||||||
|
ZPLJ 280400Z 24002MPS 210V290 9999 -SHRA FEW023 FEW040TCU SCT040 19/15 Q1026 NOSIG
|
|
@ -0,0 +1,25 @@
|
||||||
|
"""
|
||||||
|
Tests for phenny's metar.py
|
||||||
|
"""
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
import metar
|
||||||
|
import glob
|
||||||
|
|
||||||
|
|
||||||
|
class MetarTest(unittest.TestCase):
|
||||||
|
def test_files(self):
|
||||||
|
for station in glob.glob('test/metar/*.TXT'):
|
||||||
|
with open(station) as f:
|
||||||
|
w = metar.parse(f.read())
|
||||||
|
assert w.station is not None
|
||||||
|
assert w.time is not None
|
||||||
|
assert w.cover is not None
|
||||||
|
|
||||||
|
assert w.temperature > -100
|
||||||
|
assert w.temperature < 100
|
||||||
|
|
||||||
|
assert w.dewpoint > -100
|
||||||
|
assert w.dewpoint < 100
|
||||||
|
|
||||||
|
assert w.pressure is not None
|
Loading…
Reference in New Issue