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
|
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):
|
def message_listener(packet, interface):
|
||||||
global transmission_count
|
global transmission_count
|
||||||
global cooldown
|
global cooldown
|
||||||
@ -282,27 +327,39 @@ def message_listener(packet, interface):
|
|||||||
elif "?" in message or "menu" in message:
|
elif "?" in message or "menu" in message:
|
||||||
transmission_count += 1
|
transmission_count += 1
|
||||||
time.sleep(first_message_delay)
|
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):
|
if settings.get('FULL_MENU', True):
|
||||||
interface.sendText(
|
interface.sendText(menu_text_1, wantAck=True, destinationId=sender_id)
|
||||||
" --Multi-Message--\n"
|
time.sleep(subsequent_message_delay)
|
||||||
"hourly - 24h outlook\n"
|
interface.sendText(menu_text_2, wantAck=True, destinationId=sender_id)
|
||||||
"7day - 7 day simple\n"
|
|
||||||
"5day - 5 day detailed\n"
|
|
||||||
"wind - 24h wind\n\n"
|
|
||||||
" --Single Message--\n"
|
|
||||||
"2day - 2 day detailed\n"
|
|
||||||
"4day - 4 day simple\n"
|
|
||||||
"rain - 24h precipitation\n"
|
|
||||||
"temp - 24h temperature\n"
|
|
||||||
, wantAck=True, destinationId=sender_id)
|
|
||||||
else:
|
else:
|
||||||
interface.sendText(
|
simple_menu = " --Weather Commands--\n" \
|
||||||
" --Weather Commands--\n"
|
"2day - 2 day forecast\n" \
|
||||||
"2day - 2 day forecast\n"
|
"4day - 4 day forecast\n" \
|
||||||
"4day - 4 day forecast\n"
|
"temp - 24h temperature\n" \
|
||||||
"temp - 24h temperature\n"
|
|
||||||
"rain - 24h precipitation"
|
"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:
|
elif "temp" in message:
|
||||||
transmission_count += 1
|
transmission_count += 1
|
||||||
time.sleep(first_message_delay)
|
time.sleep(first_message_delay)
|
||||||
@ -418,16 +475,17 @@ def signal_handler(sig, frame):
|
|||||||
logger.info("\nInitiating shutdown...")
|
logger.info("\nInitiating shutdown...")
|
||||||
try:
|
try:
|
||||||
if interface is not None:
|
if interface is not None:
|
||||||
# logger.info("Sending shutdown command to node...")
|
|
||||||
try:
|
try:
|
||||||
# Send shutdown command
|
if settings.get('SHUTDOWN_NODE_ON_EXIT', False):
|
||||||
interface.localNode.shutdown()
|
logger.info("Sending shutdown command to node...")
|
||||||
# Give the node sufficient time to complete its shutdown process
|
interface.localNode.shutdown()
|
||||||
logger.info("Waiting for node to complete shutdown...")
|
logger.info("Waiting for node to complete shutdown...")
|
||||||
time.sleep(17) # Time delay for node to finish shutting down
|
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:
|
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...")
|
logger.info("Closing Meshtastic interface...")
|
||||||
interface.close()
|
interface.close()
|
||||||
logger.info("Shutdown complete")
|
logger.info("Shutdown complete")
|
||||||
|
|||||||
@ -16,6 +16,7 @@ class WeatherAlerts:
|
|||||||
self.check_interval = check_interval
|
self.check_interval = check_interval
|
||||||
self.message_delay = message_delay
|
self.message_delay = message_delay
|
||||||
self.settings = settings or {}
|
self.settings = settings or {}
|
||||||
|
self.channel_index = self.settings.get('ALERT_CHANNEL_INDEX', 0)
|
||||||
|
|
||||||
# Add storage for current alert data
|
# Add storage for current alert data
|
||||||
self.current_alert = None
|
self.current_alert = None
|
||||||
@ -67,7 +68,7 @@ class WeatherAlerts:
|
|||||||
self.interface.sendText(
|
self.interface.sendText(
|
||||||
formatted_msg,
|
formatted_msg,
|
||||||
wantAck=False,
|
wantAck=False,
|
||||||
destinationId='^all'
|
channelIndex=self.channel_index,
|
||||||
)
|
)
|
||||||
if i < len(messages): # Don't sleep after last message
|
if i < len(messages): # Don't sleep after last message
|
||||||
time.sleep(self.message_delay)
|
time.sleep(self.message_delay)
|
||||||
@ -101,7 +102,8 @@ class WeatherAlerts:
|
|||||||
self.interface.sendText(
|
self.interface.sendText(
|
||||||
formatted_msg,
|
formatted_msg,
|
||||||
wantAck=True,
|
wantAck=True,
|
||||||
destinationId=destination_id
|
destinationId=destination_id,
|
||||||
|
channelIndex=self.channel_index
|
||||||
)
|
)
|
||||||
if i < len(messages):
|
if i < len(messages):
|
||||||
time.sleep(self.message_delay)
|
time.sleep(self.message_delay)
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
MYNODES:
|
MYNODES:
|
||||||
- "1234567890"
|
|
||||||
- "1234567890"
|
|
||||||
FIREWALL: false # If true, only responds to node ids listed under "MYNODES:"
|
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
|
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
|
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
|
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_HOUR: 3 # Hour for daily reboot (24-hour format)
|
||||||
AUTO_REBOOT_MINUTE: 0 # Minute for daily reboot
|
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