demo version prepped
This commit is contained in:
253
backend/simulator/__init__.py
Normal file
253
backend/simulator/__init__.py
Normal file
@@ -0,0 +1,253 @@
|
||||
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)
|
||||
Reference in New Issue
Block a user