mirror of
https://github.com/oasis6212/Meshbot_weather.git
synced 2025-12-10 00:06:12 -06:00
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:
parent
dd87bb2802
commit
43e50a79f1
33
Dockerfile
Normal file
33
Dockerfile
Normal 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"]
|
||||
110
meshbot.py
110
meshbot.py
@ -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")
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
Loading…
x
Reference in New Issue
Block a user