-
Notifications
You must be signed in to change notification settings - Fork 10
/
metar-display-v4.py
executable file
·1758 lines (1454 loc) · 90.3 KB
/
metar-display-v4.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
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/usr/bin/python3
#metar-display-v4.py - by Mark Harris.
# Updated to work with New FAA API: 10-2023. Thank you to user Marty for all the hardwork.
# Updated to work with Python 3.7
# Adds TAF display when rotary switch is positioned accordingly. Default data user selectable if no rotary switch is used.
# Adds MOS data display when rotary switch is positioned accordingly.
# Adds timer routine to turn off map at night (or whenever) then back on again automatically. Pushbutton to turn on temporarily
# Fixed bug where 'LGND' wasn't filtered from URL.
# Changed welcome message to stop scrolling.
# Add auto restart if config.py is saved so new settings will automatically be read by scripts
# Add IP display with welcome message if desired.
# Added internet availability check and retry if necessary. This should help when power is disrupted and board reboots before router does.
# Added Logging capabilities which is stored in /NeoSectional/logfile.log
# Added ability to display wind direction as an arrow or numbers.
# Fixed bug when a blank screen is desired and abovekts is used as well. Thanks Lance.
# Added Top 10 list for Heat Map
# Added Gusting Winds, CALM and VRB based on Lance Blank's work. Thank you Lance.
# Added ability to detect a Rotary Switch is NOT installed and react accordingly.
# Added ability to specifiy an exclusive subset of airports to display.
# Added ability to display text rotated 180 degrees, and/or reverse order of display of multiple OLED's if wired backwards
# Added fix to Sleep Timer. Thank You to Matthew G for your code to make this work.
#Displays airport ID, wind speed in kts and wind direction on an LCD or OLED display.
#Wind direction uses an arrow to display general wind direction from the 8 cardinal points on a compass.
#The settings below can be changed to display the top X number of airports or just those whose winds are above a specified speed.
#The OLED display can be inverted, and even the highest wind can be displayed in bold font.
#A welcome message can be displayed each time the FAA weather is updated. (Multi-Oleds only)
#Also, the local and zulu time can be displayed after each group of high winds have been displayed. (Multi-Oleds only)
#To be used along with metar-v4.py if an LCD or OLED display is used.
#startup.py is run at boot-up by /etc/rc.local to create 2 threads. One running this script and the other thread running metar-v4.py
#startup.py taken from; https://raspberrypi.stackexchange.com/questions/39108/how-do-i-start-two-different-python-scripts-with-rc-local
#Currently written for 16x2 LCD panel wired in 4 bit arrangement or a Single OLED Display SSD1306.SSD1306_128_64 or 128x32 with changes to text output.
#With a TCA9548A I2C Multiplexer, up to 8 OLED displays can be used and some of the features need multiple OLED's. https://www.adafruit.com/product/2717
#For info on using the TCA9548A see;
#https://buildmedia.readthedocs.org/media/pdf/adafruit-circuitpython-tca9548a/latest/adafruit-circuitpython-tca9548a.pdf
#An IC238 Light Sensor can be used to control the brightness of the OLED displays, or a potentiometer for an LCD Display.
#For more info on the sensor visit; http://www.uugear.com/portfolio/using-light-sensor-module-with-raspberry-pi/
#Important note: to insure the displayed time is correct, follow these instructions
# sudo raspi-config
# Select Internationalisation Options
# Select I2 Change Timezone
# Select your Geographical Area
# Select your nearest City
# Select Finish
# Select Yes to reboot now
#RPI GPIO Pinouts reference
###########################
# 3V3 (1) (2) 5V #
# GPIO2 (3) (4) 5V #
# GPIO3 (5) (6) GND #
# GPIO4 (7) (8) GPIO14 #
# GND (9) (10) GPIO15 #
# GPIO17 (11) (12) GPIO18 #
# GPIO27 (13) (14) GND #
# GPIO22 (15) (16) GPIO23 #
# 3V3 (17) (18) GPIO24 #
# GPIO10 (19) (20) GND #
# GPIO9 (21) (22) GPIO25 #
# GPIO11 (23) (24) GPIO8 #
# GND (25) (26) GPIO7 #
# GPIO0 (27) (28) GPIO1 #
# GPIO5 (29) (30) GND #
# GPIO6 (31) (32) GPIO12 #
# GPIO13 (33) (34) GND #
# GPIO19 (35) (36) GPIO16 #
# GPIO26 (37) (38) GPIO20 #
# GND (39) (40) GPIO21 #
###########################
#Import needed libraries
#Misc libraries
import urllib.request, urllib.error, urllib.parse
import xml.etree.ElementTree as ET
import time
import sys
import os
from os.path import getmtime
from datetime import datetime
from datetime import timedelta
from datetime import time as time_ #part of timer fix
import operator
import RPi.GPIO as GPIO
import socket
import collections
import re
import random
import logging
import logzero
from logzero import logger
import config #User settings stored in file config.py, used by other scripts
import admin
#LCD Libraries - Only needed if an LCD Display is to be used. Comment out if you would like.
#Visit; http://www.circuitbasics.com/raspberry-pi-lcd-set-up-and-programming-in-python/ and follow info for 4-bit mode.
#To install RPLCD library;
# sudo pip3 install RPLCD
import RPLCD as RPLCD
from RPLCD.gpio import CharLCD
#OLED libraries - Only needed if OLED Display(s) are to be used. Comment out if you would like.
import smbus2 #Install smbus2; sudo pip3 install smbus2
# git clone https://github.com/adafruit/Adafruit_Python_GPIO.git
# cd Adafruit_Python_GPIO
# sudo python3 setup.py install
from Adafruit_GPIO import I2C
import Adafruit_SSD1306 #sudo pip3 install Adafruit-SSD1306
from PIL import Image
from PIL import ImageDraw
from PIL import ImageFont
# Setup rotating logfile with 3 rotations, each with a maximum filesize of 1MB:
version = admin.version #Software version
loglevel = config.loglevel
loglevels = [logging.DEBUG, logging.INFO, logging.WARNING, logging.ERROR]
logzero.loglevel(loglevels[loglevel]) #Choices in order; DEBUG, INFO, WARNING, ERROR
logzero.logfile("/NeoSectional/logfile.log", maxBytes=1e6, backupCount=1)
logger.info("\n\nStartup of metar-display-v4.py Script, Version " + version)
logger.info("Log Level Set To: " + str(loglevels[loglevel]))
#****************************************************************************
#* User defined Setting Here - Make changes in config.py instead of here. *
#****************************************************************************
# Testing Socket Time-out, thanks Eric Blevins. From https://python.readthedocs.io/en/stable/howto/urllib2.html#sockets-and-layers
# timeout in seconds
timeout = 30
socket.setdefaulttimeout(timeout)
#rotate and oled wiring order
rotyesno = config.rotyesno #Rotate 180 degrees, 0 = No, 1 = Yes
oledposorder = config.oledposorder #Oled Wiring Position, 0 = Normally pos 0-7, 1 = Backwards pos 7-0
#create list of airports to exclusively display on the OLEDs
exclusive_list = config.exclusive_list #Must be in this format: ['KFLG', 'KSEZ', 'KPHX', 'KCMR', 'KINW', 'KPAN', 'KDVT', 'KGEU']
exclusive_flag = config.exclusive_flag #0 = Do not use exclusive list, 1 = only use exclusive list
#Specific Variables to default data to display if Rotary Switch is not installed.
wind_numorarrow = config.wind_numorarrow #0 = Display Wind direction using arrows, 1 = Display wind direction using numbers.
#Typically if rotary switch is not used, METAR's will be displayed exclusively. But if metar_taf = 0, then TAF's can be the default.
hour_to_display = config.time_sw0 #hour_to_display #Offset in HOURS to choose which TAF to display
metar_taf_mos = config.data_sw0 #config.metar_taf_mos #0 = Display TAF, 1 = Display METAR, 2 = Display MOS, 3 = Heat Map
toggle_sw = -1 #Set toggle_sw to an initial value that forces rotary switch to dictate data displayed.
data_sw0 = config.data_sw0 #User selectable source of data on Rotary Switch position 0. 0 = TAF, 1 = METAR, 2 = MOS
data_sw1 = config.data_sw1 #User selectable source of data on Rotary Switch position 1. 0 = TAF, 1 = METAR, 2 = MOS
data_sw2 = config.data_sw2 #User selectable source of data on Rotary Switch position 2. 0 = TAF, 1 = METAR, 2 = MOS
data_sw3 = config.data_sw3 #User selectable source of data on Rotary Switch position 3. 0 = TAF, 1 = METAR, 2 = MOS
data_sw4 = config.data_sw4 #User selectable source of data on Rotary Switch position 4. 0 = TAF, 1 = METAR, 2 = MOS
data_sw5 = config.data_sw5 #User selectable source of data on Rotary Switch position 5. 0 = TAF, 1 = METAR, 2 = MOS
data_sw6 = config.data_sw6 #User selectable source of data on Rotary Switch position 6. 0 = TAF, 1 = METAR, 2 = MOS
data_sw7 = config.data_sw7 #User selectable source of data on Rotary Switch position 7. 0 = TAF, 1 = METAR, 2 = MOS
data_sw8 = config.data_sw8 #User selectable source of data on Rotary Switch position 8. 0 = TAF, 1 = METAR, 2 = MOS
data_sw9 = config.data_sw9 #User selectable source of data on Rotary Switch position 9. 0 = TAF, 1 = METAR, 2 = MOS
data_sw10 = config.data_sw10 #User selectable source of data on Rotary Switch position 10. 0 = TAF, 1 = METAR, 2 = MOS
data_sw11 = config.data_sw11 #User selectable source of data on Rotary Switch position 11. 0 = TAF, 1 = METAR, 2 = MOS
time_sw0 = config.time_sw0 #1 = number of hours ahead to display. Time equals time period of TAF/MOS to display.
time_sw1 = config.time_sw1 #1 = number of hours ahead to display. Time equals time period of TAF/MOS to display.
time_sw2 = config.time_sw2 #1 = number of hours ahead to display. Time equals time period of TAF/MOS to display.
time_sw3 = config.time_sw3 #1 = number of hours ahead to display. Time equals time period of TAF/MOS to display.
time_sw4 = config.time_sw4 #1 = number of hours ahead to display. Time equals time period of TAF/MOS to display.
time_sw5 = config.time_sw5 #1 = number of hours ahead to display. Time equals time period of TAF/MOS to display.
time_sw6 = config.time_sw6 #1 = number of hours ahead to display. Time equals time period of TAF/MOS to display.
time_sw7 = config.time_sw7 #1 = number of hours ahead to display. Time equals time period of TAF/MOS to display.
time_sw8 = config.time_sw8 #1 = number of hours ahead to display. Time equals time period of TAF/MOS to display.
time_sw9 = config.time_sw9 #1 = number of hours ahead to display. Time equals time period of TAF/MOS to display.
time_sw10 = config.time_sw10 #1 = number of hours ahead to display. Time equals time period of TAF/MOS to display.
time_sw11 = config.time_sw11 #1 = number of hours ahead to display. Time equals time period of TAF/MOS to display.
displayIP = config.displayIP #display IP address with welcome message, 0 = No, 1 = Yes
#MOS Config settings
prob = config.prob #probability threshhold in Percent to assume reported weather will be displayed on map or not.
#Specific settings for on/off timer. Used to turn off LED's at night if desired.
#Verify Raspberry Pi is set to the correct time zone, otherwise the timer will be off.
usetimer = config.usetimer #0 = No, 1 = Yes. Turn the timer on or off with this setting
offhour = config.offhour #Use 24 hour time. Set hour to turn off display
offminutes = config.offminutes #Set minutes to turn off display
onhour = config.onhour #Use 24 hour time. Set hour to turn on display
onminutes = config.onminutes #Set minutes to on display
#Sleep Timer settings
tempsleepon = config.tempsleepon #Set number of MINUTES to turn map on temporarily during sleep mode
sleepmsg = config.sleepmsg #Display message "Sleeping". 0 = No, 1 = Yes.
#Display type to use. Both can be used but will delay before updating each display.
lcddisplay = config.lcddisplay #1 = Yes, 0 = No. Using an LCD to display the highest winds. Scripted for 64x2 LCD display use.
oledused = config.oledused #1 = Yes, 0 = No. Using a single OLED to display the highest winds and airports
#Misc Settings - Should match the values in metar-v3.py
update_interval = config.update_interval #Number of MINUTES between FAA updates - 15 minutes is a good compromise.
metar_age = config.metar_age #Metar Age in HOURS. This will pull the latest metar that has been published within the timeframe listed here.
num2display = config.num2display #number of highest wind airports to display. Can be as high as airports listed in airports file. 5 to 10 good number.
abovekts = config.abovekts #1 = Yes, 0 = No. If "Yes" then only display high winds above value stored in 'minwinds' below.
minwinds = config.max_wind_speed #Value in knots to filter high winds. if abovekts is 1 then don't display winds less than this value on LCD/OLED
#LCD Display settings
lcdpause = config.lcdpause #pause between character movements in scroll.
#OLED Display settings
numofdisplays = config.numofdisplays #Number of OLED displays being used. 1 Oled minimum. With TCA9548A I2C Multiplexer, 8 can be used.
oledpause = config.oledpause #Pause time in seconds between airport display updates
fontsize = config.fontsize #Size of font for OLED display. 24 works well with current font type
boldhiap = config.boldhiap #1 = Yes, 0 = No. Bold the text for the airport that has the highest windspeed.
blankscr = config.blankscr #1 = Yes, 0 = No. Add a blank screen between the group of airports to display.
offset = config.offset #Pixel offset for OLED text display vertically. Leave at 3 for current font type.
border = config.border #0 = no border, 1 = yes border. Either works well.
dimswitch = config.dimswitch #0 = Full Bright, 1 = Low Bright, 2 = Medium Bright, if IC238 Light Sensor is NOT used.
dimmin = config.dimmin #Set value 0-255 for the minimum brightness (0=darker display, but not off)
dimmax = config.dimmax #Set value 0-255 for the maximum brightness (bright display)
invert = config.invert #0 = normal display, 1 = inverted display, supercedes toginv. Normal = white text on black background.
toginv = config.toginv #0 = no toggle of inverted display. 1 = toggle inverted display between groups of airports
scrolldis = config.scrolldis #0 = Scroll display to left, 1 = scroll display to right
usewelcome = config.usewelcome #0 = No, 1 = Yes. Display a welcome message on the displays?
welcome = config.welcome #will display each time the FAA weather is updated.
displaytime = config.displaytime #0 = No, 1 = Yes. Display the local and Zulu Time between hi-winds display
#*********************************
#* End of User Defined Settings *
#*********************************
#misc settings that won't normally need to be changed.
fontindex = 0 #Font selected may have various versions that are indexed. 0 = Normal. Leave at 0 unless you know otherwise.
backcolor = 0 #0 = Black, background color for OLED display. Shouldn't need to change
fontcolor = 255 #255 = White, font color for OLED display. Shouldn't need to change
temp_time_flag = 0 #Set flag for next round of tempsleepon activation (temporarily turns on map when in sleep mode)
#Set general GPIO parameters
GPIO.setmode(GPIO.BCM) #set mode to BCM and use BCM pin numbering, rather than BOARD pin numbering.
GPIO.setwarnings(False)
#Set GPIO pin 4 for IC238 Light Sensor, if used.
GPIO.setup(4, GPIO.IN) #set pin 4 as input for light sensor, if one is used. If no sensor used board remains at high brightness always.
#set GPIO pin 22 to momentary push button to force FAA Weather Data update if button is used.
GPIO.setup(22, GPIO.IN, pull_up_down=GPIO.PUD_UP)
#Setup GPIO pins for rotary switch to choose between METARs, or TAFs and which hour of TAF
#Not all the pins are required to be used. If only METARS are desired, then no Rotary Switch is needed.
#A rotary switch with up to 12 poles can be installed, but as few as 2 poles will switch between METAR's and TAF's
GPIO.setup(0, GPIO.IN, pull_up_down=GPIO.PUD_UP) #set pin 0 to ground for METARS
GPIO.setup(5, GPIO.IN, pull_up_down=GPIO.PUD_UP) #set pin 5 to ground for TAF + 1 hour
GPIO.setup(6, GPIO.IN, pull_up_down=GPIO.PUD_UP) #set pin 6 to ground for TAF + 2 hours
GPIO.setup(13, GPIO.IN, pull_up_down=GPIO.PUD_UP) #set pin 13 to ground for TAF + 3 hours
GPIO.setup(19, GPIO.IN, pull_up_down=GPIO.PUD_UP) #set pin 19 to ground for TAF + 4 hours
GPIO.setup(26, GPIO.IN, pull_up_down=GPIO.PUD_UP) #set pin 26 to ground for TAF + 5 hours
GPIO.setup(21, GPIO.IN, pull_up_down=GPIO.PUD_UP) #set pin 21 to ground for TAF + 6 hours
GPIO.setup(20, GPIO.IN, pull_up_down=GPIO.PUD_UP) #set pin 20 to ground for TAF + 7 hours
GPIO.setup(16, GPIO.IN, pull_up_down=GPIO.PUD_UP) #set pin 16 to ground for TAF + 8 hours
GPIO.setup(12, GPIO.IN, pull_up_down=GPIO.PUD_UP) #set pin 12 to ground for TAF + 9 hours
GPIO.setup(1, GPIO.IN, pull_up_down=GPIO.PUD_UP) #set pin 1 to ground for TAF + 10 hours
GPIO.setup(7, GPIO.IN, pull_up_down=GPIO.PUD_UP) #set pin 7 to ground for TAF + 11 hours
# Raspberry Pi pin configuration:
RST = None #on the PiOLED this pin isnt used
#Setup Adafruit library for OLED display.
disp = Adafruit_SSD1306.SSD1306_128_64(rst=RST) #128x64 or 128x32 - disp = Adafruit_SSD1306.SSD1306_128_32(rst=RST)
TCA_ADDR = 0x70 #use cmd i2cdetect -y 1 to ensure multiplexer shows up at addr 0x70
tca = I2C.get_i2c_device(address=TCA_ADDR)
port = 1 #Default port. set to 0 for original RPi or Orange Pi, etc
bus = smbus2.SMBus(port) #From smbus2 set bus number
#Setup paths for restart on change routine. Routine from;
#https://blog.petrzemek.net/2014/03/23/restarting-a-python-script-within-itself
LOCAL_CONFIG_FILE_PATH = '/NeoSectional/config.py'
WATCHED_FILES = [LOCAL_CONFIG_FILE_PATH, __file__]
WATCHED_FILES_MTIMES = [(f, getmtime(f)) for f in WATCHED_FILES]
logger.info('Watching ' + LOCAL_CONFIG_FILE_PATH + ' For Change')
#Timer calculations - Part of Timer Fix - Thank You to Matthew G
now = datetime.now() #Get current time and compare to timer setting
lights_out = time_(offhour, offminutes, 0)
timeoff = lights_out
lights_on = time_(onhour, onminutes, 0)
end_time = lights_on
delay_time = 10 #Number of seconds to delay before retrying to connect to the internet.
temp_lights_on = 0 #Set flag for next round if sleep timer is interrupted by button push.
#MOS related settings
mos_filepath = '/NeoSectional/GFSMAV' #location of the downloaded local MOS file.
categories = ['HR', 'CLD', 'WDR', 'WSP', 'P06', 'T06', 'POZ', 'POS', 'TYP', 'CIG','VIS','OBV'] #see legend below
obv_wx = {'N': 'None', 'HZ': 'HZ','BR': 'RA','FG': 'FG','BL': 'HZ'} #Decode from MOS to TAF/METAR
typ_wx = {'S': 'SN','Z': 'FZRA','R': 'RA'} #Decode from MOS to TAF/METAR
mos_dict = collections.OrderedDict() #Outer Dictionary, keyed by airport ID
hour_dict = collections.OrderedDict() #Middle Dictionary, keyed by hour of forcast. Will contain a list of data for categories.
ap_flag = 0 #Used to determine that an airport from our airports file is currently being read.
hmdata_dict = {} #Used for top 10 list for heat map
startnum = 0 #Used for cycling through the number of displays used.
stopnum = numofdisplays #Same
stepnum = 1 #Same
#Get info to display active IP address
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.connect(("8.8.8.8", 80))
logger.info("Settings Loaded")
#Functions
# Part of Timer Fix - Thank You to Matthew G
# See if a time falls within a range
def time_in_range(start, end, x):
if start <= end:
return start <= x <= end
else:
return start <= x or x <= end
#Functions for LCD Display
def write_to_lcd(lcd, framebuffer, num_cols):
#Write the framebuffer out to the specified LCD.
lcd.home()
for row in framebuffer:
lcd.write_string(row.ljust(num_cols)[:num_cols])
lcd.write_string('\r\n')
def loop_string(string, lcd, framebuffer, row, num_cols, delay=0.4):
padding = ' ' * num_cols
s = padding + string + padding
for i in range(len(s) - num_cols + 1):
framebuffer[row] = s[i:i+num_cols]
write_to_lcd(lcd, framebuffer, num_cols)
time.sleep(delay)
#Functions for OLED display
def tca_select(channel): #Used to tell the multiplexer which oled display to send data to.
#Select an individual channel
if channel > 7 or numofdisplays < 2: #Verify we need to use the multiplexer.
return
tca.writeRaw8(1 << channel) #from Adafruit_GPIO I2C
def oledcenter(txt, ch, font, dir=0, dim=dimswitch, onoff = 0, pause = 0): #Center text vertically and horizontally
tca_select(ch) #Select the display to write to
oleddim(dim) #Set brightness, 0 = Full bright, 1 = medium bright, 2 = low brightdef oledcenter(txt): #Center text vertically and horizontally
draw.rectangle((0, 0, width-1, height-1), outline=border, fill=backcolor) #blank the display
x1, y1, x2, y2 = 0, 0, width, height #create boundaries of display
if dir == "" or txt == '\n' or 'Updated' in txt or 'Calm' in txt: #Print text other than wind directions and speeds
pass
elif 'METARs' in txt or 'TAF' in txt or 'MOS' in txt or 'Heat' in txt: #Print text other than wind directions and speeds
pass
elif wind_numorarrow == 0: #draw wind direction using arrows
arrowdir = winddir(dir) #get proper proper arrow to display
draw.text((96, 37), arrowdir, font=arrows, fill=fontcolor) #lower right of oled
txt = txt + 'kts'
pass
else: #draw wind direction using numbers
ap, wndsp = txt.split('\n')
wnddir = str(dir)
if len(wnddir) == 2: #pad direction with zeros to get 3 digits.
wnddir = '0' + wnddir
elif len(wnddir) == 1:
wnddir = '00' + wnddir
#Calm and VRB winds contributed by Lance Black - Thank you Lance
if wnddir == '000' and wndsp == '0':
txt = ap + "\n" + 'Calm'
elif wnddir == '000' and wndsp >= '1' and gust > 0:
txt = ap + "\n" + 'VRB@' + wndsp + 'g' + str(gust)
elif wnddir == '000' and wndsp >= '1' and gust == 0:
txt = ap + "\n" + 'VRB@' + wndsp + 'kts'
elif gust == 0 or gust == '' or gust == None: #Lance Blank
txt = ap + '\n' + wnddir + chr(176) + '@' + wndsp + 'kts' #'360@21kts' layout
elif gust > 0:
txt = ap + '\n' + wnddir + '@' + wndsp + 'g' + str(gust) #Lance Blank - '360@5g12' layout
else:
txt = ap + "\n" + wndsp + 'kts'
w, h = draw.textsize(txt, font=font) #get textsize of what is to be displayed
x = (x2 - x1 - w)/2 + x1 #calculate center for text
y = (y2 - y1 - h)/2 + y1 - offset
draw.text((x, y), txt, align='center', font=font, fill=fontcolor) #Draw the text to buffer
invertoled(onoff) #invert display if set
rotate180(rotyesno) #Rotate display if setrotate180
disp.image(image) #Display image
disp.display() #display text in buffer
time.sleep(pause) #pause long enough to be read
def winddir(wndir=0): #Using the arrows.ttf font return arrow to represent wind direction at airport
if (wndir >= 338 and wndir <= 360) or (wndir >= 1 and wndir <= 22): #8 arrows representing 45 degrees each around the compass.
return 'd' #wind blowing from the north (pointing down)
elif wndir >= 23 and wndir <= 67:
return 'f' #wind blowing from the north-east (pointing lower-left)
elif wndir >= 68 and wndir <= 113:
return 'b' #wind blowing from the east (pointing left)
elif wndir >= 114 and wndir <= 159:
return 'e' #wind blowing from the south-east (pointing upper-left)
elif wndir >= 160 and wndir <= 205:
return 'c' #wind blowing from the south (pointing up)
elif wndir >= 206 and wndir <= 251:
return 'g' #wind blowing from the south-west (pointing upper-right)
elif wndir >= 252 and wndir <= 297:
return 'a' #wind blowing from the west (pointing right)
elif wndir >= 298 and wndir <= 337:
return 'h' #wind blowing from the north-west (pointing lower-right)
else:
return '' #No arrow returned
def oleddim(level=0): #Dimming routine. 0 = Full Brightness, 1 = low brightness, 2 = medium brightness. See https://www.youtube.com/watch?v=hFpXfSnDNSY a$
if level == 0: #https://github.com/adafruit/Adafruit_Python_SSD1306/blob/master/Adafruit_SSD1306/SSD1306.py for more info.
disp.command(0x81) #SSD1306_SETCONTRAST = 0x81
disp.command(dimmax)
disp.command(0xDB) #SSD1306_SETVCOMDETECT = 0xDB
disp.command(dimmax)
if level == 1 or level == 2:
disp.command(0x81) #SSD1306_SETCONTRAST = 0x81
disp.command(dimmin)
if level == 1:
disp.command(0xDB) #SSD1306_SETVCOMDETECT = 0xDB
disp.command(dimmin)
def invertoled(i): #Invert display pixels. Normal = white text on black background.
if i: #Inverted = black text on white background #0 = Normal, 1 = Inverted
disp.command(0xA7) #SSD1306_INVERTDISPLAY
else:
disp.command(0xA6) #SSD1306_NORMALDISPLAY
def rotate180(i): #Rotate display 180 degrees to allow mounting of OLED upside down
if i:
#Y Direction
disp.command(0xA0)
#X Direction
disp.command(0xC0)
else:
pass
def clearoleddisplays():
for j in range(numofdisplays):
tca_select(j)
# disp.clear() #commenting this out sped up the display refresh.
draw.rectangle((0,0,width-1,height-1), outline=border, fill=backcolor)
disp.image(image)
disp.display()
#Compare current time plus offset to TAF's time period and return difference
def comp_time(taf_time):
global current_zulu
datetimeFormat = ('%Y-%m-%dT%H:%M:%SZ')
date1 = taf_time
date2 = current_zulu
diff = datetime.strptime(date1, datetimeFormat) - datetime.strptime(date2, datetimeFormat)
diff_minutes = int(diff.seconds/60)
diff_hours = int(diff_minutes/60)
return diff.seconds, diff_minutes, diff_hours, diff.days
#Used by MOS decode routine. This routine builds mos_dict nested with hours_dict
def set_data():
global hour_dict
global mos_dict
global dat0, dat1, dat2, dat3, dat4, dat5, dat6, dat7
global apid
global temp
global keys
#Clean up line of MOS data.
if len(temp) >= 0: #this check is unneeded. Put here to vary length of list to clean up.
temp1 = []
tmp_sw = 0
for val in temp: #Check each item in the list
val = val.lstrip() #remove leading white space
val = val.rstrip('/') #remove trailing /
if len(val) == 6: #this is for T06 to build appropriate length list
temp1.append('0') #add a '0' to the front of the list. T06 doesn't report data in first 3 hours.
temp1.append(val) #add back the original value taken from T06
tmp_sw = 1 #Turn on switch so we don't go through it again.
elif len(val) > 2 and tmp_sw == 0: #if item is 1 or 2 chars long, then bypass. Otherwise fix.
pos = val.find('100') #locate first 100
tmp = val[0:pos] #capture the first value which is not a 100
temp1.append(tmp) #and store it in temp list.
k = 0
for j in range(pos, len(val), 3): #now iterate through remainder
temp1.append(val[j:j+3]) #and capture all the 100's
k += 1
else:
temp1.append(val) #Store the normal values too.
temp = temp1
#load data into appropriate lists by hours designated by current MOS file
#clean up data by removing '/' and spaces
temp0 = ([x.strip() for x in temp[0].split('/')])
temp1 = ([x.strip() for x in temp[1].split('/')])
temp2 = ([x.strip() for x in temp[2].split('/')])
temp3 = ([x.strip() for x in temp[3].split('/')])
temp4 = ([x.strip() for x in temp[4].split('/')])
temp5 = ([x.strip() for x in temp[5].split('/')])
temp6 = ([x.strip() for x in temp[6].split('/')])
temp7 = ([x.strip() for x in temp[7].split('/')])
#build a list for each data group. grab 1st element [0] in list to store.
dat0.append(temp0[0])
dat1.append(temp1[0])
dat2.append(temp2[0])
dat3.append(temp3[0])
dat4.append(temp4[0])
dat5.append(temp5[0])
dat6.append(temp6[0])
dat7.append(temp7[0])
j = 0
for key in keys: #add cat data to the hour_dict by hour
if j == 0:
hour_dict[key] = dat0
elif j == 1:
hour_dict[key] = dat1
elif j == 2:
hour_dict[key] = dat2
elif j == 3:
hour_dict[key] = dat3
elif j == 4:
hour_dict[key] = dat4
elif j == 5:
hour_dict[key] = dat5
elif j == 6:
hour_dict[key] = dat6
elif j == 7:
hour_dict[key] = dat7
j += 1
mos_dict[apid] = hour_dict #marry the hour_dict to the proper key in mos_dict
##########################
# Start of executed code #
##########################
while True:
logger.info('Start of metar-display-v4.py executed code main loop')
#Time calculations, dependent on 'hour_to_display' offset. this determines how far in the future the TAF data should be.
#This time is recalculated everytime the FAA data gets updated
zulu = datetime.utcnow() + timedelta(hours=hour_to_display) #Get current time plus Offset
current_zulu = zulu.strftime('%Y-%m-%dT%H:%M:%SZ') #Format time to match whats reported in TAF
current_hr_zulu = zulu.strftime('%H') #Zulu time formated for just the hour, to compare to MOS data
logger.debug('datetime - ' + str(datetime.utcnow()))
logger.debug('zulu - ' + str(zulu))
logger.debug('hour_to_display - ' + str(hour_to_display))
logger.debug('current_zulu - ' + str(current_zulu))
#Get current date and time
now = datetime.now()
dt_string = now.strftime("%I:%M%p") #12:00PM format
#Dictionary definitions. Need to reset whenever new weather is received
stationiddict = {} #hold the airport identifiers
windsdict = {} #holds the wind speeds by identifier
wnddirdict = {} #holds the wind direction by identifier
wxstringdict = {} #holds the weather conditions by identifier
wndgustdict = {} #hold wind gust by identifier - Mez
#read airports file - read each time weather is updated in case a change to "airports" file was made while script was running.
try:
with open("/NeoSectional/airports") as f:
airports = f.readlines()
except IOError as error:
logger.error('Airports file could not be loaded.')
logger.error(error)
break
airports = [x.strip() for x in airports]
logger.info("Airports File Loaded")
#read hmdata file and display the top 10 airports on the OLEDs
try:
with open("/NeoSectional/hmdata") as f:
hmdata = f.readlines()
except IOError as error:
logger.error('Heat Map file could not be loaded.')
logger.error(error)
break
hmdata = [x.strip() for x in hmdata]
logger.info("Heat Map File Loaded")
for line in hmdata:
hmap, numland = line.split()
hmdata_dict[hmap] = int(numland)
hmdata_sorted = sorted(hmdata_dict.items(), key=lambda x:x[1], reverse=True)
hmdata_sorted.insert(0, 'Top AP\nLandings')
print(hmdata_sorted)
#depending on what data is to be displayed, either use an URL for METARs and TAFs or read file from drive (pass).
if metar_taf_mos == 1: #Check to see if the script should display TAF data (0) or METAR data (1)
#Define URL to get weather METARS. If no METAR reported withing the last 2.5 hours, Airport LED will be white (nowx).
#url = "https://aviationweather-cprk.ncep.noaa.gov/adds/dataserver_current/httpparam?dataSource=metars&requestType=retrieve&format=xml&mostRecentForEachStation=constraint&hoursBeforeNow="+str(metar_age)+"&stationString="
url = "https://aviationweather.gov/api/data/metar?format=xml&hours=" +str(metar_age)+ "&ids="
logger.info("METAR Data Loading")
elif metar_taf_mos == 0: #TAF data
#Define URL to get weather URL for TAF. If no TAF reported for an airport, the Airport LED will be white (nowx).
#url = "https://aviationweather-cprk.ncep.noaa.gov/adds/dataserver_current/httpparam?dataSource=tafs&requestType=retrieve&format=xml&mostRecentForEachStation=constraint&hoursBeforeNow="+str(metar_age)+"&stationString="
url = "https://aviationweather.gov/api/data/taf?format=xml&hours=" +str(metar_age)+ "&ids="
logger.info("TAF Data Loading")
elif metar_taf_mos == 2: #MOS data. This is not accessible in the same way as METARs and TAF's.
pass #This elif is not strictly needed and is only here for clarity
logger.info("MOS Data Loading")
elif metar_taf_mos == 3: #Heat Map data.
pass #This elif is not strictly needed and is only here for clarity
logger.info("Heat Map Data Loading")
#Build URL to submit to FAA with the proper airports from the airports file
if metar_taf_mos != 2 and metar_taf_mos != 3:
for airportcode in airports:
if airportcode == "NULL" or airportcode == "LGND":
continue
url = url + airportcode + ","
url = url[:-1] #strip trailing comma from string
logger.debug(url)
while True: #check internet availability and retry if necessary. Power outage, map may boot quicker than router.
try:
content = urllib.request.urlopen(url)
logger.info('Internet Available')
logger.info(url)
break
except:
logger.warning('FAA Data is Not Available')
logger.info(url)
time.sleep(delay_time)
pass
root = ET.fromstring(content.read()) #Process XML data returned from FAA
#MOS decode routine
#MOS data is downloaded daily from; https://www.weather.gov/mdl/mos_gfsmos_mav to the local drive by crontab scheduling.
#Then this routine reads through the entire file looking for those airports that are in the airports file. If airport is
#found, the data needed to display the weather for the next 24 hours is captured into mos_dict, which is nested with
#hour_dict, which holds the airport's MOS data by 3 hour chunks. See; https://www.weather.gov/mdl/mos_gfsmos_mavcard for
#a breakdown of what the MOS data looks like and what each line represents.
if metar_taf_mos == 2:
#Read current MOS text file
try:
file = open(mos_filepath, 'r')
lines = file.readlines()
except IOError as error:
logger.error('MOS data file could not be loaded.')
logger.error(error)
break
for line in lines: #read the MOS data file line by line0
line = str(line)
#Ignore blank lines of MOS airport
if line.startswith(' '):
ap_flag = 0
continue
#Check for and grab date of MOS
if 'DT /' in line:
unused, dt_cat, month, unused, unused, day, unused = line.split(" ",6)
continue
#Check for and grab the Airport ID of the current MOS
if 'MOS' in line:
unused, apid, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, updt1, updt2, v13 = line.split(" ", 14)
mos_updt_time = updt1 + ' ' + updt2 #Grab the MOS report's update timestamp
dt_string = mos_updt_time
#If this Airport ID is in the airports file then grab all the info needed from this MOS
if apid in airports:
ap_flag = 1
cat_counter = 0 #used to determine if a category is being reported in MOS or not. If not, need to inject i$
dat0, dat1, dat2, dat3, dat4, dat5, dat6, dat7 = ([] for i in range(8)) #Clear lists
continue
#If we just found an airport that is in our airports file, then grab the appropriate weather data from it's MOS
if ap_flag:
xtra, cat, value = line.split(" ",2) #capture the category the line read represents
#Check if the needed categories are being read and if so, grab its data
if cat in categories:
cat_counter += 1 #used to check if a category is not in mos report for airport
if cat == 'HR': #hour designation
temp = (re.findall(r'\s?(\s*\S+)', value.rstrip())) #grab all the hours from line read
for j in range(8):
tmp = temp[j].strip()
hour_dict[tmp] = '' #create hour dictionary based on mos data
keys = list(hour_dict.keys()) #Get the hours which are the keys in this dict, so they can be prope$
else:
#Checking for missing lines of data and x out if necessary.
if (cat_counter == 5 and cat != 'P06')\
or (cat_counter == 6 and cat != 'T06')\
or (cat_counter == 7 and cat != 'POZ')\
or (cat_counter == 8 and cat != 'POS')\
or (cat_counter == 9 and cat != 'TYP'):
#calculate the number of consecutive missing cats and inject 9's into those positions
a = categories.index(last_cat)+1
b = categories.index(cat)+1
c = b - a - 1
logger.debug(apid,last_cat,cat,a,b,c)
for j in range(c):
temp = ['9', '9', '9', '9', '9', '9', '9', '9', '9', '9', '9', '9', '9', '9', '9', '9', '9', '9']
set_data()
cat_counter += 1
#Now write the orignal cat data read from the line in the mos file
cat_counter += 1
hour_dict = collections.OrderedDict() #clear out hour_dict for next airport
last_cat = cat
temp = (re.findall(r'\s?(\s*\S+)', value.rstrip())) #add the actual line of data read
set_data()
hour_dict = collections.OrderedDict() #clear out hour_dict for next airport
else:
#continue to decode the next category data that was read.
last_cat = cat #store what the last read cat was.
temp = (re.findall(r'\s?(\s*\S+)', value.rstrip()))
set_data()
hour_dict = collections.OrderedDict() #clear out hour_dict for next airport
#Now grab the data needed to display on map. Key: [airport][hr][j] - using nested dictionaries
# airport = from airport file, 4 character ID. hr = 1 of 8 three-hour periods of time, 00 03 06 09 12 15 18 21
# j = index to weather categories, in this order; 'CLD','WDR','WSP','P06', 'T06', 'POZ', 'POS', 'TYP','CIG','VIS','OBV'.
# See; https://www.weather.gov/mdl/mos_gfsmos_mavcard for description of available data.
for airport in airports:
if airport in mos_dict:
logger.debug('\n' + airport)
logger.debug(categories)
mos_time = int(current_hr_zulu) + hour_to_display
if mos_time >= 24: #check for reset at 00z
mos_time = mos_time - 24
logger.debug(keys)
for hr in keys:
logger.debug(hr + ", " + str(mos_time) + ", " + str(int(hr)+2.99))
if int(hr) <= mos_time <= int(hr)+2.99:
cld = (mos_dict[airport][hr][0])
wdr = (mos_dict[airport][hr][1]) +'0' #make wind direction end in zero
wsp = (mos_dict[airport][hr][2])
p06 = (mos_dict[airport][hr][3])
t06 = (mos_dict[airport][hr][4])
poz = (mos_dict[airport][hr][5])
pos = (mos_dict[airport][hr][6])
typ = (mos_dict[airport][hr][7])
cig = (mos_dict[airport][hr][8])
vis = (mos_dict[airport][hr][9])
obv = (mos_dict[airport][hr][10])
logger.debug(hr+", "+cld+", "+wdr+", "+wsp+", "+p06+", "+t06+", "+poz+", "+pos+", "+typ+", "+cig+", "+vis+", "+obv) #debug
#decode the weather for each airport to display on the livesectional map
flightcategory = "VFR" #start with VFR as the assumption
if cld in ("OV","BK"): #If the layer is OVC, BKN, set Flight category based on height of layer
if cig <= '2': #AGL is less than 500:
flightcategory = "LIFR"
elif cig == '3': #AGL is between 500 and 1000
flightcategory = "IFR"
elif '4' <= cig <= '5': #AGL is between 1000 and 3000:
flightcategory = "MVFR"
elif cig >= '6': #AGL is above 3000
flightcategory = "VFR"
#Check visability too.
if flightcategory != "LIFR": #if it's LIFR due to cloud layer, no reason to check any other things$
if vis <= '2': #vis < 1.0 mile:
flightcategory = "LIFR"
elif '3' <= vis < '4': #1.0 <= vis < 3.0 miles:
flightcategory = "IFR"
elif vis == '5' and flightcategory != "IFR": #3.0 <= vis <= 5.0 miles
flightcategory = "MVFR"
logger.debug(flightcategory + " |"),
logger.debug('Windspeed = ' + wsp + ' | Wind dir = ' + wdr + ' |'),
#decode reported weather using probabilities provided.
if typ == '9': #check to see if rain, freezing rain or snow is reported. If not use obv weather
wx = obv_wx[obv] #Get proper representation for obv designator
else:
wx = typ_wx[typ] #Get proper representation for typ designator
if wx == 'RA' and int(p06) < prob:
if obv != 'N':
wx = obv_wx[obv]
else:
wx = 'NONE'
if wx == 'SN' and int(pos) < prob:
wx = 'NONE'
if wx == 'FZRA' and int(poz) < prob:
wx = 'NONE'
if t06 == '' or t06 is None:
t06 = '0'
if int(t06) > prob: #check for thunderstorms
wx = 'TSRA'
else:
wx = 'NONE'
logger.debug('Reported Weather = ' + wx)
#Connect the information from MOS to the board
stationId = airport
#grab wind speeds from returned MOS data
if wsp == None: #if wind speed is blank, then bypass
windspeedkt = 0
elif wsp == '99': #Check to see if the MOS data didn't report a windspeed for this airport
windspeedkt = 0
else:
windspeedkt = int(wsp)
#grab wind direction from returned FAA data
if wdr == None: #if wind direction is blank, then bypass
winddirdegree = 0
else:
winddirdegree = int(wdr)
#grab Weather info from returned FAA data
if wx is None: #if weather string is blank, then bypass
wxstring = "NONE"
else:
wxstring = wx
logger.debug(stationId+ ", " + str(windspeedkt) + ", " + wxstring)
#Check for duplicate airport identifier and skip if found, otherwise store in dictionary. covers for dups in "airp$
if stationId in stationiddict:
logger.info(stationId + " Duplicate, only saved first metar category")
else:
stationiddict[stationId] = flightcategory #build category dictionary
if stationId in windsdict:
logger.info(stationId + " Duplicate, only saved first metar category")
else:
windsdict[stationId] = windspeedkt #build windspeed dictionary
if stationId in wnddirdict:
logger.info(stationId + " Duplicate, only saved first metar category")
else:
wnddirdict[stationId] = winddirdegree #build wind direction dictionary
if stationId in wxstringdict:
logger.info(stationId + " Duplicate, only saved first metar category")
else:
wxstringdict[stationId] = wxstring #build weather dictionary
logger.info("Decoded MOS Data for Display")
#TAF decode routine. This routine will decode the TAF, pick the appropriate time frame to display.
if metar_taf_mos == 0: #0 equals display TAF.
#start of TAF decoding routine
for data in root.iter('data'):
num_results = data.attrib['num_results'] #get number of airports reporting TAFs to be used for diagnosis only
logger.debug("\nNum of Airport TAFs = " + num_results)
for taf in root.iter('TAF'): #iterate through each airport's TAF
stationId = taf.find('station_id').text
logger.debug(stationId)
logger.debug('Current+Offset Zulu - ' + current_zulu)
taf_wx_string = ""
taf_change_indicator = ""
taf_wind_dir_degrees = ""
taf_wind_speed_kt = ""
taf_wind_gust_kt = ""
for forecast in taf.findall('forecast'): #Now look at the forecasts for the airport
# Routine inspired by Nick Cirincione.
flightcategory = "VFR" #intialize flight category
taf_time_from = forecast.find('fcst_time_from').text #get taf's from time
taf_time_to = forecast.find('fcst_time_to').text #get taf's to time
if forecast.find('wx_string') is not None:
taf_wx_string = forecast.find('wx_string').text #get weather conditions
if forecast.find('change_indicator') is not None:
taf_change_indicator = forecast.find('change_indicator').text #get change indicator
if forecast.find('wind_dir_degrees') is not None:
taf_wind_dir_degrees = forecast.find('wind_dir_degrees').text #get wind direction
if forecast.find('wind_speed_kt') is not None:
taf_wind_speed_kt = forecast.find('wind_speed_kt').text #get wind speed
if forecast.find('wind_gust_kt') is not None:
taf_wind_gust_kt = forecast.find('wind_gust_kt').text #get wind gust speed
if taf_time_from <= current_zulu <= taf_time_to: #test if current time plus offset falls within taf's timeframe
logger.debug('TAF FROM - ' + taf_time_from)
logger.debug(comp_time(taf_time_from))
logger.debug('TAF TO - ' + taf_time_to)
logger.debug(comp_time(taf_time_to))
#There can be multiple layers of clouds in each taf, but they are always listed lowest AGL first.
#Check the lowest (first) layer and see if it's overcast, broken, or obscured. If it is, then compare to cloud bas$
#This algorithm basically sets the flight category based on the lowest OVC, BKN or OVX layer.
for sky_condition in forecast.findall('sky_condition'): #for each sky_condition from the XML
sky_cvr = sky_condition.attrib['sky_cover'] #get the sky cover (BKN, OVC, SCT, etc)
logger.debug(sky_cvr)
if sky_cvr in ("OVC","BKN","OVX"): #If the layer is OVC, BKN or OVX, set Flight category based on height A$
try:
cld_base_ft_agl = sky_condition.attrib['cloud_base_ft_agl'] #get cloud base AGL from XML
logger.debug(cld_base_ft_agl) #debug
except:
cld_base_ft_agl = forecast.find('vert_vis_ft').text #get cloud base AGL from XML
# cld_base_ft_agl = sky_condition.attrib['cloud_base_ft_agl'] #get cloud base AGL from XML
# logger.debug(cld_base_ft_agl)
cld_base_ft_agl = int(cld_base_ft_agl)
if cld_base_ft_agl < 500:
flightcategory = "LIFR"
break
elif 500 <= cld_base_ft_agl < 1000:
flightcategory = "IFR"
break
elif 1000 <= cld_base_ft_agl <= 3000:
flightcategory = "MVFR"
break
elif cld_base_ft_agl > 3000:
flightcategory = "VFR"
break
#visibilty can also set flight category. If the clouds haven't set the fltcat to LIFR. See if visibility will
if flightcategory != "LIFR": #if it's LIFR due to cloud layer, no reason to check any other things that can set fl$
if forecast.find('visibility_statute_mi') is not None: #check XML if visibility value exists
visibility_statute_mi = forecast.find('visibility_statute_mi').text #get visibility number
visibility_statute_mi = float(visibility_statute_mi.strip('+'))
print (visibility_statute_mi)
if visibility_statute_mi < 1.0:
flightcategory = "LIFR"
elif 1.0 <= visibility_statute_mi < 3.0:
flightcategory = "IFR"
elif 3.0 <= visibility_statute_mi <= 5.0 and flightcategory != "IFR": #if Flight Category was already
flightcategory = "MVFR"
#Print out TAF data to screen for debugging only
logger.debug('Airport - ' + stationId)
logger.debug('Flight Category - ' + flightcategory)
logger.debug('Wind Speed - ' + taf_wind_speed_kt)
logger.debug('WX String - ' + taf_wx_string)
logger.debug('Change Indicator - ' + taf_change_indicator)
logger.debug('Wind Director Degrees - ' + taf_wind_dir_degrees)
logger.debug('Wind Gust - ' + taf_wind_gust_kt)
#grab flightcategory from returned FAA data
if flightcategory is None: #if wind speed is blank, then bypass
flightcategory = None
#grab wind speeds from returned FAA data
if taf_wind_speed_kt is None: #if wind speed is blank, then bypass
windspeedkt = 0
else:
windspeedkt = int(taf_wind_speed_kt)
#grab wind gust from returned FAA data - Lance Blank
if taf_wind_gust_kt is None or taf_wind_gust_kt == '': #if wind speed is blank, then bypass
windgustkt = 0
else:
windgustkt = int(taf_wind_gust_kt)