213 lines
5.5 KiB
Python
Executable File
213 lines
5.5 KiB
Python
Executable File
#!/usr/bin/python
|
|
|
|
## Author: Dan Srebnick, K2DLS
|
|
##
|
|
## Version: 0.4
|
|
## Release: August 24, 2017
|
|
## Switched to compressed zcs to improve capacity
|
|
## Improved messsage + \n length check
|
|
##
|
|
## Version: 0.3
|
|
## Release: August 22, 2017
|
|
## Changes: Implement NWS-CANCL messages, implement DB id consisting of
|
|
## Office + Phenomena + Significance + ETN
|
|
##
|
|
## Version: 0.2
|
|
## Release: August 19, 2017
|
|
## Changes: Make sure message + \n does not exceed 67 chars
|
|
##
|
|
## Version: 0.1
|
|
## Release: August 7, 2017
|
|
##
|
|
## License: BSD-2-Clause (/usr/local/share/noaacap/license.txt)
|
|
##
|
|
## This program is called from aprx.
|
|
##
|
|
|
|
import pytz, datetime
|
|
import string
|
|
import requests
|
|
from bs4 import BeautifulSoup
|
|
import bsddb
|
|
import os
|
|
import ConfigParser
|
|
import re
|
|
|
|
config = ConfigParser.ConfigParser()
|
|
|
|
try:
|
|
os.path.isfile('/etc/noaacap.conf')
|
|
except:
|
|
exit("\nError: /etc/noaacap.conf not found\n")
|
|
else:
|
|
config.read('/etc/noaacap.conf')
|
|
|
|
myTZ = config.get('noaacap', 'myTZ')
|
|
myZone = config.get('noaacap', 'myZone')
|
|
|
|
def aprstime(timestr,TZ):
|
|
local = pytz.timezone (TZ)
|
|
naive = datetime.datetime.strptime(timestr, "%Y-%m-%dT%H:%M:%S")
|
|
local_dt = local.localize(naive, is_dst=None)
|
|
utc_dt = local_dt.astimezone (pytz.utc)
|
|
return utc_dt.strftime ("%d%H%M")
|
|
|
|
def vtecparse(value):
|
|
line = value.split("\n")[0]
|
|
ProductClass, Action, Office, Phenomena, Significance, ETN, DTGroup = \
|
|
line.split('.')
|
|
EventBegin, EventEnd = DTGroup.split('-')
|
|
return ProductClass, Action, Office, Phenomena, Significance, ETN, \
|
|
EventBegin, EventEnd
|
|
|
|
def capchildren(children):
|
|
k = None
|
|
list = {}
|
|
for tag in children:
|
|
if tag.name == 'valueName':
|
|
k = tag.string
|
|
elif tag.name == 'value':
|
|
list[k] = tag.string
|
|
return list
|
|
|
|
def parsezcs(zcs):
|
|
zonelist = set()
|
|
inprogress = 0
|
|
newprefix = 1
|
|
for z in range(0, len(zcs)):
|
|
if not inprogress:
|
|
zone = ''
|
|
inprogress = 1
|
|
if re.match('[A-Z]',zcs[z]):
|
|
if newprefix == 1:
|
|
prefix = ''
|
|
newprefix = 0
|
|
prefix = prefix + zcs[z]
|
|
if re.match('[0-9]',zcs[z]):
|
|
zone = zone + zcs[z]
|
|
if zcs[z] == '>':
|
|
zonelist.add(prefix + zone)
|
|
inprogress = 0
|
|
endzone = zcs[z+1:z+4]
|
|
z = z + 3
|
|
startzone = int(zone) + 1
|
|
endzone = int(endzone)
|
|
for y in range(startzone, endzone):
|
|
zonelist.add(prefix + str(y).zfill(3))
|
|
elif zcs[z] == '-':
|
|
zonelist.add(prefix + zone)
|
|
inprogress = 0
|
|
if re.match('[A-Z]',zcs[z+1]):
|
|
newprefix = 1
|
|
if inprogress:
|
|
zonelist.add(prefix + zone)
|
|
return zonelist
|
|
|
|
sg = {"W":"WARN ","A":"WATCH","Y":"ADVIS","S":"STMNT","F":"4CAST",
|
|
"O":"OUTLK","N":"SYNOP"}
|
|
|
|
url = 'https://alerts.weather.gov/cap/wwaatmget.php?x=' + myZone + '&y=0'
|
|
try:
|
|
r = requests.get(url)
|
|
except:
|
|
exit('\n')
|
|
|
|
soup = BeautifulSoup(r.text, 'xml')
|
|
entries = soup.find_all('entry')
|
|
count = len(entries)
|
|
|
|
dbfile = '/dev/shm/noaacap.db'
|
|
ppmap = '/usr/local/share/noaacap/ppmap.db'
|
|
|
|
hit = 0
|
|
for i in range(0, count):
|
|
|
|
if ("no active" in entries[i].title.string):
|
|
if os.path.isfile(dbfile):
|
|
os.remove(dbfile)
|
|
break
|
|
else:
|
|
if i == 0:
|
|
alerts = bsddb.hashopen(dbfile,'c')
|
|
pp = bsddb.hashopen(ppmap,'r')
|
|
|
|
updated = str(entries[i].updated.string)
|
|
|
|
if (entries[i].status.string == "Actual"):
|
|
|
|
# Parse P-VTEC string and make sure this is an operational ProductClass
|
|
VTEC = capchildren(entries[i].parameter.children)
|
|
try:
|
|
ProductClass, Action, Office, Phenomena, Significance, ETN, \
|
|
EventBegin, EventEnd = vtecparse(VTEC['VTEC'].string)
|
|
if (ProductClass <> "/O"): #Loop if not operational
|
|
continue
|
|
except:
|
|
continue #Loop if error parsing P-VTEC
|
|
|
|
id = str(Office + Phenomena + Significance + ETN)
|
|
|
|
if alerts.has_key(id):
|
|
if alerts[id] == updated:
|
|
continue
|
|
|
|
alerts[id] = updated
|
|
|
|
effutc = aprstime(entries[i].effective.string[0:-6],myTZ)
|
|
exputc = aprstime(entries[i].expires.string[0:-6],myTZ)
|
|
|
|
# Compress effective time to 3 byte
|
|
|
|
dd = int(effutc[0:2])
|
|
hh = int(effutc[2:4])
|
|
mm = int(effutc[4:6])
|
|
|
|
t = ''
|
|
for s in (dd, hh, mm):
|
|
if 0 <= s <= 9:
|
|
t = t + chr(s+48)
|
|
elif 10 <= s <= 35:
|
|
t = t + chr(s+55)
|
|
elif 36 <= s <= 61:
|
|
t = t + chr(s+61)
|
|
|
|
# Pull specific alert to get compressed UGC zones
|
|
try:
|
|
rs = requests.get(entries[i].id.string)
|
|
except:
|
|
exit('\n')
|
|
|
|
soup2 = BeautifulSoup(rs.text, 'xml')
|
|
parms = soup2.find_all('parameter')
|
|
j = len(parms)
|
|
for j in range(0, len(parms)):
|
|
if parms[j].valueName.string == 'UGC':
|
|
zcs = parms[j].value.string
|
|
|
|
type = pp[str(Phenomena)]
|
|
event = sg[Significance]
|
|
|
|
if Action == "CAN":
|
|
event = "CANCL"
|
|
|
|
hit = 1
|
|
suffix = exputc + "z," + type + "," + zcs + "{" + t + "00"
|
|
|
|
# Make sure message does not exceed 67 chars (66 + newline)
|
|
# If it does, trim it. Also be certain that myZone
|
|
# is included in the trimmed zcs.
|
|
while len(suffix) > 66:
|
|
if not myZone in parsezcs(zcs):
|
|
zcs = myZone + "-" + zcs
|
|
suffix = exputc + "z," + type + "," + zcs + "{" + t + "00"
|
|
n = zcs.rfind('-')
|
|
zcs = zcs[0:n]
|
|
suffix = exputc + "z," + type + "," + zcs + "{" + t + "00"
|
|
|
|
print ":NWS_" + event + ":" + suffix
|
|
|
|
break
|
|
|
|
if (hit == 0):
|
|
print
|