def get_bitmap_of_weather_conditions(m: Metar.Metar, weather_desc: WeatherCondDescription): bitmap = [0] * weather_desc.total_conditions if m.present_weather() != "": current_conditions = m.present_weather().split("; ") for cond in current_conditions: bitmap[weather_desc.weather_cond_dict[cond]] = 1 return bitmap
def from_metar_to_list_of_features(airport: str, timestamp: datetime, m: Metar.Metar) -> List: result = [ airport, timestamp, m.code, m.mod, # m.station_id, m.wind_speed.value("mps") if m.wind_speed else None, m.wind_gust.value("mps") if m.wind_gust else None, m.wind_dir.value() if m.wind_dir else None, m.wind_dir_from.value() if m.wind_dir_from else None, m.wind_dir_to.value() if m.wind_dir_to else None, m.vis.value("m") if m.vis else None, m.max_vis.value("m") if m.max_vis else None, m.max_vis_dir.value() if m.max_vis_dir else None, m.runway[0][1].value("m") if m.runway else None, m.runway[0][2].value("m") if m.runway else None, m.temp.value("C") if m.temp else None, m.dewpt.value("C") if m.dewpt else None, m.press.value("HPA") if m.press else None, str(m.sky_conditions("; ")), # str(m.weather), str(m.present_weather()), # str(m.recent), # str(m.sky), # str(m.windshear), # str(m._trend), # str(m._trend_groups), # str(m._remarks), # str(m._unparsed_groups), # str(m._unparsed_remarks), ] return result
class VoiceAtis(object): STATION_SUFFIXES = ['TWR', 'APP', 'GND', 'DEL', 'DEP'] SPEECH_RATE = 150 SLEEP_TIME = 3 # s RADIO_RANGE = 180 # nm OFFSETS = [ (0x034E, 'H'), # com1freq (0x3118, 'H'), # com2freq (0x3122, 'b'), # radioActive (0x0560, 'l'), # ac Latitude (0x0568, 'l'), # ac Longitude ] WHAZZUP_URL = 'http://api.ivao.aero/getdata/whazzup/whazzup.txt.gz' WHAZZUP_METAR_URL = 'http://wx.ivao.aero/metar.php' OUR_AIRPORTS_URL = 'http://ourairports.com/data/' COM1_FREQUENCY_DEBUG = 199.99 # EDDS # COM2_FREQUENCY_DEBUG = 126.12 # LAT_DEBUG = 48.687 # LON_DEBUG = 9.205 # EDDM # COM2_FREQUENCY_DEBUG = 123.12 # LAT_DEBUG = 48.353 # LON_DEBUG = 11.786 # LIRF COM2_FREQUENCY_DEBUG = 121.85 LAT_DEBUG = 41.8 LON_DEBUG = 12.2 # LIBR COM2_FREQUENCY_DEBUG = 121.85 LAT_DEBUG = 41.8 LON_DEBUG = 12.2 WHAZZUP_TEXT_DEBUG = r'H:\My Documents\Sonstiges\voiceAtis\whazzup_1.txt' ## Setup the VoiceAtis object. # Inits logger. # Downloads airport data. def __init__(self, **optional): #TODO: Remove the debug code when tested properly. #TODO: Improve logged messages. #TODO: Create GUI. # Process optional arguments. self.debug = optional.get('Debug', debug) # Get file path. self.rootDir = os.path.dirname( os.path.dirname(os.path.abspath(__file__))) # Init logging. self.logger = VaLogger(os.path.join(self.rootDir, 'voiceAtis', 'logs')) # First log message. self.logger.info('voiceAtis started') # Read file with airport frequencies and coordinates. self.logger.info('Downloading airport data. This may take some time.') self.getAirportData() self.logger.info('Finished downloading airport data.') # Show debug Info #TODO: Remove for release. if self.debug: self.logger.info('Debug mode on.') self.logger.setLevel(ConsoleLevel='debug') ## Establishs pyuipc connection. # Return 'True' on success or if pyuipc not installed. # Return 'False' on fail. def connectPyuipc(self): try: self.pyuipcConnection = pyuipc.open(0) self.pyuipcOffsets = pyuipc.prepare_data(self.OFFSETS) self.logger.info('FSUIPC connection established.') return True except NameError: self.pyuipcConnection = None self.logger.warning( 'Error using PYUIPC, running voiceAtis without it.') return True except: self.logger.warning( 'FSUIPC: No simulator detected. Start you simulator first!') return False ## Runs an infinite loop. # i.E. for use without GUI. def runLoop(self): # Establish pyuipc connection result = False while not result: result = self.connectPyuipc() if not result: self.logger.info('Retrying in 20 seconds.') time.sleep(20) # Infinite loop. try: while True: timeSleep = self.loopRun() time.sleep(timeSleep) except KeyboardInterrupt: # Actions at Keyboard Interrupt. self.logger.info('Loop interrupted by user.') if pyuipcImported: self.pyuipc.close() ## One cyle of a loop. # Returns the requested sleep time. def loopRun(self): # Get sim data. self.getPyuipcData() # Get best suitable Airport. self.getAirport() # Handle if no airport found. if self.airport is None: self.logger.info( 'No airport found, sleeping for {} seconds...'.format( self.SLEEP_TIME)) return self.SLEEP_TIME else: self.logger.info('Airport: {}.'.format(self.airport)) # Get whazzup file if not self.debug: self.getWhazzupText() else: self.getWhazzupTextDebug() # Read whazzup text and get a station. self.parseWhazzupText() # Check if station online. if self.atisRaw is not None: self.logger.info('Station found, decoding Atis.') else: # Actions, if no station online. self.logger.info('No station online, using metar only.') with warnings.catch_warnings(): warnings.simplefilter("ignore") self.metar = Metar(self.getAirportMetar(), strict=False) self.parseVoiceMetar() # Parse atis voice with metar only. self.atisVoice = '{}, {}.'.format( self.airportInfos[self.airport][3], self.metarVoice) # Read the metar. self.readVoice() return self.SLEEP_TIME # Parse ATIS. # Information. self.getInfoIdentifier() self.parseVoiceInformation() # Metar. if not self.ivac2: self.parseMetar(self.atisRaw[2].strip()) else: for ar in self.atisRaw: if ar.startswith('METAR'): self.parseMetar(ar.replace('METAR ', '').strip()) break self.parseVoiceMetar() # Runways / TRL / TA self.parseRawRwy() self.parseVoiceRwy() # comment. self.parseVoiceComment() # Compose complete atis voice string. self.atisVoice = '{} {} {} {} Information {}, out.'.format( self.informationVoice, self.rwyVoice, self.commentVoice, self.metarVoice, self.informationIdentifier) # Read the string. self.readVoice() # After successful reading. return 0 ## Downloads and reads the whazzup from IVAO def getWhazzupText(self): urllib.urlretrieve(self.WHAZZUP_URL, 'whazzup.txt.gz') with gzip.open('whazzup.txt.gz', 'rb') as f: self.whazzupText = f.read().decode('iso-8859-15') os.remove('whazzup.txt.gz') ## Reads a whazzup file on disk. # For debug purposes. def getWhazzupTextDebug(self): with open(self.WHAZZUP_TEXT_DEBUG) as whazzupFile: self.whazzupText = whazzupFile.read() pass ## Find a station of the airport and read the ATIS string. def parseWhazzupText(self): # Find an open station for st in self.STATION_SUFFIXES: matchObj = re.search('{}\w*?_{}'.format(self.airport, st), self.whazzupText) if matchObj is not None: break if matchObj is not None: # Extract ATIS. lineStart = matchObj.start() lineEnd = self.whazzupText.find('\n', matchObj.start()) stationInfo = self.whazzupText[lineStart:lineEnd].split(':') self.ivac2 = bool(int(stationInfo[39][0]) - 1) self.atisTextRaw = stationInfo[35].encode('iso-8859-15') self.atisRaw = stationInfo[35].encode('iso-8859-15').split('^§') else: self.atisRaw = None def parseMetar(self, metarString): with warnings.catch_warnings(): warnings.simplefilter("ignore") self.metar = Metar(metarString, strict=False) ## Parse runway and transition data. # Get active runways for arrival and departure. # Get transistion level and altitude. def parseRawRwy(self): self.rwyInformation = [None, None, None, None] if not self.ivac2: strSplit = self.atisRaw[3].split(' / ') for sp in strSplit: # ARR. if sp[0:3] == 'ARR': self.rwyInformation[0] = [] arr = sp.replace('ARR RWY ', '').strip() starts = [] for ma in re.finditer('\d{2}[RLC]?', arr): starts.append(ma.start()) for st in range(len(starts)): if st < len(starts) - 1: rwy = arr[starts[st]:starts[st + 1]] else: rwy = arr[starts[st]:] curRwy = [rwy[0:2], None, None, None] if 'L' in rwy: curRwy[1] = 'Left' if 'C' in rwy: curRwy[2] = 'Center' if 'R' in rwy: curRwy[3] = 'Right' self.rwyInformation[0].append(curRwy) # DEP. elif sp[0:3] == 'DEP': self.rwyInformation[1] = [] dep = sp.replace('DEP RWY ', '').strip() starts = [] for ma in re.finditer('\d{2}[RLC]?', dep): starts.append(ma.start()) for st in range(len(starts)): if st < len(starts) - 1: rwy = dep[starts[st]:starts[st + 1]] else: rwy = dep[starts[st]:] curRwy = [rwy[0:2], None, None, None] if 'L' in rwy: curRwy[1] = 'Left' if 'C' in rwy: curRwy[2] = 'Center' if 'R' in rwy: curRwy[3] = 'Right' self.rwyInformation[1].append(curRwy) # TRL/TA elif sp[0:3] == 'TRL': self.rwyInformation[2] = sp.strip().replace('TRL FL', '') elif sp[0:2] == 'TA': self.rwyInformation[3] = sp.strip().replace('TA ', '').replace( 'FT', '') # Ivac 2 else: for ar in self.atisRaw: if ar.startswith('TA'): trlTaSplit = ar.split(' / ') self.rwyInformation[3] = trlTaSplit[0].replace('TA ', '') self.rwyInformation[2] = trlTaSplit[1].replace('TRL', '') elif ar.startswith('ARR'): curRwy = [ar[8:10], None, None, None] if 'L' in ar[8:]: curRwy[1] = 'Left' if 'C' in ar[8:]: curRwy[2] = 'Center' if 'R' in ar[8:]: curRwy[3] = 'Right' if self.rwyInformation[0] is None: self.rwyInformation[0] = [curRwy] else: self.rwyInformation[0].append(curRwy) elif ar.startswith('DEP'): curRwy = [ar[8:10], None, None, None] if 'L' in ar[8:]: curRwy[1] = 'Left' if 'C' in ar[8:]: curRwy[2] = 'Center' if 'R' in ar[8:]: curRwy[3] = 'Right' if self.rwyInformation[1] is None: self.rwyInformation[1] = [curRwy] else: self.rwyInformation[1].append(curRwy) ## Generate a string of the metar for voice generation. def parseVoiceMetar(self): self.metarVoice = 'Met report' # Time hours = parseVoiceInt('{:02d}'.format(self.metar._hour)) minutes = parseVoiceInt('{:02d}'.format(self.metar._min)) self.metarVoice = '{} time {} {}'.format(self.metarVoice, hours, minutes) # Wind if self.metar.wind_speed._value != 0: if self.metar.wind_dir is not None: self.metarVoice = '{}, wind {}, {}'.format( self.metarVoice, parseVoiceString(self.metar.wind_dir.string()), parseVoiceString(self.metar.wind_speed.string())) else: self.metarVoice = '{}, wind variable, {}'.format( self.metarVoice, parseVoiceString(self.metar.wind_speed.string())) else: self.metarVoice = '{}, wind calm'.format( self.metarVoice, self.metar.wind_dir.string(), self.metar.wind_speed.string()) if self.metar.wind_gust is not None: self.metarVoice = '{}, maximum {}'.format( self.metarVoice, parseVoiceString(self.metar.wind_gust.string())) if self.metar.wind_dir_from is not None: self.metarVoice = '{}, variable between {} and {}'.format( self.metarVoice, parseVoiceString(self.metar.wind_dir_from.string()), parseVoiceString(self.metar.wind_dir_to.string())) # Visibility. #TODO: implement directions self.metarVoice = '{}, visibility {}'.format(self.metarVoice, self.metar.vis.string()) # runway visual range rvr = self.metar.runway_visual_range().replace(';', ',') if rvr: rvrNew = '' lastEnd = 0 rvrPattern = re.compile('[0123]\d[LCR]?(?=,)') for ma in rvrPattern.finditer(rvr): rwyRaw = rvr[ma.start():ma.end()] rwyStr = parseVoiceInt(rwyRaw[0:2]) if len(rwyRaw) > 2: if rwyRaw[2] == 'L': rwyStr = '{} left'.format(rwyStr) elif rwyRaw[2] == 'C': rwyStr = '{} center'.format(rwyStr) elif rwyRaw[2] == 'R': rwyStr = '{} right'.format(rwyStr) rvrNew = '{}{}{}'.format(rvrNew, rvr[lastEnd:ma.start()], rwyStr) lastEnd = ma.end() rvrNew = '{}{}'.format(rvrNew, rvr[lastEnd:]) self.metarVoice = '{}, visual range {}'.format( self.metarVoice, rvrNew) # weather phenomena if self.metar.weather: self.metarVoice = '{}, {}'.format( self.metarVoice, self.metar.present_weather().replace(';', ',')) # clouds if self.metar.sky: self.metarVoice = '{}, {}'.format( self.metarVoice, self.metar.sky_conditions(',').replace(',', ', ').replace( 'a few', 'few')) elif 'CAVOK' in self.metar.code: self.metarVoice = '{}, clouds and visibility ok'.format( self.metarVoice) # runway condition #TODO: Implement runway conditions # Not implemented in python-metar # temperature tempValue = parseVoiceInt(str(int(self.metar.temp._value))) if self.metar.temp._units == 'C': tempUnit = 'degree Celsius' else: tempUnit = 'degree Fahrenheit' self.metarVoice = '{}, temperature {} {}'.format( self.metarVoice, tempValue, tempUnit) # dew point dewptValue = parseVoiceInt(str(int(self.metar.dewpt._value))) if self.metar.dewpt._units == 'C': dewptUnit = 'degree Celsius' else: dewptUnit = 'degree Fahrenheit' self.metarVoice = '{}, dew point {} {}'.format(self.metarVoice, dewptValue, dewptUnit) # QNH if self.metar.press._units == 'MB': pressValue = parseVoiceInt(str(int(self.metar.press._value))) self.metarVoice = '{}, Q N H {} hectopascal'.format( self.metarVoice, pressValue) else: self.metarVoice = '{}, Altimeter {}'.format( self.metarVoice, parseVoiceString(self.metar.press.string())) #TODO: implement trend self.metarVoice = '{},'.format(self.metarVoice) ## Generate a string of the information identifier for voice generation. def parseVoiceInformation(self): if not self.ivac2: timeMatch = re.search(r'\d{4}z', self.atisRaw[1]) startInd = timeMatch.start() endInd = timeMatch.end() - 1 timeStr = parseVoiceInt(self.atisRaw[1][startInd:endInd]) self.informationVoice = '{} {}.'.format( self.atisRaw[1][0:startInd - 1], timeStr) else: information = self.atisRaw[1].split(' ') airport = information[0] airport = self.airportInfos[airport][3] time = parseVoiceInt(information[4][0:4]) self.informationVoice = '{} Information {} recorded at {}.'.format( airport, self.informationIdentifier, time) ## Generate a string of the runway information for voice generation. def parseVoiceRwy(self): self.rwyVoice = '' # ARR. if self.rwyInformation[0] is not None: self.rwyVoice = '{}Arrival runway '.format(self.rwyVoice) for arr in self.rwyInformation[0]: if arr[1:4].count(None) == 3: self.rwyVoice = '{}{} and '.format(self.rwyVoice, parseVoiceInt(arr[0])) else: for si in arr[1:4]: if si is not None: self.rwyVoice = '{}{} {} and '.format( self.rwyVoice, parseVoiceInt(arr[0]), si) self.rwyVoice = '{},'.format(self.rwyVoice[0:-5]) # DEP. if self.rwyInformation[1] is not None: self.rwyVoice = '{} Departure runway '.format(self.rwyVoice) for dep in self.rwyInformation[1]: if dep[1:4].count(None) == 3: self.rwyVoice = '{}{} and '.format(self.rwyVoice, parseVoiceInt(dep[0])) else: for si in dep[1:4]: if si is not None: self.rwyVoice = '{}{} {} and '.format( self.rwyVoice, parseVoiceInt(dep[0]), si) self.rwyVoice = '{}, '.format(self.rwyVoice[0:-5]) # TRL if self.rwyInformation[2] is not None: self.rwyVoice = '{}Transition level {}, '.format( self.rwyVoice, parseVoiceInt(self.rwyInformation[2])) # TA if self.rwyInformation[3] is not None: self.rwyVoice = '{}Transition altitude {} feet,'.format( self.rwyVoice, self.rwyInformation[3]) ## Generate a string of ATIS comment for voice generation. def parseVoiceComment(self): if not self.ivac2: self.commentVoice = '{},'.format(parseVoiceString(self.atisRaw[4])) else: self.commentVoice = '' ## Reads the atis string using voice generation. def readVoice(self): # Init currently Reading with None. self.currentlyReading = None self.logger.debug('Voice Text is: {}'.format(self.atisVoice)) if pyttsxImported: # Set properties currently reading self.currentlyReading = self.airport # Init voice engine. self.engine = pyttsx.init() # Set properties. voices = self.engine.getProperty('voices') for vo in voices: if 'english' in vo.name.lower(): self.engine.setProperty('voice', vo.id) self.logger.debug('Using voice: {}'.format(vo.name)) break self.engine.setProperty('rate', self.SPEECH_RATE) # Start listener and loop. self.engine.connect('started-word', self.onWord) # Say complete ATIS self.engine.say(self.atisVoice) self.logger.info('Start reading.') self.engine.runAndWait() self.logger.info('Reading finished.') self.engine = None else: self.logger.warning( 'Speech engine not initalized, no reading. Sleeping for {} seconds...' .format(self.SLEEP_TIME)) time.sleep(self.SLEEP_TIME) ## Callback for stop of reading. # Stops reading if frequency change/com deactivation/out of range. def onWord(self, name, location, length): # @UnusedVariable self.getPyuipcData() self.getAirport() if self.airport != self.currentlyReading: self.engine.stop() self.currentlyReading = None ## Reads current frequency and COM status. def getPyuipcData(self): if pyuipcImported: results = pyuipc.read(self.pyuipcOffsets) # frequency hexCode = hex(results[0])[2:] self.com1frequency = float('1{}.{}'.format(hexCode[0:2], hexCode[2:])) hexCode = hex(results[1])[2:] self.com2frequency = float('1{}.{}'.format(hexCode[0:2], hexCode[2:])) # radio active #TODO: Test accuracy of this data (with various planes and sims) radioActiveBits = list(map(int, '{0:08b}'.format(results[2]))) if radioActiveBits[2]: self.com1active = True self.com2active = True elif radioActiveBits[0]: self.com1active = True self.com2active = False elif radioActiveBits[1]: self.com1active = False self.com2active = True else: self.com1active = False self.com2active = False # lat lon self.lat = results[3] * (90.0 / (10001750.0 * 65536.0 * 65536.0)) self.lon = results[4] * (360.0 / (65536.0 * 65536.0 * 65536.0 * 65536.0)) else: self.com1frequency = self.COM1_FREQUENCY_DEBUG self.com2frequency = self.COM2_FREQUENCY_DEBUG self.com1active = True self.com2active = True self.lat = self.LAT_DEBUG self.lon = self.LON_DEBUG # Logging. if self.com1active: com1activeStr = 'active' else: com1activeStr = 'inactive' if self.com2active: com2activeStr = 'active' else: com2activeStr = 'inactive' self.logger.debug('COM 1: {} ({}), COM 2: {} ({})'.format( self.com1frequency, com1activeStr, self.com2frequency, com2activeStr)) # self.logger.debug('COM 1 active: {}, COM 2 active: {}'.format(self.com1active,self.com2active)) ## Determine if there is an airport aplicable for ATIS reading. def getAirport(self): self.airport = None frequencies = [] if self.com1active: frequencies.append(self.com1frequency) if self.com2active: frequencies.append(self.com2frequency) if frequencies: distanceMin = self.RADIO_RANGE + 1 for ap in self.airportInfos: distance = gcDistanceNm(self.lat, self.lon, self.airportInfos[ap][1], self.airportInfos[ap][2]) if ( floor(self.airportInfos[ap][0] * 100) / 100 ) in frequencies and distance < self.RADIO_RANGE and distance < distanceMin: distanceMin = distance self.airport = ap ## Read data of airports from a given file. def getAirportDataFile(self, apFile): # Check if file exists. if not os.path.isfile(apFile): self.logger.warning('No such file: {}'.format(apFile)) return # Read the file. with open(apFile) as aptInfoFile: for li in aptInfoFile: lineSplit = re.split('[,;]', li) if not li.startswith('#') and len(lineSplit) == 5: self.airportInfos[lineSplit[0].strip()] = (float( lineSplit[1]), float(lineSplit[2]), float( lineSplit[3]), lineSplit[4].replace('\n', '')) ## Read data of airports from http://ourairports.com. def getAirportDataWeb(self): airportFreqs = {} # Read the file with frequency. with closing( urllib2.urlopen(self.OUR_AIRPORTS_URL + 'airport-frequencies.csv', timeout=5)) as apFreqFile: for li in apFreqFile: lineSplit = li.split(',') if lineSplit[3] == '"ATIS"': airportFreqs[lineSplit[2].replace('"', '')] = float( lineSplit[-1].replace('\n', '')) # Read the file with other aiport data. # Add frequency and write them to self. airportInfos. with closing(urllib2.urlopen(self.OUR_AIRPORTS_URL + 'airports.csv')) as apFile: for li in apFile: lineSplit = re.split((",(?=(?:[^\"]*\"[^\"]*\")*[^\"]*$)"), li) apCode = lineSplit[1].replace('"', '') if apCode in airportFreqs and len(apCode) <= 4: apFreq = airportFreqs[apCode] if 100.0 < apFreq < 140.0: self.airportInfos[apCode] = [ apFreq, float(lineSplit[4]), float(lineSplit[5]), lineSplit[3].replace('"', '') ] ## Reads airportData from two sources. def getAirportData(self): self.airportInfos = {} try: # Try to read airport data from web. self.getAirportDataWeb() self.getAirportDataFile( os.path.join(self.rootDir, 'airports_add.info')) collectedFromWeb = True except: # If this fails, use the airports from airports.info. self.logger.warning( 'Unable to get airport data from web. Using airports.info. Error: {}' .format(sys.exc_info()[0])) self.airportInfos = {} collectedFromWeb = False try: self.getAirportDataFile( os.path.join(self.rootDir, 'airports.info')) except: self.logger.error( 'Unable to read airport data from airports.info!') # Sort airportInfos and write them to a file for future use if collected from web. if collectedFromWeb: apInfoPath = os.path.join(self.rootDir, 'airports.info') apList = self.airportInfos.keys() apList.sort() with open(apInfoPath, 'w') as apDataFile: for ap in apList: apDataFile.write( '{:>4}; {:6.2f}; {:11.6f}; {:11.6f}; {}\n'.format( ap, self.airportInfos[ap][0], self.airportInfos[ap][1], self.airportInfos[ap][2], self.airportInfos[ap][3])) ## Determines the info identifier of the loaded ATIS. def getInfoIdentifier(self): if not self.ivac2: informationPos = re.search('information ', self.atisRaw[1]).end() informationSplit = self.atisRaw[1][informationPos:].split(' ') self.informationIdentifier = informationSplit[0] else: self.informationIdentifier = CHAR_TABLE[re.findall( r'(?<=ATIS )[A-Z](?= \d{4})', self.atisRaw[1])[0]] ## Retrieves the metar of an airport independet of an ATIS. def getAirportMetar(self): if not debug: urllib.urlretrieve(self.WHAZZUP_METAR_URL, 'whazzup_metar.txt') with open('whazzup_metar.txt', 'r') as metarFile: metarText = metarFile.read() if not debug: os.remove('whazzup_metar.txt') metarStart = metarText.find(self.airport) metarEnd = metarText.find('\n', metarStart) return metarText[metarStart:metarEnd]
print "visibility: %s" % obs.visibility() # The runway_visual_range() method summarizes the runway visibility # observations. if obs.runway: print "visual range: %s" % obs.runway_visual_range() # The 'press' attribute is a pressure object. if obs.press: print "pressure: %s" % obs.press.string("mb") # The 'precip_1hr' attribute is a precipitation object. if obs.precip_1hr: print "precipitation: %s" % obs.precip_1hr.string("in") # The present_weather() method summarizes the weather description (rain, etc.) print "weather: %s" % obs.present_weather() # The sky_conditions() method summarizes the cloud-cover observations. print "sky: %s" % obs.sky_conditions("\n ") # The remarks() method describes the remark groups that were parsed, but # are not available directly as Metar attributes. The precipitation, # min/max temperature and peak wind remarks, for instance, are stored as # attributes and won't be listed here. if obs._remarks: print "remarks:" print "- "+obs.remarks("\n- ") print "-----------------------------------------------------------------------\n"