import database as db import sqlite3 from datetime import datetime, timezone import random import asyncio import time from .sim_config import ALARM_TEXTS, TICKET_TEXTS, ASSIGNEES, SITE_IDS from logger import get_logger logger = get_logger(__name__) next_alarm_cleanup_at: float = 0.0 next_incident_cleanup_at: float = 0.0 # TODO: Simulate weather / natural disasters async def ticket_simulator(interval=60): # Generates tickets off of alarming sites. while True: conn = db.connect_to_db() try: logger.debug("Simulating tickets...") cur = conn.cursor() now = int(datetime.now(timezone.utc).timestamp()) # Find sites that have unassigned simulator alarms. Will be used to assigned to bot rows = cur.execute(""" SELECT DISTINCT site_id FROM alarms WHERE incident_id IS NULL AND created_by = 'simulator' """).fetchall() if not rows: logger.debug("No unassigned alarms — skipping ticket creation.") conn.close() await asyncio.sleep(interval) continue site_id = random.choice(rows)[0] text = random.choice(TICKET_TEXTS) assigned_to = random.choice(ASSIGNEES) # NOTE: Get the worse severity alarm that isn't assigned out. worst = cur.execute(""" SELECT MIN(severity) FROM alarms WHERE site_id = ? AND incident_id IS NULL """, (site_id,)).fetchone()[0] severity = worst or random.randint(1, 5) cur.execute(""" INSERT INTO incidents (text, severity, site_id, created, updated, assigned_to, status, created_by) VALUES (?, ?, ?, ?, ?, ?, 'active', 'simulator') """, (text, severity, site_id, now, now, assigned_to)) incident_id = cur.lastrowid cur.execute(""" UPDATE alarms SET incident_id = ?, updated = ? WHERE site_id = ? AND incident_id IS NULL """, (incident_id, now, site_id)) conn.commit() # Scan for idle robots for tickets idle = cur.execute(""" SELECT * FROM robots WHERE current_incident_id IS NULL ORDER BY RANDOM() LIMIT 1 """).fetchone() # when found, assign them a ticket. # TODO: Make this smarter, maybe path or proximity. right now they fly everywhere. if idle: site = cur.execute("SELECT lat, lon FROM cellsites WHERE id = ?", (site_id,)).fetchone() cur.execute(""" UPDATE robots SET current_incident_id = ?, target_lat = ?, target_lon = ?, updated = ? WHERE id = ? """, (incident_id, site[0], site[1], now, idle[0])) cur.execute(""" UPDATE incidents SET assigned_to = ? WHERE id = ? """, (idle[1], incident_id)) conn.commit() logger.info(f"Robot {idle[1]} dispatched to site {site_id}") conn.close() logger.info(f"Ticket created — INC #{incident_id} site={site_id} sev={severity}") except Exception as e: logger.error(f"Ticket simulator error: {e}", exc_info=True) finally: conn.close() await asyncio.sleep(interval) async def alarm_simulator(interval=60): # Generates random alarms off sites while True: conn = db.connect_to_db() try: logger.debug("Simulating alarms...") cur = conn.cursor() now = int(datetime.now(timezone.utc).timestamp()) site_id = random.choice(SITE_IDS) severity = random.randint(1, 5) text = random.choice(ALARM_TEXTS) status = "active" created_by = "simulator" cur.execute(""" INSERT INTO alarms (text, severity, site_id, created, updated, status, created_by) VALUES (?, ?, ?, ?, ?, ?, ?) """, (text, severity, site_id, now, now, status, created_by)) conn.commit() conn.close() logger.debug(f"Alarm created — site={site_id} sev={severity}") except Exception as e: logger.error(f"Alarm simulator error: {e}", exc_info=True) finally: conn.close() await asyncio.sleep(interval) async def cleanup_incidents(interval=300, max_age_seconds=3600): global next_incident_cleanup_at while True: next_incident_cleanup_at = time.time() + interval await asyncio.sleep(interval) conn = db.connect_to_db() try: cur = conn.cursor() cutoff = int(datetime.now(timezone.utc).timestamp()) - max_age_seconds cur.execute("DELETE FROM incidents WHERE created_by = 'simulator' AND created < ?", (cutoff,)) # NOTE: robots need to be reset or else will have deleted tickets cur.execute(""" UPDATE robots SET current_incident_id = NULL, current_site_id = NULL, target_lat = base_lat, target_lon = base_lon WHERE current_incident_id IS NOT NULL AND current_incident_id NOT IN (SELECT id FROM incidents) """) affected = cur.rowcount if affected: logger.info(f"Reset {affected} robots orphaned by incident cleanup") conn.commit() conn.close() logger.info(f"Incident cleanup done — removed records older than {max_age_seconds}s") except Exception as e: logger.error(f"Incident cleanup error: {e}", exc_info=True) finally: conn.close() async def cleanup_alarms(interval=300, max_age_seconds=3600): global next_alarm_cleanup_at while True: next_alarm_cleanup_at = time.time() + interval await asyncio.sleep(interval) conn = db.connect_to_db() try: cur = conn.cursor() cutoff = int(datetime.now(timezone.utc).timestamp()) - max_age_seconds cur.execute("DELETE FROM alarms WHERE created_by = 'simulator' AND created < ?", (cutoff,)) # NOTE: Here we clear any oprhaned incident_ids off the alarm after deleted. cur.execute(""" UPDATE alarms SET incident_id = NULL WHERE incident_id IS NOT NULL AND incident_id NOT IN (SELECT id FROM incidents) """) conn.commit() conn.close() logger.info(f"Alarm cleanup done — removed records older than {max_age_seconds}s") except Exception as e: logger.error(f"Alarm cleanup error: {e}", exc_info=True) finally: conn.close() async def robot_simulator(interval=3): """ This is the robot sim that will fly around and "fix" sites after being assigned tickets. The robot has a fixed time to "work" and then will close the ticket and move on. """ while True: conn = db.connect_to_db() try: conn.row_factory = sqlite3.Row cur = conn.cursor() now = int(time.time()) in_transit = cur.execute(""" SELECT r.id, r.lat, r.lon, r.target_lat, r.target_lon, r.current_incident_id, i.site_id as target_site_id FROM robots r JOIN incidents i ON r.current_incident_id = i.id WHERE r.current_incident_id IS NOT NULL AND r.current_site_id IS NULL AND r.target_lat IS NOT NULL """).fetchall() STEP = 0.2 THRESHOLD = 0.05 for robot in in_transit: dlat = robot['target_lat'] - robot['lat'] dlon = robot['target_lon'] - robot['lon'] dist = (dlat**2 + dlon**2) ** 0.5 if dist < THRESHOLD: cur.execute(""" UPDATE robots SET lat = ?, lon = ?, current_site_id = ?, updated = ? WHERE id = ? """, (robot['target_lat'], robot['target_lon'], robot['target_site_id'], now, robot['id'])) logger.info(f"Robot {robot['id']} arrived at site {robot['target_site_id']}") else: factor = min(STEP / dist, 1.0) new_lat = robot['lat'] + dlat * factor new_lon = robot['lon'] + dlon * factor cur.execute(""" UPDATE robots SET lat = ?, lon = ?, updated = ? WHERE id = ? """, (new_lat, new_lon, now, robot['id'])) WORK_DURATION = 60 # seconds on site before closing ticket on_site = cur.execute(""" SELECT id, current_incident_id, current_site_id, base_lat, base_lon, updated FROM robots WHERE current_site_id IS NOT NULL AND current_incident_id IS NOT NULL """).fetchall() for robot in on_site: time_on_site = now - robot['updated'] if time_on_site < WORK_DURATION: continue # Close the incident <- not delete cur.execute(""" UPDATE incidents SET status = 'closed', updated = ? WHERE id = ? """, (now, robot['current_incident_id'])) # End assignment and return, for more work cur.execute(""" UPDATE robots SET current_incident_id = NULL, current_site_id = NULL, target_lat = base_lat, target_lon = base_lon, updated = ? WHERE id = ? """, (now, robot['id'])) logger.info(f"Robot {robot['id']} finished INC #{robot['current_incident_id']} at site {robot['current_site_id']} — returning to base") conn.commit() except Exception as e: logger.error(f"Robot simulator error: {e}", exc_info=True) finally: conn.close() await asyncio.sleep(interval)