Add Dockerfile and implement custom location lookup in meshbot.py

- Created a Dockerfile
- Added a new function `get_custom_lookup` in meshbot.py to handle custom location queries for weather information.
- Updated message handling to include custom location lookup in the response options.
- Modified weather_alert_monitor.py to support dynamic alert channel indexing.
- Updated settings.yaml to include configuration options for custom lookup, node shutdown, and alert channel index.
This commit is contained in:
David Fries 2025-08-03 18:07:21 -06:00
parent dd87bb2802
commit 43e50a79f1
4 changed files with 126 additions and 31 deletions

33
Dockerfile Normal file
View File

@ -0,0 +1,33 @@
# Efficient Dockerfile for Meshbot Weather
FROM python:3.13-slim
# Set environment variables for Python
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1
# Create and set working directory
WORKDIR /app
# Install system dependencies (for serial, etc.)
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential \
libffi-dev \
libssl-dev \
&& rm -rf /var/lib/apt/lists/*
# Copy only requirements first for better caching
COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt
# Copy the rest of the code
COPY meshbot.py ./
COPY settings.yaml ./
COPY modules/ ./modules/
COPY img/ ./img/
# Create a non-root user for security
RUN useradd -m meshbotuser
USER meshbotuser
# Entrypoint
ENTRYPOINT ["python", "meshbot.py"]

View File

@ -229,6 +229,51 @@ def get_wind_24hour():
return wind_24hour_info
def get_custom_lookup(message):
"""
Parse message like 'loc lat/lon command' and return the weather info for that location.
Uses api.weather.gov /points/{lat},{lon} to get grid/office.
Supported commands: 2day, 4day, 5day, 7day, hourly, temp, rain, wind
"""
import re
import requests
match = re.match(r"loc\s+([+-]?\d+\.\d+)/([+-]?\d+\.\d+)\s*(\w+)?", message)
if not match:
return "Invalid location format. Use 'loc lat/lon [command]'."
lat, lon, command = match.groups()
# Get NWS grid info
try:
url = f"https://api.weather.gov/points/{lat},{lon}"
resp = requests.get(url, headers={"User-Agent": USER_AGENT})
resp.raise_for_status()
data = resp.json()
office = data['properties']['cwa']
grid_x = str(data['properties']['gridX'])
grid_y = str(data['properties']['gridY'])
except Exception as e:
return f"Entered grid is invalid or not found for {lat},{lon}: Not part of NWS coverage area."
# Create a temporary weather manager for this location
temp_manager = WeatherDataManager(office, grid_x, grid_y, USER_AGENT)
# Map commands to fetchers
fetchers = {
'2day': lambda: Forecast2DayFetcher(temp_manager).get_daily_weather(),
'4day': lambda: Forecast4DayFetcher(temp_manager).get_weekly_emoji_weather(),
'5day': lambda: NWSWeatherFetcher5Day(temp_manager).get_daily_weather(),
'7day': lambda: Forecast7DayFetcher(temp_manager).get_weekly_emoji_weather(),
'hourly': lambda: EmojiWeatherFetcher(temp_manager).get_emoji_weather(),
'temp': lambda: Temperature24HourFetcher(temp_manager).get_temperature_24hour(),
'rain': lambda: RainChanceFetcher(temp_manager).get_rain_chance(),
'wind': lambda: Wind24HourFetcher(temp_manager).get_wind_24hour(),
}
if command and command in fetchers:
result = fetchers[command]()
if isinstance(result, list):
return '\n'.join(result)
return str(result)
else:
return f"Custom location lookup: lat={lat}, lon={lon}, office={office}, grid=({grid_x},{grid_y})\nSupported commands: {', '.join(fetchers.keys())}"
def message_listener(packet, interface):
global transmission_count
global cooldown
@ -282,27 +327,39 @@ def message_listener(packet, interface):
elif "?" in message or "menu" in message:
transmission_count += 1
time.sleep(first_message_delay)
menu_text_1 = " --Multi-Message--\n" \
"hourly - 24h outlook\n" \
"7day - 7 day simple\n" \
"5day - 5 day detailed\n" \
"wind - 24h wind\n"
menu_text_2 = " --Single Message--\n" \
"2day - 2 day detailed\n" \
"4day - 4 day simple\n" \
"rain - 24h precipitation\n" \
"temp - 24h temperature\n"
if settings.get('ENABLE_CUSTOM_LOOKUP', False):
menu_text_2 += "loc lat/lon - custom location lookup\n"
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)
interface.sendText(menu_text_1, wantAck=True, destinationId=sender_id)
time.sleep(subsequent_message_delay)
interface.sendText(menu_text_2, 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"
simple_menu = " --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)
if settings.get('ENABLE_CUSTOM_LOOKUP', False):
simple_menu += "\nloc lat/lon - custom location lookup"
interface.sendText(simple_menu, wantAck=True, destinationId=sender_id)
elif "loc" in message:
transmission_count += 1
time.sleep(first_message_delay)
custom_lookup_result = get_custom_lookup(message)
if custom_lookup_result:
interface.sendText(custom_lookup_result, wantAck=True, destinationId=sender_id)
else:
interface.sendText("Invalid location format. Use 'loc lat/lon'.", wantAck=True, destinationId=sender_id)
elif "temp" in message:
transmission_count += 1
time.sleep(first_message_delay)
@ -418,16 +475,17 @@ def signal_handler(sig, frame):
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
if settings.get('SHUTDOWN_NODE_ON_EXIT', False):
logger.info("Sending shutdown command to node...")
interface.localNode.shutdown()
logger.info("Waiting for node to complete shutdown...")
time.sleep(17) # Time delay for node to finish shutting down
else:
logger.info("Skipping node shutdown; closing interface only...")
time.sleep(2) # Short delay for cleanup
except Exception as e:
logger.error(f"Error sending shutdown command: {e}")
logger.error(f"Error during shutdown cleanup: {e}")
logger.info("Closing Meshtastic interface...")
interface.close()
logger.info("Shutdown complete")

View File

@ -16,6 +16,7 @@ class WeatherAlerts:
self.check_interval = check_interval
self.message_delay = message_delay
self.settings = settings or {}
self.channel_index = self.settings.get('ALERT_CHANNEL_INDEX', 0)
# Add storage for current alert data
self.current_alert = None
@ -67,7 +68,7 @@ class WeatherAlerts:
self.interface.sendText(
formatted_msg,
wantAck=False,
destinationId='^all'
channelIndex=self.channel_index,
)
if i < len(messages): # Don't sleep after last message
time.sleep(self.message_delay)
@ -101,7 +102,8 @@ class WeatherAlerts:
self.interface.sendText(
formatted_msg,
wantAck=True,
destinationId=destination_id
destinationId=destination_id,
channelIndex=self.channel_index
)
if i < len(messages):
time.sleep(self.message_delay)

View File

@ -1,6 +1,5 @@
MYNODES:
- "1234567890"
- "1234567890"
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
@ -23,4 +22,7 @@ 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)
ENABLE_CUSTOM_LOOKUP: true # Enable/disable custom lat/lon lookup via message
SHUTDOWN_NODE_ON_EXIT: false # If true, shutdown node on exit. If false, only close interface
ALERT_CHANNEL_INDEX: 0 # Channel index for weather alerts, default is 0 (first channel)