This commit is contained in:
Aaron Johnson 2024-04-22 15:42:05 -05:00
commit f473855e29
6 changed files with 180 additions and 139 deletions

View File

@ -1,8 +1,8 @@
Package: noaacap
Version: 1.0.0
Version: 1.4.0
Section: hamradio
Priority: extra
Maintainer: Dan Srebnick <k2ie at k2ie.net>
Architecture: all
Depends: aprx (>=2.9) | direwolf (>=1.3), db-util, python3-lxml, python3-pip, python3-bs4, python3-tz, python3-bsddb3, python3-systemd
Depends: aprx (>=2.9) | direwolf (>=1.3), db-util, python3-lxml, python3-pip, python3-bs4, python3-tz, python3-bsddb3, python3-systemd,python3-dateutil
Description: APRS Weather Alerts beacon exec for aprx & Dire Wolf

View File

@ -1,7 +1,7 @@
[noaacap]
myTZ = America/New_York
myZone = NJC025
myZone = NJZ012
myResend = 30
adjZone1 = NJZ012
adjZone2 = NJZ020
adjZone1 = NJZ013
adjZone2 = NJZ014
Logging = 2

View File

@ -6,11 +6,12 @@
##
## See /usr/local/share/noaacap/CHANGELOG for change history
##
version = "1.0"
version = "1.4.0"
import sys
import pytz
import datetime
import dateutil.parser
import string
import requests
from bs4 import BeautifulSoup
@ -29,7 +30,7 @@ if len(sys.argv) == 2 and sys.argv[1] == '-v':
print("noaacap.py by K2IE, version " + version + "\n")
print("A weather alert beacon exec for aprx >= 2.9 and Direwolf >= 1.3")
print("Licensed under the BSD 2 Clause license")
print("Copyright 2017-2020 by Daniel L. Srebnick\n")
print("Copyright 2017-2024 by Daniel L. Srebnick\n")
sys.exit(0)
# We need this function early in execution
@ -95,7 +96,7 @@ except:
log.error("or 30 (for 1h if beacon cycle-size 2m)")
ErrExit()
url = 'https://alerts.weather.gov/cap/wwaatmget.php?x=' + myZone + '&y=0'
url = 'https://api.weather.gov/alerts/active.atom?zone=' + myZone
try:
r = requests.get(url, timeout=2)
@ -110,9 +111,15 @@ if r.status_code != 200:
soup = BeautifulSoup(r.text, 'xml')
entries = soup.find_all('entry')
count = len(entries)
log.debug("Entry count: " + str(count))
dbfile = '/dev/shm/noaaconf.db'
if count == 0:
log.info("Exiting - no events found")
if os.path.isfile(dbfile):
os.remove(dbfile)
print()
exit(0)
ppmap = '/usr/local/share/noaacap/ppmap.db'
sg = {"W":"WARN ","A":"WATCH","Y":"ADVIS","S":"STMNT","F":"4CAST",
@ -135,58 +142,16 @@ def vtecparse(value):
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
def move_entry(mylist,entry_val,entry_pos):
z_index = mylist.index(entry_val)
z_value = mylist.pop(z_index)
mylist.insert(entry_pos,z_value)
return mylist
hit = 0
for i in range(0, count):
log.debug("Processing entry: " + str(count))
if ("no active" in entries[i].title.string):
if os.path.isfile(dbfile):
os.remove(dbfile)
break
else:
log.info("Processing entry: " + str(i + 1) + " of " + str(count))
if i == 0:
alerts = db.DB()
alerts.open(dbfile, "Alerts", db.DB_HASH, db.DB_CREATE)
@ -198,28 +163,55 @@ for i in range(0, count):
updated = entries[i].updated.string
if (entries[i].status.string == "Actual"):
if entries[i].status.string == "Actual":
# Retrieve link for entry i
url = entries[i].id.string + ".cap"
try:
r = requests.get(url, timeout=2)
except requests.exceptions.Timeout:
log.error("Timeout exception requesting " + url)
ErrExit()
if r.status_code != 200:
log.error(str(r1.status_code) + " " + url)
ErrExit()
soup = BeautifulSoup(r.text, 'xml')
VTEC = ''
for j in soup.select('parameter'):
if j.find('valueName').text == 'VTEC':
VTEC = j.find('value').text
log.debug("VTEC String: " + VTEC)
# 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)
EventBegin, EventEnd = vtecparse(VTEC)
except:
log.debug("VTEC parse failed")
continue #Loop if error parsing P-VTEC
if (ProductClass != "/O"): #Loop if not operational
if ProductClass != "/O": #Loop if not operational
continue
EventEnd = "20" + EventEnd
# Is alert expired?
now = datetime.datetime.utcnow()
now = datetime.datetime.now(datetime.timezone.utc).replace(microsecond=0)
# Fix exit on exp = '000000T0000Z'
try:
exp = datetime.datetime.strptime(EventEnd,'%y%m%dT%H%MZ')
exp = dateutil.parser.parse(EventEnd)
except:
exp = now
log.debug('Now: ' + datetime.datetime.strftime(now,"%y-%m-%d %H:%M") + \
' Exp: ' + datetime.datetime.strftime(exp,"%y-%m-%d %H:%M"))
# log.debug('Time Now: ' + datetime.datetime.strftime(now,"%y-%m-%d %H:%M"))
# log.debug('Expiration: ' + datetime.datetime.strftime(exp,"%y-%m-%d %H:%M"))
if now > exp:
log.debug("Alert Expired")
continue
@ -228,11 +220,14 @@ for i in range(0, count):
id = bytes(str(Office + Phenomena + Significance + ETN), 'utf-8')
# log.debug("ID: " + id.decode('utf-8'))
# Do we have this alert?
if alerts.has_key(id):
if id in alerts:
# Is the updated time unchanged?
# log.debug('ID found in alerts. Now compare last updated time.')
if alerts[id].decode('utf-8') == updated:
log.debug(id.decode('utf-8') + " " + updated + " found")
log.debug(id.decode('utf-8') + " " + updated + " has already been sent")
# Is resend behavoir desired?
if myResend > 0:
@ -250,7 +245,7 @@ for i in range(0, count):
else:
continue
alerts[id] = updated
alerts.put(id, updated.encode('utf-8'))
effutc = aprstime(entries[i].effective.string[0:-6],myTZ)
exputc = aprstime(entries[i].expires.string[0:-6],myTZ)
@ -270,32 +265,88 @@ for i in range(0, count):
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, timeout=2)
except requests.exceptions.Timeout:
log.error("Timeout exception requesting " + url)
ErrExit()
if rs.status_code != 200:
log.error(str(rs.status_code) + " " + rs)
ErrExit()
soup2 = BeautifulSoup(rs.text, 'xml')
parms = soup2.find_all('parameter')
j = len(parms)
zcs = ''
for j in range(0, len(parms)):
if parms[j].valueName.string == 'UGC':
zcs = parms[j].value.string
log.debug('Parameters follow\n' + str(parms))
zcs_discrete = []
for j in soup.select('area'):
for k in j.select('geocode'):
if (k.find('valueName').text == 'UGC'):
addzcs = k.find('value').text
zcs_discrete.append(addzcs)
# This handles messages found to contain empty UGC list
if zcs == '':
if zcs_discrete == []:
log.debug('No UGC list in message')
continue
zcs = ''
sorted_zcs = (sorted(zcs_discrete))
# Move myZone to first position in case of truncation
move_entry(sorted_zcs,myZone,0)
# Move adjacent zones, if present, after myZone
adjZone2Index = 1
if adjZone1 != '' and adjZone1 in sorted_zcs:
adjZone2Index = 2
move_entry(sorted_zcs,adjZone1,1)
log.debug("adjZone1 found in sorted zcs and moved to index 1")
if adjZone2 != '' and adjZone2 in sorted_zcs:
move_entry(sorted_zcs,adjZone2,adjZone2Index)
log.debug("adjZone2 found in sorted zcs and moved to index " + str(adjZone2Index))
prev_item = ''
prev_prefix = ''
prev_suffix = ''
next_item = ''
# next_prefix = ''
next_suffix = ''
k = 0
for j in sorted_zcs:
k += 1
curr_item = j
try:
next_item = sorted_zcs[k]
except:
next_item = ''
if next_item != '':
# next_prefix = next_item[0:3]
next_suffix = next_item[3:6]
i_next_suffix = int(next_suffix)
curr_prefix = curr_item[0:3]
curr_suffix = curr_item[3:6]
i_curr_suffix = int(curr_suffix)
i_curr_plus1 = i_curr_suffix + 1
# Always print entire first item
if k == 1:
zcs = curr_item
# Did prefix change?
elif prev_prefix != curr_prefix:
zcs = zcs + "-" + curr_item
# Current suffix is 1 more than previous
elif i_prev_plus1 == i_curr_suffix:
if i_next_suffix != i_curr_plus1:
zcs = zcs + ">" + curr_suffix
# Current suffix is not 1 more than previous
elif i_prev_plus1 != i_curr_suffix:
zcs = zcs + "-" + curr_suffix
else:
log.error("Unexpected condition during zcs compression")
ErrExit()
# For debugging only
# print (zcs)
prev_item = curr_item
prev_prefix = curr_prefix
prev_suffix = curr_suffix
i_prev_suffix = int(prev_suffix)
i_prev_plus1 = i_prev_suffix + 1
log.debug("ZCS Value: " + zcs)
type = pp[bytes(Phenomena, 'utf-8')].decode('utf-8')
if Action == "CAN":
@ -307,47 +358,13 @@ for i in range(0, count):
message = exputc + "z," + type + "," + zcs
log.info("Msg: " + message)
# Pull discrete zone UGC values
soup3 = BeautifulSoup(rs.text, 'xml')
parms = soup3.find_all('geocode')
j = len(parms)
zcs_discrete = ''
for j in range(0, len(parms)):
if parms[j].valueName.string == 'UGC':
zcs_discrete = zcs_discrete + parms[j].value.string + "\n"
# Determine if adjacent zones are in original message prior
# to any necessary truncation.
adjZone1True = 0
adjZone2True = 0
if adjZone1 != '' and adjZone1 in zcs_discrete:
adjZone1True = 1
log.debug("adjZone1 found in alert data")
if adjZone2 != '' and adjZone2 in zcs_discrete:
adjZone2True = 1
log.debug("adjZone2 found in alert data")
# Make sure message does not exceed 67 chars. If it
# does, trim it. Also be certain that myZone is included
# included in the trimmed zcs.
# Make sure message does not exceed 67 chars. If it does, trim it.
n = 0
while len(message) > 67:
n = zcs.rfind('-')
zcs = zcs[0:n]
message = exputc + "z," + type + "," + zcs
if not myZone in parsezcs(zcs):
zcs = myZone + "-" + zcs
message = exputc + "z," + type + "," + zcs
if adjZone1True and not adjZone1 in parsezcs(zcs):
zcs = adjZone1 + "-" + zcs
message = exputc + "z," + type + "," + zcs
if adjZone2True and not adjZone2 in parsezcs(zcs):
zcs = adjZone2 + "-" + zcs
message = exputc + "z," + type + "," + zcs
if n > 0:
log.info("Truncated Msg: " + message)
@ -359,7 +376,7 @@ for i in range(0, count):
break
if (hit == 0):
if hit == 0:
print()
log.info("Exiting")

View File

@ -1,5 +1,21 @@
noaacap changelog
##
Version: 1.3.1
Release: April 2, 2024
Fix expiration date parsing issue
##
Version: 1.3
Release: TBD
Fixed truncation bug in compressed zone list
##
Version: 1.2
Release: March 27, 2024
Reintroduced compressed zones and other improvements
##
Version: 1.1
Release: March 25, 2024
Updated to use CAP 1.2 data source
##
Version: 1.0
Release: February 6, 2020
Added ability to specify up to 2 adjacent zones

View File

@ -1,6 +1,6 @@
BSD 2-Clause License
Copyright (c) 2017-2020, Daniel L. Srebnick
Copyright (c) 2017-2024, Daniel L. Srebnick
All rights reserved.
Redistribution and use in source and binary forms, with or without

View File

@ -1,3 +1,6 @@
The current release of noaacap is 1.4.0. If you are running older than this
version, please update before asking for support.
This software is written in python3 and has been tested with aprx 2.9.x.
If you are running a version < 2.9.0 you should first update aprx. The current
release of aprx may be found at http://thelifeofkenneth.com/aprx/debs/.
@ -14,7 +17,7 @@ https://github.com/K2IE/noaacap/releases
Once you are running aprx >= 2.9 or direwolf >= 1.3, install noaacap:
Install via sudo dpkg -i noaacap-1.0.deb. Then, to satisfy the dependencies,
Install via sudo dpkg -i noaacap-1.4.deb. Then, to satisfy the dependencies,
run sudo apt-get -f install.
==> APRX CONFIGURATION <==
@ -57,10 +60,14 @@ myTZ should be set to the Linux TZ value for the issuing office of your
preferred weather zone. For a complete list of timezones, you may use
the following command: timedatectl list-timezones | grep America.
myZone should be set to your county code. A list of county codes may be found
at https://alerts.weather.gov/. Only list one county. The program will then
send the alert for all affected zones. You could instead use a zone code if
you want to be more specific.
myZone should be set to your zone code. A list of zone codes may be found
at https://www.weather.gov/pimar/PubZone. Only list one zone. The program
will send the alert for all affected zones that will fit in the allocated 67
characters. You could instead use a county code if you wish. However the
associated zone may also be sent, wasting characters.
As an example, if you're located in Lancaster, PA, the map shows that you
are in zone 066. Enter PAZ066 for myZone.
myResend should be set to 0 for no message resends or to an interval of your
choosing. If noaacap beacons every 2 minutes, a value of 30 would provide
@ -82,3 +89,4 @@ weather alerts via this program.
Dan Srebnick, K2IE
02/06/2020
Updated 04/03/2024