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()
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
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)