aboutsummaryrefslogtreecommitdiffstats
path: root/src/plugins/weather.py
blob: fa3f482055c136537029659379696c87a1b65661 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
# -*- python -*-
'''
xpybar – xmobar replacement written in python
Copyright © 2014, 2015, 2016, 2017, 2018, 2019  Mattias Andrée (m@maandreese)

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 3 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, see <http://www.gnu.org/licenses/>.
'''

from util import *


class Weather:
    '''
    Weather monitor
    
    @variable  icao:str               The station's International Civil Aviation Organization airport code
    @variable  station:str            The station as a human readable, not necessarily only the airport name
    @variable  location:str           Human readable location, country or state (abbreviated) and country.
    @variable  latitude:float         The latitude position of the station, in degrees to north with two decimals
    @variable  longitude:float        The longitude position of the station, in degrees to east with two decimals
    @variable  headers:list<str>      Headers in the decoded metar data
    @variable  fields:dict<str, str>  Fields in the decoded metar data
    @variable  time:(int, int, int)   The time observation was made: day of month, hour and minute, in UTC
    
    The following will be `None` if not found in the data, but it is probably found if it should not be `None`.
    They can be floating point, but are most often integers.
    
    @variable  wind_dir:float?           The wind direction, `None` if variable
    @variable  wind_speed:float          The wind speed in knots
    @variable  wind_gusts:float?         The wind gusts (variability of the wind speed; towards) in knots
    @variable  wind_var:(float, float)?  The wind direction range, `None` if less than 60° variation
    @variable  temp:float                The temperature in °C
    @variable  dew:float                 The dew point in °C
    @variable  wind_chill:float          The wind chill in °C
    @variable  humidity:float            The relative humidity in %
    @variable  pressure:flaot            The pressure in hPa
    @variable  visibility:float          The visibility in statute miles
    '''
    
    
    def __init__(self, station = None):
        '''
        Constructor
        
        @param  station:str?  The station's ICAO code (International Civil Aviation Organization airport code),
                              if `None`, ~/.config/metar or /etc/metar will be used (see metar(1))
        '''
        import os, pwd
        if station is None:
            try:
                filename = os.environ['HOME'] if 'HOME' in os.environ else ''
                if len(filename) == 0:
                    filename = pwd.getpwuid(os.getuid()).pw_dir
                filename = '%s/.config/metar' % filename
                if not os.path.isfile(filename):
                    filename = '/etc/metar'
            except:
                filename = '/etc/metar'
            with open(filename, 'rb') as file:
                station = file.read()
            station = station.decode('utf-8', 'strict').split('\n')[0]
        self.icao = station
        #url = 'http://weather.noaa.gov/pub/data/observations/metar/decoded/%s.TXT' % station
        url = 'https://tgftp.nws.noaa.gov/data/observations/metar/decoded/%s.TXT' % station
        decoded = spawn_read('curl', url).split('\n')
        # How to parse: http://www.wunderground.com/metarFAQ.asp
        
        station_header, self.headers, decoded = decoded[0].split(', '), decoded[:2], decoded[2:]
        self.station, station_header = station_header[0], ', '.join(station_header[1:])
        self.location = station_header.split(' (')[0]
        self.latitude, self.longitude = station_header.split(') ')[1].split(' ')[:2]
        self.latitude,  ysign = self.latitude[:-1],  self.latitude[-1]  == 'S'
        self.longitude, xsign = self.longitude[:-1], self.longitude[-1] == 'W'
        self.latitude  = [float(x) for x in self.latitude.split('-')]
        self.longitude = [float(x) for x in self.longitude.split('-')]
        self.latitude  = self.latitude[0]  + self.latitude[1]  / 100
        self.longitude = self.longitude[0] + self.longitude[1] / 100
        self.latitude  = -(self.latitude)  if ysign else self.latitude
        self.longitude = -(self.longitude) if xsign else self.longitude
        
        self.fields = {}
        for line in decoded:
            line = line.split(': ')
            self.fields[line[0]] = ': '.join(line[1:])
        
        self.wind_dir, self.wind_speed, self.wind_gusts     = None, None, None
        self.wind_var, self.temp, self.dew, self.wind_chill = None, None, None, None
        self.pressure, self.visibility, self.humidity       = None, None, None
        
        for ob in self.fields['ob'].split(' ')[1:]:
            self.__time(ob)
            self.__wind(ob)
            self.__wind_var(ob)
            self.__temp(ob)
            self.__pressure(ob)
            self.__visibility(ob)
            # (-SHRA)-Present Weather and Obscurations  from  http://www.wunderground.com/metarFAQ.asp (p44)
            # BKN070-Sky Condition                      from  http://www.wunderground.com/metarFAQ.asp
            # (p46, p49, p59(7,8,9). p61-65, p67-77, p91+96)
        
        if 'Relative Humidity' in self.fields:
            try:
                self.humidity = float(self.fields['Relative Humidity'].replace('%', ''))
            except:
                pass
        
        get_celsius = lambda text : float(text.split('(')[1].split(')')[0])
        if 'Windchill' in self.fields:
            try:
                self.windchill = get_celsius(self.fields['Windchill'])
            except:
                pass
        if self.dew is None and 'Dew Point' in self.fields:
            try:
                self.dew = get_celsius(self.fields['Dew Point'])
            except:
                pass
        if self.temp is None and 'Temperature' in self.fields:
            try:
                self.temp = get_celsius(self.fields['Temperature'])
            except:
                pass
        
        #Wind: from the N (010 degrees) at 18 MPH (16 KT):0
        #Visibility: 3 mile(s):0
        #Sky conditions: overcast
        #Weather: precipitation
        #Precipitation last hour: A trace  -- sometimes
        #Windchill: -7 F (-22 C):2  -- sometimes
        #Pressure (altimeter): 30.19 in. Hg (1022 hPa)
    
    
    def __time(self, ob):
        ob_ = list(filter(lambda c : not ('0' <= c <= '9'), ob))
        if (len(ob_) == 1) and ob.endswith('Z'):
            self.time = (ob[0 : 2], ob[2 : 4], ob[4 : 6])
    
    def __wind(self, ob):
        if ob.endswith('KT'):
            self.wind_dir = None if ob.startswith('VRB') else float(ob[:3])
            ob = ob[3:]
            i = ob.find('G') if 'G' in ob else ob.find('K')
            self.wind_speed, ob = float(ob[:i]), ob[i:]
            self.wind_gusts = float(ob[1 : -2]) if ob[0] == 'G' else None
    
    def __wind_var(self, ob):
        if (len(ob) == 7) and (ob[3] == 'V'):
            if len(list(filter(lambda c : not ('0' <= c <= '9'), ob))) == 1:
                self.wind_var = (float(ob[:3]), float(ob[-3:]))
    
    def __temp(self, ob):
        if '/' in ob:
            ob = ob.replace('M', '-')
            if len(list(filter(lambda c : not ('0' <= c <= '9'), ob.replace('-', '0')))) == 1:
                (self.temp, self.dew) = [float(x) for x in ob.split('/')]
    
    def __pressure(self, ob):
        if (len(ob) == 5) and (ob[0] in ['Q', 'A']):
            if len(list(filter(lambda c : not ('0' <= c <= '9'), ob))) == 1:
                if ob[0] == 'Q':
                    self.pressure = float(ob[1:])
                else:
                    self.pressure = 33.86 * float(ob[1:]) / 100
    
    def __visibility(self, ob):
        # TODO p61
        if ob.endswith('SM') and (len(ob) > 0):
            if len(list(filter(lambda c : not ('0' <= c <= '9'), ob))) == 2:
                self.visibility = float(ob[:-2])