Skip to content

Commit 90ab0aa

Browse files
author
lu9abm
committed
some core changes
1 parent a0fc157 commit 90ab0aa

21 files changed

Lines changed: 441 additions & 199 deletions

.codex

Whitespace-only changes.

app.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
from xlxd_dash import app
22

33
if __name__ == "__main__":
4-
app.run(host="0.0.0.0", port=5000, debug=True)
4+
app.run(host="0.0.0.0", port=5000, debug=False)

config_template.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
"xml": "/var/log/xlxd/xlxd.xml",
2626
"pidfile": "/run/xlxd.pid",
2727
"apiURL": "http://xlxapi.rlx.lu/api.php",
28+
"offline_time": 2400,
29+
"ref_cache_ttl": 60,
2830
"callhome": false
2931
}
30-
}
32+
}

telegram/telegram.py

Lines changed: 54 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,20 @@
33
import re
44
import requests
55
import json
6-
from datetime import datetime
6+
import os
7+
import sys
78
from watchdog.observers import Observer
89
from watchdog.events import FileSystemEventHandler
9-
import os
1010
import threading
1111
import traceback
1212

13+
PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
14+
sys.path.insert(0, PROJECT_ROOT)
15+
16+
from xlxd_common import get_flag, normalize_xlxd_xml, parse_xlxd_datetime, read_xlxd_xml
17+
1318
ref_name = ""
14-
with open("./config_tg.json", "r", encoding="utf-8") as f:
19+
with open(os.path.join(PROJECT_ROOT, "config_tg.json"), "r", encoding="utf-8") as f:
1520
config = json.load(f)
1621

1722
modules = config["service"]["modules"]
@@ -25,9 +30,6 @@
2530
TG_URL = f"https://api.telegram.org/bot{config['service']['token']}/sendMessage"
2631

2732
dt_format = "%A %a %b %d %H:%M:%S %Y"
28-
call_country = "./xlxd_dash/callprefix.json"
29-
with open(call_country, "r", encoding="utf-8") as f:
30-
callprefix = json.load(f)
3133

3234
nodes_old = {}
3335
nodes_off = {}
@@ -40,46 +42,51 @@
4042

4143
def send_message(msg, dest):
4244
for CHAT_ID in dest:
43-
data = {"chat_id": CHAT_ID, "text": msg}
44-
res = requests.post(TG_URL, data=data)
45-
# print(res.text)
46-
47-
def call_flag(country):
48-
return ''.join(chr(0x1F1E6 + ord(letra) - ord('A')) for letra in country.upper())
49-
50-
def get_flag(callsign):
51-
while callsign:
52-
if callsign in callprefix:
53-
return call_flag(callprefix[callsign][0])
54-
callsign = callsign[:-1]
55-
return ""
56-
57-
def load_xml():
58-
with open(xml_file, "r", encoding="utf-8") as f:
59-
XML = f.read()
45+
while True:
46+
data = {"chat_id": CHAT_ID, "text": msg}
47+
try:
48+
res = requests.post(TG_URL, data=data, timeout=10)
49+
except Exception as e:
50+
print(f"Error: {e}")
51+
time.sleep(5)
52+
continue
6053

61-
match = re.match(r'(<\?xml.*?\?>)', XML)
62-
header = match.group(1) if match else ""
54+
if res.status_code == 200:
55+
break
6356

64-
ref_match = re.search(r"<(XLX\d+) ", XML)
65-
if ref_match:
66-
ref_name = ref_match.group(1)
57+
print(f"Error HTTP {res.status_code}. Reintentando…")
58+
time.sleep(5)
6759

68-
XML = XML[len(header):].strip()
60+
def split_node_callsign(callsign):
61+
parts = callsign.split(maxsplit=1)
62+
if len(parts) == 2:
63+
return parts[0], parts[1]
64+
return callsign, ""
6965

70-
XML = re.sub(r"<([\w]+)\s+([\w\s]+)>", lambda m: f"<{m.group(1)}_{m.group(2).replace(' ', '_')}>", XML)
71-
XML = re.sub(r"</([\w]+)\s+([\w\s]+)>", lambda m: f"</{m.group(1)}_{m.group(2).replace(' ', '_')}>", XML)
66+
def format_node_call(call, initial):
67+
return f"{call}-{initial}" if initial else call
7268

73-
XML = f"<ROOT>\n{XML}\n</ROOT>"
69+
def load_xml():
70+
xml = read_xlxd_xml(xml_file)
71+
if not xml["available"]:
72+
return xml
7473

75-
return ref_name, XML
74+
data = normalize_xlxd_xml(xml["xml"], root_name="ROOT")
75+
data["available"] = True
76+
return data
7677

7778
def analizar_xml():
7879
global nodes_old, stations_old, notif, process, nodes_off, ref_name
7980
now = time.strftime("%Y%m%d_%H%M%S", time.localtime())
8081
now_epoch = int(time.time())
8182
try:
82-
ref_name, XML = load_xml()
83+
xml = load_xml()
84+
if not xml["available"]:
85+
send_message(xml["message"], logger)
86+
return False
87+
88+
ref_name = xml["name"]
89+
XML = xml["data"]
8390

