Add prefix time window configuration options

- Introduced `prefix_heard_days` and `prefix_free_days` settings in the configuration to define time windows for showing heard and free prefixes.
- Updated `PrefixCommand` to utilize these new settings for filtering repeaters based on their last heard time.
This commit is contained in:
agessaman
2025-11-02 21:41:21 -08:00
parent ee0ceb667b
commit b3ddef401c
3 changed files with 88 additions and 20 deletions
+12
View File
@@ -252,6 +252,18 @@ use_reverse_geocoding = true
# false: Show source information (default)
hide_source = false
# Prefix heard time window (days)
# Number of days to look back when showing prefix results (default command behavior)
# Only repeaters heard within this window will be shown by default
# Use "prefix XX all" to show all repeaters regardless of time
prefix_heard_days = 7
# Prefix free time window (days)
# Number of days to look back when determining which prefixes are "free"
# Only repeaters heard within this window will be considered as using a prefix
# Repeaters not heard in this window will be excluded from used prefixes list
prefix_free_days = 30
[Weather]
# Default state for city name disambiguation
# When users type "wx seattle", it will search for "seattle, WA, USA"
+64 -20
View File
@@ -40,14 +40,18 @@ class PrefixCommand(BaseCommand):
self.show_repeater_locations = self.bot.config.getboolean('Prefix_Command', 'show_repeater_locations', fallback=True)
self.use_reverse_geocoding = self.bot.config.getboolean('Prefix_Command', 'use_reverse_geocoding', fallback=True)
self.hide_source = self.bot.config.getboolean('Prefix_Command', 'hide_source', fallback=False)
# Get time window settings from config
self.prefix_heard_days = self.bot.config.getint('Prefix_Command', 'prefix_heard_days', fallback=7)
self.prefix_free_days = self.bot.config.getint('Prefix_Command', 'prefix_free_days', fallback=30)
def get_help_text(self) -> str:
if not self.api_url or self.api_url.strip() == "":
location_note = " (with city names)" if self.show_repeater_locations else ""
return f"Look up repeaters by two-character prefix using local database{location_note}. Usage: 'prefix 1A', 'prefix free' or 'prefix available' (list available prefixes). Note: API disabled - using local data only."
return f"Look up repeaters by two-character prefix using local database{location_note}. Usage: 'prefix 1A' (shows recent), 'prefix 1A all' (shows all), 'prefix free' (list available prefixes). Note: API disabled - using local data only."
location_note = " (with city names)" if self.show_repeater_locations else ""
return f"Look up repeaters by two-character prefix{location_note}. Usage: 'prefix 1A', 'prefix free' or 'prefix available' (list available prefixes), or 'prefix refresh'."
return f"Look up repeaters by two-character prefix{location_note}. Usage: 'prefix 1A' (shows recent), 'prefix 1A all' (shows all), 'prefix free' (list available prefixes), or 'prefix refresh'."
def matches_keyword(self, message: MeshMessage) -> bool:
"""Check if message starts with 'prefix' keyword"""
@@ -95,24 +99,38 @@ class PrefixCommand(BaseCommand):
response = "❌ Unable to determine free prefixes. Try 'prefix refresh' first."
return await self.send_response(message, response)
# Check for "all" modifier
include_all = False
if len(parts) >= 3 and parts[2].upper() == "ALL":
include_all = True
# Validate prefix format
if len(command) != 2 or not command.isalnum():
response = "❌ Invalid prefix format. Use two characters (e.g., prefix 1A)"
return await self.send_response(message, response)
# Get prefix data
prefix_data = await self.get_prefix_data(command)
prefix_data = await self.get_prefix_data(command, include_all=include_all)
if prefix_data is None:
response = f"❌ No repeaters found with prefix '{command}'"
return await self.send_response(message, response)
# Add include_all flag to data for formatting
prefix_data['include_all'] = include_all
# Format response
response = self.format_prefix_response(command, prefix_data)
return await self.send_response(message, response)
async def get_prefix_data(self, prefix: str) -> Optional[Dict[str, Any]]:
"""Get prefix data from API first, enhanced with local database location data"""
async def get_prefix_data(self, prefix: str, include_all: bool = False) -> Optional[Dict[str, Any]]:
"""Get prefix data from API first, enhanced with local database location data
Args:
prefix: The two-character prefix to look up
include_all: If True, show all repeaters regardless of last_heard time.
If False (default), only show repeaters heard within prefix_heard_days.
"""
# Only refresh cache if API is configured
if self.api_url and self.api_url.strip():
current_time = time.time()
@@ -125,7 +143,7 @@ class PrefixCommand(BaseCommand):
api_data = self.cache_data.get(prefix)
# Get local database data for location enhancement
db_data = await self.get_prefix_data_from_db(prefix)
db_data = await self.get_prefix_data_from_db(prefix, include_all=include_all)
# If we have API data, enhance it with local location data
if api_data and db_data:
@@ -269,19 +287,38 @@ class PrefixCommand(BaseCommand):
except Exception as e:
self.logger.error(f"Unexpected error refreshing cache: {e}")
async def get_prefix_data_from_db(self, prefix: str) -> Optional[Dict[str, Any]]:
"""Get prefix data from the bot's SQLite database as fallback"""
async def get_prefix_data_from_db(self, prefix: str, include_all: bool = False) -> Optional[Dict[str, Any]]:
"""Get prefix data from the bot's SQLite database as fallback
Args:
prefix: The two-character prefix to look up
include_all: If True, show all repeaters regardless of last_heard time.
If False (default), only show repeaters heard within prefix_heard_days.
"""
try:
self.logger.info(f"Looking up prefix '{prefix}' in local database")
if include_all:
self.logger.info(f"Looking up prefix '{prefix}' in local database (all entries)")
else:
self.logger.info(f"Looking up prefix '{prefix}' in local database (last {self.prefix_heard_days} days)")
# Query the complete_contact_tracking table for repeaters with matching prefix
# Include inactive repeaters for location enhancement (they still have valid location data)
query = '''
SELECT name, public_key, device_type, last_heard as last_seen, latitude, longitude, city, state, country, role
FROM complete_contact_tracking
WHERE public_key LIKE ? AND role IN ('repeater', 'roomserver')
ORDER BY name
'''
# By default, only include repeaters heard within prefix_heard_days
# If include_all is True, include all repeaters regardless of last_heard time
if include_all:
query = '''
SELECT name, public_key, device_type, last_heard as last_seen, latitude, longitude, city, state, country, role
FROM complete_contact_tracking
WHERE public_key LIKE ? AND role IN ('repeater', 'roomserver')
ORDER BY name
'''
else:
query = f'''
SELECT name, public_key, device_type, last_heard as last_seen, latitude, longitude, city, state, country, role
FROM complete_contact_tracking
WHERE public_key LIKE ? AND role IN ('repeater', 'roomserver')
AND last_heard >= datetime('now', '-{self.prefix_heard_days} days')
ORDER BY name
'''
# The prefix should match the first two characters of the public key
prefix_pattern = f"{prefix}%"
@@ -385,13 +422,14 @@ class PrefixCommand(BaseCommand):
self.logger.info(f"Found {len(used_prefixes)} used prefixes from API cache")
# Add prefixes from database
# Add prefixes from database (filtered by prefix_free_days)
try:
query = '''
query = f'''
SELECT DISTINCT SUBSTR(public_key, 1, 2) as prefix
FROM complete_contact_tracking
WHERE role IN ('repeater', 'roomserver')
AND LENGTH(public_key) >= 2
AND last_heard >= datetime('now', '-{self.prefix_free_days} days')
'''
results = self.bot.db_manager.execute_query(query)
for row in results:
@@ -456,13 +494,19 @@ class PrefixCommand(BaseCommand):
node_count = data['node_count']
node_names = data['node_names']
source = data.get('source', 'api')
include_all = data.get('include_all', True) # Default to True for API responses
# Get bot name for database responses
bot_name = self.bot.config.get('Bot', 'bot_name', fallback='Bot')
if source == 'database':
# Database response format
response = f"{bot_name} has heard {node_count} repeater{'s' if node_count != 1 else ''} with prefix {prefix}:\n"
# Database response format - keep brief for character limit
if include_all:
response = f"Prefix {prefix}: {node_count} repeater{'s' if node_count != 1 else ''}\n"
else:
# Show time period for default behavior - use abbreviated form
days_str = f"{self.prefix_heard_days}d" if self.prefix_heard_days != 7 else "7d"
response = f"Prefix {prefix}: {node_count} repeater{'s' if node_count != 1 else ''} ({days_str})\n"
else:
# API response format
response = f"📡 Prefix {prefix} ({node_count} repeater{'s' if node_count != 1 else ''}):\n"
+12
View File
@@ -367,6 +367,18 @@ use_reverse_geocoding = true
# false: Show source information (default)
hide_source = false
# Prefix heard time window (days)
# Number of days to look back when showing prefix results (default command behavior)
# Only repeaters heard within this window will be shown by default
# Use "prefix XX all" to show all repeaters regardless of time
prefix_heard_days = 7
# Prefix free time window (days)
# Number of days to look back when determining which prefixes are "free"
# Only repeaters heard within this window will be considered as using a prefix
# Repeaters not heard in this window will be excluded from used prefixes list
prefix_free_days = 30
[Weather]
# Default state for city name disambiguation
# When users type "wx seattle", it will search for "seattle, WA, USA"