You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
437 lines
13 KiB
Python
437 lines
13 KiB
Python
import html
|
|
from datetime import datetime
|
|
|
|
import httpx
|
|
|
|
|
|
QUERY = """
|
|
query GetAllSensors {
|
|
sensors {
|
|
id
|
|
name
|
|
nameExtern
|
|
description
|
|
measureConcept {
|
|
id
|
|
name
|
|
description
|
|
}
|
|
}
|
|
}
|
|
""".strip()
|
|
|
|
|
|
def escape_text(value, default=""):
|
|
text = default if value is None else str(value)
|
|
return html.escape(text, quote=True)
|
|
|
|
|
|
def build_error(message):
|
|
return (
|
|
"<div style='color: var(--color-danger, #dc3545); "
|
|
"font-family: Segoe UI, Tahoma, Geneva, Verdana, sans-serif;'>"
|
|
f"{escape_text(message)}"
|
|
"</div>"
|
|
)
|
|
|
|
|
|
def build_report():
|
|
with httpx.Client() as client:
|
|
response = client.post(
|
|
f"{EXTERNAL_BASE_URL}/graphql",
|
|
json={"query": QUERY},
|
|
headers=AUTH_HEADERS,
|
|
timeout=30.0,
|
|
)
|
|
|
|
if response.status_code != 200:
|
|
return build_error(f"Fehler beim Abrufen der Sensoren: {response.status_code}")
|
|
|
|
data = response.json()
|
|
sensors = data.get("data", {}).get("sensors", [])
|
|
|
|
sensors.sort(
|
|
key=lambda s: (
|
|
(s.get("measureConcept") or {}).get("name", "").strip(),
|
|
(s.get("name") or "").strip(),
|
|
)
|
|
)
|
|
|
|
measure_concepts = {}
|
|
for sensor in sensors:
|
|
measure_concept = sensor.get("measureConcept") or {}
|
|
mc_id = str(measure_concept.get("id") or "unknown")
|
|
mc_name = (measure_concept.get("name") or "Unbekannt").strip()
|
|
mc_desc = measure_concept.get("description") or ""
|
|
|
|
if mc_id not in measure_concepts:
|
|
measure_concepts[mc_id] = {
|
|
"name": mc_name,
|
|
"description": mc_desc,
|
|
"sensors": [],
|
|
}
|
|
|
|
measure_concepts[mc_id]["sensors"].append(sensor)
|
|
|
|
total_sensors = len(sensors)
|
|
total_concepts = len(measure_concepts)
|
|
|
|
html_output = f"""
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<title>Sensor Liste</title>
|
|
<style>
|
|
body {{
|
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
margin: 0;
|
|
padding: 20px;
|
|
background-color: #f5f5f5;
|
|
color: #333;
|
|
}}
|
|
.container {{
|
|
max-width: 1200px;
|
|
margin: 0 auto;
|
|
background: white;
|
|
padding: 30px;
|
|
border-radius: 12px;
|
|
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
|
}}
|
|
.header {{
|
|
text-align: center;
|
|
margin-bottom: 30px;
|
|
padding-bottom: 20px;
|
|
border-bottom: 2px solid var(--color-primary, #007bff);
|
|
}}
|
|
.header h1 {{
|
|
color: var(--color-primary, #007bff);
|
|
margin: 0;
|
|
font-size: 2.5em;
|
|
}}
|
|
.stats {{
|
|
display: flex;
|
|
justify-content: center;
|
|
gap: 30px;
|
|
margin: 20px 0;
|
|
flex-wrap: wrap;
|
|
}}
|
|
.stat-card {{
|
|
background: linear-gradient(135deg, var(--color-primary, #007bff), #0056b3);
|
|
color: white;
|
|
padding: 20px;
|
|
border-radius: 10px;
|
|
text-align: center;
|
|
min-width: 150px;
|
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
}}
|
|
.stat-card .number {{
|
|
font-size: 2.5em;
|
|
font-weight: bold;
|
|
margin: 0;
|
|
}}
|
|
.stat-card .label {{
|
|
font-size: 0.9em;
|
|
opacity: 0.9;
|
|
margin-top: 5px;
|
|
}}
|
|
.filters {{
|
|
background: #f8f9fa;
|
|
padding: 20px;
|
|
border-radius: 8px;
|
|
margin: 20px 0;
|
|
border-left: 4px solid var(--color-primary, #007bff);
|
|
}}
|
|
.filter-group {{
|
|
margin-bottom: 15px;
|
|
}}
|
|
.filter-group label {{
|
|
display: block;
|
|
font-weight: bold;
|
|
margin-bottom: 5px;
|
|
color: #495057;
|
|
}}
|
|
.filter-group input, .filter-group select {{
|
|
width: 100%;
|
|
max-width: 300px;
|
|
padding: 8px 12px;
|
|
border: 1px solid #ddd;
|
|
border-radius: 6px;
|
|
font-size: 14px;
|
|
}}
|
|
.measure-concept {{
|
|
margin-bottom: 30px;
|
|
border: 1px solid #dee2e6;
|
|
border-radius: 8px;
|
|
overflow: hidden;
|
|
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
|
|
}}
|
|
.concept-header {{
|
|
background: linear-gradient(135deg, #495057, #6c757d);
|
|
color: white;
|
|
padding: 15px 20px;
|
|
cursor: pointer;
|
|
transition: background-color 0.3s;
|
|
}}
|
|
.concept-header:hover {{
|
|
background: linear-gradient(135deg, #6c757d, #495057);
|
|
}}
|
|
.concept-header h3 {{
|
|
margin: 0;
|
|
font-size: 1.3em;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
}}
|
|
.concept-description {{
|
|
font-size: 0.9em;
|
|
opacity: 0.9;
|
|
margin-top: 5px;
|
|
}}
|
|
.sensor-count {{
|
|
background: rgba(255,255,255,0.2);
|
|
padding: 4px 8px;
|
|
border-radius: 12px;
|
|
font-size: 0.8em;
|
|
font-weight: normal;
|
|
}}
|
|
.sensors-container {{
|
|
display: none;
|
|
background: white;
|
|
}}
|
|
.sensors-container.active {{
|
|
display: block;
|
|
}}
|
|
.sensor-grid {{
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
|
|
gap: 15px;
|
|
padding: 20px;
|
|
}}
|
|
.sensor-card {{
|
|
background: #f8f9fa;
|
|
border: 1px solid #e9ecef;
|
|
border-radius: 6px;
|
|
padding: 15px;
|
|
transition: all 0.3s;
|
|
}}
|
|
.sensor-card:hover {{
|
|
background: #e9ecef;
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
|
|
}}
|
|
.sensor-id {{
|
|
background: var(--color-primary, #007bff);
|
|
color: white;
|
|
padding: 4px 8px;
|
|
border-radius: 4px;
|
|
font-size: 0.8em;
|
|
font-weight: bold;
|
|
display: inline-block;
|
|
margin-bottom: 8px;
|
|
}}
|
|
.sensor-name {{
|
|
font-weight: bold;
|
|
color: #495057;
|
|
margin-bottom: 5px;
|
|
word-break: break-all;
|
|
}}
|
|
.sensor-extern {{
|
|
color: var(--color-success, #28a745);
|
|
font-size: 0.9em;
|
|
margin-bottom: 5px;
|
|
font-style: italic;
|
|
}}
|
|
.sensor-description {{
|
|
color: #6c757d;
|
|
font-size: 0.9em;
|
|
line-height: 1.4;
|
|
}}
|
|
.no-data {{
|
|
color: #999;
|
|
font-style: italic;
|
|
}}
|
|
.timestamp {{
|
|
text-align: center;
|
|
color: #6c757d;
|
|
font-size: 0.9em;
|
|
margin-top: 30px;
|
|
padding-top: 20px;
|
|
border-top: 1px solid #dee2e6;
|
|
}}
|
|
@media (max-width: 768px) {{
|
|
.container {{
|
|
padding: 15px;
|
|
}}
|
|
.stats {{
|
|
flex-direction: column;
|
|
align-items: center;
|
|
}}
|
|
.sensor-grid {{
|
|
grid-template-columns: 1fr;
|
|
}}
|
|
}}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<div class="header">
|
|
<h1>Sensor Uebersicht</h1>
|
|
<div class="stats">
|
|
<div class="stat-card">
|
|
<div class="number">{total_sensors}</div>
|
|
<div class="label">Sensoren gesamt</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="number">{total_concepts}</div>
|
|
<div class="label">MeasureConcepts</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="filters">
|
|
<div class="filter-group">
|
|
<label for="searchInput">Sensor suchen:</label>
|
|
<input type="text" id="searchInput" placeholder="Sensor-Name, ID oder Beschreibung..." onkeyup="filterSensors()">
|
|
</div>
|
|
<div class="filter-group">
|
|
<label for="conceptFilter">MeasureConcept filtern:</label>
|
|
<select id="conceptFilter" onchange="filterSensors()">
|
|
<option value="">Alle MeasureConcepts anzeigen</option>
|
|
"""
|
|
|
|
for mc_id, mc_data in sorted(measure_concepts.items(), key=lambda item: item[1]["name"]):
|
|
mc_name = escape_text(mc_data["name"], "Unbekannt")
|
|
sensor_count = len(mc_data["sensors"])
|
|
html_output += (
|
|
f' <option value="{escape_text(mc_id)}">'
|
|
f"{mc_name} ({sensor_count} Sensoren)</option>\n"
|
|
)
|
|
|
|
html_output += """
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="sensorsContent">
|
|
"""
|
|
|
|
for mc_id, mc_data in sorted(measure_concepts.items(), key=lambda item: item[1]["name"]):
|
|
safe_mc_id = escape_text(mc_id)
|
|
mc_name = escape_text(mc_data["name"], "Unbekannt")
|
|
mc_desc = escape_text(mc_data["description"])
|
|
sensors_in_concept = mc_data["sensors"]
|
|
|
|
html_output += f"""
|
|
<div class="measure-concept" data-concept-id="{safe_mc_id}">
|
|
<div class="concept-header" onclick="toggleConcept('{safe_mc_id}')">
|
|
<div>
|
|
<h3>
|
|
{mc_name}
|
|
<span class="sensor-count">{len(sensors_in_concept)} Sensoren</span>
|
|
</h3>
|
|
"""
|
|
if mc_desc:
|
|
html_output += f' <div class="concept-description">{mc_desc}</div>\n'
|
|
|
|
html_output += f"""
|
|
</div>
|
|
</div>
|
|
<div class="sensors-container" id="sensors-{safe_mc_id}">
|
|
<div class="sensor-grid">
|
|
"""
|
|
|
|
for sensor in sensors_in_concept:
|
|
sensor_id = escape_text(sensor.get("id"), "Unbekannt")
|
|
sensor_name = escape_text((sensor.get("name") or "").strip(), "Unbekannter Name")
|
|
sensor_extern = escape_text(sensor.get("nameExtern") or "")
|
|
sensor_desc = escape_text(sensor.get("description") or "")
|
|
|
|
html_output += f"""
|
|
<div class="sensor-card" data-sensor-name="{sensor_name}" data-sensor-id="{sensor_id}" data-sensor-desc="{sensor_desc}">
|
|
<div class="sensor-id">ID: {sensor_id}</div>
|
|
<div class="sensor-name">{sensor_name}</div>
|
|
"""
|
|
|
|
if sensor_extern and sensor_extern != "-":
|
|
html_output += f' <div class="sensor-extern">Extern: {sensor_extern}</div>\n'
|
|
|
|
if sensor_desc:
|
|
html_output += f' <div class="sensor-description">{sensor_desc}</div>\n'
|
|
else:
|
|
html_output += ' <div class="sensor-description no-data">Keine Beschreibung verfuegbar</div>\n'
|
|
|
|
html_output += " </div>\n"
|
|
|
|
html_output += """
|
|
</div>
|
|
</div>
|
|
</div>
|
|
"""
|
|
|
|
current_time = datetime.now().strftime("%d.%m.%Y um %H:%M:%S")
|
|
html_output += f"""
|
|
</div>
|
|
|
|
<div class="timestamp">
|
|
Erstellt am {current_time}
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
function toggleConcept(conceptId) {{
|
|
const container = document.getElementById('sensors-' + conceptId);
|
|
if (container) {{
|
|
container.classList.toggle('active');
|
|
}}
|
|
}}
|
|
|
|
function filterSensors() {{
|
|
const searchTerm = document.getElementById('searchInput').value.toLowerCase();
|
|
const conceptFilter = document.getElementById('conceptFilter').value;
|
|
const concepts = document.querySelectorAll('.measure-concept');
|
|
|
|
concepts.forEach(concept => {{
|
|
const conceptId = concept.getAttribute('data-concept-id');
|
|
let showConcept = false;
|
|
|
|
if (conceptFilter && conceptFilter !== conceptId) {{
|
|
concept.style.display = 'none';
|
|
return;
|
|
}}
|
|
|
|
const sensorCards = concept.querySelectorAll('.sensor-card');
|
|
sensorCards.forEach(card => {{
|
|
const sensorName = card.getAttribute('data-sensor-name').toLowerCase();
|
|
const sensorId = card.getAttribute('data-sensor-id').toLowerCase();
|
|
const sensorDesc = card.getAttribute('data-sensor-desc').toLowerCase();
|
|
|
|
if (!searchTerm ||
|
|
sensorName.includes(searchTerm) ||
|
|
sensorId.includes(searchTerm) ||
|
|
sensorDesc.includes(searchTerm)) {{
|
|
card.style.display = 'block';
|
|
showConcept = true;
|
|
}} else {{
|
|
card.style.display = 'none';
|
|
}}
|
|
}});
|
|
|
|
concept.style.display = showConcept ? 'block' : 'none';
|
|
}});
|
|
}}
|
|
|
|
document.querySelectorAll('.sensors-container').forEach(container => {{
|
|
container.classList.remove('active');
|
|
}});
|
|
</script>
|
|
</body>
|
|
</html>
|
|
"""
|
|
|
|
return html_output
|
|
|
|
|
|
result = build_report()
|
|
print(result)
|