forked from prueker/METARMap
-
Notifications
You must be signed in to change notification settings - Fork 0
/
metar.py
304 lines (275 loc) · 17.4 KB
/
metar.py
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
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
#!/usr/bin/env python3
import urllib.request
import xml.etree.ElementTree as ET
import board
import neopixel
import time
import datetime
import requests
try:
import astral
except ImportError:
astral = None
try:
import displaymetar
except ImportError:
displaymetar = None
# metar.py script iteration 1.5.1
# ---------------------------------------------------------------------------
# ------------START OF CONFIGURATION-----------------------------------------
# ---------------------------------------------------------------------------
# NeoPixel LED Configuration
LED_COUNT = 48 # Number of LED pixels.
LED_PIN = board.D18 # GPIO pin connected to the pixels (18 is PCM).
LED_BRIGHTNESS = 0.2 # Float from 0.0 (min) to 1.0 (max)
LED_ORDER = neopixel.GRB # Strip type and colour ordering
COLOR_VFR = (255,0,0) # Green
COLOR_VFR_FADE = (125,0,0) # Green Fade for wind
COLOR_MVFR = (0,0,255) # Blue
COLOR_MVFR_FADE = (0,0,125) # Blue Fade for wind
COLOR_IFR = (0,255,0) # Red
COLOR_IFR_FADE = (0,125,0) # Red Fade for wind
COLOR_LIFR = (0,125,125) # Magenta
COLOR_LIFR_FADE = (0,75,75) # Magenta Fade for wind
COLOR_CLEAR = (0,0,0) # Clear
COLOR_LIGHTNING = (255,255,255) # White
COLOR_HIGH_WINDS = (255,255,0) # Yellow
COLOR_NOT_WORKING = (153,101,21) # Purple
# ----- Blink/Fade functionality for Wind and Lightning -----
# Do you want the METARMap to be static to just show flight conditions, or do you also want blinking/fading based on current wind conditions
ACTIVATE_WINDCONDITION_ANIMATION = True # Set this to False for Static or True for animated wind conditions
#Do you want the Map to Flash white for lightning in the area
ACTIVATE_LIGHTNING_ANIMATION = True # Set this to False for Static or True for animated Lightning
# Fade instead of blink
FADE_INSTEAD_OF_BLINK = False # Set to False if you want blinking
# Blinking Windspeed Threshold
WIND_BLINK_THRESHOLD = 14 # Knots of windspeed to blink/fade
HIGH_WINDS_THRESHOLD = 24 # Knots of windspeed to trigger Yellow LED indicating very High Winds, set to -1 if you don't want to use this
ALWAYS_BLINK_FOR_GUSTS = True # Always animate for Gusts (regardless of speeds)
# Blinking Speed in seconds
BLINK_SPEED = 0.3 # Float in seconds, e.g. 0.5 for half a second
# Total blinking time in seconds.
# For example set this to 300 to keep blinking for 5 minutes if you plan to run the script every 5 minutes to fetch the updated weather
BLINK_TOTALTIME_SECONDS = 60
# ----- Daytime dimming of LEDs based on time of day or Sunset/Sunrise -----
ACTIVATE_DAYTIME_DIMMING = True # Set to True if you want to dim the map after a certain time of day
BRIGHT_TIME_START = datetime.time(7,0) # Time of day to run at LED_BRIGHTNESS in hours and minutes
DIM_TIME_START = datetime.time(19,0) # Time of day to run at LED_BRIGHTNESS_DIM in hours and minutes
LED_BRIGHTNESS_DIM = 0.1 # Float from 0.0 (min) to 1.0 (max)
USE_SUNRISE_SUNSET = True # Set to True if instead of fixed times for bright/dimming, you want to use local sunrise/sunset
LOCATION = "Philadelphia" # Nearby city for Sunset/Sunrise timing, refer to https://astral.readthedocs.io/en/latest/#cities for list of cities supported
# ----- External Display support -----
ACTIVATE_EXTERNAL_METAR_DISPLAY = True # Set to True if you want to display METAR conditions to a small external display
DISPLAY_ROTATION_SPEED = 5.0 # Float in seconds, e.g 2.0 for two seconds
# ----- Show a set of Legend LEDS at the end -----
SHOW_LEGEND = False # Set to true if you want to have a set of LEDs at the end show the legend
# You'll need to add 7 LEDs at the end of your string of LEDs
# If you want to offset the legend LEDs from the end of the last airport from the airports file,
# then change this offset variable by the number of LEDs to skip before the LED that starts the legend
OFFSET_LEGEND_BY = 0
# The order of LEDs is:
# VFR
# MVFR
# IFR
# LIFR
# LIGHTNING
# WINDY
# HIGH WINDS
# ---------------------------------------------------------------------------
# ------------END OF CONFIGURATION-------------------------------------------
# ---------------------------------------------------------------------------
print("Running metar.py at " + datetime.datetime.now().strftime('%d/%m/%Y %H:%M'))
# Figure out sunrise/sunset times if astral is being used
if astral is not None and USE_SUNRISE_SUNSET:
try:
# For older clients running python 3.5 which are using Astral 1.10.1
ast = astral.Astral()
try:
city = ast[LOCATION]
except KeyError:
print("Error: Location not recognized, please check list of supported cities and reconfigure")
else:
print(city)
sun = city.sun(date = datetime.datetime.now().date(), local = True)
BRIGHT_TIME_START = sun['sunrise'].time()
DIM_TIME_START = sun['sunset'].time()
except AttributeError:
# newer Raspberry Pi versions using Python 3.6+ using Astral 2.2
import astral.geocoder
import astral.sun
try:
city = astral.geocoder.lookup(LOCATION, astral.geocoder.database())
except KeyError:
print("Error: Location not recognized, please check list of supported cities and reconfigure")
else:
print(city)
sun = astral.sun.sun(city.observer, date = datetime.datetime.now().date(), tzinfo=city.timezone)
BRIGHT_TIME_START = sun['sunrise'].time()
DIM_TIME_START = sun['sunset'].time()
print("Sunrise:" + BRIGHT_TIME_START.strftime('%H:%M') + " Sunset:" + DIM_TIME_START.strftime('%H:%M'))
# Initialize the LED strip
bright = BRIGHT_TIME_START < datetime.datetime.now().time() < DIM_TIME_START
print("Wind animation:" + str(ACTIVATE_WINDCONDITION_ANIMATION))
print("Lightning animation:" + str(ACTIVATE_LIGHTNING_ANIMATION))
print("Daytime Dimming:" + str(ACTIVATE_DAYTIME_DIMMING) + (" using Sunrise/Sunset" if USE_SUNRISE_SUNSET and ACTIVATE_DAYTIME_DIMMING else ""))
print("External Display:" + str(ACTIVATE_EXTERNAL_METAR_DISPLAY))
pixels = neopixel.NeoPixel(LED_PIN, LED_COUNT, brightness = LED_BRIGHTNESS_DIM if (ACTIVATE_DAYTIME_DIMMING and bright == False) else LED_BRIGHTNESS, pixel_order = LED_ORDER, auto_write = False)
# Read the airports file to retrieve list of airports and use as order for LEDs
with open("/home/moisedo/metar/airports") as f:
airports = f.readlines()
airports = [x.strip() for x in airports]
try:
with open("/home/moisedo/metar/displayairports") as f2:
displayairports = f2.readlines()
displayairports = [x.strip() for x in displayairports]
print("Using subset airports for LED display")
except IOError:
print("Rotating through all airports on LED display")
displayairports = None
# Retrieve METAR from aviationweather.gov data server
# Details about parameters can be found here: https://www.aviationweather.gov/dataserver/example?datatype=metar
#url = "https://www.aviationweather.gov/adds/dataserver_current/httpparam?dataSource=metars&requestType=retrieve&format=xml&hoursBeforeNow=5&mostRecentForEachStation=true&stationString=" + ",".join([item for item in airports if item != "NULL"])
url = "https://aviationweather.gov/cgi-bin/data/dataserver.php?requestType=retrieve&dataSource=metars&stationString=" + ",".join([item for item in airports if item != "NULL"]) + "&hoursBeforeNow=5&format=xml&mostRecent=true&mostRecentForEachStation=constraint"
req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36 Edg/86.0.622.69'})
content = urllib.request.urlopen(req).read()
# Retrieve flying conditions from the service response and store in a dictionary for each airport
root = ET.fromstring(content)
conditionDict = { "NULL": {"flightCategory" : "", "windDir": "", "windSpeed" : 0, "windGustSpeed" : 0, "windGust" : False, "lightning": False, "tempC" : 0, "dewpointC" : 0, "vis" : 0, "altimHg" : 0, "obs" : "", "skyConditions" : {}, "obsTime" : datetime.datetime.now() } }
conditionDict.pop("NULL")
stationList = []
for metar in root.iter('METAR'):
stationId = metar.find('station_id').text if metar.find('station_id') is not None else None
if stationId is None:
print("Missing or malformed station ID, skipping METAR element:")
print(ET.tostring(metar, encoding='unicode')) # Log the entire METAR element for review
continue
# Check for flight_category
flight_category_element = metar.find('flight_category')
if flight_category_element is None or flight_category_element.text is None:
flightCategory = "Unknown"
print(f"Flight category missing for {stationId}, using default: {flightCategory}")
else:
flightCategory = flight_category_element.text
print(f"About to process stationId: {stationId}")
print(stationId + ":")
windDir = ""
windSpeed = 0
windGustSpeed = 0
windGust = False
lightning = False
tempC = 0
dewpointC = 0
vis = 0
altimHg = 0.0
obs = ""
skyConditions = []
if metar.find('wind_gust_kt') is not None:
windGustSpeed = int(metar.find('wind_gust_kt').text)
windGust = (True if (ALWAYS_BLINK_FOR_GUSTS or windGustSpeed > WIND_BLINK_THRESHOLD) else False)
if metar.find('wind_speed_kt') is not None:
windSpeed = int(metar.find('wind_speed_kt').text)
if metar.find('wind_dir_degrees') is not None:
windDir = metar.find('wind_dir_degrees').text
if metar.find('temp_c') is not None:
tempC = int(round(float(metar.find('temp_c').text)))
if metar.find('dewpoint_c') is not None:
dewpointC = int(round(float(metar.find('dewpoint_c').text)))
if metar.find('visibility_statute_mi') is not None:
#vis = int(round(float(metar.find('visibility_statute_mi').text)))
vis_str = metar.find('visibility_statute_mi').text
vis_str = vis_str.replace('+', '')
vis = int(round(float(vis_str)))
if metar.find('altim_in_hg') is not None:
altimHg = float(round(float(metar.find('altim_in_hg').text), 2))
if metar.find('wx_string') is not None:
obs = metar.find('wx_string').text
if metar.find('observation_time') is not None:
obsTime = datetime.datetime.fromisoformat(metar.find('observation_time').text.replace("Z","+00:00"))
for skyIter in metar.iter("sky_condition"):
skyCond = { "cover" : skyIter.get("sky_cover"), "cloudBaseFt": int(skyIter.get("cloud_base_ft_agl", default=0)) }
skyConditions.append(skyCond)
if metar.find('raw_text') is not None:
rawText = metar.find('raw_text').text
lightning = False if ((rawText.find('LTG', 4) == -1 and rawText.find('TS', 4) == -1) or rawText.find('TSNO', 4) != -1) else True
print(stationId + ":"
+ flightCategory + ":"
+ str(windDir) + "@" + str(windSpeed) + ("G" + str(windGustSpeed) if windGust else "") + ":"
+ str(vis) + "SM:"
+ obs + ":"
+ str(tempC) + "/"
+ str(dewpointC) + ":"
+ str(altimHg) + ":"
+ str(lightning))
conditionDict[stationId] = { "flightCategory" : flightCategory, "windDir": windDir, "windSpeed" : windSpeed, "windGustSpeed": windGustSpeed, "windGust": windGust, "vis": vis, "obs" : obs, "tempC" : tempC, "dewpointC" : dewpointC, "altimHg" : altimHg, "lightning": lightning, "skyConditions" : skyConditions, "obsTime": obsTime }
if displayairports is None or stationId in displayairports:
stationList.append(stationId)
# Start up external display output
disp = None
if displaymetar is not None and ACTIVATE_EXTERNAL_METAR_DISPLAY:
print("setting up external display")
disp = displaymetar.startDisplay()
displaymetar.clearScreen(disp)
# Setting LED colors based on weather conditions
looplimit = int(round(BLINK_TOTALTIME_SECONDS / BLINK_SPEED)) if (ACTIVATE_WINDCONDITION_ANIMATION or ACTIVATE_LIGHTNING_ANIMATION or ACTIVATE_EXTERNAL_METAR_DISPLAY) else 1
windCycle = False
displayTime = 0.0
displayAirportCounter = 0
numAirports = len(stationList)
while looplimit > 0:
i = 0
for airportcode in airports:
# Skip NULL entries
if airportcode == "NULL":
i += 1
continue
color = COLOR_CLEAR
conditions = conditionDict.get(airportcode, None)
windy = False
highWinds = False
lightningConditions = False
if conditions != None:
windy = True if (ACTIVATE_WINDCONDITION_ANIMATION and windCycle == True and (conditions["windSpeed"] >= WIND_BLINK_THRESHOLD or conditions["windGust"] == True)) else False
highWinds = True if (windy and HIGH_WINDS_THRESHOLD != -1 and (conditions["windSpeed"] >= HIGH_WINDS_THRESHOLD or conditions["windGustSpeed"] >= HIGH_WINDS_THRESHOLD)) else False
lightningConditions = True if (ACTIVATE_LIGHTNING_ANIMATION and windCycle == False and conditions["lightning"] == True) else False
if conditions["flightCategory"] == "VFR":
color = COLOR_VFR if not (windy or lightningConditions) else COLOR_LIGHTNING if lightningConditions else COLOR_HIGH_WINDS if highWinds else (COLOR_VFR_FADE if FADE_INSTEAD_OF_BLINK else COLOR_CLEAR) if windy else COLOR_CLEAR
elif conditions["flightCategory"] == "MVFR":
color = COLOR_MVFR if not (windy or lightningConditions) else COLOR_LIGHTNING if lightningConditions else COLOR_HIGH_WINDS if highWinds else (COLOR_MVFR_FADE if FADE_INSTEAD_OF_BLINK else COLOR_CLEAR) if windy else COLOR_CLEAR
elif conditions["flightCategory"] == "IFR":
color = COLOR_IFR if not (windy or lightningConditions) else COLOR_LIGHTNING if lightningConditions else COLOR_HIGH_WINDS if highWinds else (COLOR_IFR_FADE if FADE_INSTEAD_OF_BLINK else COLOR_CLEAR) if windy else COLOR_CLEAR
elif conditions["flightCategory"] == "LIFR":
color = COLOR_LIFR if not (windy or lightningConditions) else COLOR_LIGHTNING if lightningConditions else COLOR_HIGH_WINDS if highWinds else (COLOR_LIFR_FADE if FADE_INSTEAD_OF_BLINK else COLOR_CLEAR) if windy else COLOR_CLEAR
else:
color = COLOR_CLEAR
print("Setting LED " + str(i) + " for " + airportcode + " to " + ("lightning " if lightningConditions else "") + ("very " if highWinds else "") + ("windy " if windy else "") + (conditions["flightCategory"] if conditions != None else "None") + " " + str(color))
pixels[i] = color
i += 1
# Legend
if SHOW_LEGEND:
pixels[i + OFFSET_LEGEND_BY] = COLOR_VFR
pixels[i + OFFSET_LEGEND_BY + 1] = COLOR_MVFR
pixels[i + OFFSET_LEGEND_BY + 2] = COLOR_IFR
pixels[i + OFFSET_LEGEND_BY + 3] = COLOR_LIFR
if ACTIVATE_LIGHTNING_ANIMATION == True:
pixels[i + OFFSET_LEGEND_BY + 4] = COLOR_LIGHTNING if windCycle else COLOR_VFR # lightning
if ACTIVATE_WINDCONDITION_ANIMATION == True:
pixels[i+ OFFSET_LEGEND_BY + 5] = COLOR_VFR if not windCycle else (COLOR_VFR_FADE if FADE_INSTEAD_OF_BLINK else COLOR_CLEAR) # windy
if HIGH_WINDS_THRESHOLD != -1:
pixels[i + OFFSET_LEGEND_BY + 6] = COLOR_VFR if not windCycle else COLOR_HIGH_WINDS # high winds
# Update actual LEDs all at once
pixels.show()
# Rotate through airports METAR on external display
if disp is not None:
if displayTime <= DISPLAY_ROTATION_SPEED:
displaymetar.outputMetar(disp, stationList[displayAirportCounter], conditionDict.get(stationList[displayAirportCounter], None))
displayTime += BLINK_SPEED
else:
displayTime = 0.0
displayAirportCounter = displayAirportCounter + 1 if displayAirportCounter < numAirports-1 else 0
print("showing METAR Display for " + stationList[displayAirportCounter])
# Switching between animation cycles
time.sleep(BLINK_SPEED)
windCycle = False if windCycle else True
looplimit -= 1
print()
print("Done")