diff --git a/modules/checklist.md b/modules/checklist.md index 924d930..843cecc 100644 --- a/modules/checklist.md +++ b/modules/checklist.md @@ -13,7 +13,7 @@ The enhanced checklist module provides asset tracking and accountability feature - Notes support for additional context ### ⏰ Safety Monitoring with Time Intervals -- Set expected check-in intervals for safety monitoring +- Set expected check-in intervals for safety (minimal 20min) - Automatic tracking of overdue check-ins - Ideal for solo activities, remote work, or high-risk operations - Get alerts when someone hasn't checked in within their expected timeframe diff --git a/modules/checklist.py b/modules/checklist.py index d630aad..4044d78 100644 --- a/modules/checklist.py +++ b/modules/checklist.py @@ -35,6 +35,17 @@ def initialize_checklist_database(): except sqlite3.OperationalError: pass # Column already exists + try: + c.execute("ALTER TABLE checkin ADD COLUMN removed INTEGER DEFAULT 0") + except sqlite3.OperationalError: + pass # Column already exists + + # Add this to your DB init (if not already present) + try: + c.execute("ALTER TABLE checkout ADD COLUMN removed INTEGER DEFAULT 0") + except sqlite3.OperationalError: + pass # Column already exists + conn.commit() conn.close() return True @@ -203,7 +214,7 @@ def get_overdue_checkins(): try: c.execute(""" - SELECT checkin_id, checkin_name, checkin_date, checkin_time, expected_checkin_interval, location + SELECT checkin_id, checkin_name, checkin_date, checkin_time, expected_checkin_interval, location, checkin_notes FROM checkin WHERE expected_checkin_interval > 0 AND approved = 1 @@ -218,7 +229,7 @@ def get_overdue_checkins(): conn.close() overdue_list = [] - for checkin_id, name, date, time_str, interval, location in active_checkins: + for checkin_id, name, date, time_str, interval, location, notes in active_checkins: checkin_datetime = time.mktime(time.strptime(f"{date} {time_str}", "%Y-%m-%d %H:%M:%S")) time_since_checkin = (current_time - checkin_datetime) / 60 # in minutes @@ -229,7 +240,8 @@ def get_overdue_checkins(): 'name': name, 'location': location, 'overdue_minutes': overdue_minutes, - 'interval': interval + 'interval': interval, + 'checkin_notes': notes }) return overdue_list @@ -239,21 +251,28 @@ def get_overdue_checkins(): return [] def format_overdue_alert(): - """Format overdue check-ins as an alert message""" - overdue = get_overdue_checkins() - if not overdue: + try: + """Format overdue check-ins as an alert message""" + overdue = get_overdue_checkins() + logger.debug(f"Overdue check-ins: {overdue}") # Add this line + if not overdue: + return None + + alert = "⚠️ OVERDUE CHECK-INS:\n" + for entry in overdue: + hours = entry['overdue_minutes'] // 60 + minutes = entry['overdue_minutes'] % 60 + alert += f"{entry['name']}: {hours}h {minutes}m overdue" + # if entry['location']: + # alert += f" @ {entry['location']}" + if entry['checkin_notes']: + alert += f" 📝{entry['checkin_notes']}" + alert += "\n" + + return alert.rstrip() + except Exception as e: + logger.error(f"Checklist: Error formatting overdue alert: {e}") return None - - alert = "⚠️ OVERDUE CHECK-INS:\n" - for entry in overdue: - hours = entry['overdue_minutes'] // 60 - minutes = entry['overdue_minutes'] % 60 - alert += f"{entry['name']}: {hours}h {minutes}m overdue" - if entry['location']: - alert += f" @ {entry['location']}" - alert += "\n" - - return alert.rstrip() def list_checkin(): # list checkins @@ -262,7 +281,8 @@ def list_checkin(): try: c.execute(""" SELECT * FROM checkin - WHERE checkin_id NOT IN ( + WHERE removed = 0 + AND checkin_id NOT IN ( SELECT checkin_id FROM checkout WHERE checkout_date > checkin_date OR (checkout_date = checkin_date AND checkout_time > checkin_time) ) @@ -291,7 +311,7 @@ def list_checkin(): timeCheckedIn = f"{days}d {hours:02}:{minutes:02}:{seconds:02}" else: timeCheckedIn = f"{hours:02}:{minutes:02}:{seconds:02}" - checkin_list += "ID: " + row[1] + " checked-In for " + timeCheckedIn + checkin_list += "ID: " + str(row[0]) + " " + row[1] + " checked-In for " + timeCheckedIn if row[5] != "": checkin_list += "📝" + row[5] if row != rows[-1]: @@ -339,10 +359,10 @@ def process_checklist_command(nodeID, message, name="none", location="none"): return checkout(name, current_date, current_time, location, comment) elif "purgein" in message_lower: - return delete_checkin(nodeID) + return mark_checkin_removed_by_name(name) elif "purgeout" in message_lower: - return delete_checkout(nodeID) + return mark_checkout_removed_by_name(name) elif message_lower.startswith("checklistapprove "): try: @@ -380,4 +400,22 @@ def process_checklist_command(nodeID, message, name="none", location="none"): return list_checkin() else: - return "Invalid command." \ No newline at end of file + return "Invalid command." + +def mark_checkin_removed_by_name(name): + conn = sqlite3.connect(checklist_db) + c = conn.cursor() + c.execute("UPDATE checkin SET removed = 1 WHERE checkin_name = ?", (name,)) + affected = c.rowcount + conn.commit() + conn.close() + return f"Marked {affected} check-in(s) as removed for {name}." + +def mark_checkout_removed_by_name(name): + conn = sqlite3.connect(checklist_db) + c = conn.cursor() + c.execute("UPDATE checkout SET removed = 1 WHERE checkout_name = ?", (name,)) + affected = c.rowcount + conn.commit() + conn.close() + return f"Marked {affected} checkout(s) as removed for {name}." \ No newline at end of file diff --git a/modules/system.py b/modules/system.py index e1e312d..b646935 100644 --- a/modules/system.py +++ b/modules/system.py @@ -1115,105 +1115,135 @@ priorVolcanoAlert = "" priorEmergencyAlert = "" priorWxAlert = "" def handleAlertBroadcast(deviceID=1): - global priorVolcanoAlert, priorEmergencyAlert, priorWxAlert - alertUk = NO_ALERTS - alertDe = NO_ALERTS - alertFema = NO_ALERTS - wxAlert = NO_ALERTS - volcanoAlert = NO_ALERTS - alertWx = False - # only allow API call every 20 minutes - # the watchdog will call this function 3 times, seeing possible throttling on the API - clock = datetime.now() - if clock.minute % 20 != 0: - return False - if clock.second > 17: - return False - - # check for alerts - if wxAlertBroadcastEnabled: - alertWx = alertBrodcastNOAA() + try: + global priorVolcanoAlert, priorEmergencyAlert, priorWxAlert + alertUk = NO_ALERTS + alertDe = NO_ALERTS + alertFema = NO_ALERTS + wxAlert = NO_ALERTS + volcanoAlert = NO_ALERTS + overdueAlerts = NO_ALERTS + alertWx = False + # only allow API call every 20 minutes + # the watchdog will call this function 3 times, seeing possible throttling on the API + clock = datetime.now() + if clock.minute % 20 != 0: + return False + if clock.second > 17: + return False + + # check for alerts + if wxAlertBroadcastEnabled: + alertWx = alertBrodcastNOAA() - if emergencyAlertBrodcastEnabled: - if enableDEalerts: - alertDe = get_nina_alerts() - if enableGBalerts: - alertUk = get_govUK_alerts() + if emergencyAlertBrodcastEnabled: + if enableDEalerts: + alertDe = get_nina_alerts() + if enableGBalerts: + alertUk = get_govUK_alerts() + else: + # default USA alerts + alertFema = getIpawsAlert(latitudeValue,longitudeValue, shortAlerts=True) + + if checklist_enabled: + overdueAlerts = format_overdue_alert() + + # format alert + if alertWx: + wxAlert = f"🚨 {alertWx[1]} EAS-WX ALERT: {alertWx[0]}" else: - # default USA alerts - alertFema = getIpawsAlert(latitudeValue,longitudeValue, shortAlerts=True) + wxAlert = False - # format alert - if alertWx: - wxAlert = f"🚨 {alertWx[1]} EAS-WX ALERT: {alertWx[0]}" - else: - wxAlert = False + femaAlert = alertFema + ukAlert = alertUk + deAlert = alertDe - femaAlert = alertFema - ukAlert = alertUk - deAlert = alertDe + if overdueAlerts != NO_ALERTS and overdueAlerts != None: + logger.debug("System: Adding overdue checkin to emergency alerts") + if femaAlert and NO_ALERTS not in femaAlert and ERROR_FETCHING_DATA not in femaAlert: + femaAlert += "\n\n" + overdueAlerts + elif ukAlert and NO_ALERTS not in ukAlert and ERROR_FETCHING_DATA not in ukAlert: + ukAlert += "\n\n" + overdueAlerts + elif deAlert and NO_ALERTS not in deAlert and ERROR_FETCHING_DATA not in deAlert: + deAlert += "\n\n" + overdueAlerts + else: + # only overdue alerts to send + if overdueAlerts != "" and overdueAlerts is not None and overdueAlerts != NO_ALERTS: + if overdueAlerts != priorEmergencyAlert: + priorEmergencyAlert = overdueAlerts + else: + return False + if isinstance(emergencyAlertBroadcastCh, list): + for channel in emergencyAlertBroadcastCh: + send_message(overdueAlerts, int(channel), 0, deviceID) + else: + send_message(overdueAlerts, emergencyAlertBroadcastCh, 0, deviceID) + return True - if emergencyAlertBrodcastEnabled: - if NO_ALERTS not in femaAlert and ERROR_FETCHING_DATA not in femaAlert: - if femaAlert != priorEmergencyAlert: - priorEmergencyAlert = femaAlert - else: - return False - if isinstance(emergencyAlertBroadcastCh, list): - for channel in emergencyAlertBroadcastCh: - send_message(femaAlert, int(channel), 0, deviceID) - else: - send_message(femaAlert, emergencyAlertBroadcastCh, 0, deviceID) - return True - if NO_ALERTS not in ukAlert: - if ukAlert != priorEmergencyAlert: - priorEmergencyAlert = ukAlert - else: - return False - if isinstance(emergencyAlertBroadcastCh, list): - for channel in emergencyAlertBroadcastCh: - send_message(ukAlert, int(channel), 0, deviceID) - else: - send_message(ukAlert, emergencyAlertBroadcastCh, 0, deviceID) - return True - - if NO_ALERTS not in alertDe: - if deAlert != priorEmergencyAlert: - priorEmergencyAlert = deAlert - else: - return False - if isinstance(emergencyAlertBroadcastCh, list): - for channel in emergencyAlertBroadcastCh: - send_message(deAlert, int(channel), 0, deviceID) - else: - send_message(deAlert, emergencyAlertBroadcastCh, 0, deviceID) - return True - - if wxAlertBroadcastEnabled: - if wxAlert: - if wxAlert != priorWxAlert: - priorWxAlert = wxAlert - else: - return False - if isinstance(wxAlertBroadcastChannel, list): - for channel in wxAlertBroadcastChannel: - send_message(wxAlert, int(channel), 0, deviceID) - else: - send_message(wxAlert, wxAlertBroadcastChannel, 0, deviceID) - return True - - if volcanoAlertBroadcastEnabled: - volcanoAlert = get_volcano_usgs(latitudeValue, longitudeValue) - if volcanoAlert and NO_ALERTS not in volcanoAlert and ERROR_FETCHING_DATA not in volcanoAlert: - # check if the alert is different from the last one - if volcanoAlert != priorVolcanoAlert: - priorVolcanoAlert = volcanoAlert - if isinstance(volcanoAlertBroadcastChannel, list): - for channel in volcanoAlertBroadcastChannel: - send_message(volcanoAlert, int(channel), 0, deviceID) + if emergencyAlertBrodcastEnabled: + if NO_ALERTS not in femaAlert and ERROR_FETCHING_DATA not in femaAlert: + if femaAlert != priorEmergencyAlert: + priorEmergencyAlert = femaAlert else: - send_message(volcanoAlert, volcanoAlertBroadcastChannel, 0, deviceID) + return False + if isinstance(emergencyAlertBroadcastCh, list): + for channel in emergencyAlertBroadcastCh: + send_message(femaAlert, int(channel), 0, deviceID) + else: + send_message(femaAlert, emergencyAlertBroadcastCh, 0, deviceID) return True + if NO_ALERTS not in ukAlert: + if ukAlert != priorEmergencyAlert: + priorEmergencyAlert = ukAlert + else: + return False + if isinstance(emergencyAlertBroadcastCh, list): + for channel in emergencyAlertBroadcastCh: + send_message(ukAlert, int(channel), 0, deviceID) + else: + send_message(ukAlert, emergencyAlertBroadcastCh, 0, deviceID) + return True + + if NO_ALERTS not in alertDe: + if deAlert != priorEmergencyAlert: + priorEmergencyAlert = deAlert + else: + return False + if isinstance(emergencyAlertBroadcastCh, list): + for channel in emergencyAlertBroadcastCh: + send_message(deAlert, int(channel), 0, deviceID) + else: + send_message(deAlert, emergencyAlertBroadcastCh, 0, deviceID) + return True + + if wxAlertBroadcastEnabled: + if wxAlert: + if wxAlert != priorWxAlert: + priorWxAlert = wxAlert + else: + return False + if isinstance(wxAlertBroadcastChannel, list): + for channel in wxAlertBroadcastChannel: + send_message(wxAlert, int(channel), 0, deviceID) + else: + send_message(wxAlert, wxAlertBroadcastChannel, 0, deviceID) + return True + + if volcanoAlertBroadcastEnabled: + volcanoAlert = get_volcano_usgs(latitudeValue, longitudeValue) + if volcanoAlert and NO_ALERTS not in volcanoAlert and ERROR_FETCHING_DATA not in volcanoAlert: + # check if the alert is different from the last one + if volcanoAlert != priorVolcanoAlert: + priorVolcanoAlert = volcanoAlert + if isinstance(volcanoAlertBroadcastChannel, list): + for channel in volcanoAlertBroadcastChannel: + send_message(volcanoAlert, int(channel), 0, deviceID) + else: + send_message(volcanoAlert, volcanoAlertBroadcastChannel, 0, deviceID) + return True + except Exception as e: + logger.error(f"System: Error in handleAlertBroadcast: {e}") + return False def onDisconnect(interface): # Handle disconnection of the interface @@ -2141,7 +2171,7 @@ async def watchdog(): handleMultiPing(0, i) - if wxAlertBroadcastEnabled or emergencyAlertBrodcastEnabled or volcanoAlertBroadcastEnabled: + if wxAlertBroadcastEnabled or emergencyAlertBrodcastEnabled or volcanoAlertBroadcastEnabled or checklist_enabled: handleAlertBroadcast(i) intData = displayNodeTelemetry(0, i)