mirror of
https://github.com/oasis6212/Meshbot_weather.git
synced 2025-12-10 00:06:12 -06:00
August 2025 Update
This commit is contained in:
parent
403bcfab99
commit
dd87bb2802
41
README.md
41
README.md
@ -3,6 +3,9 @@
|
||||

|
||||
|
||||
|
||||
|
||||
|
||||
|
||||

|
||||
|
||||
|
||||
@ -30,6 +33,9 @@
|
||||

|
||||
|
||||
|
||||

|
||||
|
||||
|
||||
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.
|
||||
|
||||
|
||||

|
||||
|
||||
|
||||
## 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
BIN
img/Test.jpeg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 52 KiB |
BIN
img/newfeatures.png
Normal file
BIN
img/newfeatures.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 322 KiB |
BIN
img/wind.png
Normal file
BIN
img/wind.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 212 KiB |
BIN
img/windnew.png
Normal file
BIN
img/windnew.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 244 KiB |
155
meshbot.py
155
meshbot.py
@ -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,6 +35,8 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
"""
|
||||
|
||||
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
import threading
|
||||
@ -43,6 +44,8 @@ import time
|
||||
import yaml
|
||||
import datetime
|
||||
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,7 +236,9 @@ def message_listener(packet, interface):
|
||||
global FIREWALL
|
||||
global DUTYCYCLE
|
||||
global MYNODE
|
||||
global alerts
|
||||
|
||||
try:
|
||||
if packet is not None and packet["decoded"].get("portnum") == "TEXT_MESSAGE_APP":
|
||||
message = packet["decoded"]["text"].lower()
|
||||
sender_id = packet["from"]
|
||||
@ -248,17 +263,32 @@ def message_listener(packet, interface):
|
||||
return
|
||||
|
||||
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: # Don't delay after last message
|
||||
time.sleep(subsequent_message_delay)
|
||||
|
||||
if "test" in message:
|
||||
transmission_count += 1
|
||||
interface.sendText("🟢 ACK", wantAck=True, destinationId=sender_id)
|
||||
elif "?" in message:
|
||||
time.sleep(first_message_delay)
|
||||
interface.sendText(" ACK", wantAck=True, destinationId=sender_id)
|
||||
elif "?" in message or "menu" in message:
|
||||
transmission_count += 1
|
||||
if settings.get('FULL_MENU', True): # Default to full menu if setting not found
|
||||
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\n"
|
||||
"5day - 5 day detailed\n"
|
||||
"wind - 24h wind\n\n"
|
||||
" --Single Message--\n"
|
||||
"2day - 2 day detailed\n"
|
||||
"4day - 4 day simple\n"
|
||||
@ -275,58 +305,85 @@ def message_listener(packet, interface):
|
||||
, 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)
|
||||
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))
|
||||
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()
|
||||
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))
|
||||
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(
|
||||
"Hello, I am a weather bot, DM me \"#?\" for a list of forcast options.",
|
||||
"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="^all"
|
||||
)
|
||||
elif "7day" in message: # Changed from 10day
|
||||
if settings.get('ENABLE_7DAY_FORECAST', True): # Changed from ENABLE_10DAY_FORECAST
|
||||
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") # 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))
|
||||
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:
|
||||
@ -349,8 +406,39 @@ def message_listener(packet, interface):
|
||||
"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.
|
||||
@ -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.
Binary file not shown.
BIN
modules/__pycache__/wind_24hour.cpython-311.pyc
Normal file
BIN
modules/__pycache__/wind_24hour.cpython-311.pyc
Normal file
Binary file not shown.
@ -17,7 +17,9 @@ class EmojiWeatherFetcher:
|
||||
"showers": "🌧️",
|
||||
"thunderstorm": "⛈️",
|
||||
"snow": "🌨️",
|
||||
"fog": "🌫️"
|
||||
"fog": "🌫️",
|
||||
"haze": "🌫️"
|
||||
|
||||
}
|
||||
self.rain_emoji = "💧"
|
||||
|
||||
|
||||
@ -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
110
modules/wind_24hour.py
Normal 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)}"
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user