Files
three_60/backend/simulator/__init__.py
2026-04-01 12:40:40 -04:00

254 lines
10 KiB
Python

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)