8491
if not process:
8592
send_message(f"REF: {ref_name}", logger)
@@ -90,15 +97,15 @@ def analizar_xml():
9097
nodes_now = {}
9198
for node in root.findall(".//NODE"):
9299
callsign = (node.findtext("Callsign") or "").strip()
93-
call, initial = callsign.split(maxsplit=1)
100+
call, initial = split_node_callsign(callsign)
94101
ip = (node.findtext("IP") or "").strip()
95102
linked_module = (node.findtext("LinkedModule") or "").strip()
96103
protocol = (node.findtext("Protocol") or "").strip()
97104
connect_time = (node.findtext("ConnectTime") or "").strip()
98105
last_heard_time = (node.findtext("LastHeardTime") or "").strip()
99106

100-
connect_time_epoch = int(datetime.strptime(connect_time, dt_format).timestamp()) if connect_time else 0
101-
last_heard_time_epoch = int(datetime.strptime(last_heard_time, dt_format).timestamp()) if last_heard_time else 0
107+
connect_time_epoch = parse_xlxd_datetime(connect_time)
108+
last_heard_time_epoch = parse_xlxd_datetime(last_heard_time)
102109

103110
index = (callsign + "-" + protocol or "").strip().replace(' ','_')
104111
if index in nodes_now.keys():
@@ -137,44 +144,47 @@ def analizar_xml():
137144
for key in nuevos:
138145
Call = nodes_now[key]["Call"]
139146
Initial = nodes_now[key]["Initial"]
147+
NodeCall = format_node_call(Call, Initial)
140148
LinkedModule = nodes_now[key]["LinkedModule"]
141149
Protocol = nodes_now[key]["Protocol"]
142150
flag = get_flag(Call)
143151
if key in nodes_off.keys():
144152
del nodes_off[key]
145-
send_message(f"Online {Call}-{Initial} On {ref_name} {LinkedModule} Protocol {Protocol}", logger)
153+
send_message(f"Online {NodeCall} On {ref_name} {LinkedModule} Protocol {Protocol}", logger)
146154
else:
147155
if (LinkedModule in modules):
148156
notifto = logger + node_offline
149157
else:
150158
notifto = logger
151159

152-
send_message(f"✅{flag} {Call}-{Initial} On {ref_name} {LinkedModule} Protocol {Protocol}", notifto)
160+
send_message(f"✅{flag} {NodeCall} On {ref_name} {LinkedModule} Protocol {Protocol}", notifto)
153161

154162
### NODES - si faltan nodes que antes estaban (old - now)
155163
eliminados = nodes_old.keys() - nodes_now.keys()
156164
if eliminados:
157165
for key in eliminados:
158166
Call = nodes_old[key]["Call"]
159167
Initial = nodes_old[key]["Initial"]
168+
NodeCall = format_node_call(Call, Initial)
160169
LinkedModule = nodes_old[key]["LinkedModule"]
161170
Protocol = nodes_old[key]["Protocol"]
162-
send_message(f"Offline {Call}-{Initial} From {ref_name} {LinkedModule} Protocol {Protocol}", logger)
171+
send_message(f"Offline {NodeCall} From {ref_name} {LinkedModule} Protocol {Protocol}", logger)
163172
nodes_off[key] = nodes_old[key]
164173
nodes_off[key]["last_off"] = now_epoch
165174

166175
for key in list(nodes_off.keys()):
167176
if nodes_off[key]["last_off"] + node_delay < now_epoch:
168177
Call = nodes_off[key]["Call"]
169178
Initial = nodes_off[key]["Initial"]
179+
NodeCall = format_node_call(Call, Initial)
170180
LinkedModule = nodes_off[key]["LinkedModule"]
171181
Protocol = nodes_off[key]["Protocol"]
172182
flag = get_flag(Call)
173183
if (LinkedModule in modules):
174184
notifto = logger + node_offline
175185
else:
176186
notifto = logger
177-
send_message(f"❌{flag} {Call}-{Initial} From {ref_name} {LinkedModule} Protocol {Protocol}", notifto)
187+
send_message(f"❌{flag} {NodeCall} From {ref_name} {LinkedModule} Protocol {Protocol}", notifto)
178188

179189
del nodes_off[key]
180190

@@ -193,7 +203,7 @@ def analizar_xml():
193203
via_peer = (station.findtext("Via_peer") or "").strip()
194204
last_heard_time = (station.findtext("LastHeardTime") or "").strip()
195205

196-
last_heard_time_epoch = int(datetime.strptime(last_heard_time, dt_format).timestamp()) if last_heard_time else 0
206+
last_heard_time_epoch = parse_xlxd_datetime(last_heard_time)
197207

198208
index = (callsign or "").strip().replace(' ', '_')
199209

