Ernest_tested_6_25_2025
This commit is contained in:
parent
cb57ae8d7b
commit
4cc8509f9c
398
well-alerts.py
398
well-alerts.py
@ -24,6 +24,7 @@ import psutil
|
|||||||
import requests
|
import requests
|
||||||
import copy
|
import copy
|
||||||
import re
|
import re
|
||||||
|
import ast
|
||||||
import random
|
import random
|
||||||
import traceback
|
import traceback
|
||||||
#import math
|
#import math
|
||||||
@ -1077,7 +1078,6 @@ def load_device_configurations():
|
|||||||
mac_to_device_id[device_detail[2]] = device_detail[0]
|
mac_to_device_id[device_detail[2]] = device_detail[0]
|
||||||
alarm_device_settings_str = GetDeviceAlarmSettings(device_id)
|
alarm_device_settings_str = GetDeviceAlarmSettings(device_id)
|
||||||
|
|
||||||
|
|
||||||
#if alarm_device_settings != "":
|
#if alarm_device_settings != "":
|
||||||
# print(alarm_device_settings)
|
# print(alarm_device_settings)
|
||||||
deployment_id = device_detail[6]
|
deployment_id = device_detail[6]
|
||||||
@ -1085,6 +1085,11 @@ def load_device_configurations():
|
|||||||
# print(device_id)
|
# print(device_id)
|
||||||
device_to_deployment[device_id] = deployment_id
|
device_to_deployment[device_id] = deployment_id
|
||||||
|
|
||||||
|
if alarm_device_settings_str != None and alarm_device_settings_str != "":
|
||||||
|
device_alerts_all[device_id] = json.loads(alarm_device_settings_str)
|
||||||
|
else:
|
||||||
|
device_alerts_all[device_id] = {}
|
||||||
|
|
||||||
if deployment_id not in deployment_id_list:
|
if deployment_id not in deployment_id_list:
|
||||||
deployment_id_list.append(deployment_id)
|
deployment_id_list.append(deployment_id)
|
||||||
|
|
||||||
@ -1124,6 +1129,7 @@ def load_device_configurations():
|
|||||||
last_seen = GetRedisFloat('lastseen_'+device_id_s)
|
last_seen = GetRedisFloat('lastseen_'+device_id_s)
|
||||||
|
|
||||||
if last_seen == None:
|
if last_seen == None:
|
||||||
|
#Check from DB
|
||||||
last_seen = GetLastDetected(device_id_s, radar_threshold_signal, radar_threshold_value)
|
last_seen = GetLastDetected(device_id_s, radar_threshold_signal, radar_threshold_value)
|
||||||
if last_seen == None:
|
if last_seen == None:
|
||||||
last_seen = 0
|
last_seen = 0
|
||||||
@ -1146,7 +1152,7 @@ def load_device_configurations():
|
|||||||
if len(alarm_settings_str) > 2:
|
if len(alarm_settings_str) > 2:
|
||||||
alarm_settings = json.loads(alarm_settings_str)
|
alarm_settings = json.loads(alarm_settings_str)
|
||||||
alarm_armed_settings = alarm_settings['enabled']
|
alarm_armed_settings = alarm_settings['enabled']
|
||||||
alarm_settings["armed"] = alarm_armed_settings
|
alarm_settings["armed_states"] = alarm_armed_settings
|
||||||
alarms_settings_all[deployment_id] = alarm_settings
|
alarms_settings_all[deployment_id] = alarm_settings
|
||||||
#lets reset bit 1, so armed alarm is recognized on start if present... start of what???
|
#lets reset bit 1, so armed alarm is recognized on start if present... start of what???
|
||||||
#alarm_armed_settings = ClearBit(alarm_armed_settings, 1)
|
#alarm_armed_settings = ClearBit(alarm_armed_settings, 1)
|
||||||
@ -3084,6 +3090,9 @@ def PrepareMP3(text_to_speak):
|
|||||||
|
|
||||||
def SendPhoneCall(phone_nr, text_str):
|
def SendPhoneCall(phone_nr, text_str):
|
||||||
|
|
||||||
|
if phone_nr[0] == "-" or phone_nr[0] == "_":
|
||||||
|
return
|
||||||
|
|
||||||
phone_nr = normalize_phone_number(phone_nr)
|
phone_nr = normalize_phone_number(phone_nr)
|
||||||
#TELNYX_WEBHOOK_URL_VOICE = "http://eluxnetworks.net:1998/telnyx-webhook"
|
#TELNYX_WEBHOOK_URL_VOICE = "http://eluxnetworks.net:1998/telnyx-webhook"
|
||||||
uuid_str = PrepareMP3(text_str)
|
uuid_str = PrepareMP3(text_str)
|
||||||
@ -3124,7 +3133,7 @@ def SendPhoneCall(phone_nr, text_str):
|
|||||||
def SendAlerts(deployment_id, method, text_str, subject, user_name):
|
def SendAlerts(deployment_id, method, text_str, subject, user_name):
|
||||||
|
|
||||||
global sender
|
global sender
|
||||||
|
#return #todo remove it in production
|
||||||
if user_name == "" or user_name == None: #real request so send to all caretakers
|
if user_name == "" or user_name == None: #real request so send to all caretakers
|
||||||
|
|
||||||
care_takers = care_takers_all[int(deployment_id)]
|
care_takers = care_takers_all[int(deployment_id)]
|
||||||
@ -3222,15 +3231,77 @@ def GetTimeZoneOfDeployment(deployment_id):
|
|||||||
|
|
||||||
def StoreLastSentToRedis(deployment_id):
|
def StoreLastSentToRedis(deployment_id):
|
||||||
alarms_settings = alarms_settings_all[deployment_id]
|
alarms_settings = alarms_settings_all[deployment_id]
|
||||||
alarms_settings["last_triggered_utc"] = datetime.datetime.utcnow()
|
alarms_settings["last_triggered_utc"] = datetime.datetime.utcnow().isoformat()
|
||||||
|
try:
|
||||||
alarm_deployment_settings_str = json.dumps(alarms_settings)
|
alarm_deployment_settings_str = json.dumps(alarms_settings)
|
||||||
redis_conn.set('alarm_deployment_settings_'+str(deployment_id), alarm_deployment_settings_str)
|
redis_conn.set('alarm_deployment_settings_'+str(deployment_id), alarm_deployment_settings_str)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error: {e}")
|
||||||
|
|
||||||
def StoreDeviceAlarmsToRedis(device_id, device_alarm_settings):
|
def StoreDeviceAlarmsToRedis(device_id, device_alarm_settings):
|
||||||
alarm_device_settings_str = json.dumps(device_alarm_settings)
|
alarm_device_settings_str = json.dumps(device_alarm_settings)
|
||||||
redis_conn.set('alarm_device_settings_'+str(device_id), alarm_device_settings_str)
|
redis_conn.set('alarm_device_settings_'+str(device_id), alarm_device_settings_str)
|
||||||
|
|
||||||
|
def StoreDeviceAlarmsToDB(device_id, device_alarm_settings):
|
||||||
|
|
||||||
|
alarm_device_settings_str = json.dumps(device_alarm_settings)
|
||||||
|
|
||||||
|
conn = get_db_connection()
|
||||||
|
if not conn:
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
|
||||||
|
cur.execute("UPDATE devices SET alert_details = %s WHERE device_id = %s",
|
||||||
|
(alarm_device_settings_str, device_id))
|
||||||
|
|
||||||
|
print(f"Updated alert_details for {device_id} to {alarm_device_settings_str}")
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
# Update the ACL file
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Database error: {e}")
|
||||||
|
conn.rollback()
|
||||||
|
return False
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
def StoreLastSentToDB(deployment_id):
|
||||||
|
alarms_settings = alarms_settings_all[deployment_id]
|
||||||
|
alarm_deployment_settings_str = json.dumps(alarms_settings)
|
||||||
|
|
||||||
|
conn = get_db_connection()
|
||||||
|
if not conn:
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
|
||||||
|
cur.execute("UPDATE deployments SET alarm_details = %s WHERE deployment_id = %s",
|
||||||
|
(alarm_deployment_settings_str, deployment_id))
|
||||||
|
|
||||||
|
print(f"Updated alert_details for {deployment_id} to {alarm_deployment_settings_str}")
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
# Update the ACL file
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Database error: {e}")
|
||||||
|
conn.rollback()
|
||||||
|
return False
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
def StoreAllToRedisAndDB(deployment_id, device_id):
|
||||||
|
StoreLastSentToRedis(deployment_id)
|
||||||
|
StoreLastSentToDB(deployment_id)
|
||||||
|
device_alarm_settings = device_alerts_all[device_id]
|
||||||
|
StoreDeviceAlarmsToRedis(device_id, device_alarm_settings)
|
||||||
|
StoreDeviceAlarmsToDB(device_id, device_alarm_settings)
|
||||||
|
|
||||||
def SetupTasks():
|
def SetupTasks():
|
||||||
|
|
||||||
global local_daily_minute_last
|
global local_daily_minute_last
|
||||||
@ -3261,8 +3332,8 @@ def SetupTasks():
|
|||||||
else:
|
else:
|
||||||
alarms_settings = json.loads(alarm_deployment_settings_str)
|
alarms_settings = json.loads(alarm_deployment_settings_str)
|
||||||
|
|
||||||
if "armed" in alarms_settings:
|
if "enabled" in alarms_settings:
|
||||||
alarm_armed_settings_str = alarms_settings["armed"]
|
alarm_armed_settings_str = alarms_settings["enabled"]
|
||||||
else:
|
else:
|
||||||
alarm_armed_settings_str = "000"
|
alarm_armed_settings_str = "000"
|
||||||
|
|
||||||
@ -3277,14 +3348,6 @@ def SetupTasks():
|
|||||||
rearm_policy = "Never"
|
rearm_policy = "Never"
|
||||||
#alarm_armed_settings_str = GetRedisString(f'alarm_armed_settings_{deployment_id}')
|
#alarm_armed_settings_str = GetRedisString(f'alarm_armed_settings_{deployment_id}')
|
||||||
|
|
||||||
#alert_mode bits:
|
|
||||||
#0: 0 = default, 1 = set (so I can distinquish between nothing set (0s default) and purposefully set to 0s)
|
|
||||||
#1: Home Security bit 0 = not used, 1 = alarm armed
|
|
||||||
#2: Warning level environmental condition (Yellow) Temperatures High/Low
|
|
||||||
#3: Alarm level environmental condition (Red) Temperatures High/Low
|
|
||||||
#4: Warning level medical condition (Yellow) Too long present/absent
|
|
||||||
#5: Alarm level medical condition (Red) Too long present/absent
|
|
||||||
#6: Alarm if alone too long
|
|
||||||
|
|
||||||
utc_time = datetime.datetime.utcnow()
|
utc_time = datetime.datetime.utcnow()
|
||||||
# Calculate minutes since start of day
|
# Calculate minutes since start of day
|
||||||
@ -3316,41 +3379,55 @@ def SetupTasks():
|
|||||||
elif rearm_policy == "Never":
|
elif rearm_policy == "Never":
|
||||||
pass
|
pass
|
||||||
|
|
||||||
if do_rearm:
|
|
||||||
alarm_armed_settings, alarm_settings_str = GetAlarmSettings(deployment_id)
|
|
||||||
if len(alarm_settings_str) > 2:
|
|
||||||
alarm_settings = json.loads(alarm_settings_str)
|
|
||||||
alarm_settings["armed"] = alarm_armed_settings
|
|
||||||
alarms_settings_all[deployment_id] = alarm_settings
|
|
||||||
alarm_armed_settings_str = alarms_settings["armed"]
|
|
||||||
|
|
||||||
devices_lst = deployments_devices[deployment_id]
|
devices_lst = deployments_devices[deployment_id]
|
||||||
|
if do_rearm:
|
||||||
|
#No need to re-arm Burglar alarm it remains armed untill manually disarmed
|
||||||
|
alarm_settings_str = GetAlarmSettings(deployment_id)
|
||||||
|
if alarm_settings_str != "{}":
|
||||||
|
alarm_settings = json.loads(alarm_settings_str)
|
||||||
|
#alarm_settings["enabled"] bits:
|
||||||
|
# bit 2: Burglar Alarm (index 0), bit 1: Time alone Alarm is Set (index 1), bit 0: Time alone Warning is Set (index 2)
|
||||||
|
if alarm_settings["enabled"][1] == "1": #Time alone Alarm
|
||||||
|
alarm_settings["alone_alarm_armed"] = True
|
||||||
|
|
||||||
|
if alarm_settings["enabled"][2] == "1": #Time alone Warning
|
||||||
|
alarm_settings["alone_warning_armed"] = True
|
||||||
|
|
||||||
|
alarms_settings_all[deployment_id] = alarm_settings
|
||||||
|
StoreLastSentToRedis(deployment_id)
|
||||||
|
StoreLastSentToDB(deployment_id)
|
||||||
|
alarm_armed_settings_str = alarms_settings["armed_states"]
|
||||||
|
|
||||||
for device_id in devices_lst:
|
for device_id in devices_lst:
|
||||||
if device_id in device_alerts_all:
|
if device_id in device_alerts_all:
|
||||||
device_id_s = str(device_id)
|
device_id_s = str(device_id)
|
||||||
alarm_device_settings_str = GetDeviceAlarmSettings(device_id)
|
alarm_device_settings_str = GetDeviceAlarmSettings(device_id)
|
||||||
redis_conn.set('alarm_device_settings_'+device_id_s, alarm_device_settings_str)
|
StoreDeviceAlarmsToRedis(device_id, alarm_device_settings_str)
|
||||||
|
StoreDeviceAlarmsToDB(device_id, device_alarm_settings)
|
||||||
|
|
||||||
|
|
||||||
if alarm_armed_settings_str != "":
|
if alarm_armed_settings_str[0] == "1": #"100" Burglar alarm
|
||||||
if alarm_armed_settings_str[-1] == "1": #used
|
#Burglar alarm is triggerred on sensor read not time so nothing to be done here
|
||||||
#print(alarm_armed_settings_str)
|
pass
|
||||||
if alarm_armed_settings_str[-2] == "0": #alarm not armed, so compare individual conditions
|
|
||||||
if alarm_armed_settings_str[-5] == "1" or alarm_armed_settings_str[-6] == "1": #Too long present/absent
|
#lets check if alone
|
||||||
devices_lst = deployments_devices[deployment_id]
|
if "1" in alarm_armed_settings_str[1:]: #"010" alone warning or alarm
|
||||||
|
pass #todo
|
||||||
|
|
||||||
numbers = []
|
numbers = []
|
||||||
|
|
||||||
for device_id in devices_lst:
|
for device_id in devices_lst:
|
||||||
if device_id in device_alerts_all:
|
if device_id in device_alerts_all:
|
||||||
since_seen = time.time() - GetRedisFloat('lastseen_'+str(device_id))
|
since_seen = time.time() - GetRedisFloat('lastseen_'+str(device_id))
|
||||||
numbers.append((since_seen, device_id))
|
numbers.append((since_seen, device_id))
|
||||||
|
|
||||||
sorted_numbers = numbers
|
#In order to understand if somebody is too long in some place, or to long since it visited, we use following logic:
|
||||||
|
#For too long since last visit: we check if since_seen is larger than absent_minutes_alarm and absent_minutes_warning value
|
||||||
|
#For too long (stuck) in same place: we check (time_of_last_read_of_the_device - time_of_last_read_of_any_other_device) larger than stuck_minutes_warning and stuck_minutes_alarm value
|
||||||
second_smallest = -1
|
second_smallest = -1
|
||||||
if len(numbers) == 1:
|
if len(numbers) <2:
|
||||||
smallest = sorted_numbers[0]
|
#needs at least 2 devices for proper operation
|
||||||
if len(numbers) >= 2:
|
pass
|
||||||
|
else:
|
||||||
sorted_numbers = sorted(numbers, key=lambda x: x[0])
|
sorted_numbers = sorted(numbers, key=lambda x: x[0])
|
||||||
smallest = sorted_numbers[0]
|
smallest = sorted_numbers[0]
|
||||||
second_smallest = sorted_numbers[1]
|
second_smallest = sorted_numbers[1]
|
||||||
@ -3360,14 +3437,18 @@ def SetupTasks():
|
|||||||
device_id = tpl[1]
|
device_id = tpl[1]
|
||||||
#device_id_to_last_seen[tpl[1]] = tpl[0]
|
#device_id_to_last_seen[tpl[1]] = tpl[0]
|
||||||
device_alarm_settings = device_alerts_all[device_id]
|
device_alarm_settings = device_alerts_all[device_id]
|
||||||
|
if "enabled_alarms" in device_alarm_settings:
|
||||||
enabled_alarms_str = device_alarm_settings["enabled_alarms"]
|
enabled_alarms_str = device_alarm_settings["enabled_alarms"]
|
||||||
#lets check larm first, because if satisfied, no need to check for warning
|
armed_states = "00000000000"
|
||||||
if enabled_alarms_str[-2] == "1": #Too long present alarm
|
if "armed_states" in device_alarm_settings:
|
||||||
|
armed_states = device_alarm_settings["armed_states"]
|
||||||
|
#lets check alarms first, because if satisfied, no need to check for warning
|
||||||
|
if enabled_alarms_str[BitIndex(1)] == "1" and armed_states[BitIndex(1)] == "1": #Too long present alarm if enabled and not already triggerred (triggered = 0)!
|
||||||
if device_id == smallest[1]: #now present... how long?
|
if device_id == smallest[1]: #now present... how long?
|
||||||
if (second_smallest[0] - smallest[0]) > device_alarm_settings["stuck_minutes_alarm"] * 60:
|
if (second_smallest[0] - smallest[0]) > device_alarm_settings["stuck_minutes_alarm"] * 60:
|
||||||
#cancel alarm and warning, until re-armed
|
#cancel alarm and warning, until re-armed
|
||||||
enabled_alarms_str = set_character(enabled_alarms_str, 0, "0")
|
armed_states = set_character(armed_states, 0, "0")
|
||||||
enabled_alarms_str = set_character(enabled_alarms_str, 1, "0")
|
armed_states = set_character(armed_states, 1, "0")
|
||||||
dev_det = devices_details_map[device_id]
|
dev_det = devices_details_map[device_id]
|
||||||
location = (dev_det[4] + " " + dev_det[5].strip()).strip()
|
location = (dev_det[4] + " " + dev_det[5].strip()).strip()
|
||||||
method = device_alarm_settings["stuck_alarm_method_1"]
|
method = device_alarm_settings["stuck_alarm_method_1"]
|
||||||
@ -3376,7 +3457,7 @@ def SetupTasks():
|
|||||||
else:
|
else:
|
||||||
SendAlerts(deployment_id, method, f"Alarm: {user_first_last_name} way too long in {location}", "", "")
|
SendAlerts(deployment_id, method, f"Alarm: {user_first_last_name} way too long in {location}", "", "")
|
||||||
|
|
||||||
device_alarm_settings["enabled_alarms"] = enabled_alarms_str
|
device_alarm_settings["armed_states"] = armed_states
|
||||||
StoreLastSentToRedis(deployment_id)
|
StoreLastSentToRedis(deployment_id)
|
||||||
device_alerts_all[device_id] = device_alarm_settings
|
device_alerts_all[device_id] = device_alarm_settings
|
||||||
StoreDeviceAlarmsToRedis(device_id, device_alarm_settings)
|
StoreDeviceAlarmsToRedis(device_id, device_alarm_settings)
|
||||||
@ -3385,10 +3466,10 @@ def SetupTasks():
|
|||||||
if check_after < next_run_in_minutes:
|
if check_after < next_run_in_minutes:
|
||||||
next_run_in_minutes = check_after
|
next_run_in_minutes = check_after
|
||||||
|
|
||||||
elif enabled_alarms_str[-1] == "1": #Too long present warning
|
elif enabled_alarms_str[BitIndex(0)] == "1" and armed_states[BitIndex(0)] == "1": #Too long present warning
|
||||||
if (second_smallest[0] - smallest[0]) > device_alarm_settings["stuck_minutes_warning"] * 60:
|
if (second_smallest[0] - smallest[0]) > device_alarm_settings["stuck_minutes_warning"] * 60:
|
||||||
#cancel warning, until re-armed
|
#cancel warning, until re-armed
|
||||||
enabled_alarms_str = set_character(enabled_alarms_str, 0, "0")
|
armed_states = set_character(armed_states, 0, "0")
|
||||||
dev_det = devices_details_map[device_id]
|
dev_det = devices_details_map[device_id]
|
||||||
location = (dev_det[4] + " " + dev_det[5].strip()).strip()
|
location = (dev_det[4] + " " + dev_det[5].strip()).strip()
|
||||||
method = device_alarm_settings["stuck_warning_method_0"]
|
method = device_alarm_settings["stuck_warning_method_0"]
|
||||||
@ -3398,7 +3479,7 @@ def SetupTasks():
|
|||||||
SendAlerts(deployment_id, method, f"Warning: {user_first_last_name} too long in {location}", "", "")
|
SendAlerts(deployment_id, method, f"Warning: {user_first_last_name} too long in {location}", "", "")
|
||||||
|
|
||||||
StoreLastSentToRedis(deployment_id)
|
StoreLastSentToRedis(deployment_id)
|
||||||
device_alarm_settings["enabled_alarms"] = enabled_alarms_str
|
device_alarm_settings["armed_states"] = armed_states
|
||||||
device_alerts_all[device_id] = device_alarm_settings
|
device_alerts_all[device_id] = device_alarm_settings
|
||||||
StoreDeviceAlarmsToRedis(device_id, device_alarm_settings)
|
StoreDeviceAlarmsToRedis(device_id, device_alarm_settings)
|
||||||
else:
|
else:
|
||||||
@ -3407,11 +3488,11 @@ def SetupTasks():
|
|||||||
next_run_in_minutes = check_after
|
next_run_in_minutes = check_after
|
||||||
|
|
||||||
|
|
||||||
if enabled_alarms_str[-4] == "1": #Too long absent alarm
|
if enabled_alarms_str[BitIndex(3)] == "1" and armed_states[BitIndex(3)] == "1": #Too long absent alarm
|
||||||
if last_seen_ago > device_alarm_settings["absent_minutes_alarm"] * 60:
|
if last_seen_ago > device_alarm_settings["absent_minutes_alarm"] * 60:
|
||||||
|
|
||||||
enabled_alarms_str = set_character(enabled_alarms_str, 3, "0")
|
armed_states = set_character(armed_states, 3, "0")
|
||||||
enabled_alarms_str = set_character(enabled_alarms_str, 2, "0")
|
armed_states = set_character(armed_states, 2, "0")
|
||||||
dev_det = devices_details_map[device_id]
|
dev_det = devices_details_map[device_id]
|
||||||
location = (dev_det[4] + " " + dev_det[5].strip()).strip()
|
location = (dev_det[4] + " " + dev_det[5].strip()).strip()
|
||||||
method = device_alarm_settings["absent_alarm_method_3"]
|
method = device_alarm_settings["absent_alarm_method_3"]
|
||||||
@ -3419,7 +3500,7 @@ def SetupTasks():
|
|||||||
SendAlerts(deployment_id, method, f"Alarm: Way too long since {user_first_last_name} visited {location} ({int(last_seen_ago / 60)} minutes)", "", "")
|
SendAlerts(deployment_id, method, f"Alarm: Way too long since {user_first_last_name} visited {location} ({int(last_seen_ago / 60)} minutes)", "", "")
|
||||||
else:
|
else:
|
||||||
SendAlerts(deployment_id, method, f"Alarm: Way too long since {user_first_last_name} visited {location}", "", "")
|
SendAlerts(deployment_id, method, f"Alarm: Way too long since {user_first_last_name} visited {location}", "", "")
|
||||||
device_alarm_settings["enabled_alarms"] = enabled_alarms_str
|
device_alarm_settings["armed_states"] = armed_states
|
||||||
device_alerts_all[device_id] = device_alarm_settings
|
device_alerts_all[device_id] = device_alarm_settings
|
||||||
StoreLastSentToRedis(deployment_id)
|
StoreLastSentToRedis(deployment_id)
|
||||||
StoreDeviceAlarmsToRedis(device_id, device_alarm_settings)
|
StoreDeviceAlarmsToRedis(device_id, device_alarm_settings)
|
||||||
@ -3428,10 +3509,10 @@ def SetupTasks():
|
|||||||
if check_after < next_run_in_minutes:
|
if check_after < next_run_in_minutes:
|
||||||
next_run_in_minutes = check_after
|
next_run_in_minutes = check_after
|
||||||
|
|
||||||
if enabled_alarms_str[-3] == "1": #Too long absent alarm
|
if enabled_alarms_str[BitIndex(2)] == "1" and armed_states[BitIndex(2)] == "1": #Too long absent alarm
|
||||||
if last_seen_ago > device_alarm_settings["absent_minutes_warning"] * 60:
|
if last_seen_ago > device_alarm_settings["absent_minutes_warning"] * 60:
|
||||||
|
|
||||||
enabled_alarms_str = set_character(enabled_alarms_str, 2, "0")
|
armed_states = set_character(armed_states, 2, "0")
|
||||||
dev_det = devices_details_map[device_id]
|
dev_det = devices_details_map[device_id]
|
||||||
location = (dev_det[4] + " " + dev_det[5].strip()).strip()
|
location = (dev_det[4] + " " + dev_det[5].strip()).strip()
|
||||||
method = device_alarm_settings["absent_warning_method_2"]
|
method = device_alarm_settings["absent_warning_method_2"]
|
||||||
@ -3440,7 +3521,7 @@ def SetupTasks():
|
|||||||
else:
|
else:
|
||||||
SendAlerts(deployment_id, method, f"Warning: Too long since {user_first_last_name} visited {location}", "", "")
|
SendAlerts(deployment_id, method, f"Warning: Too long since {user_first_last_name} visited {location}", "", "")
|
||||||
|
|
||||||
device_alarm_settings["enabled_alarms"] = enabled_alarms_str
|
device_alarm_settings["armed_states"] = armed_states
|
||||||
device_alerts_all[device_id] = device_alarm_settings
|
device_alerts_all[device_id] = device_alarm_settings
|
||||||
StoreLastSentToRedis(deployment_id)
|
StoreLastSentToRedis(deployment_id)
|
||||||
StoreDeviceAlarmsToRedis(device_id, device_alarm_settings)
|
StoreDeviceAlarmsToRedis(device_id, device_alarm_settings)
|
||||||
@ -3462,8 +3543,7 @@ def SetupTasks():
|
|||||||
#"pressure_alarm_method_9":"MSG",
|
#"pressure_alarm_method_9":"MSG",
|
||||||
#"light_alarm_method_10":"MSG"
|
#"light_alarm_method_10":"MSG"
|
||||||
#how to determine when user arived here (time of any other place!)
|
#how to determine when user arived here (time of any other place!)
|
||||||
if alarm_armed_settings_str[-7] == "1": #Too long alone
|
|
||||||
pass #todo
|
|
||||||
|
|
||||||
if next_run_in_minutes > 1:
|
if next_run_in_minutes > 1:
|
||||||
next_run_in_minutes = 1
|
next_run_in_minutes = 1
|
||||||
@ -3760,6 +3840,53 @@ def FahrenheitToCelsius(F):
|
|||||||
C = (F - 32) * 5/9
|
C = (F - 32) * 5/9
|
||||||
return C
|
return C
|
||||||
|
|
||||||
|
def BitIndex(bit_nr):
|
||||||
|
return -bit_nr - 1
|
||||||
|
|
||||||
|
def RadarLarger(radar, radar_threshold):
|
||||||
|
|
||||||
|
thr_parts = radar_threshold[0].split("_")
|
||||||
|
field = thr_parts[0]
|
||||||
|
if field == "s28":
|
||||||
|
radar_val = sum(radar[1][3:]) / (7 * radar[1][0])
|
||||||
|
elif field == "m0":
|
||||||
|
radar_val = radar[0][5] / radar[1][0]
|
||||||
|
elif field == "m1":
|
||||||
|
radar_val = radar[0][6] / radar[1][0]
|
||||||
|
elif field == "m2":
|
||||||
|
radar_val = radar[0][7] / radar[1][0]
|
||||||
|
elif field == "m3":
|
||||||
|
radar_val = radar[0][8] / radar[1][0]
|
||||||
|
elif field == "m4":
|
||||||
|
radar_val = radar[0][9] / radar[1][0]
|
||||||
|
elif field == "m5":
|
||||||
|
radar_val = radar[0][10] / radar[1][0]
|
||||||
|
elif field == "m6":
|
||||||
|
radar_val = radar[0][11] / radar[1][0]
|
||||||
|
elif field == "m7":
|
||||||
|
radar_val = radar[0][12] / radar[1][0]
|
||||||
|
elif field == "m8":
|
||||||
|
radar_val = radar[0][13] / radar[1][0]
|
||||||
|
elif field == "s2":
|
||||||
|
radar_val = radar[1][3] / radar[1][0]
|
||||||
|
elif field == "s3":
|
||||||
|
radar_val = radar[1][4] / radar[1][0]
|
||||||
|
elif field == "s4":
|
||||||
|
radar_val = radar[1][5] / radar[1][0]
|
||||||
|
elif field == "s5":
|
||||||
|
radar_val = radar[1][6] / radar[1][0]
|
||||||
|
elif field == "s6":
|
||||||
|
radar_val = radar[1][7] / radar[1][0]
|
||||||
|
elif field == "s7":
|
||||||
|
radar_val = radar[1][8] / radar[1][0]
|
||||||
|
elif field == "s8":
|
||||||
|
radar_val = radar[1][9] / radar[1][0]
|
||||||
|
|
||||||
|
if radar_val > radar_threshold[1]:
|
||||||
|
return radar_val, True
|
||||||
|
else:
|
||||||
|
return radar_val, False
|
||||||
|
|
||||||
def ProcessQueue():
|
def ProcessQueue():
|
||||||
#here we are looking for alarm conditions in data
|
#here we are looking for alarm conditions in data
|
||||||
global in_queue
|
global in_queue
|
||||||
@ -3774,30 +3901,27 @@ def ProcessQueue():
|
|||||||
deployment_id = device_to_deployment[device_id]
|
deployment_id = device_to_deployment[device_id]
|
||||||
if deployment_id in alarms_settings_all:
|
if deployment_id in alarms_settings_all:
|
||||||
alarms_settings = alarms_settings_all[deployment_id]
|
alarms_settings = alarms_settings_all[deployment_id]
|
||||||
alarm_armed_settings_str = alarms_settings["armed"]
|
alarm_armed_settings_str = alarms_settings["enabled"]
|
||||||
|
# bit 2: Burglar Alarm (index 0), bit 1: Time Alone Alarm is Set (index 1), bit 0: Time alone Warning is Set (index 2)
|
||||||
|
|
||||||
#0: 0 = default, 1 = set (so I can distinquish between nothing set (0s default) and purposefully set to 0s)
|
if device_id in device_alerts_all:
|
||||||
#1: Home Security bit 0 = not used, 1 = alarm armed
|
device_alarm_settings = device_alerts_all[device_id]
|
||||||
#2: Warning level environmental condition (Yellow) Temperatures High/Low
|
|
||||||
#3: Alarm level environmental condition (Red) Temperatures High/Low
|
|
||||||
#4: Warning level medical condition (Yellow) Too long present/absent
|
|
||||||
#5: Alarm level medical condition (Red) Too long present/absent
|
|
||||||
#6: Alarm if alone too long
|
|
||||||
temp_offset = -16.0
|
|
||||||
if alarm_armed_settings_str[-1] == "1": #used
|
|
||||||
message_dict = json.loads(messagein.decode('utf-8'))
|
message_dict = json.loads(messagein.decode('utf-8'))
|
||||||
|
if "enabled_alarms" in device_alarm_settings:
|
||||||
|
#Lets check temperatures
|
||||||
|
temp_offset = -16.0
|
||||||
|
if "1" in device_alarm_settings["enabled_alarms"][BitIndex(7):BitIndex(3)]: #Temperatures Too High/Low trigger used?
|
||||||
|
enabled_alarms_str = device_alarm_settings["enabled_alarms"]
|
||||||
|
armed_states = device_alarm_settings["armed_states"] #1=armed 0=triggered
|
||||||
#print(alarm_armed_settings_str)
|
#print(alarm_armed_settings_str)
|
||||||
if alarm_armed_settings_str[-2] == "0": #alarm not armed, so compare individual conditions
|
|
||||||
if alarm_armed_settings_str[-3] == "1" or alarm_armed_settings_str[-4] == "1": #Temperatures Too High/Low
|
|
||||||
|
|
||||||
if "temperature" in message_dict:
|
if "temperature" in message_dict:
|
||||||
temperature = message_dict["temperature"] + temp_offset
|
temperature = message_dict["temperature"] + temp_offset
|
||||||
#at this point temperature is in C
|
#at this point temperature is in C
|
||||||
|
|
||||||
if temperature > 0 and temperature < 100: #ignore others
|
if temperature > 0 and temperature < 100: #ignore others
|
||||||
|
if device_id in device_alerts_all:
|
||||||
|
|
||||||
device_alarm_settings = device_alerts_all[device_id]
|
|
||||||
enabled_alarms_str = device_alarm_settings["enabled_alarms"]
|
|
||||||
|
|
||||||
'''
|
'''
|
||||||
{
|
{
|
||||||
@ -3826,7 +3950,7 @@ def ProcessQueue():
|
|||||||
"rearm_policy":"At midnight"
|
"rearm_policy":"At midnight"
|
||||||
}
|
}
|
||||||
'''
|
'''
|
||||||
|
#bits in enabled_alarms and armed_states explained
|
||||||
#"stuck_warning_method_0": "SMS", -1
|
#"stuck_warning_method_0": "SMS", -1
|
||||||
#"stuck_alarm_method_1": "PHONE", -2
|
#"stuck_alarm_method_1": "PHONE", -2
|
||||||
#"absent_warning_method_2": "SMS", -3
|
#"absent_warning_method_2": "SMS", -3
|
||||||
@ -3839,87 +3963,115 @@ def ProcessQueue():
|
|||||||
#"pressure_alarm_method_9":"MSG", -10
|
#"pressure_alarm_method_9":"MSG", -10
|
||||||
#"light_alarm_method_10":"MSG" -11
|
#"light_alarm_method_10":"MSG" -11
|
||||||
#"smell_alarm_method_11":"MSG" -12
|
#"smell_alarm_method_11":"MSG" -12
|
||||||
|
#hast ot be enabled and not triggerred to continue comparing
|
||||||
if enabled_alarms_str[-6] == "1": #Temperatures Too High Alarm!
|
if enabled_alarms_str[BitIndex(5)] == "1" and armed_states[BitIndex(5)] == "1": #Temperatures Too High Alarm!
|
||||||
if temperature > FahrenheitToCelsius(device_alarm_settings["temperature_high_alarm"]):
|
if temperature > FahrenheitToCelsius(device_alarm_settings["temperature_high_alarm"]):
|
||||||
#cancel alarm and warning, until re-armed
|
#cancel alarm and warning, until re-armed
|
||||||
enabled_alarms_str = set_character(enabled_alarms_str, 5, "0")
|
armed_states = set_character(armed_states, 5, "0")
|
||||||
enabled_alarms_str = set_character(enabled_alarms_str, 4, "0")
|
armed_states = set_character(armed_states, 4, "0")
|
||||||
dev_det = devices_details_map[device_id]
|
dev_det = devices_details_map[device_id]
|
||||||
location = (dev_det[4] + " " + dev_det[5].strip()).strip()
|
location = (dev_det[4] + " " + dev_det[5].strip()).strip()
|
||||||
method = device_alarm_settings["temperature_high_alarm_method_5"]
|
method = device_alarm_settings["temperature_high_alarm_method_5"]
|
||||||
SendAlerts(deployment_id, method, f"Alarm @ {first_last_name}: Temperature too high! ({temperature} C) in {location}", "", "")
|
first_last_name = GetBeneficiaryFromDeployment(deployment_id)
|
||||||
device_alarm_settings["enabled_alarms"] = enabled_alarms_str
|
SendAlerts(deployment_id, method, f"Alarm @ {first_last_name}: Temperature too high! ({temperature:.1f} C) in {location}", "", "")
|
||||||
|
device_alarm_settings["armed_states"] = armed_states
|
||||||
device_alerts_all[device_id] = device_alarm_settings
|
device_alerts_all[device_id] = device_alarm_settings
|
||||||
StoreLastSentToRedis(deployment_id)
|
StoreAllToRedisAndDB(deployment_id, device_id)
|
||||||
StoreDeviceAlarmsToRedis(device_id, device_alarm_settings)
|
|
||||||
elif enabled_alarms_str[-5] == "1": #Temperatures Too High Warning!
|
#at this point we also need to store it to DB, otherwise when HTML GUI is loaded fact that trigger happened will not be reflected!
|
||||||
|
elif enabled_alarms_str[BitIndex(4)] == "1" and armed_states[BitIndex(4)] == "1": #Temperatures Too High Warning!
|
||||||
|
|
||||||
if temperature > FahrenheitToCelsius(device_alarm_settings["temperature_high_warning"]):
|
if temperature > FahrenheitToCelsius(device_alarm_settings["temperature_high_warning"]):
|
||||||
#cancel alarm and warning, until re-armed
|
#cancel warning, until re-armed
|
||||||
enabled_alarms_str = set_character(enabled_alarms_str, 4, "0")
|
armed_states = set_character(armed_states, 4, "0")
|
||||||
dev_det = devices_details_map[device_id]
|
dev_det = devices_details_map[device_id]
|
||||||
location = (dev_det[4] + " " + dev_det[5].strip()).strip()
|
location = (dev_det[4] + " " + dev_det[5].strip()).strip()
|
||||||
method = device_alarm_settings["temperature_high_warning_method_4"]
|
method = device_alarm_settings["temperature_high_warning_method_4"]
|
||||||
SendAlerts(deployment_id, method, f"Warning @ {first_last_name}: Temperature too high! ({temperature} C) in {location}", "", "")
|
first_last_name = GetBeneficiaryFromDeployment(deployment_id)
|
||||||
device_alarm_settings["enabled_alarms"] = enabled_alarms_str
|
SendAlerts(deployment_id, method, f"Warning @ {first_last_name}: Temperature too high! ({temperature:.1f} C) in {location}", "", "")
|
||||||
|
device_alarm_settings["armed_states"] = armed_states
|
||||||
device_alerts_all[device_id] = device_alarm_settings
|
device_alerts_all[device_id] = device_alarm_settings
|
||||||
StoreLastSentToRedis(deployment_id)
|
StoreAllToRedisAndDB(deployment_id, device_id)
|
||||||
StoreDeviceAlarmsToRedis(device_id, device_alarm_settings)
|
|
||||||
if enabled_alarms_str[-8] == "1": #Temperatures Too Low Alarm!
|
if armed_states[BitIndex(7)] == "1" and armed_states[BitIndex(7)] == "1": #Temperatures Too Low Alarm!
|
||||||
if temperature < FahrenheitToCelsius(device_alarm_settings["temperature_low_alarm"]):
|
if temperature < FahrenheitToCelsius(device_alarm_settings["temperature_low_alarm"]):
|
||||||
#cancel alarm and warning, until re-armed
|
#cancel alarm and warning, until re-armed
|
||||||
enabled_alarms_str = set_character(enabled_alarms_str, 7, "0")
|
armed_states = set_character(armed_states, 7, "0")
|
||||||
enabled_alarms_str = set_character(enabled_alarms_str, 6, "0")
|
armed_states = set_character(armed_states, 6, "0")
|
||||||
dev_det = devices_details_map[device_id]
|
dev_det = devices_details_map[device_id]
|
||||||
location = (dev_det[4] + " " + dev_det[5].strip()).strip()
|
location = (dev_det[4] + " " + dev_det[5].strip()).strip()
|
||||||
method = device_alarm_settings["temperature_low_alarm_method_7"]
|
method = device_alarm_settings["temperature_low_alarm_method_7"]
|
||||||
SendAlerts(deployment_id, method, f"Alarm @ {first_last_name} Temperature too low! ({temperature} C) in {location}", "", "")
|
first_last_name = GetBeneficiaryFromDeployment(deployment_id)
|
||||||
device_alarm_settings["enabled_alarms"] = enabled_alarms_str
|
SendAlerts(deployment_id, method, f"Alarm @ {first_last_name} Temperature too low! ({temperature:.1f} C) in {location}", "", "")
|
||||||
|
device_alarm_settings["armed_states"] = armed_states
|
||||||
device_alerts_all[device_id] = device_alarm_settings
|
device_alerts_all[device_id] = device_alarm_settings
|
||||||
StoreLastSentToRedis(deployment_id)
|
StoreAllToRedisAndDB(deployment_id, device_id)
|
||||||
StoreDeviceAlarmsToRedis(device_id, device_alarm_settings)
|
|
||||||
|
|
||||||
elif enabled_alarms_str[-7] == "1": #Temperatures Too Low Warning!
|
elif armed_states[BitIndex(6)] == "1" and armed_states[BitIndex(6)] == "1": #Temperatures Too Low Warning!
|
||||||
|
|
||||||
if temperature < FahrenheitToCelsius(device_alarm_settings["temperature_low_warning"]):
|
if temperature < FahrenheitToCelsius(device_alarm_settings["temperature_low_warning"]):
|
||||||
#cancel alarm and warning, until re-armed
|
#cancel warning, until re-armed
|
||||||
enabled_alarms_str = set_character(enabled_alarms_str, 6, "0")
|
armed_states = set_character(armed_states, 6, "0")
|
||||||
dev_det = devices_details_map[device_id]
|
dev_det = devices_details_map[device_id]
|
||||||
location = (dev_det[4] + " " + dev_det[5].strip()).strip()
|
location = (dev_det[4] + " " + dev_det[5].strip()).strip()
|
||||||
method = device_alarm_settings["temperature_low_warning_method_6"]
|
method = device_alarm_settings["temperature_low_warning_method_6"]
|
||||||
SendAlerts(deployment_id, method, f"Warning @ {first_last_name} Temperature too low! ({temperature} C) in {location}", "", "")
|
first_last_name = GetBeneficiaryFromDeployment(deployment_id)
|
||||||
device_alarm_settings["enabled_alarms"] = enabled_alarms_str
|
SendAlerts(deployment_id, method, f"Warning @ {first_last_name} Temperature too low! ({temperature:.1f} C) in {location}", "", "")
|
||||||
|
device_alarm_settings["armed_states"] = armed_states
|
||||||
device_alerts_all[device_id] = device_alarm_settings
|
device_alerts_all[device_id] = device_alarm_settings
|
||||||
StoreLastSentToRedis(deployment_id)
|
StoreAllToRedisAndDB(deployment_id, device_id)
|
||||||
StoreDeviceAlarmsToRedis(device_id, device_alarm_settings)
|
|
||||||
|
|
||||||
logger.info(f"{tim}, {mac}, {temperature}")
|
#logger.info(f"{tim}, {mac}, {temperature}")
|
||||||
else: #radar packet
|
else: #radar packet
|
||||||
pass
|
pass
|
||||||
else: #alarm is armed
|
|
||||||
|
#lets check if alarm condition
|
||||||
|
if alarm_armed_settings_str[BitIndex(2)] == "1": #alarm is armed!
|
||||||
if "radar" in message_dict:
|
if "radar" in message_dict:
|
||||||
|
enabled_alarms_str = device_alarm_settings["enabled_alarms"]
|
||||||
|
armed_states = device_alarm_settings["armed_states"] #1=armed 0=triggered
|
||||||
|
if enabled_alarms_str[BitIndex(8)] == "1" and armed_states[BitIndex(8)] == "1":
|
||||||
radar = message_dict["radar"]
|
radar = message_dict["radar"]
|
||||||
|
radar_threshold_str = GetRedisString(f"radar_threshold{device_id}")
|
||||||
|
radar_threshold = ast.literal_eval(radar_threshold_str)
|
||||||
|
radar_value, larger = RadarLarger(radar, radar_threshold)
|
||||||
|
print(device_id, radar_value, larger)
|
||||||
|
if larger:
|
||||||
|
|
||||||
|
#cancel alarm for this room, until re-armed
|
||||||
|
armed_states = set_character(armed_states, 8, "0")
|
||||||
|
dev_det = devices_details_map[device_id]
|
||||||
|
location = (dev_det[4] + " " + dev_det[5].strip()).strip()
|
||||||
|
method = device_alarm_settings["radar_alarm_method_8"]
|
||||||
|
first_last_name = GetBeneficiaryFromDeployment(deployment_id)
|
||||||
|
SendAlerts(deployment_id, method, f"At {first_last_name} burglar alarm is triggered in the {location}", "", "")
|
||||||
|
device_alarm_settings["armed_states"] = armed_states
|
||||||
|
device_alerts_all[device_id] = device_alarm_settings
|
||||||
|
StoreAllToRedisAndDB(deployment_id, device_id)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
pass #alarm not setup for this device
|
pass #alarm not setup for this device
|
||||||
#print(f"{deployment_id} not in {alarms_settings_all}")
|
#print(f"{deployment_id} not in {alarms_settings_all}")
|
||||||
else:
|
else:
|
||||||
logger.error(f"MAC: {mac} not part of any deployment")
|
pass
|
||||||
|
#logger.error(f"MAC: {mac} not part of any deployment")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error: {str(e)} {traceback.format_exc()}")
|
logger.error(f"Error: {str(e)} {traceback.format_exc()}")
|
||||||
|
|
||||||
def CheckMessageSends():
|
def CheckRedisMessages():
|
||||||
|
|
||||||
requests_count = 0
|
requests_count = 0
|
||||||
|
#print(f"CheckRedisMessages")
|
||||||
# Check if queue exists and has items. These items are from manual GUI interactions in alerts page
|
# Check if queue exists and has items. These items are from manual GUI interactions in alerts page
|
||||||
|
|
||||||
|
#Any test messages requested to be sent?
|
||||||
queue_length = redis_conn.llen('send_requests')
|
queue_length = redis_conn.llen('send_requests')
|
||||||
|
if queue_length > 0:
|
||||||
|
|
||||||
if queue_length == 0:
|
print(f"Processing send_requests message from queue...")
|
||||||
return 0
|
|
||||||
|
|
||||||
print(f"Processing {queue_length} messages from queue...")
|
|
||||||
|
|
||||||
# Process each item
|
# Process each item
|
||||||
for i in range(queue_length):
|
for i in range(queue_length):
|
||||||
@ -3976,7 +4128,38 @@ def CheckMessageSends():
|
|||||||
logger.error(f"Failed to parse JSON from queue item: {e}")
|
logger.error(f"Failed to parse JSON from queue item: {e}")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
print(f"Total requests processed: {requests_count}")
|
#Any test messages requested to be sent?
|
||||||
|
queue_length = redis_conn.llen('new_alarms')
|
||||||
|
if queue_length > 0:
|
||||||
|
for i in range(queue_length):
|
||||||
|
|
||||||
|
print(f"Processing send_requests message from queue...")
|
||||||
|
item_json = redis_conn.rpop('new_alarms')
|
||||||
|
|
||||||
|
if item_json is None:
|
||||||
|
break
|
||||||
|
|
||||||
|
try:
|
||||||
|
record = json.loads(item_json)
|
||||||
|
deployment_id = int(record["deployment_id"])
|
||||||
|
device_id = int(record["device_id"])
|
||||||
|
print(record)
|
||||||
|
|
||||||
|
device_alarms_json = GetRedisString('alarm_device_settings_'+str(device_id))
|
||||||
|
deployment_alarms_json = GetRedisString('alarm_deployment_settings_'+str(deployment_id))
|
||||||
|
|
||||||
|
alarms_settings_all[deployment_id] = json.loads(deployment_alarms_json)
|
||||||
|
device_alerts_all[device_id] = json.loads(device_alarms_json)
|
||||||
|
print(device_alarms_json)
|
||||||
|
print(deployment_alarms_json)
|
||||||
|
|
||||||
|
#method = record["method"]
|
||||||
|
#location_str = record["location"]
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
logger.error(f"Failed to parse JSON from queue item: {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
#print(f"Total requests processed: {requests_count}")
|
||||||
return requests_count
|
return requests_count
|
||||||
|
|
||||||
# --- Main Execution ---
|
# --- Main Execution ---
|
||||||
@ -4015,6 +4198,7 @@ if __name__ == "__main__":
|
|||||||
SendAlerts(21, "EMAIL", f"well-alert was started", "Program started", "robster")
|
SendAlerts(21, "EMAIL", f"well-alert was started", "Program started", "robster")
|
||||||
#SendAlerts(21, "SMS", f"Test: User way too long ({120} minutes) in Bathroom", "")
|
#SendAlerts(21, "SMS", f"Test: User way too long ({120} minutes) in Bathroom", "")
|
||||||
#SendAlerts(21, "PHONE", f"Test: User way too long ({120} minutes) in Bathroom", "")
|
#SendAlerts(21, "PHONE", f"Test: User way too long ({120} minutes) in Bathroom", "")
|
||||||
|
SendPhoneCall("-4086462191", "Hi Robert. How are you?")
|
||||||
#SendPhoneCall("4086462191", "Hi Robert. How are you?", "")
|
#SendPhoneCall("4086462191", "Hi Robert. How are you?", "")
|
||||||
#SendPhoneCall("4085505424", "Hi Fred. How are you? Are you hungry?", "")
|
#SendPhoneCall("4085505424", "Hi Fred. How are you? Are you hungry?", "")
|
||||||
#SendPhoneCall("4087055709", "Hi Bernhard. How are you? Are you hungry?", "")
|
#SendPhoneCall("4087055709", "Hi Bernhard. How are you? Are you hungry?", "")
|
||||||
@ -4033,7 +4217,7 @@ if __name__ == "__main__":
|
|||||||
# Keep the main thread alive until stop_event is set by signal or error
|
# Keep the main thread alive until stop_event is set by signal or error
|
||||||
while not stop_event.is_set():
|
while not stop_event.is_set():
|
||||||
# Can add periodic health checks here if needed
|
# Can add periodic health checks here if needed
|
||||||
CheckMessageSends()
|
CheckRedisMessages()
|
||||||
time.sleep(1) # Check stop_event periodically
|
time.sleep(1) # Check stop_event periodically
|
||||||
|
|
||||||
logger.info("Stop event received, waiting for monitoring thread to finish...")
|
logger.info("Stop event received, waiting for monitoring thread to finish...")
|
||||||
|
|||||||
222
wh1998v2.py
Normal file
222
wh1998v2.py
Normal file
@ -0,0 +1,222 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
import requests
|
||||||
|
import re
|
||||||
|
import base64
|
||||||
|
from flask import Flask, request, Response, jsonify
|
||||||
|
import logging
|
||||||
|
import logging.handlers
|
||||||
|
import sys
|
||||||
|
import argparse
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
# --- Configuration Loading ---
|
||||||
|
def load_env_file(filepath):
|
||||||
|
if not os.path.exists(filepath): return False
|
||||||
|
print(f"INFO: Loading environment file: {filepath}")
|
||||||
|
try:
|
||||||
|
load_dotenv(dotenv_path=filepath, override=True); return True
|
||||||
|
except Exception as e: print(f"ERROR: Failed to load env file {filepath}: {e}"); return False
|
||||||
|
|
||||||
|
env_loaded = any(load_env_file(os.path.join(os.getcwd(), f)) for f in ['.env', 'env'])
|
||||||
|
if not env_loaded: print("INFO: No .env or env file found.")
|
||||||
|
|
||||||
|
# --- Global Configuration ---
|
||||||
|
LOG_LEVEL_DEFAULT = os.environ.get("LOG_LEVEL", "INFO").upper()
|
||||||
|
LOG_FILE_NAME_DEFAULT = os.environ.get("LOG_FILE_PATH", "webhook.log")
|
||||||
|
TELNYX_API_KEY_DEFAULT = os.environ.get("TELNYX_API_KEY", None)
|
||||||
|
TELNYX_API_BASE_URL = os.environ.get("TELNYX_API_BASE_URL", "https://api.telnyx.com/v2")
|
||||||
|
DEFAULT_TTS_VOICE_DEFAULT = os.environ.get("TELNYX_DEFAULT_TTS_VOICE", "female")
|
||||||
|
DEFAULT_TTS_LANGUAGE_DEFAULT = os.environ.get("TELNYX_DEFAULT_TTS_LANGUAGE", "en-US")
|
||||||
|
CLIENT_STATE_PREFIX_DEFAULT = os.environ.get("TELNYX_CLIENT_STATE_PREFIX", "app_state")
|
||||||
|
DTMF_TIMEOUT_SECONDS_DEFAULT = int(os.environ.get("DTMF_TIMEOUT_SECONDS", 10))
|
||||||
|
INBOUND_GREETING_DEFAULT = os.environ.get("INBOUND_GREETING", "Thank you for calling Wellnuo. We are processing your request. Goodbye.")
|
||||||
|
|
||||||
|
# --- Application Setup ---
|
||||||
|
app = Flask(__name__)
|
||||||
|
app_logger = logging.getLogger("TelnyxWebhookApp_Fixed")
|
||||||
|
|
||||||
|
# --- Helper Functions ---
|
||||||
|
def setup_logging(level, file_path, name="TelnyxWebhookApp_Fixed"):
|
||||||
|
global app_logger; app_logger = logging.getLogger(name)
|
||||||
|
numeric_level = getattr(logging, level.upper(), logging.INFO)
|
||||||
|
app_logger.setLevel(numeric_level); app_logger.propagate = False
|
||||||
|
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - [%(funcName)s:%(lineno)d] - %(message)s')
|
||||||
|
if app_logger.hasHandlers(): app_logger.handlers.clear()
|
||||||
|
console_handler = logging.StreamHandler(sys.stdout); console_handler.setFormatter(formatter); app_logger.addHandler(console_handler)
|
||||||
|
try:
|
||||||
|
file_handler = logging.handlers.RotatingFileHandler(file_path, maxBytes=10*1024*1024, backupCount=5, encoding='utf-8')
|
||||||
|
file_handler.setFormatter(formatter); app_logger.addHandler(file_handler)
|
||||||
|
app_logger.info(f"Logging configured. Level: {level}. File: {os.path.abspath(file_path)}")
|
||||||
|
except Exception as e: app_logger.error(f"Failed to config file logger at {file_path}: {e}")
|
||||||
|
|
||||||
|
def find_custom_header(headers, name):
|
||||||
|
if not headers: return None
|
||||||
|
for header in headers:
|
||||||
|
if header.get('name', '').lower() == name.lower(): return header.get('value')
|
||||||
|
return None
|
||||||
|
|
||||||
|
def encode_state(parts):
|
||||||
|
plain_state = "|".join(map(str, parts))
|
||||||
|
base64_state = base64.b64encode(plain_state.encode('utf-8')).decode('ascii')
|
||||||
|
app_logger.debug(f"Encoded state: '{plain_state}' -> '{base64_state}'")
|
||||||
|
return base64_state
|
||||||
|
|
||||||
|
def decode_state(b64_state):
|
||||||
|
if not b64_state: return []
|
||||||
|
try:
|
||||||
|
decoded_plain = base64.b64decode(b64_state).decode('utf-8')
|
||||||
|
parts = decoded_plain.split('|'); app_logger.debug(f"Decoded state: '{b64_state}' -> '{decoded_plain}' -> {parts}"); return parts
|
||||||
|
except Exception as e: app_logger.error(f"Failed to decode client_state '{b64_state}': {e}"); return []
|
||||||
|
|
||||||
|
def send_telnyx_command(action_path, params, api_key):
|
||||||
|
if not api_key: app_logger.error(f"CMDFAIL ('{action_path}'): API_KEY not set."); return None
|
||||||
|
ccid = params.get("call_control_id");
|
||||||
|
if not ccid: app_logger.error(f"CMDFAIL ('{action_path}'): call_control_id missing."); return None
|
||||||
|
endpoint = f"{TELNYX_API_BASE_URL}/calls/{ccid}/{action_path}";
|
||||||
|
body = {k: v for k, v in params.items() if k != 'call_control_id'}
|
||||||
|
headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json", "Accept": "application/json"}
|
||||||
|
app_logger.info(f"SENDCMD ('{action_path}')"); app_logger.debug(f" Endpoint: POST {endpoint}"); app_logger.debug(f" JSON Payload: {json.dumps(body, indent=2)}")
|
||||||
|
try:
|
||||||
|
r = requests.post(endpoint, json=body, headers=headers, timeout=10)
|
||||||
|
r.raise_for_status()
|
||||||
|
app_logger.info(f"CMDOK ('{action_path}'): Telnyx accepted. Status: {r.status_code}"); return r.json()
|
||||||
|
except requests.exceptions.HTTPError as e:
|
||||||
|
app_logger.error(f"CMDFAIL ('{action_path}'): Telnyx rejected. Status: {e.response.status_code}")
|
||||||
|
try: app_logger.error(f" Telnyx Err Detail: {json.dumps(e.response.json(), indent=2)}")
|
||||||
|
except json.JSONDecodeError: app_logger.error(f" Raw Err Body: {e.response.text[:500]}")
|
||||||
|
except requests.exceptions.RequestException as e: app_logger.exception(f"CMDFAIL ('{action_path}'): Network error")
|
||||||
|
return None
|
||||||
|
|
||||||
|
# --- Webhook Route Handler ---
|
||||||
|
@app.route('/<path:webhook_path_received>', methods=['POST'])
|
||||||
|
def handle_telnyx_webhook(webhook_path_received):
|
||||||
|
global app_args
|
||||||
|
if f"/{webhook_path_received}" != app_args.webhook_path: app_logger.warning(f"REQ Unknown Path: '/{webhook_path_received}'"); return "Not found", 404
|
||||||
|
app_logger.info(f"REQ <<< Path: '/{webhook_path_received}', From: {request.remote_addr}")
|
||||||
|
try:
|
||||||
|
webhook_data = request.get_json(); app_logger.debug(f"REQ Payload Full: {json.dumps(webhook_data, indent=2)}")
|
||||||
|
data, payload = webhook_data.get('data', {}), webhook_data.get('data', {}).get('payload', {})
|
||||||
|
event_type, record_type, ccid = data.get('event_type'), data.get('record_type'), payload.get('call_control_id')
|
||||||
|
app_logger.info(f"EVENT '{event_type}' ({record_type})" + (f", CCID: {ccid}" if ccid else ""))
|
||||||
|
|
||||||
|
if record_type == 'message':
|
||||||
|
app_logger.info(f" -> SMS Event received. From: {payload.get('from',{}).get('phone_number')}, Text: '{payload.get('text','')}'")
|
||||||
|
return Response(status=204)
|
||||||
|
elif record_type != 'event':
|
||||||
|
app_logger.warning(f" Unknown Record Type '{record_type}'. Ignoring."); return Response(status=204)
|
||||||
|
|
||||||
|
b64_client_state = payload.get("client_state"); decoded_parts = decode_state(b64_client_state)
|
||||||
|
state_name = decoded_parts[0] if decoded_parts else None
|
||||||
|
if state_name: app_logger.info(f" State Name Received: '{state_name}'")
|
||||||
|
current_api_key = app_args.api_key
|
||||||
|
|
||||||
|
# --- State Machine Logic ---
|
||||||
|
if event_type == 'call.answered':
|
||||||
|
if payload.get('direction') == 'incoming':
|
||||||
|
app_logger.info(" -> Inbound call detected. Playing generic greeting and hanging up.")
|
||||||
|
next_state = encode_state(['INBOUND_GREETING_HUP'])
|
||||||
|
speak_params = {"payload": app_args.inbound_greeting, "voice": app_args.default_tts_voice, "language": app_args.default_tts_language, "call_control_id": ccid, "client_state": next_state}
|
||||||
|
send_telnyx_command("actions/speak", speak_params, current_api_key)
|
||||||
|
else: # Outgoing call
|
||||||
|
audio_url = find_custom_header(payload.get('custom_headers'), 'X-Audio-Url')
|
||||||
|
tts_payload = find_custom_header(payload.get('custom_headers'), 'X-TTS-Payload')
|
||||||
|
media_type = "audio" if audio_url else "tts" if tts_payload else "none"
|
||||||
|
media_value = audio_url or tts_payload
|
||||||
|
if media_value:
|
||||||
|
app_logger.info(f" -> Outbound call answered. Playing main message directly.")
|
||||||
|
next_state = encode_state(['MAIN_MEDIA_PLAYED', media_type, media_value])
|
||||||
|
if media_type == "audio":
|
||||||
|
send_telnyx_command("actions/playback_start", {"audio_url": media_value, "call_control_id": ccid, "client_state": next_state}, current_api_key)
|
||||||
|
elif media_type == "tts":
|
||||||
|
speak_params = {"payload": media_value, "voice": app_args.default_tts_voice, "language": app_args.default_tts_language, "call_control_id": ccid, "client_state": next_state}
|
||||||
|
send_telnyx_command("actions/speak", speak_params, current_api_key)
|
||||||
|
else:
|
||||||
|
app_logger.warning(" -> Outbound call, but no audio/tts payload. Hanging up.")
|
||||||
|
send_telnyx_command("actions/hangup", {"call_control_id": ccid}, current_api_key)
|
||||||
|
|
||||||
|
elif event_type in ['call.speak.ended', 'call.playback.ended']:
|
||||||
|
app_logger.info(f" Playback/Speak Ended: Status='{payload.get('status')}'")
|
||||||
|
if state_name == 'INBOUND_GREETING_HUP':
|
||||||
|
app_logger.info(" -> Inbound greeting finished. Hanging up.")
|
||||||
|
send_telnyx_command("actions/hangup", {"call_control_id": ccid}, current_api_key)
|
||||||
|
elif state_name in ['MAIN_MEDIA_PLAYED', 'REPLAYING_MEDIA']:
|
||||||
|
app_logger.info(f" -> Main message finished. Playing options menu.")
|
||||||
|
_, media_type, media_value = decoded_parts
|
||||||
|
next_state = encode_state(['WAITING_DTMF', media_type, media_value])
|
||||||
|
options_prompt = "press 0 to repeat the message or press pound to hang up."
|
||||||
|
gather_params = {
|
||||||
|
"payload": options_prompt, "voice": app_args.default_tts_voice, "language": app_args.default_tts_language,
|
||||||
|
"valid_digits": "0#", "max_digits": 1, "timeout_millis": app_args.dtmf_timeout_seconds * 1000, "terminating_digit": "#",
|
||||||
|
"call_control_id": ccid, "client_state": next_state
|
||||||
|
}
|
||||||
|
send_telnyx_command("actions/gather_using_speak", gather_params, current_api_key)
|
||||||
|
else:
|
||||||
|
app_logger.warning(f" -> Playback/Speak ended with unhandled state '{state_name}'. Hanging up.")
|
||||||
|
send_telnyx_command("actions/hangup", {"call_control_id": ccid}, current_api_key)
|
||||||
|
|
||||||
|
elif event_type == 'call.dtmf.received':
|
||||||
|
digit = payload.get('digit')
|
||||||
|
app_logger.info(f" DTMF Received: Digit='{digit}'")
|
||||||
|
if digit == '#':
|
||||||
|
app_logger.info(" -> '#' received. Terminating call immediately.")
|
||||||
|
send_telnyx_command("actions/hangup", {"call_control_id": ccid}, current_api_key)
|
||||||
|
|
||||||
|
elif event_type == 'call.gather.ended':
|
||||||
|
app_logger.info(f" -> Gather ended. Digits received: '{payload.get('digits')}', Status: '{payload.get('status')}'")
|
||||||
|
if state_name == 'WAITING_DTMF':
|
||||||
|
digits = payload.get('digits')
|
||||||
|
_, media_type, media_value = decoded_parts
|
||||||
|
if digits == "0":
|
||||||
|
app_logger.info(" -> '0' pressed. Replaying main message.")
|
||||||
|
# Note: No silence buffer on replay for responsiveness.
|
||||||
|
next_state = encode_state(['REPLAYING_MEDIA', media_type, media_value])
|
||||||
|
if media_type == "audio":
|
||||||
|
send_telnyx_command("actions/playback_start", {"audio_url": media_value, "call_control_id": ccid, "client_state": next_state}, current_api_key)
|
||||||
|
elif media_type == "tts":
|
||||||
|
speak_params = {"payload": media_value, "voice": app_args.default_tts_voice, "language": app_args.default_tts_language, "call_control_id": ccid, "client_state": next_state}
|
||||||
|
send_telnyx_command("actions/speak", speak_params, current_api_key)
|
||||||
|
else: # Includes '#' having already triggered hangup, timeout, or other hangup condition
|
||||||
|
app_logger.info(" -> Gather ended with non-repeat condition. Hanging up.")
|
||||||
|
send_telnyx_command("actions/hangup", {"call_control_id": ccid}, current_api_key)
|
||||||
|
else:
|
||||||
|
app_logger.warning(f" -> Gather ended with unhandled state '{state_name}'.")
|
||||||
|
|
||||||
|
elif event_type == 'call.hangup':
|
||||||
|
app_logger.info(f" Call Hangup Event: Cause='{payload.get('cause')}'")
|
||||||
|
else:
|
||||||
|
app_logger.info(f" -> Unhandled Voice Event: '{event_type}' with state '{state_name}'.")
|
||||||
|
|
||||||
|
return Response(status=204)
|
||||||
|
except Exception as e: app_logger.exception("REQFAIL Unhandled Ex"); return "Internal Server Error", 500
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
parser = argparse.ArgumentParser(description="Telnyx IVR Webhook (Fixed): Async, Repeat, DTMF Hangup.", formatter_class=argparse.RawTextHelpFormatter)
|
||||||
|
parser.add_argument('--port', type=int, default=1998, help='Port (default: 1998).')
|
||||||
|
parser.add_argument('--host', default='0.0.0.0', help='Host address (default: 0.0.0.0).')
|
||||||
|
parser.add_argument('--webhook-path', default='/telnyx-webhook', help="URL path (default: /telnyx-webhook).")
|
||||||
|
parser.add_argument('--log-level', default=LOG_LEVEL_DEFAULT, choices=['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'], help=f'Log level (default: {LOG_LEVEL_DEFAULT}).')
|
||||||
|
parser.add_argument('--log-file', default=LOG_FILE_NAME_DEFAULT, help=f'Log file path (default: {LOG_FILE_NAME_DEFAULT}).')
|
||||||
|
parser.add_argument('--api-key', default=TELNYX_API_KEY_DEFAULT, help='Telnyx API Key (default: from env).')
|
||||||
|
parser.add_argument('--dtmf-timeout-seconds', type=int, default=DTMF_TIMEOUT_SECONDS_DEFAULT, help=f'Timeout for DTMF (default: {DTMF_TIMEOUT_SECONDS_DEFAULT}s).')
|
||||||
|
parser.add_argument('--default-tts-voice', default=DEFAULT_TTS_VOICE_DEFAULT, help=f'Default TTS voice (default: {DEFAULT_TTS_VOICE_DEFAULT}).')
|
||||||
|
parser.add_argument('--default-tts-language', default=DEFAULT_TTS_LANGUAGE_DEFAULT, help=f'Default TTS language (default: {DEFAULT_TTS_LANGUAGE_DEFAULT}).')
|
||||||
|
parser.add_argument('--client-state-prefix', default=CLIENT_STATE_PREFIX_DEFAULT, help=f'Prefix for client_state (default: {CLIENT_STATE_PREFIX_DEFAULT}).')
|
||||||
|
parser.add_argument('--inbound-greeting', default=INBOUND_GREETING_DEFAULT, help=f'TTS greeting for inbound calls.')
|
||||||
|
parser.add_argument('--debug', action='store_true', help='Run Flask in debug mode.')
|
||||||
|
app_args = parser.parse_args()
|
||||||
|
|
||||||
|
if not app_args.webhook_path.startswith('/'): app_args.webhook_path = '/' + app_args.webhook_path
|
||||||
|
if not app_args.api_key: print("CRITICAL WARNING: Telnyx API Key not set. Voice commands will FAIL.")
|
||||||
|
|
||||||
|
setup_logging("DEBUG" if app_args.debug else app_args.log_level.upper(), app_args.log_file, name="TelnyxWebhookApp_Final")
|
||||||
|
if not app_args.debug: logging.getLogger('werkzeug').setLevel(logging.WARNING)
|
||||||
|
|
||||||
|
app_logger.info("--- Telnyx Webhook Listener Starting (Final) ---")
|
||||||
|
app_logger.info(f" Configuration: {vars(app_args)}")
|
||||||
|
app_logger.info("------------------------------------------------")
|
||||||
|
|
||||||
|
app.run(host=app_args.host, port=app_args.port, debug=app_args.debug, use_reloader=app_args.debug)
|
||||||
|
|
||||||
|
|
||||||
Loading…
x
Reference in New Issue
Block a user