Ernest_tested_6_25_2025

This commit is contained in:
RZ_MINIX\rober 2025-06-25 15:53:03 -07:00
parent cb57ae8d7b
commit 4cc8509f9c
2 changed files with 710 additions and 304 deletions

View File

@ -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()
alarm_deployment_settings_str = json.dumps(alarms_settings) try:
redis_conn.set('alarm_deployment_settings_'+str(deployment_id), alarm_deployment_settings_str) alarm_deployment_settings_str = json.dumps(alarms_settings)
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,154 +3379,171 @@ def SetupTasks():
elif rearm_policy == "Never": elif rearm_policy == "Never":
pass pass
devices_lst = deployments_devices[deployment_id]
if do_rearm: if do_rearm:
alarm_armed_settings, alarm_settings_str = GetAlarmSettings(deployment_id) #No need to re-arm Burglar alarm it remains armed untill manually disarmed
if len(alarm_settings_str) > 2: alarm_settings_str = GetAlarmSettings(deployment_id)
if alarm_settings_str != "{}":
alarm_settings = json.loads(alarm_settings_str) alarm_settings = json.loads(alarm_settings_str)
alarm_settings["armed"] = alarm_armed_settings #alarm_settings["enabled"] bits:
alarms_settings_all[deployment_id] = alarm_settings # bit 2: Burglar Alarm (index 0), bit 1: Time alone Alarm is Set (index 1), bit 0: Time alone Warning is Set (index 2)
alarm_armed_settings_str = alarms_settings["armed"] if alarm_settings["enabled"][1] == "1": #Time alone Alarm
alarm_settings["alone_alarm_armed"] = True
devices_lst = deployments_devices[deployment_id] 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
devices_lst = deployments_devices[deployment_id]
numbers = []
for device_id in devices_lst: #lets check if alone
if device_id in device_alerts_all: if "1" in alarm_armed_settings_str[1:]: #"010" alone warning or alarm
since_seen = time.time() - GetRedisFloat('lastseen_'+str(device_id)) pass #todo
numbers.append((since_seen, device_id))
sorted_numbers = numbers numbers = []
second_smallest = -1 for device_id in devices_lst:
if len(numbers) == 1: if device_id in device_alerts_all:
smallest = sorted_numbers[0] since_seen = time.time() - GetRedisFloat('lastseen_'+str(device_id))
if len(numbers) >= 2: numbers.append((since_seen, device_id))
sorted_numbers = sorted(numbers, key=lambda x: x[0])
smallest = sorted_numbers[0]
second_smallest = sorted_numbers[1]
device_id_to_last_seen = {}
for tpl in sorted_numbers:
last_seen_ago = tpl[0]
device_id = tpl[1]
#device_id_to_last_seen[tpl[1]] = tpl[0]
device_alarm_settings = device_alerts_all[device_id]
enabled_alarms_str = device_alarm_settings["enabled_alarms"]
#lets check larm first, because if satisfied, no need to check for warning
if enabled_alarms_str[-2] == "1": #Too long present alarm
if device_id == smallest[1]: #now present... how long?
if (second_smallest[0] - smallest[0]) > device_alarm_settings["stuck_minutes_alarm"] * 60:
#cancel alarm and warning, until re-armed
enabled_alarms_str = set_character(enabled_alarms_str, 0, "0")
enabled_alarms_str = set_character(enabled_alarms_str, 1, "0")
dev_det = devices_details_map[device_id]
location = (dev_det[4] + " " + dev_det[5].strip()).strip()
method = device_alarm_settings["stuck_alarm_method_1"]
if method.upper() != "PHONE":
SendAlerts(deployment_id, method, f"Alarm: {user_first_last_name} way too long ({int((second_smallest[0] - smallest[0]) / 60)} minutes) in {location}", "", "")
else:
SendAlerts(deployment_id, method, f"Alarm: {user_first_last_name} way too long in {location}", "", "")
device_alarm_settings["enabled_alarms"] = enabled_alarms_str #In order to understand if somebody is too long in some place, or to long since it visited, we use following logic:
StoreLastSentToRedis(deployment_id) #For too long since last visit: we check if since_seen is larger than absent_minutes_alarm and absent_minutes_warning value
device_alerts_all[device_id] = device_alarm_settings #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
StoreDeviceAlarmsToRedis(device_id, device_alarm_settings) second_smallest = -1
else: if len(numbers) <2:
check_after = device_alarm_settings["stuck_minutes_alarm"] - int((second_smallest[0] - smallest[0])/60) #needs at least 2 devices for proper operation
if check_after < next_run_in_minutes: pass
next_run_in_minutes = check_after else:
sorted_numbers = sorted(numbers, key=lambda x: x[0])
smallest = sorted_numbers[0]
second_smallest = sorted_numbers[1]
device_id_to_last_seen = {}
for tpl in sorted_numbers:
last_seen_ago = tpl[0]
device_id = tpl[1]
#device_id_to_last_seen[tpl[1]] = tpl[0]
device_alarm_settings = device_alerts_all[device_id]
if "enabled_alarms" in device_alarm_settings:
enabled_alarms_str = device_alarm_settings["enabled_alarms"]
armed_states = "00000000000"
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 (second_smallest[0] - smallest[0]) > device_alarm_settings["stuck_minutes_alarm"] * 60:
#cancel alarm and warning, until re-armed
armed_states = set_character(armed_states, 0, "0")
armed_states = set_character(armed_states, 1, "0")
dev_det = devices_details_map[device_id]
location = (dev_det[4] + " " + dev_det[5].strip()).strip()
method = device_alarm_settings["stuck_alarm_method_1"]
if method.upper() != "PHONE":
SendAlerts(deployment_id, method, f"Alarm: {user_first_last_name} way too long ({int((second_smallest[0] - smallest[0]) / 60)} minutes) in {location}", "", "")
else:
SendAlerts(deployment_id, method, f"Alarm: {user_first_last_name} way too long in {location}", "", "")
elif enabled_alarms_str[-1] == "1": #Too long present warning device_alarm_settings["armed_states"] = armed_states
if (second_smallest[0] - smallest[0]) > device_alarm_settings["stuck_minutes_warning"] * 60: StoreLastSentToRedis(deployment_id)
#cancel warning, until re-armed device_alerts_all[device_id] = device_alarm_settings
enabled_alarms_str = set_character(enabled_alarms_str, 0, "0") StoreDeviceAlarmsToRedis(device_id, device_alarm_settings)
dev_det = devices_details_map[device_id] else:
location = (dev_det[4] + " " + dev_det[5].strip()).strip() check_after = device_alarm_settings["stuck_minutes_alarm"] - int((second_smallest[0] - smallest[0])/60)
method = device_alarm_settings["stuck_warning_method_0"] if check_after < next_run_in_minutes:
if method.upper() != "PHONE": next_run_in_minutes = check_after
SendAlerts(deployment_id, method, f"Warning: {user_first_last_name} too long ({int((second_smallest[0] - smallest[0]) / 60)} minutes) in {location}", "", "")
else:
SendAlerts(deployment_id, method, f"Warning: {user_first_last_name} too long in {location}", "", "")
StoreLastSentToRedis(deployment_id) elif enabled_alarms_str[BitIndex(0)] == "1" and armed_states[BitIndex(0)] == "1": #Too long present warning
device_alarm_settings["enabled_alarms"] = enabled_alarms_str if (second_smallest[0] - smallest[0]) > device_alarm_settings["stuck_minutes_warning"] * 60:
device_alerts_all[device_id] = device_alarm_settings #cancel warning, until re-armed
StoreDeviceAlarmsToRedis(device_id, device_alarm_settings) armed_states = set_character(armed_states, 0, "0")
else: dev_det = devices_details_map[device_id]
check_after = device_alarm_settings["stuck_minutes_warning"] - int((second_smallest[0] - smallest[0])/60) location = (dev_det[4] + " " + dev_det[5].strip()).strip()
if check_after < next_run_in_minutes: method = device_alarm_settings["stuck_warning_method_0"]
next_run_in_minutes = check_after if method.upper() != "PHONE":
SendAlerts(deployment_id, method, f"Warning: {user_first_last_name} too long ({int((second_smallest[0] - smallest[0]) / 60)} minutes) in {location}", "", "")
else:
SendAlerts(deployment_id, method, f"Warning: {user_first_last_name} too long in {location}", "", "")
StoreLastSentToRedis(deployment_id)
device_alarm_settings["armed_states"] = armed_states
device_alerts_all[device_id] = device_alarm_settings
StoreDeviceAlarmsToRedis(device_id, device_alarm_settings)
else:
check_after = device_alarm_settings["stuck_minutes_warning"] - int((second_smallest[0] - smallest[0])/60)
if check_after < next_run_in_minutes:
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"]
if method.upper() != "PHONE": if method.upper() != "PHONE":
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)
else: else:
check_after = device_alarm_settings["absent_minutes_alarm"] - int(last_seen_ago/60) check_after = device_alarm_settings["absent_minutes_alarm"] - int(last_seen_ago/60)
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"]
if method.upper() != "PHONE": if method.upper() != "PHONE":
SendAlerts(deployment_id, method, f"Warning: Too long since {user_first_last_name} visited {location} ({int(last_seen_ago / 60)} minutes)", "", "") SendAlerts(deployment_id, method, f"Warning: Too long since {user_first_last_name} visited {location} ({int(last_seen_ago / 60)} minutes)", "", "")
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)
else: else:
check_after = device_alarm_settings["absent_minutes_warning"] - int(last_seen_ago/60) check_after = device_alarm_settings["absent_minutes_warning"] - int(last_seen_ago/60)
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
#"stuck_warning_method_0": "SMS", #"stuck_warning_method_0": "SMS",
#"stuck_alarm_method_1": "PHONE", #"stuck_alarm_method_1": "PHONE",
#"absent_warning_method_2": "SMS", #"absent_warning_method_2": "SMS",
#"absent_alarm_method_3": "PHONE", #"absent_alarm_method_3": "PHONE",
#"temperature_high_warning_method_4": "SMS", #"temperature_high_warning_method_4": "SMS",
#"temperature_high_alarm_method_5": "PHONE", #"temperature_high_alarm_method_5": "PHONE",
#"temperature_low_warning_method_6": "SMS", #"temperature_low_warning_method_6": "SMS",
#"temperature_low_alarm_method_7": "PHONE", #"temperature_low_alarm_method_7": "PHONE",
#"radar_alarm_method_8":"MSG", #"radar_alarm_method_8":"MSG",
#"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,209 +3901,265 @@ 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'))
#print(alarm_armed_settings_str) if "enabled_alarms" in device_alarm_settings:
if alarm_armed_settings_str[-2] == "0": #alarm not armed, so compare individual conditions #Lets check temperatures
if alarm_armed_settings_str[-3] == "1" or alarm_armed_settings_str[-4] == "1": #Temperatures Too High/Low 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)
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"]
''' '''
{ {
"enabled_alarms":"000000000101", "enabled_alarms":"000000000101",
"armed_states":"000000000000", "armed_states":"000000000000",
"stuck_minutes_warning":"771.3", "stuck_minutes_warning":"771.3",
"stuck_warning_method_0":"SMS", "stuck_warning_method_0":"SMS",
"stuck_minutes_alarm":600, "stuck_minutes_alarm":600,
"stuck_alarm_method_1":"PHONE", "stuck_alarm_method_1":"PHONE",
"absent_minutes_warning":"-1013.4", "absent_minutes_warning":"-1013.4",
"absent_warning_method_2":"SMS", "absent_warning_method_2":"SMS",
"absent_minutes_alarm":30, "absent_minutes_alarm":30,
"absent_alarm_method_3":"PHONE", "absent_alarm_method_3":"PHONE",
"temperature_high_warning":"85", "temperature_high_warning":"85",
"temperature_high_warning_method_4": "temperature_high_warning_method_4":
"SMS","temperature_high_alarm":"95", "SMS","temperature_high_alarm":"95",
"temperature_high_alarm_method_5":"PHONE", "temperature_high_alarm_method_5":"PHONE",
"temperature_low_warning":"60", "temperature_low_warning":"60",
"temperature_low_warning_method_6":"SMS", "temperature_low_warning_method_6":"SMS",
"temperature_low_alarm":"50", "temperature_low_alarm":"50",
"temperature_low_alarm_method_7":"PHONE", "temperature_low_alarm_method_7":"PHONE",
"radar_alarm_method_8":"MSG", "radar_alarm_method_8":"MSG",
"pressure_alarm_method_9":"MSG", "pressure_alarm_method_9":"MSG",
"light_alarm_method_10":"MSG", "light_alarm_method_10":"MSG",
"smell_alarm_method_11":"EMAIL", "smell_alarm_method_11":"EMAIL",
"rearm_policy":"At midnight" "rearm_policy":"At midnight"
} }
''' '''
#bits in enabled_alarms and armed_states explained
#"stuck_warning_method_0": "SMS", -1
#"stuck_alarm_method_1": "PHONE", -2
#"absent_warning_method_2": "SMS", -3
#"absent_alarm_method_3": "PHONE", -4
#"temperature_high_warning_method_4": "SMS", -5
#"temperature_high_alarm_method_5": "PHONE", -6
#"temperature_low_warning_method_6": "SMS", -7
#"temperature_low_alarm_method_7": "PHONE", -8
#"radar_alarm_method_8":"MSG", -9
#"pressure_alarm_method_9":"MSG", -10
#"light_alarm_method_10":"MSG" -11
#"smell_alarm_method_11":"MSG" -12
#hast ot be enabled and not triggerred to continue comparing
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"]):
#cancel alarm and warning, until re-armed
armed_states = set_character(armed_states, 5, "0")
armed_states = set_character(armed_states, 4, "0")
dev_det = devices_details_map[device_id]
location = (dev_det[4] + " " + dev_det[5].strip()).strip()
method = device_alarm_settings["temperature_high_alarm_method_5"]
first_last_name = GetBeneficiaryFromDeployment(deployment_id)
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
StoreAllToRedisAndDB(deployment_id, device_id)
#"stuck_warning_method_0": "SMS", -1 #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!
#"stuck_alarm_method_1": "PHONE", -2 elif enabled_alarms_str[BitIndex(4)] == "1" and armed_states[BitIndex(4)] == "1": #Temperatures Too High Warning!
#"absent_warning_method_2": "SMS", -3
#"absent_alarm_method_3": "PHONE", -4
#"temperature_high_warning_method_4": "SMS", -5
#"temperature_high_alarm_method_5": "PHONE", -6
#"temperature_low_warning_method_6": "SMS", -7
#"temperature_low_alarm_method_7": "PHONE", -8
#"radar_alarm_method_8":"MSG", -9
#"pressure_alarm_method_9":"MSG", -10
#"light_alarm_method_10":"MSG" -11
#"smell_alarm_method_11":"MSG" -12
if enabled_alarms_str[-6] == "1": #Temperatures Too High Alarm! if temperature > FahrenheitToCelsius(device_alarm_settings["temperature_high_warning"]):
if temperature > FahrenheitToCelsius(device_alarm_settings["temperature_high_alarm"]): #cancel warning, until re-armed
#cancel alarm and warning, until re-armed armed_states = set_character(armed_states, 4, "0")
enabled_alarms_str = set_character(enabled_alarms_str, 5, "0") dev_det = devices_details_map[device_id]
enabled_alarms_str = set_character(enabled_alarms_str, 4, "0") location = (dev_det[4] + " " + dev_det[5].strip()).strip()
method = device_alarm_settings["temperature_high_warning_method_4"]
first_last_name = GetBeneficiaryFromDeployment(deployment_id)
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
StoreAllToRedisAndDB(deployment_id, device_id)
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"]):
#cancel alarm and warning, until re-armed
armed_states = set_character(armed_states, 7, "0")
armed_states = set_character(armed_states, 6, "0")
dev_det = devices_details_map[device_id]
location = (dev_det[4] + " " + dev_det[5].strip()).strip()
method = device_alarm_settings["temperature_low_alarm_method_7"]
first_last_name = GetBeneficiaryFromDeployment(deployment_id)
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
StoreAllToRedisAndDB(deployment_id, device_id)
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"]):
#cancel warning, until re-armed
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_high_alarm_method_5"] method = device_alarm_settings["temperature_low_warning_method_6"]
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"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)
elif enabled_alarms_str[-5] == "1": #Temperatures Too High Warning!
if temperature > FahrenheitToCelsius(device_alarm_settings["temperature_high_warning"]): #logger.info(f"{tim}, {mac}, {temperature}")
#cancel alarm and warning, until re-armed
enabled_alarms_str = set_character(enabled_alarms_str, 4, "0")
dev_det = devices_details_map[device_id]
location = (dev_det[4] + " " + dev_det[5].strip()).strip()
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}", "", "")
device_alarm_settings["enabled_alarms"] = enabled_alarms_str
device_alerts_all[device_id] = device_alarm_settings
StoreLastSentToRedis(deployment_id)
StoreDeviceAlarmsToRedis(device_id, device_alarm_settings)
if enabled_alarms_str[-8] == "1": #Temperatures Too Low Alarm!
if temperature < FahrenheitToCelsius(device_alarm_settings["temperature_low_alarm"]):
#cancel alarm and warning, until re-armed
enabled_alarms_str = set_character(enabled_alarms_str, 7, "0")
enabled_alarms_str = set_character(enabled_alarms_str, 6, "0")
dev_det = devices_details_map[device_id]
location = (dev_det[4] + " " + dev_det[5].strip()).strip()
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}", "", "")
device_alarm_settings["enabled_alarms"] = enabled_alarms_str
device_alerts_all[device_id] = device_alarm_settings
StoreLastSentToRedis(deployment_id)
StoreDeviceAlarmsToRedis(device_id, device_alarm_settings)
elif enabled_alarms_str[-7] == "1": #Temperatures Too Low Warning!
if temperature < FahrenheitToCelsius(device_alarm_settings["temperature_low_warning"]):
#cancel alarm and warning, until re-armed
enabled_alarms_str = set_character(enabled_alarms_str, 6, "0")
dev_det = devices_details_map[device_id]
location = (dev_det[4] + " " + dev_det[5].strip()).strip()
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}", "", "")
device_alarm_settings["enabled_alarms"] = enabled_alarms_str
device_alerts_all[device_id] = device_alarm_settings
StoreLastSentToRedis(deployment_id)
StoreDeviceAlarmsToRedis(device_id, device_alarm_settings)
logger.info(f"{tim}, {mac}, {temperature}")
else: #radar packet else: #radar packet
pass pass
else: #alarm is armed
if "radar" in message_dict: #lets check if alarm condition
radar = message_dict["radar"] if alarm_armed_settings_str[BitIndex(2)] == "1": #alarm is armed!
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_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
for i in range(queue_length):
item_json = redis_conn.rpop('send_requests')
# Process each item if item_json is None:
for i in range(queue_length): break
item_json = redis_conn.rpop('send_requests')
if item_json is None: try:
break record = json.loads(item_json)
requests_count += 1
try: # Print the record
record = json.loads(item_json) print(f"Request #{requests_count}:")
requests_count += 1 for key, value in record.items():
print(f" {key}: {value}")
# Print the record method = record["method"]
print(f"Request #{requests_count}:") location_str = record["location"]
for key, value in record.items(): location = location_str.split("_")[2]
print(f" {key}: {value}") deployment_id = record["deployment_id"]
content = record["content"]
feature = record["feature"]
enabledCellContent = record["enabledCellContent"]
currentUnits = record["currentUnits"]
currentAlertTableMode = record["currentAlertTableMode"] #Warning/Alarm
test_only = record["test_only"]
#action = record["action"]
user_name = record["user_name"]
user_first_last_name = GetBeneficiaryFromDeployment(deployment_id)
method = record["method"] if feature == "stuck":
location_str = record["location"] msg_ext = f"{currentAlertTableMode}: {content} {user_first_last_name} is spending more than {enabledCellContent} in {location}"
location = location_str.split("_")[2] elif feature == "absent":
deployment_id = record["deployment_id"] msg_ext = f"{currentAlertTableMode}: {content} {user_first_last_name} did not visit {location} in more than {enabledCellContent[1:-1]} {currentUnits}"
content = record["content"] elif feature == "tempLow":
feature = record["feature"] msg_ext = f"{currentAlertTableMode}: {content} temperature is lower then {enabledCellContent} {currentUnits} in {location} at {user_first_last_name}"
enabledCellContent = record["enabledCellContent"] elif feature == "tempHigh":
currentUnits = record["currentUnits"] msg_ext = f"{currentAlertTableMode}: {content} temperature is higher then {enabledCellContent} {currentUnits} in {location} at {user_first_last_name}"
currentAlertTableMode = record["currentAlertTableMode"] #Warning/Alarm elif feature == "pressure":
test_only = record["test_only"] msg_ext = f"{currentAlertTableMode}: {content} door was opened or closed in the {location} at {user_first_last_name}"
#action = record["action"] elif feature == "radar":
user_name = record["user_name"] msg_ext = f"{currentAlertTableMode}: {content} motion detected in the {location} at {user_first_last_name}"
user_first_last_name = GetBeneficiaryFromDeployment(deployment_id) else:
msg_ext = f"{currentAlertTableMode}: {content} {feature} in {location} at {user_first_last_name}"
if feature == "stuck": SendAlerts(deployment_id, method, msg_ext, "Test message", user_name)
msg_ext = f"{currentAlertTableMode}: {content} {user_first_last_name} is spending more than {enabledCellContent} in {location}" #these are testing messages, so do not count them as real triggered... so do not update in REDIS
elif feature == "absent": #StoreLastSentToRedis(deployment_id)
msg_ext = f"{currentAlertTableMode}: {content} {user_first_last_name} did not visit {location} in more than {enabledCellContent[1:-1]} {currentUnits}"
elif feature == "tempLow":
msg_ext = f"{currentAlertTableMode}: {content} temperature is lower then {enabledCellContent} {currentUnits} in {location} at {user_first_last_name}"
elif feature == "tempHigh":
msg_ext = f"{currentAlertTableMode}: {content} temperature is higher then {enabledCellContent} {currentUnits} in {location} at {user_first_last_name}"
elif feature == "pressure":
msg_ext = f"{currentAlertTableMode}: {content} door was opened or closed in the {location} at {user_first_last_name}"
elif feature == "radar":
msg_ext = f"{currentAlertTableMode}: {content} motion detected in the {location} at {user_first_last_name}"
else:
msg_ext = f"{currentAlertTableMode}: {content} {feature} in {location} at {user_first_last_name}"
SendAlerts(deployment_id, method, msg_ext, "Test message", user_name) print("-" * 40)
#these are testing messages, so do not count them as real triggered... so do not update in REDIS
#StoreLastSentToRedis(deployment_id)
print("-" * 40) except json.JSONDecodeError as e:
logger.error(f"Failed to parse JSON from queue item: {e}")
continue
except json.JSONDecodeError as e: #Any test messages requested to be sent?
logger.error(f"Failed to parse JSON from queue item: {e}") queue_length = redis_conn.llen('new_alarms')
continue if queue_length > 0:
for i in range(queue_length):
print(f"Total requests processed: {requests_count}") 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
View 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)