@@ -287,4 +297,5 @@ def INITmonitor():
287297
process = analizar_xml()
288298
if process:
289299
INITmonitor()
290-
time.sleep(1)
300+
else:
301+
time.sleep(30)

xlxd_common.py

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import json
2+
import os
3+
import re
4+
import fcntl
5+
from datetime import datetime
6+
7+
8+
DT_FORMAT = "%A %a %b %d %H:%M:%S %Y"
9+
CALLPREFIX_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "xlxd_dash", "callprefix.json")
10+
11+
with open(CALLPREFIX_PATH, "r", encoding="utf-8") as f:
12+
CALLPREFIX = json.load(f)
13+
14+
15+
def read_xlxd_xml(xml_file):
16+
if not os.path.exists(xml_file):
17+
return {
18+
"available": False,
19+
"message": "XLXD data is not available yet.",
20+
"detail": f"XML file not found: {xml_file}",
21+
}
22+
23+
try:
24+
with open(xml_file, "r", encoding="utf-8") as f:
25+
try:
26+
fcntl.flock(f, fcntl.LOCK_SH | fcntl.LOCK_NB)
27+
except BlockingIOError:
28+
return {
29+
"available": False,
30+
"message": "XLXD data is being updated.",
31+
"detail": f"XML file is locked: {xml_file}",
32+
}
33+
34+
return {"available": True, "xml": f.read()}
35+
except Exception as e:
36+
return {
37+
"available": False,
38+
"message": "XLXD data could not be read.",
39+
"detail": str(e),
40+
}
41+
42+
43+
def normalize_xlxd_xml(xml_text, root_name="DATA"):
44+
ref_name = ""
45+
match = re.match(r"(<\?xml.*?\?>)", xml_text)
46+
header = match.group(1) if match else ""
47+
48+
ref_match = re.search(r"<(XLX[\w\d]{3}) ", xml_text)
49+
if ref_match:
50+
ref_name = ref_match.group(1)
51+
52+
xml_text = xml_text[len(header):].strip()
53+
xml_text = re.sub(
54+
r"<([\w]+)\s+([\w\s]+)>",
55+
lambda m: f"<{m.group(1)}_{m.group(2).replace(' ', '_')}>",
56+
xml_text,
57+
)
58+
xml_text = re.sub(
59+
r"</([\w]+)\s+([\w\s]+)>",
60+
lambda m: f"</{m.group(1)}_{m.group(2).replace(' ', '_')}>",
61+
xml_text,
62+
)
63+
64+
return {"name": ref_name, "data": f"<{root_name}>\n{xml_text}\n</{root_name}>"}
65+
66+
67+
def parse_xlxd_datetime(value):
68+
if not value:
69+
return 0
70+
return int(datetime.strptime(value, DT_FORMAT).timestamp())
71+
72+
73+
def call_flag(country):
74+
return "".join(chr(0x1F1E6 + ord(letter) - ord("A")) for letter in country.upper())
75+
76+
77+
def get_flag_info(callsign):
78+
while callsign:
79+
if callsign in CALLPREFIX:
80+
return [call_flag(CALLPREFIX[callsign][0]), CALLPREFIX[callsign][1]]
81+
callsign = callsign[:-1]
82+
return ["", ""]
83+
84+
85+
def get_flag(callsign):
86+
return get_flag_info(callsign)[0]
87+
88+
89+
def mask_ip(ip, mask):
90+
if mask == 0:
91+
return False
92+
return ".".join(ip.split(".")[:mask] + ["?"] * (4 - mask))
93+
94+
95+
def as_list(value):
96+
if not value:
97+
return []
98+
if isinstance(value, list):
99+
return value
100+
return [value]
101+
102+
103+
def safe_int(value, default=0):
104+
try:
105+
return int(value)
106+
except (TypeError, ValueError):
107+
return default
108+
109+
110+
def is_online(last_contact, now, offline_time):
111+
return safe_int(last_contact) > now - safe_int(offline_time, 2400)

xlxd_dash/checks.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,13 @@ def run_checks():
88
if not bool(re.fullmatch(r"XLX[A-Z0-9]{3}", config["service"]["name"])):
99
raise SystemExit(f"XLX name not set or invalid\nCheck GitHub page for more information.")
1010

11-
checkfiles = [
12-
config["xlxd"]["pidfile"],
13-
config["xlxd"]["xml"],
14-
]
15-
for file in checkfiles:
16-
if not Path(file).exists():
17-
raise SystemExit(f"Error reading file {file}\nCheck GitHub page for more information.")
11+
pid_file = config["xlxd"]["pidfile"]
12+
if not Path(pid_file).exists():
13+
print(f"Warning: PID file not found: {pid_file}")
14+
15+
xml_file = config["xlxd"]["xml"]
16+
if not Path(xml_file).exists():
17+
print(f"Warning: XML file not found yet: {xml_file}")
1818

1919
if config["xlxd"]["callhome"] == True:
2020
print("Startup callhome...")

0 commit comments

Comments
 (0)