mirror of
https://github.com/agessaman/meshcore-bot.git
synced 2026-05-24 16:25:19 +00:00
Updated database schema for complete contact tracking and optimized handling of repeater commands. Revised method for removing contacts from the device to use meshcore.py.
This commit is contained in:
@@ -177,16 +177,47 @@ class PathCommand(BaseCommand):
|
||||
}
|
||||
continue
|
||||
|
||||
# Fallback to database if API cache doesn't have this prefix
|
||||
query = '''
|
||||
SELECT name, public_key, device_type, last_seen, is_active, latitude, longitude, city, state, country
|
||||
FROM repeater_contacts
|
||||
WHERE public_key LIKE ?
|
||||
ORDER BY is_active DESC, last_seen DESC
|
||||
'''
|
||||
# Fallback to complete tracking database if API cache doesn't have this prefix
|
||||
# First try complete tracking database (all heard contacts, filtered by role)
|
||||
if hasattr(self.bot, 'repeater_manager'):
|
||||
try:
|
||||
# Get repeater devices from complete database (repeaters and roomservers)
|
||||
complete_db = await self.bot.repeater_manager.get_repeater_devices(include_historical=True)
|
||||
|
||||
results = []
|
||||
for row in complete_db:
|
||||
if row['public_key'].startswith(node_id):
|
||||
results.append({
|
||||
'name': row['name'],
|
||||
'public_key': row['public_key'],
|
||||
'device_type': row['device_type'],
|
||||
'last_seen': row['last_heard'],
|
||||
'is_active': row['is_currently_tracked'],
|
||||
'latitude': row['latitude'],
|
||||
'longitude': row['longitude'],
|
||||
'city': row['city'],
|
||||
'state': row['state'],
|
||||
'country': row['country'],
|
||||
'advert_count': row['advert_count'],
|
||||
'signal_strength': row['signal_strength'],
|
||||
'hop_count': row['hop_count'],
|
||||
'role': row['role']
|
||||
})
|
||||
except Exception as e:
|
||||
self.logger.debug(f"Error getting complete database: {e}")
|
||||
results = []
|
||||
|
||||
prefix_pattern = f"{node_id}%"
|
||||
results = self.bot.db_manager.execute_query(query, (prefix_pattern,))
|
||||
# Fallback to legacy database if complete tracking fails
|
||||
if not results:
|
||||
query = '''
|
||||
SELECT name, public_key, device_type, last_seen, is_active, latitude, longitude, city, state, country
|
||||
FROM repeater_contacts
|
||||
WHERE public_key LIKE ?
|
||||
ORDER BY is_active DESC, last_seen DESC
|
||||
'''
|
||||
|
||||
prefix_pattern = f"{node_id}%"
|
||||
results = self.bot.db_manager.execute_query(query, (prefix_pattern,))
|
||||
|
||||
if results:
|
||||
# Check for ID collisions (multiple repeaters with same prefix)
|
||||
|
||||
@@ -87,6 +87,16 @@ class RepeaterCommand(BaseCommand):
|
||||
batch_size = int(arg)
|
||||
break
|
||||
response = await self._handle_update_geolocation(dry_run, batch_size)
|
||||
elif subcommand == "auto-purge":
|
||||
response = await self._handle_auto_purge(args)
|
||||
elif subcommand == "purge-status":
|
||||
response = await self._handle_purge_status()
|
||||
elif subcommand == "test-purge":
|
||||
response = await self._handle_test_purge()
|
||||
elif subcommand == "debug-purge":
|
||||
response = await self._handle_debug_purge()
|
||||
elif subcommand == "geocode":
|
||||
response = await self._handle_geocode(args)
|
||||
elif subcommand == "help":
|
||||
response = self.get_help()
|
||||
else:
|
||||
@@ -397,24 +407,22 @@ class RepeaterCommand(BaseCommand):
|
||||
try:
|
||||
stats = await self.bot.repeater_manager.get_purging_stats()
|
||||
|
||||
lines = []
|
||||
lines.append("📊 **Repeater Management Statistics**")
|
||||
lines.append("")
|
||||
lines.append(f"📡 Total repeaters cataloged: {stats.get('total_repeaters', 0)}")
|
||||
lines.append(f"🟢 Active repeaters: {stats.get('active_repeaters', 0)}")
|
||||
lines.append(f"🔴 Purged repeaters: {stats.get('purged_repeaters', 0)}")
|
||||
lines.append("")
|
||||
# Shortened for LoRa transmission
|
||||
total = stats.get('total_repeaters', 0)
|
||||
active = stats.get('active_repeaters', 0)
|
||||
purged = stats.get('purged_repeaters', 0)
|
||||
|
||||
response = f"📊 Stats: {total} total, {active} active, {purged} purged"
|
||||
|
||||
recent_activity = stats.get('recent_activity_7_days', {})
|
||||
if recent_activity:
|
||||
lines.append("📈 **Recent Activity (7 days):**")
|
||||
activity_summary = []
|
||||
for action, count in recent_activity.items():
|
||||
action_icon = {"added": "➕", "purged": "➖", "restored": "🔄"}.get(action, "📝")
|
||||
lines.append(f" {action_icon} {action.title()}: {count}")
|
||||
else:
|
||||
lines.append("📈 **Recent Activity (7 days):** None")
|
||||
activity_summary.append(f"{action}:{count}")
|
||||
if activity_summary:
|
||||
response += f" | 7d: {', '.join(activity_summary)}"
|
||||
|
||||
return "\n".join(lines)
|
||||
return response
|
||||
|
||||
except Exception as e:
|
||||
return f"❌ Error getting statistics: {e}"
|
||||
@@ -430,21 +438,20 @@ class RepeaterCommand(BaseCommand):
|
||||
if not status:
|
||||
return "❌ Failed to get contact list status"
|
||||
|
||||
lines = []
|
||||
lines.append("📊 **Contact Status**")
|
||||
lines.append(f"📱 {status['current_contacts']}/{status['estimated_limit']} ({status['usage_percentage']:.0f}%)")
|
||||
lines.append(f"👥 {status['companion_count']} companions, 📡 {status['repeater_count']} repeaters")
|
||||
lines.append(f"⏰ {status['stale_contacts_count']} stale contacts")
|
||||
# Shortened for LoRa transmission
|
||||
current = status['current_contacts']
|
||||
limit = status['estimated_limit']
|
||||
usage = status['usage_percentage']
|
||||
companions = status['companion_count']
|
||||
repeaters = status['repeater_count']
|
||||
stale = status['stale_contacts_count']
|
||||
|
||||
# Status indicators
|
||||
if status['is_at_limit']:
|
||||
lines.append("🚨 **CRITICAL**: 95%+ full!")
|
||||
return f"📊 {current}/{limit} ({usage:.0f}%) | 👥{companions} 📡{repeaters} ⏰{stale} | 🚨 FULL!"
|
||||
elif status['is_near_limit']:
|
||||
lines.append("⚠️ **WARNING**: 80%+ full")
|
||||
return f"📊 {current}/{limit} ({usage:.0f}%) | 👥{companions} 📡{repeaters} ⏰{stale} | ⚠️ NEAR"
|
||||
else:
|
||||
lines.append("✅ Adequate space")
|
||||
|
||||
return "\n".join(lines)
|
||||
return f"📊 {current}/{limit} ({usage:.0f}%) | 👥{companions} 📡{repeaters} ⏰{stale} | ✅ OK"
|
||||
|
||||
except Exception as e:
|
||||
return f"❌ Error getting contact status: {e}"
|
||||
@@ -553,6 +560,200 @@ class RepeaterCommand(BaseCommand):
|
||||
except Exception as e:
|
||||
return f"❌ Error discovering contacts: {e}"
|
||||
|
||||
async def _handle_stats(self) -> str:
|
||||
"""Show statistics about the complete repeater tracking database"""
|
||||
if not hasattr(self.bot, 'repeater_manager'):
|
||||
return "Repeater manager not initialized. Please check bot configuration."
|
||||
|
||||
try:
|
||||
stats = await self.bot.repeater_manager.get_contact_statistics()
|
||||
|
||||
response = "📊 **Complete Contact Tracking Statistics:**\n\n"
|
||||
response += f"• **Total Contacts Ever Heard:** {stats.get('total_heard', 0)}\n"
|
||||
response += f"• **Currently Tracked by Device:** {stats.get('currently_tracked', 0)}\n"
|
||||
response += f"• **Recent Activity (24h):** {stats.get('recent_activity', 0)}\n\n"
|
||||
|
||||
if stats.get('by_role'):
|
||||
response += "**By MeshCore Role:**\n"
|
||||
# Display roles in logical order
|
||||
role_order = ['repeater', 'roomserver', 'companion', 'sensor', 'gateway', 'bot']
|
||||
for role in role_order:
|
||||
if role in stats['by_role']:
|
||||
count = stats['by_role'][role]
|
||||
role_display = role.title()
|
||||
if role == 'roomserver':
|
||||
role_display = 'RoomServer'
|
||||
response += f"• {role_display}: {count}\n"
|
||||
|
||||
# Show any other roles not in the standard list
|
||||
for role, count in stats['by_role'].items():
|
||||
if role not in role_order:
|
||||
response += f"• {role.title()}: {count}\n"
|
||||
response += "\n"
|
||||
|
||||
if stats.get('by_type'):
|
||||
response += "**By Device Type:**\n"
|
||||
for device_type, count in stats['by_type'].items():
|
||||
response += f"• {device_type}: {count}\n"
|
||||
|
||||
return response
|
||||
|
||||
except Exception as e:
|
||||
return f"❌ Error getting repeater statistics: {e}"
|
||||
|
||||
async def _handle_auto_purge(self, args: List[str]) -> str:
|
||||
"""Handle auto-purge commands"""
|
||||
if not hasattr(self.bot, 'repeater_manager'):
|
||||
return "Repeater manager not initialized. Please check bot configuration."
|
||||
|
||||
try:
|
||||
if not args:
|
||||
# Show auto-purge status
|
||||
status = await self.bot.repeater_manager.get_auto_purge_status()
|
||||
# Shortened for LoRa transmission (130 char limit)
|
||||
current = status.get('current_count', 0)
|
||||
limit = status.get('contact_limit', 300)
|
||||
usage = status.get('usage_percentage', 0)
|
||||
enabled = status.get('enabled', False)
|
||||
|
||||
if status.get('is_at_limit', False):
|
||||
response = f"🔄 Auto-Purge: {'ON' if enabled else 'OFF'} | {current}/{limit} ({usage:.0f}%) | 🚨 FULL!"
|
||||
elif status.get('is_near_limit', False):
|
||||
response = f"🔄 Auto-Purge: {'ON' if enabled else 'OFF'} | {current}/{limit} ({usage:.0f}%) | ⚠️ NEAR LIMIT"
|
||||
else:
|
||||
response = f"🔄 Auto-Purge: {'ON' if enabled else 'OFF'} | {current}/{limit} ({usage:.0f}%) | ✅ OK"
|
||||
|
||||
return response
|
||||
|
||||
elif args[0].lower() == "trigger":
|
||||
# Manually trigger auto-purge
|
||||
success = await self.bot.repeater_manager.check_and_auto_purge()
|
||||
if success:
|
||||
return "✅ Auto-purge triggered successfully"
|
||||
else:
|
||||
return "ℹ️ Auto-purge check completed (no purging needed or failed)"
|
||||
|
||||
elif args[0].lower() == "enable":
|
||||
# Enable auto-purge
|
||||
self.bot.repeater_manager.auto_purge_enabled = True
|
||||
return "✅ Auto-purge enabled"
|
||||
|
||||
elif args[0].lower() == "disable":
|
||||
# Disable auto-purge
|
||||
self.bot.repeater_manager.auto_purge_enabled = False
|
||||
return "❌ Auto-purge disabled"
|
||||
|
||||
elif args[0].lower() == "monitor":
|
||||
# Run periodic monitoring
|
||||
await self.bot.repeater_manager.periodic_contact_monitoring()
|
||||
return "📊 Periodic contact monitoring completed"
|
||||
|
||||
else:
|
||||
return "❌ Unknown auto-purge command. Use: `!repeater auto-purge [trigger|enable|disable|monitor]`"
|
||||
|
||||
except Exception as e:
|
||||
return f"❌ Error with auto-purge command: {e}"
|
||||
|
||||
async def _handle_purge_status(self) -> str:
|
||||
"""Show detailed purge status and recommendations"""
|
||||
if not hasattr(self.bot, 'repeater_manager'):
|
||||
return "Repeater manager not initialized. Please check bot configuration."
|
||||
|
||||
try:
|
||||
status = await self.bot.repeater_manager.get_auto_purge_status()
|
||||
|
||||
# Shortened for LoRa transmission
|
||||
current = status.get('current_count', 0)
|
||||
limit = status.get('contact_limit', 300)
|
||||
usage = status.get('usage_percentage', 0)
|
||||
threshold = status.get('threshold', 280)
|
||||
enabled = status.get('enabled', False)
|
||||
|
||||
if status.get('is_at_limit', False):
|
||||
response = f"📊 Purge: {'ON' if enabled else 'OFF'} | {current}/{limit} ({usage:.0f}%) | 🚨 FULL! Run trigger now!"
|
||||
elif status.get('is_near_limit', False):
|
||||
response = f"📊 Purge: {'ON' if enabled else 'OFF'} | {current}/{limit} ({usage:.0f}%) | ⚠️ Near {threshold}"
|
||||
else:
|
||||
response = f"📊 Purge: {'ON' if enabled else 'OFF'} | {current}/{limit} ({usage:.0f}%) | ✅ Healthy"
|
||||
|
||||
return response
|
||||
|
||||
except Exception as e:
|
||||
return f"❌ Error getting purge status: {e}"
|
||||
|
||||
async def _handle_test_purge(self) -> str:
|
||||
"""Test the improved purge system"""
|
||||
if not hasattr(self.bot, 'repeater_manager'):
|
||||
return "Repeater manager not initialized. Please check bot configuration."
|
||||
|
||||
try:
|
||||
result = await self.bot.repeater_manager.test_purge_system()
|
||||
|
||||
if result.get('success', False):
|
||||
# Shortened for LoRa transmission
|
||||
contact = result.get('test_contact', 'Unknown')
|
||||
initial = result.get('initial_count', 0)
|
||||
final = result.get('final_count', 0)
|
||||
removed = result.get('contacts_removed', 0)
|
||||
method = result.get('purge_method', 'Unknown')
|
||||
response = f"🧪 Test: {contact} | {initial}→{final} (-{removed}) | {method} | ✅ OK"
|
||||
else:
|
||||
# Shortened for LoRa transmission
|
||||
error = result.get('error', 'Unknown error')
|
||||
count = result.get('contact_count', 0)
|
||||
response = f"🧪 Test FAILED | {count} contacts | {error[:50]}..."
|
||||
|
||||
return response
|
||||
|
||||
except Exception as e:
|
||||
return f"❌ Error testing purge system: {e}"
|
||||
|
||||
async def _handle_debug_purge(self) -> str:
|
||||
"""Debug the purge system to see what repeaters are available"""
|
||||
if not hasattr(self.bot, 'repeater_manager'):
|
||||
return "Repeater manager not initialized. Please check bot configuration."
|
||||
|
||||
try:
|
||||
# Get device contact info
|
||||
total_contacts = len(self.bot.meshcore.contacts)
|
||||
repeater_count = 0
|
||||
repeaters_info = []
|
||||
|
||||
for contact_key, contact_data in self.bot.meshcore.contacts.items():
|
||||
if self.bot.repeater_manager._is_repeater_device(contact_data):
|
||||
repeater_count += 1
|
||||
name = contact_data.get('adv_name', contact_data.get('name', 'Unknown'))
|
||||
device_type = 'Repeater'
|
||||
if contact_data.get('type') == 3:
|
||||
device_type = 'RoomServer'
|
||||
|
||||
last_seen = contact_data.get('last_seen', contact_data.get('last_advert', 'Unknown'))
|
||||
repeaters_info.append(f"• {name} ({device_type}) - Last seen: {last_seen}")
|
||||
|
||||
# Shortened for LoRa transmission
|
||||
response = f"🔍 Debug: {total_contacts} total, {repeater_count} repeaters"
|
||||
|
||||
if repeaters_info:
|
||||
# Show first 3 repeaters only
|
||||
for info in repeaters_info[:3]:
|
||||
response += f" | {info[:30]}..."
|
||||
if len(repeaters_info) > 3:
|
||||
response += f" | +{len(repeaters_info) - 3} more"
|
||||
else:
|
||||
response += " | ❌ No repeaters found"
|
||||
|
||||
# Test the purge selection
|
||||
test_repeaters = await self.bot.repeater_manager._get_repeaters_for_purging(3)
|
||||
if test_repeaters:
|
||||
response += f" | Test: {len(test_repeaters)} available"
|
||||
else:
|
||||
response += " | Test: ❌ None available"
|
||||
|
||||
return response
|
||||
|
||||
except Exception as e:
|
||||
return f"❌ Error debugging purge system: {e}"
|
||||
|
||||
async def _handle_auto(self, args: List[str]) -> str:
|
||||
"""Toggle manual contact addition setting"""
|
||||
if not hasattr(self.bot, 'repeater_manager'):
|
||||
@@ -768,6 +969,11 @@ class RepeaterCommand(BaseCommand):
|
||||
• `manage` - Manage contact list to prevent hitting limits
|
||||
• `manage --dry-run` - Show what management actions would be taken
|
||||
• `add <name> [key]` - Add a discovered contact to contact list
|
||||
• `auto-purge` - Show auto-purge status and controls
|
||||
• `auto-purge trigger` - Manually trigger auto-purge
|
||||
• `auto-purge enable/disable` - Enable/disable auto-purge
|
||||
• `purge-status` - Show detailed purge status and recommendations
|
||||
• `test-purge` - Test the improved purge system
|
||||
• `discover` - Discover companion contacts
|
||||
• `auto <on|off>` - Toggle manual contact addition setting
|
||||
• `test` - Test meshcore-cli command functionality
|
||||
@@ -779,12 +985,17 @@ class RepeaterCommand(BaseCommand):
|
||||
• `!repeater manage --dry-run` - Preview management actions
|
||||
• `!repeater add "John"` - Add contact named John
|
||||
• `!repeater discover` - Discover new companion contacts
|
||||
• `!repeater auto-purge` - Check auto-purge status
|
||||
• `!repeater auto-purge trigger` - Manually trigger auto-purge
|
||||
• `!repeater purge-status` - Detailed purge status
|
||||
• `!repeater auto off` - Disable manual contact addition
|
||||
• `!repeater test` - Test meshcore-cli commands
|
||||
• `!repeater purge all` - Purge all repeaters
|
||||
• `!repeater purge all force` - Force purge all repeaters
|
||||
• `!repeater purge 30` - Purge repeaters older than 30 days
|
||||
• `!repeater stats` - Show management statistics
|
||||
• `!repeater geocode` - Show geocoding status
|
||||
• `!repeater geocode trigger` - Manually trigger geocoding
|
||||
|
||||
**Note:** This system helps manage both repeater contacts and overall contact list capacity. It automatically removes stale contacts and old repeaters when approaching device limits.
|
||||
|
||||
@@ -795,3 +1006,66 @@ class RepeaterCommand(BaseCommand):
|
||||
• `auto_manage_contacts = device`: Device handles auto-addition, bot manages capacity
|
||||
• `auto_manage_contacts = bot`: Bot automatically adds companion contacts and manages capacity
|
||||
• `auto_manage_contacts = false`: Manual mode - use !repeater commands to manage contacts"""
|
||||
|
||||
async def _handle_geocode(self, args: List[str]) -> str:
|
||||
"""Handle geocoding commands"""
|
||||
if not hasattr(self.bot, 'repeater_manager'):
|
||||
return "Repeater manager not initialized. Please check bot configuration."
|
||||
|
||||
try:
|
||||
if not args:
|
||||
# Show geocoding status
|
||||
status = await self._get_geocoding_status()
|
||||
return status
|
||||
elif args[0] == "trigger":
|
||||
# Manually trigger background geocoding
|
||||
await self.bot.repeater_manager._background_geocoding()
|
||||
return "🌍 Background geocoding triggered"
|
||||
elif args[0] == "status":
|
||||
# Show detailed geocoding status
|
||||
return await self._get_geocoding_status()
|
||||
else:
|
||||
return "Usage: !repeater geocode [trigger|status]\n trigger - Manually trigger geocoding\n status - Show geocoding status"
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error in geocoding command: {e}")
|
||||
return f"❌ Geocoding error: {e}"
|
||||
|
||||
async def _get_geocoding_status(self) -> str:
|
||||
"""Get geocoding status"""
|
||||
try:
|
||||
# Count contacts needing geocoding
|
||||
needing_geocoding = self.bot.repeater_manager.db_manager.execute_query('''
|
||||
SELECT COUNT(*) as count
|
||||
FROM complete_contact_tracking
|
||||
WHERE latitude IS NOT NULL
|
||||
AND longitude IS NOT NULL
|
||||
AND (city IS NULL OR city = '')
|
||||
AND last_geocoding_attempt IS NULL
|
||||
''')
|
||||
|
||||
# Count contacts with geocoding data
|
||||
with_geocoding = self.bot.repeater_manager.db_manager.execute_query('''
|
||||
SELECT COUNT(*) as count
|
||||
FROM complete_contact_tracking
|
||||
WHERE city IS NOT NULL AND city != ''
|
||||
''')
|
||||
|
||||
# Count total contacts with coordinates
|
||||
with_coords = self.bot.repeater_manager.db_manager.execute_query('''
|
||||
SELECT COUNT(*) as count
|
||||
FROM complete_contact_tracking
|
||||
WHERE latitude IS NOT NULL AND longitude IS NOT NULL
|
||||
''')
|
||||
|
||||
needing = needing_geocoding[0]['count'] if needing_geocoding else 0
|
||||
with_geo = with_geocoding[0]['count'] if with_geocoding else 0
|
||||
total_coords = with_coords[0]['count'] if with_coords else 0
|
||||
|
||||
# Shortened for LoRa (130 char limit)
|
||||
if needing > 0:
|
||||
return f"🌍 Geocoding: {with_geo}/{total_coords} done, {needing} pending"
|
||||
else:
|
||||
return f"🌍 Geocoding: {with_geo}/{total_coords} complete ✅"
|
||||
|
||||
except Exception as e:
|
||||
return f"❌ Geocoding status error: {e}"
|
||||
|
||||
+8
-3
@@ -514,15 +514,20 @@ use_zulu_time = false
|
||||
# Subscribe to RAW_DATA events for full packet data
|
||||
self.meshcore.subscribe(EventType.RAW_DATA, on_raw_data)
|
||||
|
||||
# Subscribe to NEW_CONTACT events for automatic contact management
|
||||
self.meshcore.subscribe(EventType.NEW_CONTACT, on_new_contact)
|
||||
|
||||
# Note: Debug mode commands are not available in current meshcore-cli version
|
||||
# The meshcore library handles debug output automatically when needed
|
||||
|
||||
# Start auto message fetching
|
||||
await self.meshcore.start_auto_message_fetching()
|
||||
|
||||
# Delay NEW_CONTACT subscription to ensure device is fully ready
|
||||
self.logger.info("Delaying NEW_CONTACT subscription to ensure device readiness...")
|
||||
await asyncio.sleep(5) # Wait 5 seconds for device to be fully ready
|
||||
|
||||
# Subscribe to NEW_CONTACT events for automatic contact management
|
||||
self.meshcore.subscribe(EventType.NEW_CONTACT, on_new_contact)
|
||||
self.logger.info("NEW_CONTACT subscription active - ready to receive new contact events")
|
||||
|
||||
self.logger.info("Message handlers setup complete")
|
||||
|
||||
async def start(self):
|
||||
|
||||
+128
-7
@@ -5,7 +5,10 @@ Processes incoming messages and routes them to appropriate command handlers
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
from typing import Optional
|
||||
import time
|
||||
import json
|
||||
import re
|
||||
from typing import List, Optional, Dict, Any
|
||||
from meshcore import EventType
|
||||
|
||||
from .models import MeshMessage
|
||||
@@ -295,6 +298,8 @@ class MessageHandler:
|
||||
try:
|
||||
payload = event.payload
|
||||
self.logger.info(f"📦 RAW_DATA EVENT RECEIVED: {payload}")
|
||||
self.logger.info(f"📦 Event type: {type(event)}")
|
||||
self.logger.info(f"📦 Metadata: {metadata}")
|
||||
|
||||
# This should contain the full packet data we need
|
||||
if hasattr(payload, 'data') or 'data' in payload:
|
||||
@@ -314,6 +319,9 @@ class MessageHandler:
|
||||
packet_info = self.decode_meshcore_packet(raw_hex)
|
||||
if packet_info:
|
||||
self.logger.info(f"✅ SUCCESSFULLY DECODED RAW PACKET: {packet_info}")
|
||||
|
||||
# Check if this is an advertisement packet and track it
|
||||
await self._process_advertisement_packet(packet_info, metadata)
|
||||
else:
|
||||
self.logger.warning("❌ Failed to decode raw packet data")
|
||||
else:
|
||||
@@ -328,6 +336,44 @@ class MessageHandler:
|
||||
import traceback
|
||||
self.logger.error(traceback.format_exc())
|
||||
|
||||
async def _process_advertisement_packet(self, packet_info: Dict, metadata=None):
|
||||
"""Process advertisement packets for complete repeater tracking"""
|
||||
try:
|
||||
# Check if this is an advertisement packet
|
||||
if packet_info.get('payload_type') == 'ADVERT' or packet_info.get('type') == 'advert':
|
||||
self.logger.debug(f"Processing advertisement packet: {packet_info}")
|
||||
|
||||
# Extract repeater information
|
||||
advert_data = {
|
||||
'public_key': packet_info.get('sender_id', ''),
|
||||
'name': packet_info.get('name', packet_info.get('adv_name', 'Unknown')),
|
||||
'type': packet_info.get('device_type', 2), # Default to repeater
|
||||
'adv_name': packet_info.get('adv_name', packet_info.get('name', 'Unknown'))
|
||||
}
|
||||
|
||||
# Extract signal information from metadata
|
||||
signal_info = {}
|
||||
if metadata:
|
||||
signal_info.update(metadata)
|
||||
|
||||
# Add hop count if available
|
||||
if 'hops' in packet_info:
|
||||
signal_info['hops'] = packet_info['hops']
|
||||
|
||||
# Track this advertisement in the complete database
|
||||
if hasattr(self.bot, 'repeater_manager'):
|
||||
# Track all advertisements regardless of type
|
||||
success = await self.bot.repeater_manager.track_contact_advertisement(
|
||||
advert_data, signal_info
|
||||
)
|
||||
if success:
|
||||
self.logger.debug(f"Tracked contact advertisement: {advert_data.get('name', 'Unknown')}")
|
||||
else:
|
||||
self.logger.warning(f"Failed to track contact advertisement: {advert_data.get('name', 'Unknown')}")
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error processing advertisement packet: {e}")
|
||||
|
||||
async def handle_rf_log_data(self, event, metadata=None):
|
||||
"""Handle RF log data events to cache SNR information and store raw packet data"""
|
||||
try:
|
||||
@@ -1340,7 +1386,9 @@ class MessageHandler:
|
||||
async def handle_new_contact(self, event, metadata=None):
|
||||
"""Handle NEW_CONTACT events for automatic contact management"""
|
||||
try:
|
||||
self.logger.info(f"New contact discovered: {event}")
|
||||
self.logger.info(f"🔍 NEW_CONTACT EVENT RECEIVED: {event}")
|
||||
self.logger.info(f"📦 Event type: {type(event)}")
|
||||
self.logger.info(f"📦 Event payload: {event.payload if hasattr(event, 'payload') else 'No payload'}")
|
||||
|
||||
# Extract contact information from the event
|
||||
contact_data = event.payload if hasattr(event, 'payload') else event
|
||||
@@ -1355,16 +1403,89 @@ class MessageHandler:
|
||||
|
||||
self.logger.info(f"Processing new contact: {contact_name} (key: {public_key[:16]}...)")
|
||||
|
||||
# Check if this is a repeater (we don't want to auto-manage repeaters)
|
||||
# Extract additional signal information from the event
|
||||
signal_info = {}
|
||||
if metadata:
|
||||
signal_info.update(metadata)
|
||||
|
||||
# Try to get signal data from recent RF data correlation
|
||||
# Only collect RSSI/SNR for zero-hop (direct) advertisements
|
||||
try:
|
||||
# Look for recent RF data that might correlate with this contact
|
||||
recent_rf_data = self.bot.message_handler.recent_rf_data
|
||||
if recent_rf_data:
|
||||
# Find RF data that might match this contact's public key
|
||||
for rf_entry in recent_rf_data[-10:]: # Check last 10 RF entries
|
||||
if 'routing_info' in rf_entry:
|
||||
routing_info = rf_entry['routing_info']
|
||||
|
||||
# Only collect signal data for direct (zero-hop) advertisements
|
||||
path_length = routing_info.get('path_length', 0)
|
||||
if path_length == 0:
|
||||
# Direct advertisement - collect signal data
|
||||
if 'snr' in rf_entry:
|
||||
signal_info['snr'] = rf_entry['snr']
|
||||
if 'rssi' in rf_entry:
|
||||
signal_info['rssi'] = rf_entry['rssi']
|
||||
signal_info['hops'] = 0
|
||||
self.logger.debug(f"📡 Direct advertisement - collecting signal data: SNR={rf_entry.get('snr')}, RSSI={rf_entry.get('rssi')}")
|
||||
else:
|
||||
# Multi-hop advertisement - only collect hop count, not signal data
|
||||
signal_info['hops'] = path_length
|
||||
self.logger.debug(f"📡 Multi-hop advertisement ({path_length} hops) - skipping signal data collection")
|
||||
break
|
||||
except Exception as e:
|
||||
self.logger.debug(f"Could not correlate RF data: {e}")
|
||||
|
||||
# Log captured signal information
|
||||
if signal_info:
|
||||
self.logger.info(f"📡 Signal data: {signal_info}")
|
||||
else:
|
||||
self.logger.info(f"📡 No signal data available")
|
||||
|
||||
# Check if this is a repeater or companion
|
||||
if hasattr(self.bot, 'repeater_manager'):
|
||||
is_repeater = self.bot.repeater_manager._is_repeater_device(contact_data)
|
||||
|
||||
if is_repeater:
|
||||
self.logger.info(f"New contact '{contact_name}' is a repeater - will be managed by repeater system")
|
||||
# Let the repeater manager handle it
|
||||
await self.bot.repeater_manager.scan_and_catalog_repeaters()
|
||||
# REPEATER: Track directly in SQLite database (no device contact list)
|
||||
self.logger.info(f"📡 New repeater discovered: {contact_name} - tracking in database only")
|
||||
|
||||
# Track repeater in complete database with signal info
|
||||
await self.bot.repeater_manager.track_contact_advertisement(contact_data, signal_info)
|
||||
|
||||
# Check if auto-purge is needed (run after tracking to ensure data is captured)
|
||||
await self.bot.repeater_manager.check_and_auto_purge()
|
||||
|
||||
self.logger.info(f"✅ Repeater {contact_name} tracked in database - not added to device contacts")
|
||||
return
|
||||
else:
|
||||
# COMPANION: Track in database AND add to device contact list
|
||||
self.logger.info(f"👤 New companion discovered: {contact_name} - will be added to device contacts")
|
||||
|
||||
# Track companion in complete database with signal info
|
||||
await self.bot.repeater_manager.track_contact_advertisement(contact_data, signal_info)
|
||||
|
||||
# Add companion to device contact list
|
||||
try:
|
||||
result = await self.bot.meshcore.commands.add_contact(contact_data)
|
||||
if hasattr(result, 'type') and result.type.name == 'OK':
|
||||
self.logger.info(f"✅ Companion {contact_name} added to device contacts")
|
||||
else:
|
||||
self.logger.warning(f"❌ Failed to add companion {contact_name} to device: {result}")
|
||||
except Exception as e:
|
||||
self.logger.error(f"❌ Error adding companion {contact_name} to device: {e}")
|
||||
|
||||
# Check if auto-purge is needed
|
||||
await self.bot.repeater_manager.check_and_auto_purge()
|
||||
return
|
||||
|
||||
# For non-repeater contacts, handle based on auto_manage_contacts setting
|
||||
# Fallback: Track in database for unknown contact types
|
||||
if hasattr(self.bot, 'repeater_manager'):
|
||||
await self.bot.repeater_manager.track_contact_advertisement(contact_data)
|
||||
await self.bot.repeater_manager.check_and_auto_purge()
|
||||
|
||||
# For unknown contact types, handle based on auto_manage_contacts setting
|
||||
if hasattr(self.bot, 'repeater_manager'):
|
||||
auto_manage_setting = self.bot.config.get('Bot', 'auto_manage_contacts', fallback='false').lower()
|
||||
|
||||
|
||||
+751
-109
@@ -29,6 +29,11 @@ class RepeaterManager:
|
||||
|
||||
# Check for and handle database schema migration
|
||||
self._migrate_database_schema()
|
||||
|
||||
# Initialize auto-purge monitoring
|
||||
self.contact_limit = 300 # MeshCore device limit (will be updated from device info)
|
||||
self.auto_purge_threshold = 280 # Start purging when 280+ contacts
|
||||
self.auto_purge_enabled = True
|
||||
|
||||
def _init_repeater_tables(self):
|
||||
"""Initialize repeater-specific database tables"""
|
||||
@@ -51,6 +56,30 @@ class RepeaterManager:
|
||||
purge_count INTEGER DEFAULT 0
|
||||
''')
|
||||
|
||||
# Create complete_contact_tracking table for all heard contacts
|
||||
self.db_manager.create_table('complete_contact_tracking', '''
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
public_key TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
role TEXT NOT NULL,
|
||||
device_type TEXT,
|
||||
first_heard TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
last_heard TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
advert_count INTEGER DEFAULT 1,
|
||||
latitude REAL,
|
||||
longitude REAL,
|
||||
city TEXT,
|
||||
state TEXT,
|
||||
country TEXT,
|
||||
raw_advert_data TEXT,
|
||||
signal_strength REAL,
|
||||
hop_count INTEGER,
|
||||
is_currently_tracked BOOLEAN DEFAULT 0,
|
||||
last_advert_timestamp TIMESTAMP,
|
||||
location_accuracy REAL,
|
||||
contact_source TEXT DEFAULT 'advertisement'
|
||||
''')
|
||||
|
||||
# Create purging_log table for audit trail
|
||||
self.db_manager.create_table('purging_log', '''
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
@@ -68,6 +97,14 @@ class RepeaterManager:
|
||||
cursor.execute('CREATE INDEX IF NOT EXISTS idx_device_type ON repeater_contacts(device_type)')
|
||||
cursor.execute('CREATE INDEX IF NOT EXISTS idx_last_seen ON repeater_contacts(last_seen)')
|
||||
cursor.execute('CREATE INDEX IF NOT EXISTS idx_is_active ON repeater_contacts(is_active)')
|
||||
|
||||
# Indexes for complete contact tracking table
|
||||
cursor.execute('CREATE INDEX IF NOT EXISTS idx_complete_public_key ON complete_contact_tracking(public_key)')
|
||||
cursor.execute('CREATE INDEX IF NOT EXISTS idx_complete_role ON complete_contact_tracking(role)')
|
||||
cursor.execute('CREATE INDEX IF NOT EXISTS idx_complete_last_heard ON complete_contact_tracking(last_heard)')
|
||||
cursor.execute('CREATE INDEX IF NOT EXISTS idx_complete_currently_tracked ON complete_contact_tracking(is_currently_tracked)')
|
||||
cursor.execute('CREATE INDEX IF NOT EXISTS idx_complete_location ON complete_contact_tracking(latitude, longitude)')
|
||||
cursor.execute('CREATE INDEX IF NOT EXISTS idx_complete_role_tracked ON complete_contact_tracking(role, is_currently_tracked)')
|
||||
conn.commit()
|
||||
|
||||
self.logger.info("Repeater contacts database initialized successfully")
|
||||
@@ -105,6 +142,426 @@ class RepeaterManager:
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error during database schema migration: {e}")
|
||||
|
||||
async def track_contact_advertisement(self, advert_data: Dict, signal_info: Dict = None) -> bool:
|
||||
"""Track any contact advertisement in the complete tracking database"""
|
||||
try:
|
||||
# Extract basic information
|
||||
public_key = advert_data.get('public_key', '')
|
||||
name = advert_data.get('name', advert_data.get('adv_name', 'Unknown'))
|
||||
device_type = advert_data.get('type', 'Unknown')
|
||||
|
||||
if not public_key:
|
||||
self.logger.warning("No public key in advertisement data")
|
||||
return False
|
||||
|
||||
# Determine role and device type
|
||||
role = self._determine_contact_role(advert_data)
|
||||
device_type_str = self._determine_device_type(device_type, name)
|
||||
|
||||
# Extract location data
|
||||
self.logger.debug(f"🔍 Extracting location data for {name}...")
|
||||
location_info = self._extract_location_data(advert_data)
|
||||
self.logger.debug(f"📍 Location data extracted: {location_info}")
|
||||
|
||||
# Extract signal information
|
||||
signal_strength = None
|
||||
hop_count = None
|
||||
if signal_info:
|
||||
signal_strength = signal_info.get('rssi', signal_info.get('signal_strength'))
|
||||
hop_count = signal_info.get('hops', signal_info.get('hop_count'))
|
||||
|
||||
# Check if this contact is already in our complete tracking
|
||||
existing = self.db_manager.execute_query(
|
||||
'SELECT id, advert_count, last_heard FROM complete_contact_tracking WHERE public_key = ?',
|
||||
(public_key,)
|
||||
)
|
||||
|
||||
current_time = datetime.now()
|
||||
|
||||
if existing:
|
||||
# Update existing entry
|
||||
advert_count = existing[0]['advert_count'] + 1
|
||||
self.db_manager.execute_update('''
|
||||
UPDATE complete_contact_tracking
|
||||
SET last_heard = ?, advert_count = ?, role = ?, device_type = ?,
|
||||
latitude = ?, longitude = ?, city = ?, state = ?, country = ?,
|
||||
raw_advert_data = ?, signal_strength = ?, hop_count = ?,
|
||||
last_advert_timestamp = ?
|
||||
WHERE public_key = ?
|
||||
''', (
|
||||
current_time, advert_count, role, device_type_str,
|
||||
location_info['latitude'], location_info['longitude'],
|
||||
location_info['city'], location_info['state'], location_info['country'],
|
||||
json.dumps(advert_data), signal_strength, hop_count,
|
||||
current_time, public_key
|
||||
))
|
||||
|
||||
self.logger.debug(f"Updated contact tracking: {name} ({role}) - count: {advert_count}")
|
||||
else:
|
||||
# Insert new entry
|
||||
self.db_manager.execute_update('''
|
||||
INSERT INTO complete_contact_tracking
|
||||
(public_key, name, role, device_type, first_heard, last_heard, advert_count,
|
||||
latitude, longitude, city, state, country, raw_advert_data,
|
||||
signal_strength, hop_count, last_advert_timestamp)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
''', (
|
||||
public_key, name, role, device_type_str, current_time, current_time, 1,
|
||||
location_info['latitude'], location_info['longitude'],
|
||||
location_info['city'], location_info['state'], location_info['country'],
|
||||
json.dumps(advert_data), signal_strength, hop_count, current_time
|
||||
))
|
||||
|
||||
self.logger.info(f"Added new contact to complete tracking: {name} ({role})")
|
||||
|
||||
# Update the currently_tracked flag based on device contact list
|
||||
await self._update_currently_tracked_status(public_key)
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error tracking contact advertisement: {e}")
|
||||
return False
|
||||
|
||||
def _determine_contact_role(self, contact_data: Dict) -> str:
|
||||
"""Determine the role of a contact based on MeshCore specifications"""
|
||||
name = contact_data.get('name', contact_data.get('adv_name', '')).lower()
|
||||
device_type = contact_data.get('type', 0)
|
||||
|
||||
# Check device type first (most reliable indicator)
|
||||
if device_type == 2:
|
||||
return 'repeater'
|
||||
elif device_type == 3:
|
||||
return 'roomserver'
|
||||
|
||||
# Check name-based indicators for role detection
|
||||
if any(keyword in name for keyword in ['repeater', 'rpt', 'rp']):
|
||||
return 'repeater'
|
||||
elif any(keyword in name for keyword in ['room', 'server', 'rs', 'roomserver']):
|
||||
return 'roomserver'
|
||||
elif any(keyword in name for keyword in ['sensor', 'sens']):
|
||||
return 'sensor'
|
||||
elif any(keyword in name for keyword in ['bot', 'automated', 'automation']):
|
||||
return 'bot'
|
||||
elif any(keyword in name for keyword in ['gateway', 'gw', 'bridge']):
|
||||
return 'gateway'
|
||||
else:
|
||||
# Default to companion for unknown contacts (human users)
|
||||
return 'companion'
|
||||
|
||||
def _determine_device_type(self, device_type: int, name: str) -> str:
|
||||
"""Determine device type string from numeric type and name following MeshCore specs"""
|
||||
if device_type == 3:
|
||||
return 'RoomServer'
|
||||
elif device_type == 2:
|
||||
return 'Repeater'
|
||||
elif device_type == 1:
|
||||
return 'Companion'
|
||||
else:
|
||||
# Fallback to name-based detection
|
||||
name_lower = name.lower()
|
||||
if 'room' in name_lower or 'server' in name_lower or 'roomserver' in name_lower:
|
||||
return 'RoomServer'
|
||||
elif 'repeater' in name_lower or 'rpt' in name_lower:
|
||||
return 'Repeater'
|
||||
elif 'sensor' in name_lower or 'sens' in name_lower:
|
||||
return 'Sensor'
|
||||
elif 'gateway' in name_lower or 'gw' in name_lower or 'bridge' in name_lower:
|
||||
return 'Gateway'
|
||||
elif 'bot' in name_lower or 'automated' in name_lower:
|
||||
return 'Bot'
|
||||
else:
|
||||
return 'Companion' # Default to companion for human users
|
||||
|
||||
async def _update_currently_tracked_status(self, public_key: str):
|
||||
"""Update the is_currently_tracked flag based on device contact list"""
|
||||
try:
|
||||
# Check if this repeater is currently in the device's contact list
|
||||
is_tracked = False
|
||||
if hasattr(self.bot.meshcore, 'contacts'):
|
||||
for contact_key, contact_data in self.bot.meshcore.contacts.items():
|
||||
if contact_data.get('public_key', contact_key) == public_key:
|
||||
is_tracked = True
|
||||
break
|
||||
|
||||
# Update the flag
|
||||
self.db_manager.execute_update(
|
||||
'UPDATE complete_contact_tracking SET is_currently_tracked = ? WHERE public_key = ?',
|
||||
(is_tracked, public_key)
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error updating currently tracked status: {e}")
|
||||
|
||||
async def get_complete_contact_database(self, role_filter: str = None, include_historical: bool = True) -> List[Dict]:
|
||||
"""Get complete contact database for path estimation and analysis"""
|
||||
try:
|
||||
if include_historical:
|
||||
if role_filter:
|
||||
# Get all contacts of specific role ever heard
|
||||
query = '''
|
||||
SELECT public_key, name, role, device_type, first_heard, last_heard,
|
||||
advert_count, latitude, longitude, city, state, country,
|
||||
signal_strength, hop_count, is_currently_tracked, last_advert_timestamp
|
||||
FROM complete_contact_tracking
|
||||
WHERE role = ?
|
||||
ORDER BY last_heard DESC
|
||||
'''
|
||||
results = self.db_manager.execute_query(query, (role_filter,))
|
||||
else:
|
||||
# Get all contacts ever heard
|
||||
query = '''
|
||||
SELECT public_key, name, role, device_type, first_heard, last_heard,
|
||||
advert_count, latitude, longitude, city, state, country,
|
||||
signal_strength, hop_count, is_currently_tracked, last_advert_timestamp
|
||||
FROM complete_contact_tracking
|
||||
ORDER BY last_heard DESC
|
||||
'''
|
||||
results = self.db_manager.execute_query(query)
|
||||
else:
|
||||
if role_filter:
|
||||
# Get only currently tracked contacts of specific role
|
||||
query = '''
|
||||
SELECT public_key, name, role, device_type, first_heard, last_heard,
|
||||
advert_count, latitude, longitude, city, state, country,
|
||||
signal_strength, hop_count, is_currently_tracked, last_advert_timestamp
|
||||
FROM complete_contact_tracking
|
||||
WHERE role = ? AND is_currently_tracked = 1
|
||||
ORDER BY last_heard DESC
|
||||
'''
|
||||
results = self.db_manager.execute_query(query, (role_filter,))
|
||||
else:
|
||||
# Get only currently tracked contacts
|
||||
query = '''
|
||||
SELECT public_key, name, role, device_type, first_heard, last_heard,
|
||||
advert_count, latitude, longitude, city, state, country,
|
||||
signal_strength, hop_count, is_currently_tracked, last_advert_timestamp
|
||||
FROM complete_contact_tracking
|
||||
WHERE is_currently_tracked = 1
|
||||
ORDER BY last_heard DESC
|
||||
'''
|
||||
results = self.db_manager.execute_query(query)
|
||||
|
||||
return results
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error getting complete repeater database: {e}")
|
||||
return []
|
||||
|
||||
async def get_contact_statistics(self) -> Dict:
|
||||
"""Get statistics about the complete contact tracking database"""
|
||||
try:
|
||||
stats = {}
|
||||
|
||||
# Total contacts ever heard
|
||||
total_result = self.db_manager.execute_query(
|
||||
'SELECT COUNT(*) as count FROM complete_contact_tracking'
|
||||
)
|
||||
stats['total_heard'] = total_result[0]['count'] if total_result else 0
|
||||
|
||||
# Currently tracked contacts
|
||||
current_result = self.db_manager.execute_query(
|
||||
'SELECT COUNT(*) as count FROM complete_contact_tracking WHERE is_currently_tracked = 1'
|
||||
)
|
||||
stats['currently_tracked'] = current_result[0]['count'] if current_result else 0
|
||||
|
||||
# Recent activity (last 24 hours)
|
||||
recent_result = self.db_manager.execute_query(
|
||||
'SELECT COUNT(*) as count FROM complete_contact_tracking WHERE last_heard > datetime("now", "-1 day")'
|
||||
)
|
||||
stats['recent_activity'] = recent_result[0]['count'] if recent_result else 0
|
||||
|
||||
# Role breakdown
|
||||
role_result = self.db_manager.execute_query(
|
||||
'SELECT role, COUNT(*) as count FROM complete_contact_tracking GROUP BY role'
|
||||
)
|
||||
stats['by_role'] = {row['role']: row['count'] for row in role_result}
|
||||
|
||||
# Device type breakdown
|
||||
type_result = self.db_manager.execute_query(
|
||||
'SELECT device_type, COUNT(*) as count FROM complete_contact_tracking GROUP BY device_type'
|
||||
)
|
||||
stats['by_type'] = {row['device_type']: row['count'] for row in type_result}
|
||||
|
||||
return stats
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error getting contact statistics: {e}")
|
||||
return {}
|
||||
|
||||
async def get_contacts_by_role(self, role: str, include_historical: bool = True) -> List[Dict]:
|
||||
"""Get contacts filtered by specific MeshCore role (repeater, roomserver, companion, sensor, gateway, bot)"""
|
||||
return await self.get_complete_contact_database(role_filter=role, include_historical=include_historical)
|
||||
|
||||
async def get_repeater_devices(self, include_historical: bool = True) -> List[Dict]:
|
||||
"""Get all repeater devices (repeaters and roomservers) following MeshCore terminology"""
|
||||
repeater_db = await self.get_complete_contact_database(role_filter='repeater', include_historical=include_historical)
|
||||
roomserver_db = await self.get_complete_contact_database(role_filter='roomserver', include_historical=include_historical)
|
||||
return repeater_db + roomserver_db
|
||||
|
||||
async def get_companion_contacts(self, include_historical: bool = True) -> List[Dict]:
|
||||
"""Get all companion contacts (human users) following MeshCore terminology"""
|
||||
return await self.get_complete_contact_database(role_filter='companion', include_historical=include_historical)
|
||||
|
||||
async def get_sensor_devices(self, include_historical: bool = True) -> List[Dict]:
|
||||
"""Get all sensor devices following MeshCore terminology"""
|
||||
return await self.get_complete_contact_database(role_filter='sensor', include_historical=include_historical)
|
||||
|
||||
async def get_gateway_devices(self, include_historical: bool = True) -> List[Dict]:
|
||||
"""Get all gateway devices following MeshCore terminology"""
|
||||
return await self.get_complete_contact_database(role_filter='gateway', include_historical=include_historical)
|
||||
|
||||
async def get_bot_devices(self, include_historical: bool = True) -> List[Dict]:
|
||||
"""Get all bot/automated devices following MeshCore terminology"""
|
||||
return await self.get_complete_contact_database(role_filter='bot', include_historical=include_historical)
|
||||
|
||||
async def check_and_auto_purge(self) -> bool:
|
||||
"""Check contact limit and auto-purge repeaters if needed"""
|
||||
try:
|
||||
if not self.auto_purge_enabled:
|
||||
return False
|
||||
|
||||
# Get current contact count
|
||||
current_count = len(self.bot.meshcore.contacts)
|
||||
|
||||
if current_count >= self.auto_purge_threshold:
|
||||
self.logger.info(f"🔄 Auto-purge triggered: {current_count}/{self.contact_limit} contacts (threshold: {self.auto_purge_threshold})")
|
||||
|
||||
# Calculate how many to purge
|
||||
target_count = self.auto_purge_threshold - 20 # Leave some buffer
|
||||
purge_count = current_count - target_count
|
||||
|
||||
if purge_count > 0:
|
||||
success = await self._auto_purge_repeaters(purge_count)
|
||||
if success:
|
||||
self.logger.info(f"✅ Auto-purged {purge_count} repeaters, now at {len(self.bot.meshcore.contacts)}/{self.contact_limit} contacts")
|
||||
return True
|
||||
else:
|
||||
self.logger.warning(f"❌ Auto-purge failed to remove {purge_count} repeaters")
|
||||
return False
|
||||
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error in auto-purge check: {e}")
|
||||
return False
|
||||
|
||||
async def _auto_purge_repeaters(self, count: int) -> bool:
|
||||
"""Automatically purge repeaters using intelligent selection"""
|
||||
try:
|
||||
# Get all repeaters sorted by priority (least important first)
|
||||
repeaters_to_purge = await self._get_repeaters_for_purging(count)
|
||||
|
||||
if not repeaters_to_purge:
|
||||
self.logger.warning("No repeaters available for auto-purge")
|
||||
# Log some debugging info
|
||||
total_contacts = len(self.bot.meshcore.contacts)
|
||||
repeater_count = sum(1 for contact_data in self.bot.meshcore.contacts.values() if self._is_repeater_device(contact_data))
|
||||
self.logger.debug(f"Debug: {total_contacts} total contacts, {repeater_count} repeaters found")
|
||||
return False
|
||||
|
||||
purged_count = 0
|
||||
for repeater in repeaters_to_purge:
|
||||
try:
|
||||
# Use the improved purge method
|
||||
public_key = repeater['public_key']
|
||||
success = await self.purge_repeater_from_contacts(public_key, "Auto-purge - contact limit management")
|
||||
|
||||
if success:
|
||||
purged_count += 1
|
||||
self.logger.info(f"🗑️ Auto-purged repeater: {repeater['name']} (last seen: {repeater['last_seen']})")
|
||||
else:
|
||||
self.logger.warning(f"Failed to auto-purge repeater: {repeater['name']}")
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error auto-purging repeater {repeater['name']}: {e}")
|
||||
continue
|
||||
|
||||
self.logger.info(f"✅ Auto-purge completed: {purged_count}/{count} repeaters removed")
|
||||
return purged_count > 0
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error in auto-purge execution: {e}")
|
||||
return False
|
||||
|
||||
async def _get_repeaters_for_purging(self, count: int) -> List[Dict]:
|
||||
"""Get list of repeaters to purge based on intelligent criteria from device contacts"""
|
||||
try:
|
||||
# Get repeaters directly from device contacts, not database
|
||||
device_repeaters = []
|
||||
|
||||
for contact_key, contact_data in self.bot.meshcore.contacts.items():
|
||||
# Check if this is a repeater device
|
||||
if self._is_repeater_device(contact_data):
|
||||
public_key = contact_data.get('public_key', contact_key)
|
||||
name = contact_data.get('adv_name', contact_data.get('name', 'Unknown'))
|
||||
device_type = 'Repeater'
|
||||
if contact_data.get('type') == 3:
|
||||
device_type = 'RoomServer'
|
||||
|
||||
# Get last seen timestamp
|
||||
last_seen = contact_data.get('last_seen', contact_data.get('last_advert', contact_data.get('timestamp')))
|
||||
if last_seen:
|
||||
try:
|
||||
if isinstance(last_seen, str):
|
||||
last_seen_dt = datetime.fromisoformat(last_seen.replace('Z', '+00:00'))
|
||||
elif isinstance(last_seen, (int, float)):
|
||||
last_seen_dt = datetime.fromtimestamp(last_seen)
|
||||
else:
|
||||
last_seen_dt = last_seen
|
||||
except:
|
||||
last_seen_dt = datetime.now() - timedelta(days=30) # Default to old
|
||||
else:
|
||||
last_seen_dt = datetime.now() - timedelta(days=30) # Default to old
|
||||
|
||||
device_repeaters.append({
|
||||
'public_key': public_key,
|
||||
'name': name,
|
||||
'device_type': device_type,
|
||||
'last_seen': last_seen_dt.strftime('%Y-%m-%d %H:%M:%S'),
|
||||
'latitude': contact_data.get('adv_lat'),
|
||||
'longitude': contact_data.get('adv_lon'),
|
||||
'city': contact_data.get('city'),
|
||||
'state': contact_data.get('state'),
|
||||
'country': contact_data.get('country')
|
||||
})
|
||||
|
||||
# Sort by priority (oldest first)
|
||||
device_repeaters.sort(key=lambda x: (
|
||||
# Priority 1: Very old (7+ days)
|
||||
1 if (datetime.now() - datetime.strptime(x['last_seen'], '%Y-%m-%d %H:%M:%S')).days >= 7 else
|
||||
# Priority 2: Medium old (3-7 days)
|
||||
2 if (datetime.now() - datetime.strptime(x['last_seen'], '%Y-%m-%d %H:%M:%S')).days >= 3 else
|
||||
# Priority 3: Recent (0-3 days)
|
||||
3,
|
||||
# Within same priority, oldest first
|
||||
x['last_seen']
|
||||
))
|
||||
|
||||
# Apply additional filtering criteria
|
||||
filtered_repeaters = []
|
||||
for repeater in device_repeaters:
|
||||
# Skip repeaters with recent activity (last 24 hours)
|
||||
last_seen_dt = datetime.strptime(repeater['last_seen'], '%Y-%m-%d %H:%M:%S')
|
||||
if last_seen_dt > datetime.now() - timedelta(hours=24):
|
||||
continue
|
||||
|
||||
# Skip repeaters with location data (might be important)
|
||||
if repeater['latitude'] and repeater['longitude']:
|
||||
continue
|
||||
|
||||
filtered_repeaters.append(repeater)
|
||||
|
||||
if len(filtered_repeaters) >= count:
|
||||
break
|
||||
|
||||
self.logger.debug(f"Found {len(device_repeaters)} device repeaters, {len(filtered_repeaters)} available for purging")
|
||||
return filtered_repeaters[:count]
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error getting repeaters for purging: {e}")
|
||||
return []
|
||||
|
||||
def _extract_location_data(self, contact_data: Dict) -> Dict[str, Optional[str]]:
|
||||
"""Extract location data from contact_data JSON"""
|
||||
location_info = {
|
||||
@@ -213,12 +670,58 @@ class RepeaterManager:
|
||||
# Invalid coordinates
|
||||
location_info['latitude'] = None
|
||||
location_info['longitude'] = None
|
||||
else:
|
||||
# Valid coordinates - try reverse geocoding if we don't have city/state/country
|
||||
if not location_info['city'] or not location_info['state'] or not location_info['country']:
|
||||
try:
|
||||
# Use reverse geocoding to get city/state/country
|
||||
city = self._get_city_from_coordinates(lat, lon)
|
||||
if city:
|
||||
location_info['city'] = city
|
||||
|
||||
# Get state and country from coordinates
|
||||
state, country = self._get_state_country_from_coordinates(lat, lon)
|
||||
if state:
|
||||
location_info['state'] = state
|
||||
if country:
|
||||
location_info['country'] = country
|
||||
|
||||
except Exception as e:
|
||||
self.logger.debug(f"Reverse geocoding failed: {e}")
|
||||
|
||||
except Exception as e:
|
||||
self.logger.debug(f"Error extracting location data: {e}")
|
||||
|
||||
return location_info
|
||||
|
||||
def _get_state_country_from_coordinates(self, latitude: float, longitude: float) -> tuple[Optional[str], Optional[str]]:
|
||||
"""Get state and country from coordinates using reverse geocoding"""
|
||||
try:
|
||||
from geopy.geocoders import Nominatim
|
||||
|
||||
# Initialize geocoder
|
||||
geolocator = Nominatim(user_agent="meshcore-bot")
|
||||
|
||||
# Perform reverse geocoding
|
||||
location = geolocator.reverse(f"{latitude}, {longitude}")
|
||||
if location:
|
||||
address = location.raw.get('address', {})
|
||||
|
||||
# Get state/province
|
||||
state = (address.get('state') or
|
||||
address.get('province') or
|
||||
address.get('region'))
|
||||
|
||||
# Get country
|
||||
country = address.get('country')
|
||||
|
||||
return state, country
|
||||
|
||||
except Exception as e:
|
||||
self.logger.debug(f"Reverse geocoding for state/country failed: {e}")
|
||||
|
||||
return None, None
|
||||
|
||||
def _get_city_from_coordinates(self, latitude: float, longitude: float) -> Optional[str]:
|
||||
"""Get city name from coordinates using reverse geocoding, with neighborhood for large cities"""
|
||||
try:
|
||||
@@ -689,22 +1192,38 @@ class RepeaterManager:
|
||||
return results
|
||||
|
||||
async def purge_repeater_from_contacts(self, public_key: str, reason: str = "Manual purge") -> bool:
|
||||
"""Remove a specific repeater from the device's contact list"""
|
||||
"""Remove a specific repeater from the device's contact list using proper MeshCore API"""
|
||||
self.logger.info(f"Starting purge process for public_key: {public_key}")
|
||||
self.logger.debug(f"Purge reason: {reason}")
|
||||
|
||||
try:
|
||||
# Find the contact in meshcore
|
||||
# Find the contact in meshcore using proper MeshCore methods
|
||||
contact_to_remove = None
|
||||
contact_name = None
|
||||
contact_key = None
|
||||
|
||||
self.logger.debug(f"Searching through {len(self.bot.meshcore.contacts)} contacts...")
|
||||
for contact_key, contact_data in self.bot.meshcore.contacts.items():
|
||||
if contact_data.get('public_key', contact_key) == public_key:
|
||||
contact_to_remove = contact_data
|
||||
contact_name = contact_data.get('adv_name', contact_data.get('name', 'Unknown'))
|
||||
self.logger.debug(f"Found contact: {contact_name} (key: {contact_key})")
|
||||
break
|
||||
|
||||
# Try to find contact using MeshCore helper methods first
|
||||
try:
|
||||
# Method 1: Try to find by public key prefix
|
||||
contact_to_remove = self.bot.meshcore.get_contact_by_key_prefix(public_key[:8])
|
||||
if contact_to_remove:
|
||||
contact_name = contact_to_remove.get('adv_name', contact_to_remove.get('name', 'Unknown'))
|
||||
contact_key = public_key
|
||||
self.logger.debug(f"Found contact using key prefix: {contact_name}")
|
||||
except Exception as e:
|
||||
self.logger.debug(f"Key prefix lookup failed: {e}")
|
||||
|
||||
# Method 2: Fallback to manual search
|
||||
if not contact_to_remove:
|
||||
for key, contact_data in self.bot.meshcore.contacts.items():
|
||||
if contact_data.get('public_key', key) == public_key:
|
||||
contact_to_remove = contact_data
|
||||
contact_name = contact_data.get('adv_name', contact_data.get('name', 'Unknown'))
|
||||
contact_key = key
|
||||
self.logger.debug(f"Found contact manually: {contact_name} (key: {contact_key})")
|
||||
break
|
||||
|
||||
if not contact_to_remove:
|
||||
self.logger.warning(f"Repeater with public key {public_key} not found in current contacts")
|
||||
@@ -741,104 +1260,27 @@ class RepeaterManager:
|
||||
# Track whether device removal was successful
|
||||
device_removal_successful = False
|
||||
|
||||
# Actually remove the contact from the device using meshcore-cli API
|
||||
# Add timeout and error handling for LoRa communication
|
||||
# Remove the contact using the proper MeshCore API
|
||||
try:
|
||||
import asyncio
|
||||
|
||||
self.logger.info(f"Starting removal of contact '{contact_name}' from device...")
|
||||
self.logger.info(f"Removing contact '{contact_name}' from device using MeshCore API...")
|
||||
self.logger.debug(f"Contact details: public_key={public_key}, name='{contact_name}'")
|
||||
|
||||
# Check if we have a valid public key
|
||||
if not public_key or public_key.strip() == '':
|
||||
self.logger.error(f"Cannot remove contact '{contact_name}': no public key available")
|
||||
return False
|
||||
# Use the MeshCore API to remove the contact
|
||||
result = await asyncio.wait_for(
|
||||
self.bot.meshcore.commands.remove_contact(public_key),
|
||||
timeout=30.0
|
||||
)
|
||||
|
||||
# Use asyncio.wait_for to add timeout for LoRa communication
|
||||
try:
|
||||
self.logger.info(f"Sending remove_contact command for '{contact_name}' (key: {public_key[:16]}...) (timeout: 30s)...")
|
||||
start_time = asyncio.get_event_loop().time()
|
||||
|
||||
# Use the meshcore-cli API for device commands
|
||||
from meshcore_cli.meshcore_cli import next_cmd
|
||||
import sys
|
||||
import io
|
||||
|
||||
# Capture stdout/stderr to catch "Unknown contact" messages
|
||||
old_stdout = sys.stdout
|
||||
old_stderr = sys.stderr
|
||||
captured_output = io.StringIO()
|
||||
captured_errors = io.StringIO()
|
||||
|
||||
try:
|
||||
sys.stdout = captured_output
|
||||
sys.stderr = captured_errors
|
||||
|
||||
# Use contact name instead of public key for removal
|
||||
contact_name = contact_data.get('adv_name', contact_data.get('name', 'Unknown'))
|
||||
result = await asyncio.wait_for(
|
||||
next_cmd(self.bot.meshcore, ["remove_contact", contact_name]),
|
||||
timeout=30.0 # 30 second timeout for LoRa communication
|
||||
)
|
||||
finally:
|
||||
sys.stdout = old_stdout
|
||||
sys.stderr = old_stderr
|
||||
|
||||
# Get captured output
|
||||
stdout_content = captured_output.getvalue()
|
||||
stderr_content = captured_errors.getvalue()
|
||||
all_output = stdout_content + stderr_content
|
||||
|
||||
end_time = asyncio.get_event_loop().time()
|
||||
duration = end_time - start_time
|
||||
self.logger.info(f"Remove command completed in {duration:.2f} seconds")
|
||||
|
||||
# Check if removal was successful
|
||||
# Note: meshcore-cli prints "Unknown contact" to stdout/stderr if contact doesn't exist
|
||||
self.logger.debug(f"Command result: {result}")
|
||||
self.logger.debug(f"Captured output: {all_output}")
|
||||
|
||||
# Check if the captured output indicates the contact was unknown (doesn't exist)
|
||||
if "unknown contact" in all_output.lower():
|
||||
self.logger.warning(f"Contact '{contact_name}' was not found on device - this suggests the contact list is out of sync")
|
||||
# Don't mark as successful - we need to actually remove contacts that exist
|
||||
device_removal_successful = False
|
||||
elif result is not None:
|
||||
self.logger.info(f"Successfully removed contact '{contact_name}' from device")
|
||||
|
||||
# Verify the contact was actually removed by checking if it still exists
|
||||
await asyncio.sleep(1) # Give device time to process
|
||||
contact_still_exists = False
|
||||
for check_key, check_data in self.bot.meshcore.contacts.items():
|
||||
if check_data.get('public_key', check_key) == public_key:
|
||||
contact_still_exists = True
|
||||
break
|
||||
|
||||
if contact_still_exists:
|
||||
self.logger.warning(f"Contact '{contact_name}' still exists after removal command - removal may have failed")
|
||||
device_removal_successful = False
|
||||
else:
|
||||
self.logger.info(f"Verified: Contact '{contact_name}' successfully removed from device")
|
||||
device_removal_successful = True
|
||||
else:
|
||||
self.logger.warning(f"Contact removal command returned no result for '{contact_name}'")
|
||||
device_removal_successful = False
|
||||
|
||||
except asyncio.TimeoutError:
|
||||
end_time = asyncio.get_event_loop().time()
|
||||
duration = end_time - start_time
|
||||
self.logger.warning(f"Timeout removing contact '{contact_name}' after {duration:.2f} seconds (LoRa communication)")
|
||||
device_removal_successful = False
|
||||
except Exception as cmd_error:
|
||||
end_time = asyncio.get_event_loop().time()
|
||||
duration = end_time - start_time
|
||||
self.logger.error(f"Command error removing contact '{contact_name}' after {duration:.2f} seconds: {cmd_error}")
|
||||
self.logger.debug(f"Error type: {type(cmd_error).__name__}")
|
||||
# Check if removal was successful
|
||||
if hasattr(result, 'type') and result.type.name == 'OK':
|
||||
device_removal_successful = True
|
||||
self.logger.info(f"✅ Successfully removed contact '{contact_name}' from device")
|
||||
else:
|
||||
self.logger.warning(f"❌ MeshCore API removal failed: {result}")
|
||||
device_removal_successful = False
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Failed to remove contact '{contact_name}' from device: {e}")
|
||||
self.logger.debug(f"Error type: {type(e).__name__}")
|
||||
device_removal_successful = False
|
||||
|
||||
# Only mark as inactive in database if device removal was successful
|
||||
@@ -1403,12 +1845,11 @@ class RepeaterManager:
|
||||
# Get current contact count
|
||||
current_contacts = len(self.bot.meshcore.contacts) if hasattr(self.bot.meshcore, 'contacts') else 0
|
||||
|
||||
# Get device info to determine contact limit
|
||||
device_info = self.bot.meshcore.device_info if hasattr(self.bot.meshcore, 'device_info') else {}
|
||||
# Update contact limit from device info
|
||||
await self._update_contact_limit_from_device()
|
||||
|
||||
# Typical MeshCore contact limits (these may vary by device)
|
||||
# Most devices have a limit around 200-500 contacts
|
||||
estimated_limit = device_info.get('contact_limit', 200) # Default assumption
|
||||
# Use the updated contact limit
|
||||
estimated_limit = self.contact_limit
|
||||
|
||||
# Calculate usage percentage
|
||||
usage_percentage = (current_contacts / estimated_limit) * 100 if estimated_limit > 0 else 0
|
||||
@@ -1564,16 +2005,15 @@ class RepeaterManager:
|
||||
self.logger.warning(f"Skipping stale contact '{contact_name}': no public key available")
|
||||
continue
|
||||
|
||||
# Remove from device
|
||||
from meshcore_cli.meshcore_cli import next_cmd
|
||||
# Remove from device using MeshCore API
|
||||
result = await asyncio.wait_for(
|
||||
next_cmd(self.bot.meshcore, ["remove_contact", public_key]),
|
||||
self.bot.meshcore.commands.remove_contact(public_key),
|
||||
timeout=15.0
|
||||
)
|
||||
|
||||
if result is not None:
|
||||
if hasattr(result, 'type') and result.type.name == 'OK':
|
||||
removed_count += 1
|
||||
self.logger.info(f"Successfully removed stale contact: {contact_name}")
|
||||
self.logger.info(f"✅ Successfully removed stale contact: {contact_name}")
|
||||
|
||||
# Log the removal
|
||||
self.db_manager.execute_update(
|
||||
@@ -1581,7 +2021,7 @@ class RepeaterManager:
|
||||
('stale_contact_removal', f'Removed stale contact: {contact_name} (last seen {contact["days_stale"]} days ago)')
|
||||
)
|
||||
else:
|
||||
self.logger.warning(f"Failed to remove stale contact: {contact_name}")
|
||||
self.logger.warning(f"❌ Failed to remove stale contact: {contact_name} - {result}")
|
||||
|
||||
# Small delay between removals
|
||||
await asyncio.sleep(1)
|
||||
@@ -2032,4 +2472,206 @@ class RepeaterManager:
|
||||
'errors': 1,
|
||||
'skipped': 0,
|
||||
'error': str(e)
|
||||
}
|
||||
|
||||
async def periodic_contact_monitoring(self):
|
||||
"""Periodic monitoring of contact limit and auto-purge if needed"""
|
||||
try:
|
||||
if not self.auto_purge_enabled:
|
||||
return
|
||||
|
||||
current_count = len(self.bot.meshcore.contacts)
|
||||
|
||||
# Log current status
|
||||
if current_count >= self.auto_purge_threshold:
|
||||
self.logger.warning(f"⚠️ Contact limit monitoring: {current_count}/{self.contact_limit} contacts (threshold: {self.auto_purge_threshold})")
|
||||
|
||||
# Trigger auto-purge
|
||||
await self.check_and_auto_purge()
|
||||
elif current_count >= self.auto_purge_threshold - 20:
|
||||
self.logger.info(f"📊 Contact limit monitoring: {current_count}/{self.contact_limit} contacts (approaching threshold)")
|
||||
else:
|
||||
self.logger.debug(f"📊 Contact limit monitoring: {current_count}/{self.contact_limit} contacts (healthy)")
|
||||
|
||||
# Background geocoding for contacts missing location data
|
||||
await self._background_geocoding()
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error in periodic contact monitoring: {e}")
|
||||
|
||||
async def _background_geocoding(self):
|
||||
"""Background geocoding for contacts missing location data"""
|
||||
try:
|
||||
# Find contacts with coordinates but missing city data
|
||||
contacts_needing_geocoding = self.db_manager.execute_query('''
|
||||
SELECT id, name, latitude, longitude, city, state, country
|
||||
FROM complete_contact_tracking
|
||||
WHERE latitude IS NOT NULL
|
||||
AND longitude IS NOT NULL
|
||||
AND (city IS NULL OR city = '')
|
||||
AND last_geocoding_attempt IS NULL
|
||||
ORDER BY last_heard DESC
|
||||
LIMIT 1
|
||||
''')
|
||||
|
||||
if not contacts_needing_geocoding:
|
||||
return
|
||||
|
||||
contact = contacts_needing_geocoding[0]
|
||||
contact_id = contact['id']
|
||||
name = contact['name']
|
||||
lat = contact['latitude']
|
||||
lon = contact['longitude']
|
||||
|
||||
self.logger.debug(f"🌍 Background geocoding: {name} ({lat}, {lon})")
|
||||
|
||||
# Attempt geocoding
|
||||
try:
|
||||
# Get city from coordinates
|
||||
city = self._get_city_from_coordinates(lat, lon)
|
||||
|
||||
# Get state and country from coordinates
|
||||
state, country = self._get_state_country_from_coordinates(lat, lon)
|
||||
|
||||
# Update the contact with geocoded data
|
||||
updates = []
|
||||
params = []
|
||||
|
||||
if city:
|
||||
updates.append("city = ?")
|
||||
params.append(city)
|
||||
|
||||
if state:
|
||||
updates.append("state = ?")
|
||||
params.append(state)
|
||||
|
||||
if country:
|
||||
updates.append("country = ?")
|
||||
params.append(country)
|
||||
|
||||
# Always update the geocoding attempt timestamp
|
||||
updates.append("last_geocoding_attempt = ?")
|
||||
params.append(datetime.now())
|
||||
|
||||
if updates:
|
||||
params.append(contact_id)
|
||||
query = f"UPDATE complete_contact_tracking SET {', '.join(updates)} WHERE id = ?"
|
||||
self.db_manager.execute_update(query, params)
|
||||
|
||||
self.logger.info(f"✅ Background geocoding successful: {name} → {city or 'Unknown'}, {state or 'Unknown'}, {country or 'Unknown'}")
|
||||
else:
|
||||
# Mark as attempted even if no data was found
|
||||
self.db_manager.execute_update(
|
||||
'UPDATE complete_contact_tracking SET last_geocoding_attempt = ? WHERE id = ?',
|
||||
(datetime.now(), contact_id)
|
||||
)
|
||||
self.logger.debug(f"🌍 Background geocoding: {name} - no additional location data found")
|
||||
|
||||
except Exception as e:
|
||||
# Mark as attempted even if geocoding failed
|
||||
self.db_manager.execute_update(
|
||||
'UPDATE complete_contact_tracking SET last_geocoding_attempt = ? WHERE id = ?',
|
||||
(datetime.now(), contact_id)
|
||||
)
|
||||
self.logger.debug(f"🌍 Background geocoding failed for {name}: {e}")
|
||||
|
||||
except Exception as e:
|
||||
self.logger.debug(f"Background geocoding error: {e}")
|
||||
|
||||
async def _update_contact_limit_from_device(self):
|
||||
"""Update contact limit from device using proper MeshCore API"""
|
||||
try:
|
||||
# Use the correct MeshCore API to get device info
|
||||
device_info = await self.bot.meshcore.commands.send_device_query()
|
||||
|
||||
# Check if the query was successful
|
||||
if hasattr(device_info, 'type') and device_info.type.name == 'DEVICE_INFO':
|
||||
max_contacts = device_info.payload.get("max_contacts")
|
||||
|
||||
if max_contacts and max_contacts > 100:
|
||||
self.contact_limit = max_contacts
|
||||
# Update threshold to be 20 contacts below the limit
|
||||
self.auto_purge_threshold = max(200, max_contacts - 20)
|
||||
self.logger.debug(f"Updated contact limit from device query: {self.contact_limit} (threshold: {self.auto_purge_threshold})")
|
||||
return True
|
||||
else:
|
||||
self.logger.debug(f"Device returned invalid max_contacts: {max_contacts}")
|
||||
else:
|
||||
self.logger.debug(f"Device query failed: {device_info}")
|
||||
|
||||
except Exception as e:
|
||||
self.logger.debug(f"Could not update contact limit from device: {e}")
|
||||
|
||||
# Keep default values if device query failed
|
||||
self.logger.debug(f"Using default contact limit: {self.contact_limit}")
|
||||
return False
|
||||
|
||||
async def get_auto_purge_status(self) -> Dict:
|
||||
"""Get current auto-purge configuration and status"""
|
||||
try:
|
||||
# Update contact limit from device info
|
||||
await self._update_contact_limit_from_device()
|
||||
|
||||
current_count = len(self.bot.meshcore.contacts)
|
||||
return {
|
||||
'enabled': self.auto_purge_enabled,
|
||||
'contact_limit': self.contact_limit,
|
||||
'threshold': self.auto_purge_threshold,
|
||||
'current_count': current_count,
|
||||
'usage_percentage': (current_count / self.contact_limit) * 100,
|
||||
'is_near_limit': current_count >= self.auto_purge_threshold,
|
||||
'is_at_limit': current_count >= self.contact_limit
|
||||
}
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error getting auto-purge status: {e}")
|
||||
return {
|
||||
'enabled': False,
|
||||
'error': str(e)
|
||||
}
|
||||
|
||||
async def test_purge_system(self) -> Dict:
|
||||
"""Test the improved purge system with a single contact"""
|
||||
try:
|
||||
# Find a test contact to purge
|
||||
test_contact = None
|
||||
test_public_key = None
|
||||
|
||||
# Look for a repeater contact to test with
|
||||
for key, contact_data in self.bot.meshcore.contacts.items():
|
||||
if self._is_repeater_device(contact_data):
|
||||
test_contact = contact_data
|
||||
test_public_key = contact_data.get('public_key', key)
|
||||
break
|
||||
|
||||
if not test_contact:
|
||||
return {
|
||||
'success': False,
|
||||
'error': 'No repeater contacts found to test with',
|
||||
'contact_count': len(self.bot.meshcore.contacts)
|
||||
}
|
||||
|
||||
contact_name = test_contact.get('adv_name', test_contact.get('name', 'Unknown'))
|
||||
initial_count = len(self.bot.meshcore.contacts)
|
||||
|
||||
self.logger.info(f"Testing purge system with contact: {contact_name}")
|
||||
|
||||
# Test the purge
|
||||
success = await self.purge_repeater_from_contacts(test_public_key, "Test purge - system validation")
|
||||
|
||||
final_count = len(self.bot.meshcore.contacts)
|
||||
|
||||
return {
|
||||
'success': success,
|
||||
'test_contact': contact_name,
|
||||
'initial_count': initial_count,
|
||||
'final_count': final_count,
|
||||
'contacts_removed': initial_count - final_count,
|
||||
'purge_method': 'Improved MeshCore API'
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error testing purge system: {e}")
|
||||
return {
|
||||
'success': False,
|
||||
'error': str(e)
|
||||
}
|
||||
Reference in New Issue
Block a user