August 2025 Update

This commit is contained in:
oasis6212 2025-08-03 18:55:46 -04:00 committed by GitHub
parent 403bcfab99
commit dd87bb2802
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 476 additions and 222 deletions

View File

@ -3,6 +3,9 @@
![Meshbot](img/meshbot_weather.png)
![](img/1.png)
@ -30,6 +33,9 @@
![](img/menu.png)
![](img/windnew.png)
MeshBot Weather is a spinoff of [MeshBot](https://github.com/868meshbot/meshbot) with a detailed focus on weather. Designed to run on a computer or a
Raspberry Pi with a connected Meshtastic device.
@ -56,11 +62,19 @@ Our Mission:
- Forecasts are generated for any location. Not limited to towns or cities.
- Optional firewall, when enabled, the bot will only respond to messages from nodes that have been included in its whitelist.
![](img/newfeatures.png)
## Bot interaction
Your bot will be accessible through the meshtastic mesh network through the node name. DM the bot/node and issue any of the following commands:
- ? : receive a message with a menu of all weather commands.
NOTE: Commands are not case-sensitive.
- ? or menu : receive a message with a menu of all weather commands.
- hourly : 24 hour hourly forecast with temp, rain chance, and sky conditions in emoji form (Multi message return)
- 5day : 5 day detailed forecast (Multi message return)
- 7day : 7 day forecast in emoji form (Multi message return)
@ -68,13 +82,15 @@ Your bot will be accessible through the meshtastic mesh network through the node
- 2day : Today and tomorrow's detailed forecast (Single message return)
- rain : Rain chance every hour for the next 24 hours (Single message return)
- temp : Predicted temperature every hour for the next 24 hours (Single message return)
- wind : Hourly wind information for next 24 hours (Multi message return)
Commands below are not listed in the help menu:
- alert : Get full alert info for the last-issued alert.
- alert-status : Runs a check on the alert system. Returns ok if good or error code if an issue is found
- test : bot will return an acknowledgement of message received
- advertise : When received, the bot will send out a message on the public channel introducing itself along with its menu
command.
- advertise : When received, the bot will message the public channel introducing itself along with its menu command.
## Requirements, Set these up first before installing the program
@ -185,8 +201,10 @@ USER_AGENT_EMAIL: "contact@example.com"
ALERT_LAT: "34.0522"
ALERT_LON: "-118.2433"
ALERT_CHECK_INTERVAL: 300
FIRST_MESSAGE_DELAY: 3
MESSAGE_DELAY: 15
ALERT_INCLUDE_DESCRIPTION: false
ENABLE_FULL_ALERT_COMMAND: true
ENABLE_7DAY_FORECAST: true
ENABLE_5DAY_FORECAST: true
ENABLE_HOURLY_WEATHER: true
@ -231,6 +249,11 @@ want alerts for. Make sure you only go up to 4 places past the decimal point on
From what I have gathered, they allow up to once a minute for alert checking. Your milage may very.
- FIRST_MESSAGE_DELAY: 3 # Delay in seconds between receiving a request and sending the first message back. This is
experimental. Hoping this may help with dropped 1st part of reply's, by giving the network a few seconds to settle down.
feel free to experiment with different values.
- MESSAGE_DELAY: Delay in seconds between split messages. To short of a delay can cause messages to arrive out of order.
@ -239,6 +262,10 @@ such as every county, town, and area affected. You can expect about 4 or 5 messa
a single message when set to false.
- ENABLE_FULL_ALERT_COMMAND: #set to false to disable the "alert" command. Can produce up to 8 messages, may want to
disable on a high traffic mesh.
- ENABLE_7DAY_FORECAST: ENABLE_5DAY_FORECAST: ENABLE_HOURLY_WEATHER: # These calls produce 2 to 4 messages each. If you
are on a high-traffic mesh, you may want to disable these.
@ -285,6 +312,14 @@ For the alert settings in the settings.yaml file, enter your gps coordinates or
earlier in this process. Use no more than four digits after the decimal point.
## Closing the program
Press "Ctrl + c" once to tell the program to close. The program will command the node to shutdown and give it time to
do so.
Letting the node complete its shutdown sequence will prevent data loss in the node itself.
Pressing "Ctrl + c" twice will force a hard shutdown of the program and connected node.
## API Handaling details

BIN
img/Test.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

BIN
img/newfeatures.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 322 KiB

BIN
img/wind.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 212 KiB

BIN
img/windnew.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 244 KiB

View File

@ -2,12 +2,11 @@
# -*- coding: utf-8 -*-
"""
Weather Bot
Meshbot Weather
=======================
meshbot.py: A message bot designed for Meshtastic, providing information from modules upon request:
* weather forcast
* weather Alerts
Author:
- Andy
@ -36,13 +35,17 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"""
import argparse
import logging
import threading
import time
import yaml
import datetime
import random
import random
import signal
import sys
try:
import meshtastic.serial_interface
@ -65,19 +68,21 @@ from modules.weather_data_manager import WeatherDataManager
from modules.weather_alert_monitor import WeatherAlerts
from modules.forecast_4day import Forecast4DayFetcher
from modules.forecast_7day import Forecast7DayFetcher
from modules.wind_24hour import Wind24HourFetcher
UNRECOGNIZED_MESSAGES = [
"Oops! I didn't recognize that command. Type '?' to see a list of options.",
"I'm not sure what you mean. Type '?' for available commands.",
"That command isn't in my vocabulary. Send '?' to see what I understand.",
"Hmm, I don't know that one. Send '?' for a list of commands I know.",
"Sorry, I didn't catch that. Send '?' to see what commands you can use.",
"Well that's definitely not in my programming. Type '?' before we both crash.",
"Oh sure, just make up commands. Type '?' for the real ones."
"Oops! I didn't recognize that command. Type 'menu' to see a list of options.",
"I'm not sure what you mean. Type 'menu' for available commands.",
"That command isn't in my vocabulary. Send 'menu' to see what I understand.",
"Hmm, I don't know that one. Send 'menu' for a list of commands I know.",
"Sorry, I didn't catch that. Send 'menu' to see what commands you can use.",
"Well that's definitely not in my programming. Type 'menu' before we both crash.",
"Oh sure, just make up commands. Type 'menu' for the real ones."
]
interface = None
def find_serial_ports():
ports = [port.device for port in serial.tools.list_ports.comports()]
filtered_ports = [
@ -97,6 +102,7 @@ MYNODES = ""
DM_MODE = ""
FIREWALL = ""
DUTYCYCLE = ""
alerts = None
with open("settings.yaml", "r") as file:
settings = yaml.safe_load(file)
@ -141,6 +147,7 @@ rain_chance_fetcher = RainChanceFetcher(weather_manager)
nws_weather_fetcher_5day = NWSWeatherFetcher5Day(weather_manager)
forecast_4day = Forecast4DayFetcher(weather_manager)
forecast_7day = Forecast7DayFetcher(weather_manager)
wind_24hour = Wind24HourFetcher(weather_manager)
def get_temperature_24hour():
@ -216,6 +223,12 @@ def get_forecast_4day():
return "\n".join(forecast_4day_info)
def get_wind_24hour():
global wind_24hour_info
wind_24hour_info = wind_24hour.get_wind_24hour()
return wind_24hour_info
def message_listener(packet, interface):
global transmission_count
global cooldown
@ -223,134 +236,209 @@ def message_listener(packet, interface):
global FIREWALL
global DUTYCYCLE
global MYNODE
global alerts
if packet is not None and packet["decoded"].get("portnum") == "TEXT_MESSAGE_APP":
message = packet["decoded"]["text"].lower()
sender_id = packet["from"]
# Check if it's a DM
is_direct_message = False
if "to" in packet:
is_direct_message = str(packet["to"]) == str(MYNODE)
try:
if packet is not None and packet["decoded"].get("portnum") == "TEXT_MESSAGE_APP":
message = packet["decoded"]["text"].lower()
sender_id = packet["from"]
# Check if it's a DM
is_direct_message = False
if "to" in packet:
is_direct_message = str(packet["to"]) == str(MYNODE)
# Only log if it's a DM
if is_direct_message:
logger.info(f"Message {packet['decoded']['text']} from {packet['from']}")
logger.info(f"transmission count {transmission_count}")
# Only log if it's a DM
if is_direct_message:
logger.info(f"Message {packet['decoded']['text']} from {packet['from']}")
logger.info(f"transmission count {transmission_count}")
# Enforce DM_MODE
if DM_MODE and not is_direct_message:
return
# Enforce DM_MODE
if DM_MODE and not is_direct_message:
return
# firewall logging
if FIREWALL and not any(node in str(packet["from"]) for node in MYNODES):
logger.warning(f"Firewall blocked message from {packet['from']}: {message}")
return
# firewall logging
if FIREWALL and not any(node in str(packet["from"]) for node in MYNODES):
logger.warning(f"Firewall blocked message from {packet['from']}: {message}")
return
if (transmission_count < 16 or DUTYCYCLE == False):
if "test" in message:
transmission_count += 1
interface.sendText("🟢 ACK", wantAck=True, destinationId=sender_id)
elif "?" in message:
transmission_count += 1
if settings.get('FULL_MENU', True): # Default to full menu if setting not found
interface.sendText(
" --Multi-Message--\n"
"hourly - 24h outlook\n"
"7day - 7 day simple\n"
"5day - 5 day detailed\n\n"
" --Single Message--\n"
"2day - 2 day detailed\n"
"4day - 4 day simple\n"
"rain - 24h precipitation\n"
"temp - 24h temperature\n"
, wantAck=True, destinationId=sender_id)
else:
interface.sendText(
" --Weather Commands--\n"
"2day - 2 day forecast\n"
"4day - 4 day forecast\n"
"temp - 24h temperature\n"
"rain - 24h precipitation"
, wantAck=True, destinationId=sender_id)
elif "temp" in message:
transmission_count += 1
interface.sendText(get_temperature_24hour(), wantAck=True, destinationId=sender_id)
elif "2day" in message:
transmission_count += 1
interface.sendText(get_forecast_2day(), wantAck=True, destinationId=sender_id)
elif "hourly" in message:
if settings.get('ENABLE_HOURLY_WEATHER', True):
transmission_count += 1
weather_data = get_emoji_weather()
messages = split_message(weather_data)
if (transmission_count < 16 or DUTYCYCLE == False):
first_message_delay = settings.get('FIRST_MESSAGE_DELAY', 3)
subsequent_message_delay = settings.get('MESSAGE_DELAY', 10)
# Helper function to handle message sequences
def send_message_sequence(messages, message_type=""):
for i, msg in enumerate(messages):
if i == 0: # First message
time.sleep(first_message_delay)
interface.sendText(msg, wantAck=True, destinationId=sender_id)
if i < len(messages) - 1:
time.sleep(settings.get('MESSAGE_DELAY', 10))
else:
interface.sendText("Hourly weather module is disabled.", wantAck=True, destinationId=sender_id)
elif "rain" in message:
transmission_count += 1
interface.sendText(get_rain_chance(), wantAck=True, destinationId=sender_id)
elif "5day" in message:
if settings.get('ENABLE_5DAY_FORECAST', True):
if i < len(messages) - 1: # Don't delay after last message
time.sleep(subsequent_message_delay)
if "test" in message:
transmission_count += 1
weather_messages = nws_weather_fetcher_5day.get_daily_weather()
for i, msg in enumerate(weather_messages):
interface.sendText(msg, wantAck=True, destinationId=sender_id)
if i < len(weather_messages) - 1:
time.sleep(settings.get('MESSAGE_DELAY', 10))
else:
interface.sendText("5-day forecast module is disabled.", wantAck=True, destinationId=sender_id)
elif "4day" in message:
transmission_count += 1
interface.sendText(get_forecast_4day(), wantAck=True, destinationId=sender_id)
elif "advertise" in message:
transmission_count += 1
interface.sendText(
"Hello, I am a weather bot, DM me \"#?\" for a list of forcast options.",
wantAck=True,
destinationId="^all"
)
elif "7day" in message: # Changed from 10day
if settings.get('ENABLE_7DAY_FORECAST', True): # Changed from ENABLE_10DAY_FORECAST
time.sleep(first_message_delay)
interface.sendText(" ACK", wantAck=True, destinationId=sender_id)
elif "?" in message or "menu" in message:
transmission_count += 1
weather_data = forecast_7day.get_weekly_emoji_weather()
messages = split_message(weather_data, message_type="7day") # Changed from 10day
for i, msg in enumerate(messages):
interface.sendText(msg, wantAck=True, destinationId=sender_id)
if i < len(messages) - 1:
time.sleep(settings.get('MESSAGE_DELAY', 10))
else:
interface.sendText("7-day forecast module is disabled.", wantAck=True, destinationId=sender_id)
elif "alert-status" in message:
transmission_count += 1
interface.sendText(get_weather_alert_status(), wantAck=True, destinationId=sender_id)
else:
# If it's a DM but doesn't match any command, send a random help message
if is_direct_message:
time.sleep(first_message_delay)
if settings.get('FULL_MENU', True):
interface.sendText(
" --Multi-Message--\n"
"hourly - 24h outlook\n"
"7day - 7 day simple\n"
"5day - 5 day detailed\n"
"wind - 24h wind\n\n"
" --Single Message--\n"
"2day - 2 day detailed\n"
"4day - 4 day simple\n"
"rain - 24h precipitation\n"
"temp - 24h temperature\n"
, wantAck=True, destinationId=sender_id)
else:
interface.sendText(
" --Weather Commands--\n"
"2day - 2 day forecast\n"
"4day - 4 day forecast\n"
"temp - 24h temperature\n"
"rain - 24h precipitation"
, wantAck=True, destinationId=sender_id)
elif "temp" in message:
transmission_count += 1
time.sleep(first_message_delay)
interface.sendText(get_temperature_24hour(), wantAck=True, destinationId=sender_id)
elif "2day" in message:
transmission_count += 1
time.sleep(first_message_delay)
interface.sendText(get_forecast_2day(), wantAck=True, destinationId=sender_id)
elif "hourly" in message:
if settings.get('ENABLE_HOURLY_WEATHER', True):
transmission_count += 1
weather_data = get_emoji_weather()
messages = split_message(weather_data)
send_message_sequence(messages)
else:
time.sleep(first_message_delay)
interface.sendText("Hourly weather module is disabled.", wantAck=True, destinationId=sender_id)
elif "rain" in message:
transmission_count += 1
time.sleep(first_message_delay)
interface.sendText(get_rain_chance(), wantAck=True, destinationId=sender_id)
elif "5day" in message:
if settings.get('ENABLE_5DAY_FORECAST', True):
transmission_count += 1
weather_messages = nws_weather_fetcher_5day.get_daily_weather()
send_message_sequence(weather_messages)
else:
time.sleep(first_message_delay)
interface.sendText("5-day forecast module is disabled.", wantAck=True, destinationId=sender_id)
elif "4day" in message:
transmission_count += 1
time.sleep(first_message_delay)
interface.sendText(get_forecast_4day(), wantAck=True, destinationId=sender_id)
elif "wind" in message:
transmission_count += 1
weather_data = wind_24hour.get_wind_24hour()
if isinstance(weather_data, list):
weather_text = '\n'.join(weather_data)
messages = split_message(weather_text, max_length=180, message_type="Wind")
send_message_sequence(messages)
else:
time.sleep(first_message_delay)
interface.sendText(weather_data, wantAck=True, destinationId=sender_id)
elif "advertise" in message:
transmission_count += 1
interface.sendText(
random.choice(UNRECOGNIZED_MESSAGES),
"Hello all! I am a weather bot that does weather alerts and forecasts. "
"You can DM me \"?\" for a list of my forecast commands.\n\n"
"For more information, check me out on Github. https://github.com/oasis6212/Meshbot_weather",
wantAck=True,
destinationId=sender_id
destinationId="^all"
)
elif "7day" in message:
if settings.get('ENABLE_7DAY_FORECAST', True):
transmission_count += 1
weather_data = forecast_7day.get_weekly_emoji_weather()
messages = split_message(weather_data, message_type="7day")
send_message_sequence(messages)
else:
time.sleep(first_message_delay)
interface.sendText("7-day forecast module is disabled.", wantAck=True, destinationId=sender_id)
elif "alert-status" in message:
transmission_count += 1
interface.sendText(get_weather_alert_status(), wantAck=True, destinationId=sender_id)
elif "alert" in message:
transmission_count += 1
if alerts:
if not alerts.broadcast_full_alert(sender_id):
time.sleep(first_message_delay)
if not settings.get('ENABLE_FULL_ALERT_COMMAND', True):
interface.sendText(
"The full-alert command is disabled in settings.",
wantAck=True,
destinationId=sender_id
)
else:
interface.sendText(
"No active alerts at this time.",
wantAck=True,
destinationId=sender_id
)
else:
# If it's a DM but doesn't match any command, send a random help message
if is_direct_message:
transmission_count += 1
interface.sendText(
random.choice(UNRECOGNIZED_MESSAGES),
wantAck=True,
destinationId=sender_id
)
if transmission_count >= 11 and DUTYCYCLE == True:
if not cooldown:
interface.sendText(
"❌ Bot has reached duty cycle, entering cool down... ❄",
wantAck=False,
if transmission_count >= 11 and DUTYCYCLE == True:
if not cooldown:
interface.sendText(
"❌ Bot has reached duty cycle, entering cool down... ❄",
wantAck=False,
)
logger.info("Cooldown enabled.")
cooldown = True
logger.info(
"Duty cycle limit reached. Please wait before transmitting again."
)
logger.info("Cooldown enabled.")
cooldown = True
logger.info(
"Duty cycle limit reached. Please wait before transmitting again."
)
except KeyError as e:
node_name = interface.getMyNodeInfo().get('user', {}).get('longName', 'Unknown')
logger.error(f'Attached node "{node_name}" was unable to decode incoming message, possible key mismatch in its node-database.')
return
def signal_handler(sig, frame):
"""Perform a graceful shutdown when CTRL+C is pressed"""
global interface
logger.info("\nInitiating shutdown...")
try:
if interface is not None:
# logger.info("Sending shutdown command to node...")
try:
# Send shutdown command
interface.localNode.shutdown()
# Give the node sufficient time to complete its shutdown process
logger.info("Waiting for node to complete shutdown...")
time.sleep(17) # Time delay for node to finish shutting down
except Exception as e:
logger.error(f"Error sending shutdown command: {e}")
logger.info("Closing Meshtastic interface...")
interface.close()
logger.info("Shutdown complete")
except Exception as e:
logger.error(f"Error during shutdown: {e}")
sys.exit(0)
def main():
global interface, alerts # Add alerts to global declaration
signal.signal(signal.SIGINT, signal_handler)
logger.info("Starting program.")
reset_transmission_count()
if settings.get('DUTYCYCLE', False):
@ -358,7 +446,6 @@ def main():
parser = argparse.ArgumentParser(description="Meshbot_Weather a bot for Meshtastic devices")
parser.add_argument("--port", type=str, help="Specify the serial port to probe")
parser.add_argument("--db", type=str, help="Specify DB: mpowered or liam")
parser.add_argument("--host", type=str, help="Specify meshtastic host (IP address) if using API")
args = parser.parse_args()
@ -383,7 +470,7 @@ def main():
logger.info("No serial ports found.")
exit(0)
logger.info(f"Press CTRL-C x2 to terminate the program")
logger.info(f"Press CTRL-C to terminate the program")
# Create interface
if args.host:
@ -472,6 +559,7 @@ def get_my_node_id(interface):
logger.error(f"Failed to get node info: {e}")
return ''
def get_weather_alert_status():
"""
Check if the weather alert monitor is functioning properly.
@ -486,14 +574,14 @@ def get_weather_alert_status():
headers = {
"User-Agent": f"({settings.get('USER_AGENT_APP')}, {settings.get('USER_AGENT_EMAIL')})"
}
# Test the API connection
response = requests.get(base_url, params=params, headers=headers)
response.raise_for_status()
# If we get here, the connection is working
return "🟢 Alert System: Active and monitoring for weather alerts"
except requests.exceptions.RequestException as e:
logger.error(f"Weather Alert Monitor Status Check Failed: {str(e)}")
return "🔴 Alert System: Unable to connect to weather service"
@ -501,5 +589,6 @@ def get_weather_alert_status():
logger.error(f"Weather Alert Monitor Status Check Failed: {str(e)}")
return "🔴 Alert System: Service interrupted - check logs"
if __name__ == "__main__":
main()

Binary file not shown.

View File

@ -17,7 +17,9 @@ class EmojiWeatherFetcher:
"showers": "🌧️",
"thunderstorm": "⛈️",
"snow": "🌨️",
"fog": "🌫️"
"fog": "🌫️",
"haze": "🌫️"
}
self.rain_emoji = "💧"

View File

@ -8,35 +8,105 @@ logger = logging.getLogger(__name__)
class WeatherAlerts:
def __init__(self, lat, lon, interface, user_agent_app, user_agent_email, check_interval=300, sender_id=None, message_delay=10, settings=None):
"""Initialize the WeatherAlerts class.
Args:
lat (str): Latitude for the alert location
lon (str): Longitude for the alert location
interface: Interface object for sending messages
user_agent_app (str): App name for the user agent
user_agent_email (str): Contact email for the user agent
check_interval (int, optional): Time between alert checks in seconds. Defaults to 300.
sender_id (str, optional): Sender ID for messages. Defaults to None.
message_delay (int, optional): Delay between split messages in seconds. Defaults to 10.
settings (dict, optional): Additional settings dictionary. Defaults to None.
"""
self.lat = lat
self.lon = lon
def __init__(self, lat, lon, interface, user_agent_app, user_agent_email, check_interval=300, message_delay=7, settings=None):
self.base_url = f"https://api.weather.gov/alerts/active"
self.params = {"point": f"{lat},{lon}"}
self.headers = {"User-Agent": f"({user_agent_app}, {user_agent_email})"}
self.interface = interface
self.sender_id = sender_id
self.check_interval = check_interval
self.message_delay = message_delay
self.settings = settings or {}
# Add storage for current alert data
self.current_alert = None
self.last_alert_id = None
self.base_url = "https://api.weather.gov/alerts/active"
self.params = {
"point": f"{self.lat},{self.lon}"
}
self.headers = {
"User-Agent": f"({user_agent_app}, {user_agent_email})"
}
def check_alerts(self):
"""Check for new weather alerts and send notifications if needed."""
try:
logger.info("Updated weather alerts")
response = requests.get(self.base_url, params=self.params, headers=self.headers)
response.raise_for_status()
data = response.json()
if not data.get('features'):
self.current_alert = None # Clear current alert if no active alerts
return
latest_alert = data['features'][0]
alert_id = latest_alert['properties']['id']
if alert_id == self.last_alert_id:
return
self.last_alert_id = alert_id
self.current_alert = latest_alert # Store the full alert data
alert_props = latest_alert['properties']
# Check if description should be included
include_description = self.settings.get('ALERT_INCLUDE_DESCRIPTION', True)
# Prepare the alert message based on settings
if include_description:
full_message = (
f"{alert_props['headline']}\n"
f"Description: {alert_props['description']}"
)
else:
full_message = alert_props['headline']
# Split message into chunks and send
messages = self.split_message(full_message)
if not self.interface or not hasattr(self.interface, 'sendText'):
logger.error("Interface not properly configured for sending messages")
return
for i, msg in enumerate(messages, 1):
formatted_msg = f"--({i}/{len(messages)}) Alert--\n{msg}"
self.interface.sendText(
formatted_msg,
wantAck=False,
destinationId='^all'
)
if i < len(messages): # Don't sleep after last message
time.sleep(self.message_delay)
except requests.exceptions.RequestException as e:
logger.error(f"Failed to fetch weather alerts: {str(e)}")
except Exception as e:
logger.error(f"Error checking weather alerts: {str(e)}")
def broadcast_full_alert(self, destination_id):
"""Broadcast the full alert information including description."""
# Check if full-alert command is enabled
if not self.settings.get('ENABLE_FULL_ALERT_COMMAND', True):
return False # Do nothing if full-alert command is disabled
# Check if there's a current alert
if not self.current_alert:
return False # Do nothing if no active alerts
# Get alert properties
alert_props = self.current_alert['properties']
full_message = (
f"{alert_props['headline']}\n"
f"Description: {alert_props['description']}"
)
# Split and send messages
messages = self.split_message(full_message)
for i, msg in enumerate(messages, 1):
formatted_msg = f"--({i}/{len(messages)}) Alert--\n{msg}"
self.interface.sendText(
formatted_msg,
wantAck=True,
destinationId=destination_id
)
if i < len(messages):
time.sleep(self.message_delay)
return True
def split_message(self, text, max_length=175):
"""Split a message into chunks of specified maximum length, preserving whole words.
@ -85,60 +155,6 @@ class WeatherAlerts:
return messages
def check_alerts(self):
"""Check for new weather alerts and send notifications if needed."""
try:
logger.info("Updated weather alerts")
response = requests.get(self.base_url, params=self.params, headers=self.headers)
response.raise_for_status() # Raise exception for bad status codes
data = response.json()
if not data.get('features'):
return
latest_alert = data['features'][0]
alert_id = latest_alert['properties']['id']
if alert_id == self.last_alert_id:
return
self.last_alert_id = alert_id
alert_props = latest_alert['properties']
# Check if description should be included
include_description = self.settings.get('ALERT_INCLUDE_DESCRIPTION', True)
# Prepare the alert message based on settings
if include_description:
full_message = (
f"{alert_props['headline']}\n"
f"Description: {alert_props['description']}"
)
else:
full_message = alert_props['headline']
# Split message into chunks and send
messages = self.split_message(full_message)
if not self.interface or not hasattr(self.interface, 'sendText'):
logger.error("Interface not properly configured for sending messages")
return
for i, msg in enumerate(messages, 1):
formatted_msg = f"--({i}/{len(messages)}) Alert--\n{msg}"
self.interface.sendText(
formatted_msg,
wantAck=False,
destinationId='^all'
)
if i < len(messages): # Don't sleep after last message
time.sleep(self.message_delay)
except requests.exceptions.RequestException as e:
logger.error(f"Failed to fetch weather alerts: {str(e)}")
except Exception as e:
logger.error(f"Error checking weather alerts: {str(e)}")
def start_monitoring(self):
"""Start continuous monitoring of weather alerts in a separate thread."""

110
modules/wind_24hour.py Normal file
View File

@ -0,0 +1,110 @@
from datetime import datetime
import requests
import logging
class Wind24HourFetcher:
"""
A class to fetch and format 24-hour wind speed and direction data
"""
def __init__(self, weather_manager):
"""
Initialize the Wind24HourFetcher with a weather manager
Args:
weather_manager: WeatherDataManager instance for fetching weather data
"""
self.weather_manager = weather_manager
self.wind_direction_map = {
'N': 'N',
'NNE': 'NE',
'NE': 'NE',
'ENE': 'NE',
'E': 'E',
'ESE': 'SE',
'SE': 'SE',
'SSE': 'SE',
'S': 'S',
'SSW': 'SW',
'SW': 'SW',
'WSW': 'SW',
'W': 'W',
'WNW': 'NW',
'NW': 'NW',
'NNW': 'NW'
}
def _get_direction_abbrev(self, direction):
"""
Convert full wind direction to two-letter abbreviation
Args:
direction: Full wind direction (e.g., 'NNW', 'ESE')
Returns:
Two-letter wind direction abbreviation
"""
return self.wind_direction_map.get(direction, '--')
def get_wind_24hour(self):
"""
Fetch and format 24-hour wind data
Returns:
List of formatted strings containing hourly wind speed and direction
"""
try:
data = self.weather_manager.get_hourly_data()
if not data:
return "Error: Unable to fetch weather data"
periods = data['properties']['periods']
result = []
count = 0
# Create timezone-aware current time
current_time = datetime.now().astimezone()
for period in periods:
try:
time_str = period['startTime']
dt = datetime.fromisoformat(time_str.replace('Z', '+00:00'))
# Skip periods that are in the past
if dt <= current_time:
continue
if count >= 24: # Limit to 24 entries
break
hour = dt.strftime("%I").lstrip('0')
period_str = dt.strftime("%p").lower()
# Format time based on hour digits
if len(hour) == 1:
time_str = f"{hour}{period_str}"
else:
time_str = f"{hour}{period_str[0]}"
# Get and format wind data
wind_speed = round(float(period['windSpeed'].split()[0]))
wind_dir = period['windDirection']
dir_abbrev = self._get_direction_abbrev(wind_dir)
formatted_entry = f"{time_str}:{wind_speed}mph {dir_abbrev}"
result.append(formatted_entry)
count += 1
except Exception as e:
logging.error(f"Error processing period: {str(e)}")
continue
if not result:
return "Error: Could not process weather data"
return result # Return list instead of joined string
except requests.exceptions.RequestException as e:
return f"Error fetching weather data: {str(e)}"
except Exception as e:
return f"Unexpected error: {str(e)}"

View File

@ -1,19 +1,21 @@
MYNODES:
- "1234567890"
- "1234567890"
DM_MODE: true # if true bot responds to direct messages only. Recommend not changing this
FIREWALL: false # if True only responds to node ids listed under "MYNODES:"
DUTYCYCLE: false # if true will limit to 10% duty cycle
NWS_OFFICE: "HNX" #Location settings for the weather forcast api calls, see readme for details
NWS_GRID_X: "67"
NWS_GRID_Y: "80"
USER_AGENT_APP: "myweatherapp" #used for NWS API calls, can be whatever you want, more unique the better.
USER_AGENT_EMAIL: "contact@example.com" #your email, in the event NWS detects excess api calls they can contact you
ALERT_LAT: "33.5748" #Location settings for alerts, use your coordinates. No more than 4 digits past the decimal point
ALERT_LON: "-104.6314"
FIREWALL: false # If true, only responds to node ids listed under "MYNODES:"
DM_MODE: true # If true, bot responds to direct messages only. Recommend not changing this
DUTYCYCLE: false # If true, will limit to 10% duty cycle
NWS_OFFICE: "BGM" # Location settings for the weather forecast api calls, see readme for details
NWS_GRID_X: "84"
NWS_GRID_Y: "89"
USER_AGENT_APP: "myweatherapp" # Used for NWS API calls, can be whatever you want, more unique the better.
USER_AGENT_EMAIL: "contact@example.com" # Your email, in the event NWS detects excess api calls they can contact you
ALERT_LAT: "37.9807" # Location settings for alerts, use your coordinates. No more than 4 digits past the decimal point
ALERT_LON: "-101.7526"
ALERT_CHECK_INTERVAL: 300 # Time in seconds between alert checks (default: 300 = 5 minutes)
MESSAGE_DELAY: 10 # Delay between sending split messages
ALERT_INCLUDE_DESCRIPTION: false # Set to false to exclude description from alerts
FIRST_MESSAGE_DELAY: 0 # Delay in seconds between receiving a request and sending the first message back.
MESSAGE_DELAY: 15 # Delay in seconds between subsequent messages of a multi-message response
ALERT_INCLUDE_DESCRIPTION: false # Set to false to exclude description from automatically issued alerts
ENABLE_FULL_ALERT_COMMAND: true # Set to false to disable the alert request command
ENABLE_7DAY_FORECAST: true # Set to false to disable 7-day forecast module
ENABLE_5DAY_FORECAST: true # Set to false to disable 5-day forecast module
ENABLE_HOURLY_WEATHER: true # Set to false to disable hourly weather module
@ -21,4 +23,4 @@ FULL_MENU: true # When true, includes all weather commands. When false, shows o
ENABLE_AUTO_REBOOT: false # Set to true to enable automatic daily reboot of the connected node
AUTO_REBOOT_HOUR: 3 # Hour for daily reboot (24-hour format)
AUTO_REBOOT_MINUTE: 0 # Minute for daily reboot
REBOOT_DELAY_SECONDS: 10 # Delay in seconds before reboot occurs (recommend not changing this)
REBOOT_DELAY_SECONDS: 10 # Delay in seconds before reboot occurs (recommend not changing this)