From e1b47484f24f84c181fa96245c5bda8cebea60f5 Mon Sep 17 00:00:00 2001 From: SpudGunMan Date: Wed, 30 Jul 2025 08:34:20 -0700 Subject: [PATCH] NOAA Coastal Marine Forcast data using older but handy products with new mwx --- README.md | 4 +++ config.template | 10 ++++++- install.sh | 4 +-- mesh_bot.py | 6 +++-- modules/locationdata.py | 59 +++++++++++++++++++++++++++++++++++++++++ modules/settings.py | 4 +++ modules/system.py | 6 +++++ 7 files changed, 88 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 5ab0ba4..41e17d4 100644 --- a/README.md +++ b/README.md @@ -148,6 +148,9 @@ enabled = True lat = 48.50 lon = -123.0 UseMeteoWxAPI = True + +pzzEnabled = False # NOAA Coastal Waters Forecasts Enable NOAA Coastal Waters Forecasts (PZZ) +pzzZoneID = 132 # My Forecast Zone ID, https://www.weather.gov/marine select location and then look at the 'Forecast-by-Zone Map' and select PZZ zone ``` ### Module Settings @@ -421,6 +424,7 @@ There is no direct support for MQTT in the code, however, reports from Discord a | `valert` | Returns USGS Volcano Data | | | `wx` and `wxc` | Return local weather forecast (wxc is metric value), NOAA or Open Meteo for weather forecasting | | | `wxa` and `wxalert` | Return NOAA alerts. Short title or expanded details | | +| `mwx` | Return the NOAA Coastal Marine Forcast data | | ### Bulletin Board & Mail | Command | Description | | diff --git a/config.template b/config.template index 6bd5286..9fefad7 100644 --- a/config.template +++ b/config.template @@ -108,7 +108,8 @@ SentryChannel = 2 # holdoff time multiplied by seconds(20) of the watchdog SentryHoldoff = 9 # list of ignored nodes numbers ex: 2813308004,4258675309 -sentryIgnoreList = +sentryIgnoreList = + # HighFlying Node alert highFlyingAlert = True # Altitude in meters to trigger the alert @@ -149,6 +150,13 @@ NOAAalertCount = 2 # use Open-Meteo API for weather data not NOAA useful for non US locations UseMeteoWxAPI = False +# NOAA Coastal Waters Forecasts Enable NOAA Coastal Waters Forecasts (PZZ) +pzzEnabled = False +# My Forecast Zone ID, https://www.weather.gov/marine select location and then look at the 'Forecast-by-Zone Map' and select PZZ zone +pzzZoneID = 132 +# number of data points to return, default is 3 +pzzForecastDays = 3 + # USGS Hydrology unique identifiers, LID or USGS ID https://waterdata.usgs.gov riverListDefault = diff --git a/install.sh b/install.sh index df4fbce..4c44c2f 100755 --- a/install.sh +++ b/install.sh @@ -356,5 +356,5 @@ exit 0 # after install shenannigans -# add 'bee = True' to config.ini General section. You will likley want to clean the txt up a bit -# wget https://courses.cs.washington.edu/courses/cse163/20wi/files/lectures/L04/bee-movie.txt -O bee.txt +# add 'bee = True' to config.ini General section. +# wget https://gist.github.com/MattIPv4/045239bc27b16b2bcf7a3a9a4648c08a -O bee.txt diff --git a/mesh_bot.py b/mesh_bot.py index 63bef74..52947fd 100755 --- a/mesh_bot.py +++ b/mesh_bot.py @@ -67,6 +67,7 @@ def auto_response(message, snr, rssi, hop, pkiStatus, message_from_id, channel_n "messages": lambda: handle_messages(message, deviceID, channel_number, msg_history, publicChannel, isDM), "moon": lambda: handle_moon(message_from_id, deviceID, channel_number), "motd": lambda: handle_motd(message, message_from_id, isDM), + "mwx": lambda: handle_mwx(message_from_id, deviceID, channel_number), "ping": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number), "pinging": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number), "pong": lambda: "🏓PING!!🛜", @@ -750,6 +751,9 @@ def handle_riverFlow(message, message_from_id, deviceID): msg = get_flood_noaa(location[0], location[1], userRiver) return msg +def handle_mwx(message_from_id, deviceID, cmd): + # NOAA Coastal and Marine Weather PZZ + return get_nws_marine(zone=pzzZoneID, days=pzzForecastDays) def handle_wxc(message_from_id, deviceID, cmd): location = get_node_location(message_from_id, deviceID) @@ -1431,8 +1435,6 @@ async def start_rx(): # check if the FIPS codes are set if myStateFIPSList == ['']: logger.warning(f"System: No FIPS codes set for iPAWS Alerts") - - if emergency_responder_enabled: logger.debug(f"System: Emergency Responder Enabled on channels {emergency_responder_alert_channel} for interface {emergency_responder_alert_interface}") if volcanoAlertBroadcastEnabled: diff --git a/modules/locationdata.py b/modules/locationdata.py index 8f15f74..b32ce68 100644 --- a/modules/locationdata.py +++ b/modules/locationdata.py @@ -699,3 +699,62 @@ def get_volcano_usgs(lat=0, lon=0): # return the alerts alerts = abbreviate_noaa(alerts) return alerts + +def get_nws_marine(zone, days=3): + # forcast from NWS coastal products + marine_pzz_url = "https://tgftp.nws.noaa.gov/data/forecasts/marine/coastal/pz/pzz" + str(zone) + ".txt" + try: + marine_pzz_data = requests.get(marine_pzz_url, timeout=urlTimeoutSeconds) + if not marine_pzz_data.ok: + logger.warning("Location:Error fetching NWS Marine PZ data") + return ERROR_FETCHING_DATA + except (requests.exceptions.RequestException): + logger.warning("Location:Error fetching NWS Marine PZ data") + return ERROR_FETCHING_DATA + + marine_pzz_data = marine_pzz_data.text + #validate data + todayDate = today.strftime("%Y%m%d") + if marine_pzz_data.startswith("Expires:"): + expires = marine_pzz_data.split(";;")[0].split(":")[1] + expires_date = expires[:8] + if expires_date < todayDate: + logger.debug("Location: NWS Marine PZ data expired") + return NO_DATA_NOGPS + else: + logger.debug("Location: NWS Marine PZ data not valid") + return NO_DATA_NOGPS + + # process the marine forecast data + marine_pzz_lines = marine_pzz_data.split("\n") + marine_pzz_report = "" + day_blocks = [] + current_block = "" + in_forecast = False + + for line in marine_pzz_lines: + if line.startswith(".") and "..." in line: + in_forecast = True + if current_block: + day_blocks.append(current_block.strip()) + current_block = "" + current_block += line.strip() + " " + elif in_forecast and line.strip() != "": + current_block += line.strip() + " " + if current_block: + day_blocks.append(current_block.strip()) + + # Only keep up to pzzDays blocks + for block in day_blocks[:days]: + marine_pzz_report += block + "\n" + + # remove last newline + if marine_pzz_report.endswith("\n"): + marine_pzz_report = marine_pzz_report[:-1] + + # abbreviate the report + marine_pzz_report = abbreviate_noaa(marine_pzz_report) + if marine_pzz_report == "": + return NO_DATA_NOGPS + return marine_pzz_report + diff --git a/modules/settings.py b/modules/settings.py index e03c666..32509aa 100644 --- a/modules/settings.py +++ b/modules/settings.py @@ -251,6 +251,10 @@ try: n2yoAPIKey = config['location'].get('n2yoAPIKey', '') # default empty satListConfig = config['location'].get('satList', '25544').split(',') # default 25544 ISS riverListDefault = config['location'].get('riverList', '').split(',') # default 12061500 Skagit River + pzzEnabled = config['location'].getboolean('pzzEnabled', False) # default False + pzzZoneID = config['location'].getint('pzzZoneID', 100) # default 100, PZZ132 for Seattle area + pzzForecastDays = config['location'].getint('pzzForecastDays', 3) # default 3 days + # location alerts emergencyAlertBrodcastEnabled = config['location'].getboolean('eAlertBroadcastEnabled', False) # default False wxAlertBroadcastEnabled = config['location'].getboolean('wxAlertBroadcastEnabled', False) # default False diff --git a/modules/system.py b/modules/system.py index 45e9d19..3e58ca7 100644 --- a/modules/system.py +++ b/modules/system.py @@ -98,6 +98,12 @@ if wxAlertBroadcastEnabled or emergencyAlertBrodcastEnabled or volcanoAlertBroad # limited subset, this should be done better but eh.. trap_list = trap_list + ("wx", "wxc", "wxa", "wxalert", "ea", "ealert", "valert") help_message = help_message + ", wxalert, ealert, valert" + +# NOAA Coastal Waters Forecasts PZZ +if pzzEnabled: + from modules.locationdata import * # from the spudgunman/meshing-around repo + trap_list = trap_list + ("mwx",) + help_message = help_message + ", mwx" # BBS Configuration if bbs_enabled: