Initial upload

This commit is contained in:
oasis6212 2025-06-28 11:55:45 -04:00 committed by GitHub
parent 4f4035c370
commit 96ee91bf75
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
37 changed files with 1630 additions and 2 deletions

318
README.md
View File

@ -1,2 +1,316 @@
# Meshbot_weather # MeshBot Weather
A weather bot with alerts and forecast. Designed to run on a Raspberry pi with a connected Meshtastic radio.
![Meshbot](img/meshbot_weather.png)
![](img/1.png)
![](img/2.png)
![](img/hourly.png)
![](img/5day.png)
![](img/temp.png)
![](img/4day.png)
![](img/alertstatus.png)
![](img/help.png)
![](img/menu.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.
Our Mission:
- To provide accurate weather forecast via the mesh.
- To provide weather alerts anywhere, including locations that don't receive NOAA radio broadcast.
- To allow simple customization of different parameters to better suit your deployment.
- To be lightweight, if the computer can run Python, it can run MeshBot Weather
- To be opensource an open community, modify at will. Please publish and share your meshtastic work.
## Features
- Utilizes the National Weather Service, the official source for NOAA-issued EAS alerts.
- Automatically sends severe weather alerts to all devices on the mesh network.
- Weather forcast: A selection of Multi-day and hourly forcast available on demand.
- Easily accesible menu that can be called by sending "?" to the bot.
- Help message reply when the bot receives an unrecognized instruction.
- Alert system test command to varify the weather alert api is responding and is configured correctly.
- Detailed multi-message outputs for deployments on private and low-traffic meshes.
- Includes a variety of single message options for use on meshes that are high traffic / high utilization.
- Ability to enforce single message use by disabling multi-message outputs via the settings.yaml file.
- Configurable node daily reboot function. Useful if your current firmware is a little less than stable.
- Forcast are generated for any location. Not limited to towns or cities.
## Bot interaction
You 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.
- hourly : 24 hour hourly forcast with temp, rain chance, and sky conditions in emoji form. (Multi message return)
- 5day : 5 day detailed forcast (Multi message return)
- 7day : 7 day forcast in emoji form (Multi message return)
- 4day : 4 day simple forcast in emoji form (Single message return)
- 2day : Today and tomorrows detailed forcast (Single message return)
- rain : Rain chance every hour for next 24 hours (Single message return)
- temp : Predicted temperature every hour for the next 24 hours (Single message return)
Commands below are not listed in the help menu:
- 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.
- tst-detail : as #test above only more detail e.g snr,rssi, hop count (thanks to [rohanki](https://github.com/rohanki))
- advertise : When received the bot will send out a message on the public channel introducing itself along with its menu
command.
## Requirements
- [Python](https://www.python.org/) 3.11 or above
- Access to a [Meshtastic](https://meshtastic.org) device
- [Serial Drivers](https://meshtastic.org/docs/getting-started/serial-drivers/) for your meshtastic device
- Internet connection for the bot.
## Meshbot Weather Installation
1. Open your terminal, then clone this repository to your local machine:
```
git clone https://github.com/oasis6212/meshbot_weather.git
```
2. Navigate into the folder
```
cd meshbot_weather
```
3. Setup a virtual environment
```
python3 -m venv .venv
```
4. Activate virtual environment
```
. .venv/bin/activate
```
5. Install the required dependencies using pip:
```
pip install -r requirements.txt
```
6. Connect your Meshtastic device to your computer via USB
## How to run the program on various operating systems:
Example on Linux:
```
python meshbot.py --port /dev/ttyUSB0
```
Example on OSX:
```
python meshbot.py --port /dev/cu.usbserial-0001
```
Example on Windows:
```
python meshbot.py --port COM7
```
Example using TCP client:
```
python meshbot.py --host meshtastic.local
or
python meshbot.py --host 192.168.0.100
```
For a list of avaiable ports:
```
python meshbot.py --help
```
## Configuration
The ''settings.yaml'' file; it's where you can configure different options. Can be edited in notepad.
Example Content:
```
MYNODES:
- "3663493700"
- "1234567890"
DM_MODE: true
FIREWALL: false
DUTYCYCLE: false
NWS_OFFICE: "LOX"
NWS_GRID_X: "155"
NWS_GRID_Y: "45"
USER_AGENT_APP: "myweatherapp"
USER_AGENT_EMAIL: "contact@example.com"
ALERT_LAT: "34.0522"
ALERT_LON: "-118.2433"
ALERT_CHECK_INTERVAL: 300
MESSAGE_DELAY: 15
ALERT_INCLUDE_DESCRIPTION: false
ENABLE_7DAY_FORECAST: true
ENABLE_5DAY_FORECAST: true
ENABLE_HOURLY_WEATHER: true
FULL_MENU: true
ENABLE_AUTO_REBOOT: false
AUTO_REBOOT_HOUR: 3
AUTO_REBOOT_MINUTE: 0
REBOOT_DELAY_SECONDS: 10
```
Description
- MYNODES = A list of nodes (in int/number form) that are permitted to interact with the bot
- DM_MODE = true: Only respond to DMs; false: responds to all traffic (recommend keeping this set to true)
- FIREWALL = false: if true only responds to MYNODES
- DUTYCYCLE: false: If true, limits itself to 10% Dutycycle
- NWS_OFFICE: NWS_GRID_X: NWS_GRID_Y: #settings for the weather forcast api calls, see below to learn how to set up.
- USER_AGENT_APP: "myweatherapp" #used for NWS (National Weather Service) API calls, can be whatever you want, more
unique the better. This is what NWS uses instead of an API key.
- USER_AGENT_EMAIL: "contact@example.com" #your email, in the event NWS detects excess api calls they will throttle you.
Gives you the opportunity to fix the issue and stop getting throttled.
- ALERT_LAT: "34.0522" ALERT_LON: "-118.2433" #settings for alerts, put in the latitude, and longitude of the area you
want alerts for. Make sure you only go up to 4 places past the decimal point on each.
- ALERT_CHECK_INTERVAL: # Time in seconds. How often the alert API is called. NWS does not publish allowable limits.
From what I have gathered, they allow up to once a minute for alert checking. Your milage may very.
- MESSAGE_DELAY: Delay in seconds between split messages. To short of a delay can cause messages to arrive out of order.
- ALERT_INCLUDE_DESCRIPTION: #Set to false to exclude description from alerts. Descriptions will include alot of detail
such as every county, town, and area affected. You can expect about 4 or 5 messages when description is set to "true" vs
a single message when set to false.
- 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.
- FULL_MENU: false # When true, includes all weather commands. When false, shows only forecast options that return a
single message.
- ENABLE_AUTO_REBOOT: false # Some firmware versions may experience Wi-Fi instability after the node has been running
for several days. If you encounter this issue, consider enabling the auto-reboot function by setting this to "true".
- AUTO_REBOOT_HOUR: 3 # Hour for daily reboot (24-hour format)
- AUTO_REBOOT_MINUTE: 0 # Minute for daily reboot.
- REBOOT_DELAY_SECONDS: 10 # This delay is executed on the node itself to give it time to prepare. Recommend not
changing this.
## How to get your NWS_OFFICE, NWS_GRID_X, and NWS_GRID_Y
To get your NWS office and grid coordinates:
1. Go to (https://weather.gov)
2. Enter your address
3. The URL will change to something like: `https://forecast.weather.gov/MapClick.php?lat=XX.XXXX&lon=YY.YYYY`
4. Visit (https://api.weather.gov/points/XX.XXXX,YY.YYYY) (using your coordinates)
Example: https://api.weather.gov/points/36.3741,-119.2702
(If you know them, you can replace the coordinates in this link here with yours and skip the first part. Don't enter
more than 4 digits past the decimal point in your latitude and longitude numbers.)
5. Look for the `gridId` (NWS_OFFICE) and `gridX` (NWS_GRID_X),`gridY` (NWS_GRID_Y) values in the response. You will
have to scroll down the page some.
![](./img/Grid.png)
Enter this info into the yaml file.
For the alert settings in the yaml file, enter your gps coordinates or use the coordinates you retrieved earlier in this
process. Use no more than four digits after the decimal point.
## API Handaling details
To prevent excessive api calls, the bot will check if it currently has the data being requested and if it is
less than an hour old. If both those conditions are met, the bot will use its catched data. If not, it will refresh the
weather info. It will not produce more than two api calls per hour for weather forcast. One for hourly data and the
other for daily data. If there are no mesh side weather requests, then no api calls are made.
Alerts are refreshed every five minutes by default. This is configurable via the "settings.yaml" file. Due to the nature
of the data being requested, this is considered acceptable. The NWS does not post its api call limits, but will throttle
you if they deem it excessive. What I've gathered from home automation groups is you can make the alert api call up to
every minute without issue.
You should set these options in your settings.yaml file
USER_AGENT_APP: "myweatherapp"
USER_AGENT_EMAIL: "contact@example.com"
Most weather api's use a key to identify your specific instance. Instead, the NWS uses this to identify you. It's more
convenient because you don't have to actually sign up for anything, and instead just use unique info instead.
The bot will work with the defaults here, but if you run it with these, your api calls will be added up along with
everyone else running the defaults. This could cause your API request to be throttled.
## Contributors
- [868meshbot](https://github.com/868meshbot), [oasis6212](https://github.com/oasis6212)
## Acknowledgements
This project utilizes the Meshtastic Python library, which provides communication capabilities for Meshtastic devices.
For more information about Meshtastic, visit [meshtastic.org](https://meshtastic.org/).
## License
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
## Legal
This project is neither endorsed by nor supported by Meshtastic.
Meshtastic® is a registered trademark of Meshtastic LLC. Meshtastic software components are released under various
licenses, see GitHub for details. No warranty is provided - use at your own risk.

BIN
img/1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

BIN
img/2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

BIN
img/2day.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

BIN
img/4day.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

BIN
img/5day.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 255 KiB

BIN
img/Grid.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

BIN
img/alertstatus.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

BIN
img/help.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

BIN
img/hourly.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 210 KiB

BIN
img/menu.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

BIN
img/meshbot_weather.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 238 KiB

BIN
img/rain.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

BIN
img/temp.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

505
meshbot.py Normal file
View File

@ -0,0 +1,505 @@
# !python3
# -*- coding: utf-8 -*-
"""
Weather Bot
=======================
meshbot.py: A message bot designed for Meshtastic, providing information from modules upon request:
* weather forcast
* weather Alerts
Author:
- Andy
- April 2024
MIT License
Copyright (c) 2024 Andy
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
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
try:
import meshtastic.serial_interface
import meshtastic.tcp_interface
from pubsub import pub
except ImportError:
print(
"ERROR: Missing meshtastic library!\nYou can install it via pip:\npip install meshtastic\n"
)
import serial.tools.list_ports
import requests
from modules.temperature_24hour import Temperature24HourFetcher
from modules.forecast_2day import Forecast2DayFetcher
from modules.hourly_weather import EmojiWeatherFetcher
from modules.rain_24hour import RainChanceFetcher
from modules.forecast_5day import NWSWeatherFetcher5Day
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
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."
]
def find_serial_ports():
ports = [port.device for port in serial.tools.list_ports.comports()]
filtered_ports = [
port for port in ports if "COM" in port.upper() or "USB" in port.upper()
]
return filtered_ports
logging.basicConfig(
level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s"
)
logger = logging.getLogger()
# GLOBALS
MYNODE = ""
MYNODES = ""
DM_MODE = ""
FIREWALL = ""
DUTYCYCLE = ""
with open("settings.yaml", "r") as file:
settings = yaml.safe_load(file)
MYNODES = settings.get("MYNODES")
DM_MODE = settings.get("DM_MODE")
FIREWALL = settings.get("FIREWALL")
DUTYCYCLE = settings.get("DUTYCYCLE")
NWS_OFFICE = settings.get("NWS_OFFICE", "HNX")
NWS_GRID_X = settings.get("NWS_GRID_X", "67")
NWS_GRID_Y = settings.get("NWS_GRID_Y", "80")
USER_AGENT_APP = settings.get("USER_AGENT_APP", "myweatherapp")
USER_AGENT_EMAIL = settings.get("USER_AGENT_EMAIL", "contact@example.com")
USER_AGENT = f"({USER_AGENT_APP}, {USER_AGENT_EMAIL})"
logger.info(f"DUTYCYCLE: {DUTYCYCLE}")
logger.info(f"DM_MODE: {DM_MODE}")
logger.info(f"FIREWALL: {FIREWALL}")
transmission_count = 0
cooldown = False
temperature_24hour_info = None
forecast_2day_info = None
emoji_weather_info = None
rain_chance_info = None
weather_manager = WeatherDataManager(
NWS_OFFICE,
NWS_GRID_X,
NWS_GRID_Y,
USER_AGENT
)
# Initialize weather classes with weather manager
temperature_24hour = Temperature24HourFetcher(weather_manager)
forecast_2day = Forecast2DayFetcher(weather_manager)
emoji_weather_fetcher = EmojiWeatherFetcher(weather_manager)
rain_chance_fetcher = RainChanceFetcher(weather_manager)
nws_weather_fetcher_5day = NWSWeatherFetcher5Day(weather_manager)
forecast_4day = Forecast4DayFetcher(weather_manager)
forecast_7day = Forecast7DayFetcher(weather_manager)
def get_temperature_24hour():
global temperature_24hour_info
temperature_24hour_info = temperature_24hour.get_temperature_24hour()
return temperature_24hour_info
def get_forecast_2day():
global forecast_2day_info
forecast_2day_info = forecast_2day.get_daily_weather()
return forecast_2day_info
def get_emoji_weather():
global emoji_weather_info
emoji_weather_info = emoji_weather_fetcher.get_emoji_weather()
return emoji_weather_info
def get_rain_chance():
global rain_chance_info
rain_chance_info = rain_chance_fetcher.get_rain_chance()
return rain_chance_info
def reset_transmission_count():
global transmission_count
if settings.get('DUTYCYCLE', False):
transmission_count -= 1
if transmission_count < 0:
transmission_count = 0
logger.info(f"Reducing transmission count {transmission_count}")
threading.Timer(180.0, reset_transmission_count).start()
def reset_cooldown():
global cooldown
cooldown = False
logger.info("Cooldown Disabled.")
threading.Timer(240.0, reset_cooldown).start()
def split_message(message, max_length=175, message_type="Hourly"):
lines = message.split('\n')
messages = []
current_message = []
current_length = 0
for line in lines:
line_length = len(line.encode('utf-8')) + (1 if current_message else 0)
if current_length + line_length > max_length:
messages.append('\n'.join(current_message))
current_message = []
current_length = 0
current_message.append(line)
current_length += line_length
if current_message:
messages.append('\n'.join(current_message))
for i in range(len(messages)):
messages[i] = f"--({i + 1}/{len(messages)}) {message_type}\n" + messages[i]
return messages
def get_forecast_4day():
global forecast_4day_info
forecast_4day_info = forecast_4day.get_weekly_emoji_weather()
return "\n".join(forecast_4day_info)
def message_listener(packet, interface):
global transmission_count
global cooldown
global DM_MODE
global FIREWALL
global DUTYCYCLE
global MYNODE
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}")
# 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
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)
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("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):
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
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:
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,
)
logger.info("Cooldown enabled.")
cooldown = True
logger.info(
"Duty cycle limit reached. Please wait before transmitting again."
)
def main():
logger.info("Starting program.")
reset_transmission_count()
if settings.get('DUTYCYCLE', False):
reset_cooldown()
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()
if args.port:
serial_ports = [args.port]
logger.info(f"Serial port {serial_ports}\n")
elif args.host:
ip_host = args.host
print(ip_host)
logger.info(f"Meshtastic API host {ip_host}\n")
else:
serial_ports = find_serial_ports()
if serial_ports:
logger.info("Available serial ports:")
for port in serial_ports:
logger.info(port)
logger.info(
"Im not smart enough to work out the correct port, please use the --port argument with a relevent meshtastic port"
)
else:
logger.info("No serial ports found.")
exit(0)
logger.info(f"Press CTRL-C x2 to terminate the program")
# Create interface
if args.host:
interface = meshtastic.tcp_interface.TCPInterface(hostname=ip_host, noProto=False)
else:
interface = meshtastic.serial_interface.SerialInterface(serial_ports[0])
global MYNODE
MYNODE = get_my_node_id(interface)
logger.info(f"Automatically detected MYNODE ID: {MYNODE}")
if DM_MODE and not MYNODE:
logger.error("DM_MODE is enabled but failed to get MYNODE ID. Please check connection to device.")
exit(1)
if settings.get('ENABLE_AUTO_REBOOT', True):
reboot_thread = threading.Thread(
target=schedule_daily_reboot,
args=(interface,),
daemon=True
)
reboot_thread.start()
logger.info("Daily reboot scheduler started")
try:
my_info = interface.getMyNodeInfo()
logger.info("Connected to Meshtastic Node:")
logger.info(f"Node Name: {my_info.get('user', {}).get('longName', 'Unknown')}")
except Exception as e:
logger.error(f"Failed to get node info: {e}")
message_delay = settings.get('MESSAGE_DELAY', 10)
alerts = WeatherAlerts(
settings.get("ALERT_LAT"),
settings.get("ALERT_LON"),
interface,
settings.get("USER_AGENT_APP"),
settings.get("USER_AGENT_EMAIL"),
settings.get("ALERT_CHECK_INTERVAL", 300),
message_delay=message_delay,
settings=settings
)
alerts.start_monitoring()
pub.subscribe(message_listener, "meshtastic.receive")
while True:
time.sleep(1)
def schedule_daily_reboot(interface):
if not settings.get('ENABLE_AUTO_REBOOT', True):
return
reboot_hour = settings.get('AUTO_REBOOT_HOUR', 3)
reboot_minute = settings.get('AUTO_REBOOT_MINUTE', 0)
reboot_delay = settings.get('REBOOT_DELAY_SECONDS', 10)
while True:
now = datetime.datetime.now()
next_reboot = now.replace(
hour=reboot_hour,
minute=reboot_minute,
second=0,
microsecond=0
)
if now >= next_reboot:
next_reboot += datetime.timedelta(days=1)
seconds_until_reboot = (next_reboot - now).total_seconds()
time.sleep(seconds_until_reboot)
try:
logger.info(f"Executing scheduled reboot at {next_reboot}")
interface.localNode.reboot(secs=reboot_delay)
except Exception as e:
logger.error(f"Failed to execute scheduled reboot: {e}")
def get_my_node_id(interface):
try:
my_info = interface.getMyNodeInfo()
return str(my_info.get('num', ''))
except Exception as e:
logger.error(f"Failed to get node info: {e}")
return ''
def get_weather_alert_status():
"""
Check if the weather alert monitor is functioning properly.
Returns a status message indicating if the system is working or not.
"""
try:
# Build the API URL and parameters similar to weather_alert_monitor.py
base_url = f"https://api.weather.gov/alerts/active"
params = {
"point": f"{settings.get('ALERT_LAT')},{settings.get('ALERT_LON')}"
}
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"
except Exception as e:
logger.error(f"Weather Alert Monitor Status Check Failed: {str(e)}")
return "🔴 Alert System: Service interrupted - check logs"
if __name__ == "__main__":
main()

0
modules/__init__.py Normal file
View File

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

51
modules/forecast_2day.py Normal file
View File

@ -0,0 +1,51 @@
import logging
class Forecast2DayFetcher:
def __init__(self, weather_manager):
self.weather_manager = weather_manager
def get_daily_weather(self):
try:
data = self.weather_manager.get_daily_data()
if not data:
return "Error: Unable to fetch weather data"
periods = data['properties']['periods'][:5] # Get first 5 periods
result = []
for i, period in enumerate(periods):
name = period['name']
temp = period['temperature']
forecast = period['shortForecast']
if 'night' in name.lower():
if 'tonight' in name.lower():
output = f"Tonight's Low {temp}. {forecast}"
else:
day = name.replace(' Night', '')
output = f"{day} Night Low {temp}. {forecast}"
else:
if name == "Today":
output = f"Today's High {temp}. {forecast}"
else:
output = f"{name} High {temp}. {forecast}"
# Add double newline before all periods except the first one
if i > 0:
output = "\n\n" + output
result.append(output)
# Join and ensure total length is under 200
full_text = "".join(result)
if len(full_text) > 200:
# If too long, reduce to 3 periods
result = result[:3]
full_text = "".join(result)
return full_text
except Exception as e:
error_msg = f"Error fetching weather data: {str(e)}"
logging.error(error_msg)
return error_msg

125
modules/forecast_4day.py Normal file
View File

@ -0,0 +1,125 @@
import logging
from datetime import datetime
class Forecast4DayFetcher:
def __init__(self, weather_manager):
self.weather_manager = weather_manager
self.weather_emojis = {
"clear": "🌙",
"sunny": "☀️",
"partly sunny": "🌤️",
"mostly sunny": "🌤️",
"partly cloudy": "",
"mostly cloudy": "🌥️",
"cloudy": "☁️",
"rain": "🌧️",
"showers": "🌧️",
"thunderstorm": "⛈️",
"snow": "🌨️",
"fog": "🌫️"
}
self.rain_emoji = "💧"
def _get_emoji(self, forecast):
forecast = forecast.lower()
if "thunderstorm" in forecast:
return self.weather_emojis["thunderstorm"]
for key, emoji in self.weather_emojis.items():
if key in forecast:
return emoji
return "🌡️"
def _get_rain_chance(self, properties):
prob = properties.get('probabilityOfPrecipitation', {}).get('value', 0)
return 0 if prob is None else prob
def _format_day_name(self, name, is_first_period=False):
# Handle special cases for the first period
if is_first_period:
if 'night' in name.lower():
return "Tonight"
return "Today"
# For other periods, use first 3 letters of the day name
name = name.split()[0]
return name[:3]
def get_weekly_emoji_weather(self):
try:
data = self.weather_manager.get_daily_data()
if not data:
return ["Error: Unable to fetch weather data"]
if 'properties' not in data or 'periods' not in data['properties']:
return ["Error: Invalid weather data format"]
periods = data['properties']['periods']
result = []
# Check if first period is night
starts_with_night = 'night' in periods[0]['name'].lower()
# If starting with night, we need 9 periods to get 4 full days
needed_periods = 9 if starts_with_night else 8
periods = periods[:needed_periods]
if starts_with_night:
# First line will only have night data
night_period = periods[0]
day_name = self._format_day_name(night_period['name'], True)
low_temp = night_period['temperature']
night_emoji = self._get_emoji(night_period['shortForecast'])
night_rain = self._get_rain_chance(night_period)
line = f"{day_name} {self.rain_emoji}{night_rain}% ❌ {night_emoji} ❌ ↓{low_temp}°"
result.append(line)
# Process remaining days
for i in range(1, len(periods) - 1, 2):
day_period = periods[i]
night_period = periods[i + 1]
day_name = self._format_day_name(day_period['name'])
day_rain = self._get_rain_chance(day_period)
night_rain = self._get_rain_chance(night_period)
max_rain = max(day_rain, night_rain)
day_emoji = self._get_emoji(day_period['shortForecast'])
night_emoji = self._get_emoji(night_period['shortForecast'])
high_temp = day_period['temperature']
low_temp = night_period['temperature']
line = f"{day_name} {self.rain_emoji}{max_rain}% {day_emoji} {night_emoji}{high_temp}° ↓{low_temp}°"
result.append(line)
else:
# Process all days normally, with special handling for first day
for i in range(0, min(len(periods), 8), 2):
day_period = periods[i]
night_period = periods[i + 1] if i + 1 < len(periods) else None
if not night_period:
continue
day_name = self._format_day_name(day_period['name'], i == 0)
day_rain = self._get_rain_chance(day_period)
night_rain = self._get_rain_chance(night_period)
max_rain = max(day_rain, night_rain)
day_emoji = self._get_emoji(day_period['shortForecast'])
night_emoji = self._get_emoji(night_period['shortForecast'])
high_temp = day_period['temperature']
low_temp = night_period['temperature']
line = f"{day_name} {self.rain_emoji}{max_rain}% {day_emoji} {night_emoji}{high_temp}° ↓{low_temp}°"
result.append(line)
return result[:4] # Only return first 4 days
except Exception as e:
error_msg = f"Error fetching weather data: {str(e)}"
logging.error(error_msg)
return [error_msg]

65
modules/forecast_5day.py Normal file
View File

@ -0,0 +1,65 @@
import logging
class NWSWeatherFetcher5Day:
def __init__(self, weather_manager):
self.weather_manager = weather_manager
def get_daily_weather(self):
try:
data = self.weather_manager.get_daily_data()
if not data:
return ["Error: Unable to fetch weather data"]
periods = data['properties']['periods'][:10] # Get 10 periods (5 days)
result = []
for period in periods:
name = period['name']
temp = period['temperature']
forecast = period['shortForecast']
if 'night' in name.lower():
if 'tonight' in name.lower():
output = f"Tonight's Low {temp}. {forecast}"
else:
day = name.replace(' Night', '')
output = f"{day} Night Low {temp}. {forecast}"
else:
if name == "Today":
output = f"Today's High {temp}. {forecast}"
else:
output = f"{name} High {temp}. {forecast}"
result.append(output)
# Format with double newlines before splitting
formatted_text = "\n\n".join(result)
# Split into chunks that fit within character limit
chunks = []
while formatted_text:
if len(formatted_text) <= 175:
chunks.append(formatted_text)
break
else:
# Find last double newline before 175 chars
split_point = formatted_text[:175].rfind('\n\n')
if split_point == -1:
split_point = 175
chunks.append(formatted_text[:split_point])
formatted_text = formatted_text[split_point:].lstrip()
# Add message numbering
messages = []
for i, chunk in enumerate(chunks):
messages.append(f"--({i + 1}/{len(chunks)}) 5-Day\n\n{chunk}")
return messages
except Exception as e:
error_msg = f"Error fetching weather data: {str(e)}"
logging.error(error_msg)
return [error_msg]

131
modules/forecast_7day.py Normal file
View File

@ -0,0 +1,131 @@
class Forecast7DayFetcher: # Changed from Forecast10DayFetcher
def __init__(self, weather_manager):
self.weather_manager = weather_manager
self.weather_emojis = {
"clear": "🌙",
"sunny": "☀️",
"partly sunny": "🌤️",
"mostly sunny": "🌤️",
"partly cloudy": "",
"mostly cloudy": "🌥️",
"cloudy": "☁️",
"rain": "🌧️",
"showers": "🌧️",
"thunderstorm": "⛈️",
"snow": "🌨️",
"fog": "🌫️"
}
self.rain_emoji = "💧"
def _get_emoji(self, forecast):
forecast = forecast.lower()
if "thunderstorm" in forecast:
return self.weather_emojis["thunderstorm"]
for key, emoji in self.weather_emojis.items():
if key in forecast:
return emoji
return "🌡️"
def _get_rain_chance(self, properties):
prob = properties.get('probabilityOfPrecipitation', {}).get('value', 0)
return 0 if prob is None else prob
def _format_day_name(self, name, is_first_period=False):
# Handle special cases for the first period
if is_first_period:
if 'night' in name.lower():
return "Tonight"
return "Today"
# For other periods, use first 3 letters of the day name
name = name.split()[0]
return name[:3]
def get_weekly_emoji_weather(self):
try:
data = self.weather_manager.get_daily_data()
if not data:
return "Error: Unable to fetch weather data"
if 'properties' not in data or 'periods' not in data['properties']:
return "Error: Invalid weather data format"
periods = data['properties']['periods']
result = []
# Check if first period is night
starts_with_night = 'night' in periods[0]['name'].lower()
# If starting with night, we need 15 periods to get 7 full days
needed_periods = 15 if starts_with_night else 14
periods = periods[:needed_periods]
if starts_with_night:
# First line will only have night data
night_period = periods[0]
day_name = self._format_day_name(night_period['name'], True)
low_temp = night_period['temperature']
night_emoji = self._get_emoji(night_period['shortForecast'])
night_rain = self._get_rain_chance(night_period)
line = f"{day_name} {self.rain_emoji}{night_rain}% ❌ {night_emoji} ❌ ↓{low_temp}°"
result.append(line)
# Process remaining days
for i in range(1, len(periods) - 1, 2):
if len(result) >= 10: # Stop after 10 days
break
day_period = periods[i]
night_period = periods[i + 1]
day_name = self._format_day_name(day_period['name'])
day_rain = self._get_rain_chance(day_period)
night_rain = self._get_rain_chance(night_period)
max_rain = max(day_rain, night_rain)
day_emoji = self._get_emoji(day_period['shortForecast'])
night_emoji = self._get_emoji(night_period['shortForecast'])
high_temp = day_period['temperature']
low_temp = night_period['temperature']
line = f"{day_name} {self.rain_emoji}{max_rain}% {day_emoji} {night_emoji}{high_temp}° ↓{low_temp}°"
result.append(line)
else:
# Process all days normally, with special handling for first day
for i in range(0, min(len(periods), 20), 2):
if len(result) >= 10: # Stop after 10 days
break
day_period = periods[i]
night_period = periods[i + 1] if i + 1 < len(periods) else None
if not night_period:
continue
day_name = self._format_day_name(day_period['name'], i == 0)
day_rain = self._get_rain_chance(day_period)
night_rain = self._get_rain_chance(night_period)
max_rain = max(day_rain, night_rain)
day_emoji = self._get_emoji(day_period['shortForecast'])
night_emoji = self._get_emoji(night_period['shortForecast'])
high_temp = day_period['temperature']
low_temp = night_period['temperature']
line = f"{day_name} {self.rain_emoji}{max_rain}% {day_emoji} {night_emoji}{high_temp}° ↓{low_temp}°"
result.append(line)
# Change the return to limit to 7 days
return "\n".join(result[:7])
except Exception as e:
error_msg = f"Error fetching weather data: {str(e)}"
logging.error(error_msg)
return error_msg
import logging
from datetime import datetime

93
modules/hourly_weather.py Normal file
View File

@ -0,0 +1,93 @@
import logging
from datetime import datetime
class EmojiWeatherFetcher:
def __init__(self, weather_manager):
self.weather_manager = weather_manager
self.weather_emojis = {
"clear": "🌙", # Changed from ☀️ to 🌙
"sunny": "☀️",
"partly sunny": "🌤️",
"mostly sunny": "🌤️",
"partly cloudy": "",
"mostly cloudy": "🌥️",
"cloudy": "☁️",
"rain": "🌧️",
"showers": "🌧️",
"thunderstorm": "⛈️",
"snow": "🌨️",
"fog": "🌫️"
}
self.rain_emoji = "💧"
def _get_emoji(self, forecast):
forecast = forecast.lower()
# First check for thunderstorm specifically
if "thunderstorm" in forecast:
return self.weather_emojis["thunderstorm"]
# Then check for other conditions
for key, emoji in self.weather_emojis.items():
if key in forecast:
return emoji
return "🌡️" # Default emoji if no match found
def _get_rain_chance(self, properties):
prob = properties.get('probabilityOfPrecipitation', {}).get('value', 0)
return 0 if prob is None else prob
def _format_time(self, dt):
# Get hour without leading zero
hour = dt.strftime("%I").lstrip('0')
# Get period (AM/PM)
period_str = dt.strftime("%p").lower()
# Format time based on hour length
if len(hour) == 1:
# Single digit hour - keep the 'm'
return f"{hour}{period_str}"
else:
# Double digit hour - use 'a' or 'p' with a space
return f"{hour}{period_str[0]} "
def get_emoji_weather(self):
try:
data = self.weather_manager.get_hourly_data()
if not data:
return "Error: Unable to fetch weather data"
if 'properties' not in data or 'periods' not in data['properties']:
return "Error: Invalid weather data format"
result = []
count = 0
# Create timezone-aware current time in UTC
current_time = datetime.now().astimezone()
for period in data['properties']['periods']:
time_str = period['startTime']
# Parse the API time (which includes timezone info)
dt = datetime.fromisoformat(time_str.replace('Z', '+00:00'))
# Skip periods that are in the past
if dt <= current_time:
continue
if count >= 23: # Limit to 23 entries
break
time_format = self._format_time(dt)
emoji = self._get_emoji(period['shortForecast'])
temp = str(round(period['temperature']))
rain_chance = self._get_rain_chance(period)
line = f"{time_format}{emoji}{temp}°{self.rain_emoji}{rain_chance}%"
result.append(line)
count += 1
return "\n".join(result)
except Exception as e:
error_msg = f"Error: {str(e)}"
logging.error(error_msg)
return error_msg

50
modules/rain_24hour.py Normal file
View File

@ -0,0 +1,50 @@
from datetime import datetime
class RainChanceFetcher:
def __init__(self, weather_manager):
self.weather_manager = weather_manager
def get_rain_chance(self):
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
for period in periods:
try:
if count >= 24:
break
time_str = period['startTime']
dt = datetime.fromisoformat(time_str.replace('Z', '+00:00'))
hour = dt.strftime("%I").lstrip('0')
period_str = dt.strftime("%p").lower()
if len(hour) == 1:
time_str = f"{hour}{period_str}"
else:
time_str = f"{hour}{period_str[0]}"
prob = period.get('probabilityOfPrecipitation', {}).get('value', 0)
rain_chance = 0 if prob is None else prob
formatted_entry = f"{time_str}:{rain_chance}%"
result.append(formatted_entry)
count += 1
except Exception as e:
continue
if not result:
return "Error: Could not process weather data"
return "\n".join(result)
except Exception as e:
return f"Unexpected error: {str(e)}"

View File

@ -0,0 +1,51 @@
from datetime import datetime
import requests
class Temperature24HourFetcher:
def __init__(self, weather_manager):
self.weather_manager = weather_manager
def get_temperature_24hour(self):
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
for period in periods:
try:
if count >= 24:
break
time_str = period['startTime']
dt = datetime.fromisoformat(time_str.replace('Z', '+00:00'))
hour = dt.strftime("%I").lstrip('0')
period_str = dt.strftime("%p").lower()
if len(hour) == 1:
time_str = f"{hour}{period_str}"
else:
time_str = f"{hour}{period_str[0]}"
temp = round(float(period['temperature']))
formatted_entry = f"{time_str}:{temp}°"
result.append(formatted_entry)
count += 1
except Exception as e:
continue
if not result:
return "Error: Could not process weather data"
return "\n".join(result)
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

@ -0,0 +1,155 @@
import time
import threading
import requests
import logging
from datetime import datetime
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
self.interface = interface
self.sender_id = sender_id
self.check_interval = check_interval
self.message_delay = message_delay
self.settings = settings or {}
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 split_message(self, text, max_length=175):
"""Split a message into chunks of specified maximum length, preserving whole words.
Args:
text (str): Text to split
max_length (int, optional): Maximum length of each chunk. Defaults to 175.
Returns:
list: List of message chunks
"""
messages = []
lines = text.split('\n')
current_chunk = []
current_length = 0
for line in lines:
words = line.split()
for word in words:
# Check if adding this word would exceed the limit
# +1 for space, another +1 for potential newline
word_length = len(word) + (1 if current_chunk else 0)
if current_length + word_length > max_length:
# Current chunk is full, save it and start a new one
if current_chunk:
messages.append(' '.join(current_chunk))
current_chunk = [word]
current_length = len(word)
else:
current_chunk.append(word)
current_length += word_length
# Add a newline after each original line if we're continuing the same chunk
if current_chunk:
current_length += 1 # Account for the newline
if current_length > max_length:
# If adding newline would exceed limit, start new chunk
messages.append(' '.join(current_chunk))
current_chunk = []
current_length = 0
# Don't forget the last chunk
if current_chunk:
messages.append(' '.join(current_chunk))
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."""
def monitor():
while True:
try:
self.check_alerts()
except Exception as e:
logger.error(f"Error in monitor thread: {str(e)}")
finally:
time.sleep(self.check_interval)
monitor_thread = threading.Thread(target=monitor, daemon=True)
monitor_thread.start()

View File

@ -0,0 +1,64 @@
import requests
import logging
from datetime import datetime, timedelta
class WeatherDataManager:
def __init__(self, office="HNX", grid_x="67", grid_y="80", user_agent="(myweatherapp, contact@example.com)"):
self.hourly_url = f"https://api.weather.gov/gridpoints/{office}/{grid_x},{grid_y}/forecast/hourly"
self.daily_url = f"https://api.weather.gov/gridpoints/{office}/{grid_x},{grid_y}/forecast"
self.headers = {"User-Agent": user_agent}
self.hourly_data = None
self.daily_data = None
self.last_hourly_update = None
self.last_daily_update = None
self.update_interval = timedelta(hours=1) # Update every hour
def _fetch_hourly_data(self):
try:
response = requests.get(self.hourly_url, headers=self.headers)
if response.status_code == 200:
self.hourly_data = response.json()
self.last_hourly_update = datetime.now()
logging.info("Updated hourly weather data")
return True
else:
logging.error(f"Failed to fetch hourly data: {response.status_code}")
return False
except Exception as e:
logging.error(f"Error fetching hourly weather data: {str(e)}")
return False
def _fetch_daily_data(self):
try:
response = requests.get(self.daily_url, headers=self.headers)
if response.status_code == 200:
self.daily_data = response.json()
self.last_daily_update = datetime.now()
logging.info("Updated daily weather data")
return True
else:
logging.error(f"Failed to fetch daily data: {response.status_code}")
return False
except Exception as e:
logging.error(f"Error fetching daily weather data: {str(e)}")
return False
def needs_update(self, last_update):
if last_update is None:
return True
return datetime.now() - last_update > self.update_interval
def get_hourly_data(self):
if self.needs_update(self.last_hourly_update):
self._fetch_hourly_data()
return self.hourly_data
def get_daily_data(self):
if self.needs_update(self.last_daily_update):
self._fetch_daily_data()
return self.daily_data
def force_update(self):
"""Force an immediate update of both hourly and daily data"""
return self._fetch_hourly_data() and self._fetch_daily_data()

BIN
requirements.txt Normal file

Binary file not shown.

24
settings.yaml Normal file
View File

@ -0,0 +1,24 @@
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"
ALERT_CHECK_INTERVAL: 300 # Time in seconds between alert checks (default: 300 = 5 minutes)
MESSAGE_DELAY: 7 # Delay between sending split messages
ALERT_INCLUDE_DESCRIPTION: false # Set to false to exclude description from alerts
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
FULL_MENU: true # When true, includes all weather commands. When false, shows only single message options.
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)