mirror of
https://github.com/agessaman/meshcore-bot.git
synced 2026-08-06 18:59:37 +00:00
feat: Add reload configuration functionality and update admin commands
- Introduced a new method `reload_config` to allow dynamic reloading of the configuration without restarting the bot, ensuring seamless updates to settings. - Added a helper method `_get_radio_settings` to retrieve current radio settings for comparison during reload. - Updated `admin_commands` in `config.ini.example` to include the new `reload` command, enabling admin users to reload configurations on-the-fly. - Enhanced the `setup_scheduled_messages` method in the scheduler to clear existing jobs before reloading, preventing duplicates.
This commit is contained in:
+2
-1
@@ -145,7 +145,8 @@ admin_pubkeys =
|
||||
|
||||
# Commands that require admin access (comma-separated)
|
||||
# These commands will only work for users in the admin_pubkeys list
|
||||
admin_commands = repeater,webviewer
|
||||
# reload: Reload configuration without restarting (radio settings cannot be changed)
|
||||
admin_commands = repeater,webviewer,reload
|
||||
|
||||
[Plugin_Overrides]
|
||||
# Plugin Overrides - Use alternative plugin implementations
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Reload Command
|
||||
Allows admin users to reload the bot configuration without restarting
|
||||
"""
|
||||
|
||||
from .base_command import BaseCommand
|
||||
from ..models import MeshMessage
|
||||
|
||||
|
||||
class ReloadCommand(BaseCommand):
|
||||
"""Command for reloading bot configuration"""
|
||||
|
||||
# Plugin metadata
|
||||
name = "reload"
|
||||
keywords = ["reload", "reloadconfig", "configreload"]
|
||||
description = "Reload bot configuration without restart (DM only, admin only)"
|
||||
requires_dm = True
|
||||
cooldown_seconds = 2
|
||||
category = "admin"
|
||||
|
||||
def __init__(self, bot):
|
||||
"""Initialize the reload command.
|
||||
|
||||
Args:
|
||||
bot: The bot instance.
|
||||
"""
|
||||
super().__init__(bot)
|
||||
|
||||
def can_execute(self, message: MeshMessage) -> bool:
|
||||
"""Check if this command can be executed (admin only)"""
|
||||
if not self.requires_admin_access():
|
||||
return False
|
||||
return super().can_execute(message)
|
||||
|
||||
def requires_admin_access(self) -> bool:
|
||||
"""Reload command requires admin access"""
|
||||
return True
|
||||
|
||||
def get_help_text(self) -> str:
|
||||
"""Get help text for the reload command.
|
||||
|
||||
Returns:
|
||||
str: The help text for this command.
|
||||
"""
|
||||
return ("Reloads the bot configuration from config.ini without restarting.\n"
|
||||
"Note: Radio/connection settings cannot be changed via reload.\n"
|
||||
"If radio settings changed, restart the bot instead.\n"
|
||||
"Usage: reload")
|
||||
|
||||
async def execute(self, message: MeshMessage) -> bool:
|
||||
"""Execute the reload command.
|
||||
|
||||
Args:
|
||||
message: The message triggering the command.
|
||||
|
||||
Returns:
|
||||
bool: True if executed successfully, False otherwise.
|
||||
"""
|
||||
# Call the bot's reload_config method
|
||||
success, msg = self.bot.reload_config()
|
||||
|
||||
if success:
|
||||
await self.send_response(message, f"✓ {msg}")
|
||||
else:
|
||||
await self.send_response(message, f"✗ {msg}")
|
||||
|
||||
return True
|
||||
+134
-1
@@ -16,7 +16,7 @@ import signal
|
||||
import atexit
|
||||
import sqlite3
|
||||
from pathlib import Path
|
||||
from typing import Optional, List, Dict, Any
|
||||
from typing import Optional, List, Dict, Any, Tuple
|
||||
from dataclasses import dataclass
|
||||
|
||||
# Import the official meshcore package
|
||||
@@ -218,6 +218,139 @@ class MeshCoreBot:
|
||||
|
||||
self.config.read(self.config_file)
|
||||
|
||||
def _get_radio_settings(self) -> Dict[str, Any]:
|
||||
"""Get current radio/connection settings from config.
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: Dictionary containing all radio-related settings.
|
||||
"""
|
||||
return {
|
||||
'connection_type': self.config.get('Connection', 'connection_type', fallback='ble').lower(),
|
||||
'serial_port': self.config.get('Connection', 'serial_port', fallback=''),
|
||||
'ble_device_name': self.config.get('Connection', 'ble_device_name', fallback=''),
|
||||
'hostname': self.config.get('Connection', 'hostname', fallback=''),
|
||||
'tcp_port': self.config.getint('Connection', 'tcp_port', fallback=5000),
|
||||
'timeout': self.config.getint('Connection', 'timeout', fallback=30),
|
||||
}
|
||||
|
||||
def reload_config(self) -> Tuple[bool, str]:
|
||||
"""Reload configuration from file without restarting the bot.
|
||||
|
||||
This method reloads the configuration file and updates all components
|
||||
that depend on it. It will reject the reload if radio/connection settings
|
||||
have changed, as those require a full restart.
|
||||
|
||||
Returns:
|
||||
Tuple[bool, str]: (success, message) tuple indicating if reload succeeded
|
||||
and a descriptive message.
|
||||
"""
|
||||
try:
|
||||
# Store current radio settings before reload
|
||||
old_radio_settings = self._get_radio_settings()
|
||||
|
||||
# Create a temporary config parser to check new settings
|
||||
import configparser
|
||||
new_config = configparser.ConfigParser()
|
||||
if not Path(self.config_file).exists():
|
||||
return (False, "Config file not found")
|
||||
|
||||
new_config.read(self.config_file)
|
||||
|
||||
# Get new radio settings
|
||||
new_radio_settings = {
|
||||
'connection_type': new_config.get('Connection', 'connection_type', fallback='ble').lower(),
|
||||
'serial_port': new_config.get('Connection', 'serial_port', fallback=''),
|
||||
'ble_device_name': new_config.get('Connection', 'ble_device_name', fallback=''),
|
||||
'hostname': new_config.get('Connection', 'hostname', fallback=''),
|
||||
'tcp_port': new_config.getint('Connection', 'tcp_port', fallback=5000),
|
||||
'timeout': new_config.getint('Connection', 'timeout', fallback=30),
|
||||
}
|
||||
|
||||
# Check if radio settings changed
|
||||
if old_radio_settings != new_radio_settings:
|
||||
changed_settings = []
|
||||
for key in old_radio_settings:
|
||||
if old_radio_settings[key] != new_radio_settings[key]:
|
||||
changed_settings.append(f"{key}: {old_radio_settings[key]} -> {new_radio_settings[key]}")
|
||||
return (False, f"Radio settings changed. Restart required. Changes: {', '.join(changed_settings)}")
|
||||
|
||||
# Radio settings unchanged, proceed with reload
|
||||
self.logger.info("Reloading configuration (radio settings unchanged)")
|
||||
|
||||
# Reload the config
|
||||
self.config.read(self.config_file)
|
||||
|
||||
# Update rate limiters
|
||||
new_rate_limit = self.config.getint('Bot', 'rate_limit_seconds', fallback=10)
|
||||
self.rate_limiter = RateLimiter(new_rate_limit)
|
||||
|
||||
new_bot_tx_rate_limit = self.config.getfloat('Bot', 'bot_tx_rate_limit_seconds', fallback=1.0)
|
||||
self.bot_tx_rate_limiter = BotTxRateLimiter(new_bot_tx_rate_limit)
|
||||
|
||||
new_nominatim_rate_limit = self.config.getfloat('Bot', 'nominatim_rate_limit_seconds', fallback=1.1)
|
||||
self.nominatim_rate_limiter = NominatimRateLimiter(new_nominatim_rate_limit)
|
||||
|
||||
# Update transmission delay
|
||||
self.tx_delay_ms = self.config.getint('Bot', 'tx_delay_ms', fallback=250)
|
||||
|
||||
# Update translator if language changed
|
||||
try:
|
||||
new_language = self.config.get('Localization', 'language', fallback='en')
|
||||
new_translation_path = self.config.get('Localization', 'translation_path', fallback='translations/')
|
||||
if (not hasattr(self, 'translator') or
|
||||
getattr(self.translator, 'language', None) != new_language or
|
||||
getattr(self.translator, 'translation_path', None) != new_translation_path):
|
||||
self.translator = Translator(new_language, new_translation_path)
|
||||
self.logger.info(f"Translator reloaded with language: {new_language}")
|
||||
|
||||
# Reload translated keywords for all commands
|
||||
if hasattr(self, 'command_manager'):
|
||||
for cmd_name, cmd_instance in self.command_manager.commands.items():
|
||||
if hasattr(cmd_instance, '_load_translated_keywords'):
|
||||
cmd_instance._load_translated_keywords()
|
||||
except (OSError, ValueError, FileNotFoundError, json.JSONDecodeError) as e:
|
||||
self.logger.warning(f"Failed to reload translator: {e}")
|
||||
|
||||
# Update solar conditions config
|
||||
set_config(self.config)
|
||||
|
||||
# Update command manager (keywords, custom syntax, banned users, monitor channels)
|
||||
if hasattr(self, 'command_manager'):
|
||||
self.command_manager.keywords = self.command_manager.load_keywords()
|
||||
self.command_manager.custom_syntax = self.command_manager.load_custom_syntax()
|
||||
self.command_manager.banned_users = self.command_manager.load_banned_users()
|
||||
self.command_manager.monitor_channels = self.command_manager.load_monitor_channels()
|
||||
self.logger.info("Command manager config reloaded")
|
||||
|
||||
# Update scheduler (scheduled messages)
|
||||
if hasattr(self, 'scheduler'):
|
||||
self.scheduler.setup_scheduled_messages()
|
||||
self.logger.info("Scheduler config reloaded")
|
||||
|
||||
# Update channel manager max_channels if changed
|
||||
if hasattr(self, 'channel_manager'):
|
||||
new_max_channels = self.config.getint('Bot', 'max_channels', fallback=40)
|
||||
if hasattr(self.channel_manager, 'max_channels'):
|
||||
old_max_channels = self.channel_manager.max_channels
|
||||
self.channel_manager.max_channels = new_max_channels
|
||||
if old_max_channels != new_max_channels:
|
||||
self.logger.info(f"Channel manager max_channels updated to {new_max_channels}")
|
||||
# Note: We don't invalidate the channel cache here because channels are fetched
|
||||
# from the device, not from config. The cache should remain valid after reload.
|
||||
|
||||
# Note: Service plugins check config on-demand, so they'll pick up changes automatically
|
||||
# Feed manager and other services that need explicit reload can be added here if needed
|
||||
|
||||
self.logger.info("Configuration reloaded successfully")
|
||||
return (True, "Configuration reloaded successfully")
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"Error reloading configuration: {e}"
|
||||
self.logger.error(error_msg)
|
||||
import traceback
|
||||
self.logger.error(traceback.format_exc())
|
||||
return (False, error_msg)
|
||||
|
||||
def create_default_config(self) -> None:
|
||||
"""Create default configuration file.
|
||||
|
||||
|
||||
@@ -42,6 +42,10 @@ class MessageScheduler:
|
||||
|
||||
def setup_scheduled_messages(self):
|
||||
"""Setup scheduled messages from config"""
|
||||
# Clear existing scheduled jobs to avoid duplicates on reload
|
||||
schedule.clear()
|
||||
self.scheduled_messages.clear()
|
||||
|
||||
if self.bot.config.has_section('Scheduled_Messages'):
|
||||
self.logger.info("Found Scheduled_Messages section")
|
||||
for time_str, message_info in self.bot.config.items('Scheduled_Messages'):
|
||||
|
||||
Reference in New Issue
Block a user