# Copyright (C) 2002 Tobias Klausmann # Modified by Jerome Alet # Code contributed by Jerome Alet and Davide Di Blasi # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. # # By reading this code you agree not to ridicule the author =) import string import re import urllib2 import types import math, fpformat __author__ = "klausman-usenet@tuts.net" __version__ = "0.5" __doc__ = """Pymetar v%s (c) 2002 Tobias Klausman Pymetar is a python module and command line tool designed to fetch Metar reports from the NOAA (http://www.noaa.gov) and allow access to the included weather information. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. Please e-mail bugs to: %s""" % (__version__, __author__) # # What a boring list to type ! # # It seems the NOAA doesn't want to return plain text, but considering the # format of their response, this is not to save bandwidth :-) # _WeatherConditions = { "DZ" : ("Drizzle", "rain", { "" : "Moderate drizzle", "-" : "Light drizzle", "+" : "Heavy drizzle", "VC" : "Drizzle in the vicinity", "MI" : "Shallow drizzle", "BC" : "Patches of drizzle", "PR" : "Partial drizzle", "TS" : ("Thunderstorm", "storm"), "BL" : "Windy drizzle", "SH" : "Showers", "DR" : "Drifting drizzle", "FZ" : "Freezing drizzle", }), "RA" : ("Rain", "rain", { "" : "Moderate rain", "-" : "Light rain", "+" : "Heavy rain", "VC" : "Rain in the vicinity", "MI" : "Shallow rain", "BC" : "Patches of rain", "PR" : "Partial rainfall", "TS" : ("Thunderstorm", "storm"), "BL" : "Blowing rainfall", "SH" : "Rain showers", "DR" : "Drifting rain", "FZ" : "Freezing rain", }), "SN" : ("Snow", "snow", { "" : "Moderate snow", "-" : "Light snow", "+" : "Heavy snow", "VC" : "Snow in the vicinity", "MI" : "Shallow snow", "BC" : "Patches of snow", "PR" : "Partial snowfall", "TS" : ("Snowstorm", "storm"), "BL" : "Blowing snowfall", "SH" : "Snowfall showers", "DR" : "Drifting snow", "FZ" : "Freezing snow", }), "SG" : ("Snow grains", "snow", { "" : "Moderate snow grains", "-" : "Light snow grains", "+" : "Heavy snow grains", "VC" : "Snow grains in the vicinity", "MI" : "Shallow snow grains", "BC" : "Patches of snow grains", "PR" : "Partial snow grains", "TS" : ("Snowstorm", "storm"), "BL" : "Blowing snow grains", "SH" : "Snow grain showers", "DR" : "Drifting snow grains", "FZ" : "Freezing snow grains", }), "IC" : ("Ice crystals", "snow", { "" : "Moderate ice crystals", "-" : "Few ice crystals", "+" : "Heavy ice crystals", "VC" : "Ice crystals in the vicinity", "BC" : "Patches of ice crystals", "PR" : "Partial ice crystals", "TS" : ("Ice crystal storm", "storm"), "BL" : "Blowing ice crystals", "SH" : "Showers of ice crystals", "DR" : "Drifting ice crystals", "FZ" : "Freezing ice crystals", }), "PE" : ("Ice pellets", "snow", { "" : "Moderate ice pellets", "-" : "Few ice pellets", "+" : "Heavy ice pellets", "VC" : "Ice pellets in the vicinity", "MI" : "Shallow ice pellets", "BC" : "Patches of ice pellets", "PR" : "Partial ice pellets", "TS" : ("Ice pellets storm", "storm"), "BL" : "Blowing ice pellets", "SH" : "Showers of ice pellets", "DR" : "Drifting ice pellets", "FZ" : "Freezing ice pellets", }), "GR" : ("Hail", "rain", { "" : "Moderate hail", "-" : "Light hail", "+" : "Heavy hail", "VC" : "Hail in the vicinity", "MI" : "Shallow hail", "BC" : "Patches of hail", "PR" : "Partial hail", "TS" : ("Hailstorm", "storm"), "BL" : "Blowing hail", "SH" : "Hail showers", "DR" : "Drifting hail", "FZ" : "Freezing hail", }), "GS" : ("Small hail", "rain", { "" : "Moderate small hail", "-" : "Light small hail", "+" : "Heavy small hail", "VC" : "Small hail in the vicinity", "MI" : "Shallow small hail", "BC" : "Patches of small hail", "PR" : "Partial small hail", "TS" : ("Small hailstorm", "storm"), "BL" : "Blowing small hail", "SH" : "Showers of small hail", "DR" : "Drifting small hail", "FZ" : "Freezing small hail", }), "UP" : ("Precipitation", "rain", { "" : "Moderate precipitation", "-" : "Light precipitation", "+" : "Heavy precipitation", "VC" : "Precipitation in the vicinity", "MI" : "Shallow precipitation", "BC" : "Patches of precipitation", "PR" : "Partial precipitation", "TS" : ("Unknown thunderstorm", "storm"), "BL" : "Blowing precipitation", "SH" : "Showers, type unknown", "DR" : "Drifting precipitation", "FZ" : "Freezing precipitation", }), "BR" : ("Mist", "fog", { "" : "Moderate mist", "-" : "Light mist", "+" : "Thick mist", "VC" : "Mist in the vicinity", "MI" : "Shallow mist", "BC" : "Patches of mist", "PR" : "Partial mist", "BL" : "Mist with wind", "DR" : "Drifting mist", "FZ" : "Freezing mist", }), "FG" : ("Fog", "fog", { "" : "Moderate fog", "-" : "Light fog", "+" : "Thick fog", "VC" : "Fog in the vicinity", "MI" : "Shallow fog", "BC" : "Patches of fog", "PR" : "Partial fog", "BL" : "Fog with wind", "DR" : "Drifting fog", "FZ" : "Freezing fog", }), "FU" : ("Smoke", "fog", { "" : "Moderate smoke", "-" : "Thin smoke", "+" : "Thick smoke", "VC" : "Smoke in the vicinity", "MI" : "Shallow smoke", "BC" : "Patches of smoke", "PR" : "Partial smoke", "TS" : ("Smoke w/ thunders", "storm"), "BL" : "Smoke with wind", "DR" : "Drifting smoke", }), "VA" : ("Volcanic ash", "fog", { "" : "Moderate volcanic ash", "+" : "Thick volcanic ash", "VC" : "Volcanic ash in the vicinity", "MI" : "Shallow volcanic ash", "BC" : "Patches of volcanic ash", "PR" : "Partial volcanic ash", "TS" : ("Volcanic ash w/ thunders", "storm"), "BL" : "Blowing volcanic ash", "SH" : "Showers of volcanic ash", "DR" : "Drifting volcanic ash", "FZ" : "Freezing volcanic ash", }), "SA" : ("Sand", "fog", { "" : "Moderate sand", "-" : "Light sand", "+" : "Heavy sand", "VC" : "Sand in the vicinity", "BC" : "Patches of sand", "PR" : "Partial sand", "BL" : "Blowing sand", "DR" : "Drifting sand", }), "HZ" : ("Haze", "fog", { "" : "Moderate haze", "-" : "Light haze", "+" : "Thick haze", "VC" : "Haze in the vicinity", "MI" : "Shallow haze", "BC" : "Patches of haze", "PR" : "Partial haze", "BL" : "Haze with wind", "DR" : "Drifting haze", "FZ" : "Freezing haze", }), "PY" : ("Sprays", "fog", { "" : "Moderate sprays", "-" : "Light sprays", "+" : "Heavy sprays", "VC" : "Sprays in the vicinity", "MI" : "Shallow sprays", "BC" : "Patches of sprays", "PR" : "Partial sprays", "BL" : "Blowing sprays", "DR" : "Drifting sprays", "FZ" : "Freezing sprays", }), "DU" : ("Dust", "fog", { "" : "Moderate dust", "-" : "Light dust", "+" : "Heavy dust", "VC" : "Dust in the vicinity", "BC" : "Patches of dust", "PR" : "Partial dust", "BL" : "Blowing dust", "DR" : "Drifting dust", }), "SQ" : ("Squall", "storm", { "" : "Moderate squall", "-" : "Light squall", "+" : "Heavy squall", "VC" : "Squall in the vicinity", "PR" : "Partial squall", "TS" : "Thunderous squall", "BL" : "Blowing squall", "DR" : "Drifting squall", "FZ" : "Freezing squall", }), "SS" : ("Sandstorm", "fog", { "" : "Moderate sandstorm", "-" : "Light sandstorm", "+" : "Heavy sandstorm", "VC" : "Sandstorm in the vicinity", "MI" : "Shallow sandstorm", "PR" : "Partial sandstorm", "TS" : ("Thunderous sandstorm", "storm"), "BL" : "Blowing sandstorm", "DR" : "Drifting sandstorm", "FZ" : "Freezing sandstorm", }), "DS" : ("Duststorm", "fog", { "" : "Moderate duststorm", "-" : "Light duststorm", "+" : "Heavy duststorm", "VC" : "Duststorm in the vicinity", "MI" : "Shallow duststorm", "PR" : "Partial duststorm", "TS" : ("Thunderous duststorm", "storm"), "BL" : "Blowing duststorm", "DR" : "Drifting duststorm", "FZ" : "Freezing duststorm", }), "PO" : ("Dustwhirls", "fog", { "" : "Moderate dustwhirls", "-" : "Light dustwhirls", "+" : "Heavy dustwhirls", "VC" : "Dustwhirls in the vicinity", "MI" : "Shallow dustwhirls", "BC" : "Patches of dustwhirls", "PR" : "Partial dustwhirls", "BL" : "Blowing dustwhirls", "DR" : "Drifting dustwhirls", }), "+FC" : ("Tornado", "storm", { "" : "Moderate tornado", "+" : "Raging tornado", "VC" : "Tornado in the vicinity", "PR" : "Partial tornado", "TS" : "Thunderous tornado", "BL" : "Tornado", "DR" : "Drifting tornado", "FZ" : "Freezing tornado", }), "FC" : ("Funnel cloud", "fog", { "" : "Moderate funnel cloud", "-" : "Light funnel cloud", "+" : "Thick funnel cloud", "VC" : "Funnel cloud in the vicinity", "MI" : "Shallow funnel cloud", "BC" : "Patches of funnel cloud", "PR" : "Partial funnel cloud", "BL" : "Funnel cloud w/ wind", "DR" : "Drifting funnel cloud", }), } # # RegExps to extract the different parts from the Metar encoded report TIME_RE_STR = r"^([0-9]{6})Z$" WIND_RE_STR = r"^(([0-9]{3})|VRB)([0-9]?[0-9]{2})(G[0-9]?[0-9]{2})?KT$" VIS_RE_STR = r"^(([0-9]?[0-9])|(M?1/[0-9]?[0-9]))SM$" CLOUD_RE_STR= r"^(CLR|SKC|BKN|SCT|FEW|OVC)([0-9]{3})?$" TEMP_RE_STR = r"^(M?[0-9][0-9])/(M?[0-9][0-9])$" PRES_RE_STR = r"^(A|Q)([0-9]{4})$" COND_RE_STR = r"^(-|\\+)?(VC|MI|BC|PR|TS|BL|SH|DR|FZ)?(DZ|RA|SN|SG|IC|PE|GR|GS|UP|BR|FG|FU|VA|SA|HZ|PY|DU|SQ|SS|DS|PO|\\+?FC)$" class MetarReport: """ This class provides a means to download and parse the decoded METAR reports from http://weather.noaa.gov/ In order to find the report relevant for you, find the closest station at http://www.nws.noaa.gov/oso/siteloc.shtml and feed its 4-letter station code to fetchMetarReport(), case does not matter. All methods return metric values, no matter what the station provides. The conversion factors were taken from the excellent "units" program. If None is returned that means the information wasn't available or could not be parsed. If you have the suspicion that failure to parse an information was this libs fault, please save the report generating the error send it to me with a few lines detailing the problem. Thanks! """ def match_WeatherPart(self, regexp) : """ Returns the matching part of the encoded Metar report. regexp: the regexp needed to extract this part. Returns the first matching string or None. WARNING: Some Metar reports may contain several matching strings, only the first one is taken into account, e.g. FEW020 SCT075 BKN053 """ if self.code is not None : rg = re.compile(regexp) for wpart in self.getRawMetarCode().split() : match = rg.match(wpart) if match : return match.string[match.start(0) : match.end(0)] def extractSkyConditions(self) : """ Extracts sky condition information from the encoded report. Returns a tuple containing the description of the sky conditions as a string and a suggested pixmap name for an icon representing said sky condition. """ wcond = self.match_WeatherPart(COND_RE_STR) if wcond is not None : if (len(wcond) > 3) and (wcond.startswith('+') or wcond.startswith('-')) : wcond = wcond[1:] if wcond.startswith('+') or wcond.startswith('-') : pphen = 1 elif len(wcond) < 4 : pphen = 0 else : pphen = 2 squal = wcond[:pphen] sphen = wcond[pphen : pphen + 4] phenomenon = _WeatherConditions.get(sphen, None) if phenomenon is not None : (name, pixmap, phenomenon) = phenomenon pheninfo = phenomenon.get(squal, name) if type(pheninfo) != types.TupleType : return (pheninfo, pixmap) else : # contains pixmap info return pheninfo def parseLatLong(self, latlong): """ Parse Lat or Long in METAR notation into float values. N and E are +, S and W are -. Expects one positional string and returns one float value. """ # I know, I could invert this if and put # the rest of the function into its block, # but I find this to be more readable if latlong is None: return None s = string.strip(string.upper(latlong)) elms = string.split(s,'-') ud = elms[-1][-1] elms[-1] = elms[-1][:-1] elms = map(string.atoi, elms) coords = 0.0 elen = len(elms) if elen > 2: coords = coords + float(elms[2])/3600.0 if elen > 1: coords = coords + float(elms[1])/60.0 coords=coords + float(elms[0]) if ud in ('W','S'): coords=-1.0*coords f,i=math.modf(coords) if elen > 2: f=float(fpformat.sci(f,4)) elif elen > 1: f=float(fpformat.sci(f,2)) else: f=0.0 return f+i def extractCloudInformation(self) : """ Extracts cloud information. returns None or a tuple (sky type as a string of text and suggested pixmap name) """ wcloud = self.match_WeatherPart(CLOUD_RE_STR) if wcloud is not None : stype = wcloud[:3] if (stype == "CLR") or (stype == "SKC") : return ("Clear sky", "sun") elif stype == "BKN" : return ("Broken clouds", "suncloud") elif stype == "SCT" : return ("Scattered clouds", "suncloud") elif stype == "FEW" : return ("Few clouds", "suncloud") elif stype == "OVC" : return ("Overcast", "cloud") def getFullReport(self): """ Return the complete weather report. """ return self.fullreport def getTemperatureCelsius(self): """ Return the temperature in degrees Celsius. """ return self.temp def getTemperatureFahrenheit(self): """ Return the temperature in degrees Fahrenheit. """ return (self.temp * (9.0/5.0)) + 32.0 def getDewPointCelsius(self): """ Return dewpoint in degrees Celsius. """ return self.dewp def getDewPointFahrenheit(self): """ Return dewpoint in degrees Fahrenheit. """ return (self.dewp * (9.0/5.0)) + 32.0 def getWindSpeed(self): """ Return the wind speed in meters per second. """ return self.windspeed def getWindDirection(self): """ Return wind direction in degrees. """ return self.winddir def getWindCompass(self): """ Return wind direction as compass direction (e.g. NE or SSE) """ return self.windcomp def getVisibilityKilometers(self): """ Return visibility in km. """ return self.vis def getVisibilityMiles(self): """ Return visibility in miles. """ return self.vis / 1.609344 def getHumidity(self): """ Return relative humidity in percent. """ return self.humid def getPressure(self): """ Return pressure in hPa. """ return self.press def getRawMetarCode(self): """ Return the encoded weather report. """ return self.code def getWeather(self): """ Return short weather conditions """ return self.weather def getSkyConditions(self): """ Return sky conditions """ return self.sky def getStationName(self): """ Return full station name """ return self.fulln def getStationCity(self): """ Return city-part of station name """ return self.stat_city def getStationCountry(self): """ Return country-part of station name """ return self.stat_country def getCycle(self): """ Return cycle value. The cycle value is not the frequency or delay between observations but the "time slot" in which the observation was made. There are 24 cycle slots every day which usually last from N:45 to N+1:45. The cycle from 23:45 to 0:45 is cycle 0. """ return self.cycle def getStationPosition(self): """ Return latitude, longitude and altitude above sea level of station as a tuple. Some stations don't deliver altitude, for those, None is returned as altitude. The lat/longs are expressed as follows: xx-yyd where xx is degrees, yy minutes and d the direction. Thus 51-14N means 51 degrees, 14 minutes north. d may take the values N, S for latitues and W, E for longitudes. Latitude and Longitude may include seconds. Altitude is always given as meters above sea level, including a trailing M. Schipohl Int. Airport Amsterdam has, for example: ('52-18N', '004-46E', '-2M') Moenchengladbach (where I live): ('51-14N', '063-03E', None) If you need lat and long as float values, look at getStationPositionFloat() instead """ return (self.latitude, self.longitude, self.altitude) def getStationPositionFloat(self): """ Return latitude and longitude as float values in a tuple (lat,long,alt) """ return (self.latf,self.longf,self.altitude) def getStationLatitude(self) : """ Return the station's latitude in dd-mm[-ss]D format : dd : degrees mm : minutes ss : seconds D : direction (N, S, E, W) """ return self.latitude def getStationLatitudeFloat(self): """ Return latitude as a float """ return self.latf def getStationLongitude(self) : """ Return the station's longitude in dd-mm[-ss]D format : dd : degrees mm : minutes ss : seconds D : direction (N, S, E, W) """ return self.longitude def getStationLongitudeFloat(self): """ Return Longitude as a float """ return (self.longf) def getStationAltitude(self) : """ Return the station's altitude above the sea in meters. """ return self.altitude def getReportURL(self): """ Return the URL from which the report was fetched. """ return self.reporturl def getTime(self): """ Return the time when the observation was made. Note that this is *not* the time when the report was fetched by us Format: YYYY.MM.DD HHMM UTC Example: 2002.04.01 1020 UTC """ return self.rtime def getISOTime(self): """ Return the time when the observation was made in ISO 8601 format (e.g. 2002-07-25 15:12:00Z) """ return(self.metar_to_iso8601(self.rtime)) def getPixmap(self): """ Return a suggested pixmap name, without extension, depending on current weather. """ return self.pixmap def metar_to_iso8601(self, metardate) : """ Converts a metar date to an ISO8601 date. """ if metardate is not None: (date, hour, tz) = metardate.split() (year, month, day) = date.split('.') # assuming tz is always 'UTC', aka 'Z' return "%s-%s-%s %s:%s:00Z" % (year, month, day, hour[:2], hour[2:4]) def strreverse(self, str): """ Reverses a string """ listr=list(str) listr.reverse() return (string.join(listr,"")) def __init__(self, MetarStationCode = None): """ Get initial report if a station code is passed as a parameter, otherwise initialize fields. """ if MetarStationCode is not None : self.fetchMetarReport(MetarStationCode) else : self._ClearAllFields() def _ClearAllFields(self): """ Clears all fields values. """ # until finished, report is invalid self.valid = 0 # Clear all self.fullreport=None self.temp=None self.windspeed=None self.winddir=None self.vis=None self.dewp=None self.humid=None self.press=None self.code=None self.weather=None self.sky=None self.fulln=None self.cycle=None self.windcomp=None self.rtime=None self.pixmap=None self.latitude=None self.longitude=None self.altitude=None self.stat_city=None self.stat_country=None self.reportutl=None self.latf=None self.longf=None def fetchMetarReport(self, station, baseurl="http://weather.noaa.gov/pub/data/observations/metar/decoded/"): """ Retrieve the decoded METAR report from the given base URL and parse it. Fill the data properties in the class instance with the values found in the report, converting them to metric values where necessary. """ self._ClearAllFields() station=string.upper(station) self.reporturl="%s%s.TXT" % (baseurl, station) # Honor HTTP_PROXY environment var # Thanks to Davide Di Blasi for suggesting this urllib2.install_opener( urllib2.build_opener(urllib2.ProxyHandler, urllib2.HTTPHandler)) fn=urllib2.urlopen(self.reporturl) # Dump entire report in a variable self.fullreport=fn.read() # if fn.info().status is 0, everything seems to be in order if not fn.info().status: lines=string.split(self.fullreport,"\n") for line in lines: # Most lines start with a Header and a colon try: header,data=string.split(line,":",1) except ValueError: header=data=line header=string.strip(header) data=string.strip(data) # The station identifier and location if (string.find(header,"("+station+")")!=-1): try: loc,p=string.split(data,"(",1) loc=string.strip(loc) rloc=self.strreverse(loc) rcoun,rcity=string.split(rloc,",",1) except ValueError: city="" coun="" p=data try: id,lat,long,ht=string.split(p," ") ht = int(ht[:-1]) # skip the 'M' for meters except ValueError: id,lat,long=string.split(p," ") ht=None self.stat_city=string.strip(self.strreverse(rcity)) self.stat_country=string.strip(self.strreverse(rcoun)) self.fulln=loc self.latitude = lat self.longitude = long self.latf=self.parseLatLong(lat) self.longf=self.parseLatLong(long) self.altitude = ht # The line containing the date/time of the report elif (string.find(data,"UTC")!=-1): local,rt=string.split(data,"/") self.rtime=string.strip(rt) # temperature elif (header=="Temperature"): t,i=string.split(data," ",1) self.temp=(float(t)-32)*(5.0/9.0) # wind direction and speed elif (header == "Wind"): if (string.find(data,"Calm")!=-1): self.windspeed=0.0 self.winddir=None self.windcomp=None elif (string.find(data,"Variable")!=-1): v,a,speed,r=string.split(data," ",3) self.windspeed=(float(speed)*0.44704) self.winddir=None self.windcomp=None else: f,t,comp,deg,r,d,speed,r=string.split(data," ",7) self.winddir=int(deg[1:]) self.windcomp=string.strip(comp) self.windspeed=(float(speed)*0.44704) # visibility elif (header=="Visibility"): for d in data.split() : try : self.vis = float(d)*1.609344 break except ValueError : pass # dew point elif (header=="Dew Point"): dp,i=string.split(data," ",1) self.dewp=(float(dp)-32)*(5.0/9.0) # humidity elif (header=="Relative Humidity"): h,i=string.split(data,"%",1) self.humid=int(h) # pressure elif (header=="Pressure (altimeter)"): p,r=string.split(data," ",1) self.press=(float(p)*33.863886) # short weather description ("rain", "mist", ...) elif (header=="Weather"): self.weather=data # short description of sky cond. elif (header=="Sky conditions"): self.sky=data # the encoded weather report elif (header=="ob"): self.code=string.strip(data) # the cycle value describes the cycle in # which the observations were made elif (header=="cycle"): self.cycle=int(data) # decode cloud and sky conditions informations from # the raw report, this will suggest us a pixmap to use # to graphically represent the weather. cloudinfo = self.extractCloudInformation() if cloudinfo is not None : (cloudinfo, cloudpixmap) = cloudinfo else : (cloudinfo, cloudpixmap) = (None, None) conditions = self.extractSkyConditions() if conditions is not None : (conditions, condpixmap) = conditions else : (conditions, condpixmap) = (None, None) # fill the weather information self.weather = self.weather or conditions or cloudinfo # Pixmap guessed from general conditions has priority # over pixmap guessed from clouds self.pixmap = condpixmap or cloudpixmap # report is complete self.valid = 1 # Even if the report is bad, toss the fd del fn