poprawki
This commit is contained in:
52
app.py
52
app.py
@@ -7,7 +7,7 @@ import logging
|
||||
from flask import Flask, render_template, jsonify, request
|
||||
from flask_socketio import SocketIO
|
||||
from influxdb import InfluxDBClient
|
||||
from datetime import datetime, timedelta
|
||||
from datetime import datetime, timedelta, timezone
|
||||
import config
|
||||
import hashlib
|
||||
|
||||
@@ -144,17 +144,22 @@ def get_timeseries(phase_id):
|
||||
finally:
|
||||
client.close()
|
||||
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
@app.route('/api/events')
|
||||
def get_events():
|
||||
client = get_influx_client()
|
||||
range_p = request.args.get('range', '24h')
|
||||
start_p = request.args.get('start')
|
||||
end_p = request.args.get('end')
|
||||
|
||||
now_utc = datetime.now(timezone.utc)
|
||||
|
||||
# Blokada dla zakresów powyżej 61 dni
|
||||
if not (start_p and end_p) and "d" in range_p:
|
||||
try:
|
||||
days = int(''.join(filter(str.isdigit, range_p)))
|
||||
if days > 61:
|
||||
return jsonify({"error": "range_too_large", "message": "Zbyt duży zakres. Zaznacz obszar na wykresie."})
|
||||
except: pass
|
||||
|
||||
if start_p and end_p:
|
||||
dt_view_start = datetime.fromisoformat(start_p.replace('Z', '+00:00'))
|
||||
dt_view_end = datetime.fromisoformat(end_p.replace('Z', '+00:00'))
|
||||
@@ -164,7 +169,6 @@ def get_events():
|
||||
num = int(''.join(filter(str.isdigit, clean_range)))
|
||||
unit = clean_range[-1]
|
||||
delta = timedelta(hours=num) if unit == 'h' else timedelta(days=num)
|
||||
|
||||
dt_view_start = now_utc - delta
|
||||
dt_view_end = now_utc
|
||||
time_filter = f"time > now() - {clean_range} - 24h"
|
||||
@@ -172,12 +176,13 @@ def get_events():
|
||||
all_events = []
|
||||
try:
|
||||
for p_id, p_cfg in config.PHASES.items():
|
||||
# min() + fill(0) dla pewnego wykrywania zaników
|
||||
query = f'''
|
||||
SELECT mean("value") AS volts
|
||||
SELECT min("value") AS volts
|
||||
FROM "{config.MEASUREMENT}"
|
||||
WHERE "entity_id" = '{p_cfg["entity_id"]}'
|
||||
AND {time_filter}
|
||||
GROUP BY time(1m) fill(none)
|
||||
GROUP BY time(1m) fill(0)
|
||||
'''
|
||||
result = client.query(query)
|
||||
points = list(result.get_points())
|
||||
@@ -185,14 +190,31 @@ def get_events():
|
||||
i = 0
|
||||
while i < len(points):
|
||||
val = points[i].get('volts')
|
||||
if val is not None and float(val) < 207:
|
||||
if val is None:
|
||||
i += 1
|
||||
continue
|
||||
|
||||
v_now = float(val)
|
||||
ev_type = None
|
||||
|
||||
if v_now < 100: ev_type = "zanik"
|
||||
elif 100 <= v_now < 207: ev_type = "niskie"
|
||||
elif v_now > 253: ev_type = "wysokie"
|
||||
|
||||
if ev_type:
|
||||
start_str = points[i]['time']
|
||||
dt_s = datetime.fromisoformat(start_str.replace('Z', '+00:00'))
|
||||
|
||||
j = i
|
||||
while j + 1 < len(points):
|
||||
v_next = points[j+1].get('volts')
|
||||
if v_next is not None and float(v_next) < 207:
|
||||
v_next_val = points[j+1].get('volts')
|
||||
next_type = None
|
||||
if v_next_val is not None:
|
||||
v_next = float(v_next_val)
|
||||
if v_next < 100: next_type = "zanik"
|
||||
elif 100 <= v_next < 207: next_type = "niskie"
|
||||
elif v_next > 253: next_type = "wysokie"
|
||||
|
||||
if next_type == ev_type:
|
||||
j += 1
|
||||
else:
|
||||
break
|
||||
@@ -200,19 +222,19 @@ def get_events():
|
||||
end_str = points[j]['time']
|
||||
dt_e = datetime.fromisoformat(end_str.replace('Z', '+00:00'))
|
||||
|
||||
duration = (dt_e - dt_s).total_seconds() / 120
|
||||
duration = (dt_e - dt_s).total_seconds() / 60 + 1
|
||||
|
||||
if duration >= 0.5:
|
||||
if duration >= 5.1:
|
||||
if dt_e >= dt_view_start and dt_s <= dt_view_end:
|
||||
all_events.append({
|
||||
"start": start_str,
|
||||
"end": end_str,
|
||||
"phase": p_id,
|
||||
"duration": round(duration, 1)
|
||||
"type": ev_type,
|
||||
"duration": int(round(duration, 0))
|
||||
})
|
||||
i = j
|
||||
i += 1
|
||||
|
||||
return jsonify(sorted(all_events, key=lambda x: x['start'], reverse=True))
|
||||
except Exception as e:
|
||||
app.logger.error(f"Event Logic Error: {e}")
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
--text-main: #c9d1d9;
|
||||
--blue-accent: #58a6ff;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--bg-dark);
|
||||
color: var(--text-main);
|
||||
@@ -13,12 +14,15 @@ body {
|
||||
margin: 0;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/* Siatka wskaźników (gauges) */
|
||||
.gauge-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 10px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.gauge-card {
|
||||
background-color: var(--card-bg);
|
||||
border: 1px solid var(--border-color);
|
||||
@@ -26,41 +30,53 @@ body {
|
||||
padding: 10px 5px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.gauge-canvas-container {
|
||||
max-width: 80px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.gauge-label {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: var(--blue-accent);
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.voltage-value {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 800;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
/* Wybór zakresu czasu - Poprawiona responsywność */
|
||||
.time-btn-container {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
overflow-x: auto;
|
||||
gap: 6px;
|
||||
padding-bottom: 10px;
|
||||
flex-wrap: wrap; /* Pozwala na zawijanie przycisków do nowej linii */
|
||||
gap: 8px;
|
||||
padding: 10px 0;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.time-btn {
|
||||
font-size: 0.75rem !important;
|
||||
padding: 5px 12px !important;
|
||||
padding: 6px 12px !important;
|
||||
white-space: nowrap;
|
||||
border-color: var(--border-color) !important;
|
||||
border: 1px solid var(--border-color) !important;
|
||||
color: var(--blue-accent) !important;
|
||||
background: transparent;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.time-btn.active {
|
||||
background-color: #1f6feb !important;
|
||||
color: white !important;
|
||||
border-color: #1f6feb !important;
|
||||
}
|
||||
|
||||
/* Wykres główny */
|
||||
.main-chart-card {
|
||||
background-color: var(--card-bg);
|
||||
border: 1px solid var(--border-color);
|
||||
@@ -69,15 +85,99 @@ body {
|
||||
height: 55vh;
|
||||
min-height: 350px;
|
||||
}
|
||||
footer { padding: 20px 0; opacity: 0.7; }
|
||||
@media (max-width: 576px) {
|
||||
.voltage-value { font-size: 0.95rem; }
|
||||
.main-chart-card { height: 50vh; padding: 10px; }
|
||||
|
||||
/* Lista zdarzeń (logi) */
|
||||
.events-card {
|
||||
background-color: var(--card-bg);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 12px;
|
||||
padding: 15px;
|
||||
margin-top: 15px;
|
||||
}
|
||||
.events-card { background-color: var(--card-bg); border: 1px solid var(--border-color); border-radius: 12px; padding: 15px; }
|
||||
.event-item { display: flex; align-items: center; padding: 8px 0; border-bottom: 1px solid #30363d; }
|
||||
.event-item:last-child { border-bottom: none; }
|
||||
.event-badge { width: 12px; height: 12px; border-radius: 50%; margin-right: 12px; flex-shrink: 0; }
|
||||
.event-time { font-family: monospace; font-size: 0.85rem; color: #8b949e; margin-right: 15px; }
|
||||
.event-desc { font-size: 0.9rem; color: #c9d1d9; }
|
||||
.no-events { color: #8b949e; font-style: italic; text-align: center; padding: 10px; }
|
||||
|
||||
.event-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 10px 0;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.event-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.event-badge {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
margin-right: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.event-time {
|
||||
font-family: monospace;
|
||||
font-size: 0.85rem;
|
||||
color: #8b949e;
|
||||
margin-right: 15px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.event-desc {
|
||||
font-size: 0.9rem;
|
||||
color: var(--text-main);
|
||||
}
|
||||
|
||||
.no-events {
|
||||
color: #8b949e;
|
||||
font-style: italic;
|
||||
text-align: center;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
footer {
|
||||
padding: 20px 0;
|
||||
opacity: 0.7;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@media (max-width: 576px) {
|
||||
.voltage-value {
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.main-chart-card {
|
||||
height: 45vh;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.time-btn {
|
||||
flex: 1 1 calc(30% - 10px);
|
||||
min-width: 70px;
|
||||
font-size: 0.7rem !important;
|
||||
padding: 8px 4px !important;
|
||||
}
|
||||
|
||||
.event-item {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.event-badge {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.event-time {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.event-warning {
|
||||
background-color: rgba(255, 187, 51, 0.1);
|
||||
border: 1px dashed #ffbb33;
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
margin: 10px 0;
|
||||
text-align: center;
|
||||
color: #ffbb33;
|
||||
}
|
||||
@@ -260,57 +260,88 @@ function renderEventLog(events, range) {
|
||||
const container = document.getElementById('eventLogContainer');
|
||||
if (!container) return;
|
||||
container.innerHTML = '';
|
||||
|
||||
|
||||
if (events && events.error === "range_too_large") {
|
||||
container.innerHTML = `
|
||||
<div style="text-align: center; padding: 30px; color: #ffbb33; border: 1px dashed #ffbb33; border-radius: 12px; margin: 10px;">
|
||||
<div style="font-size: 2rem; margin-bottom: 10px;">⚠️</div>
|
||||
<p style="margin: 0;">${events.message}</p>
|
||||
</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
if (events && events.length > 0) {
|
||||
events.forEach(ev => {
|
||||
const start = new Date(ev.start);
|
||||
const end = new Date(ev.end);
|
||||
const dur = ev.duration;
|
||||
const dur = ev.duration;
|
||||
const phase = phasesConfig[ev.phase];
|
||||
|
||||
const typeConfig = {
|
||||
'zanik': { label: 'Brak zasilania', color: '#ff4444' },
|
||||
'niskie': { label: 'Zbyt niskie', color: '#ffbb33' },
|
||||
'wysokie': { label: 'Zbyt wysokie', color: '#aa66cc' }
|
||||
};
|
||||
const config = typeConfig[ev.type] || { label: 'Problem', color: '#888' };
|
||||
|
||||
const item = document.createElement('div');
|
||||
item.className = 'event-item';
|
||||
item.style.display = 'flex';
|
||||
item.style.alignItems = 'center';
|
||||
item.style.gap = '10px';
|
||||
item.style.justifyContent = 'space-between';
|
||||
|
||||
const dateOpt = { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit', hour12: false };
|
||||
const timeRangeStr = `${start.toLocaleTimeString('pl-PL', {hour:'2-digit', minute:'2-digit'})} - ${end.toLocaleTimeString('pl-PL', {hour:'2-digit', minute:'2-digit'})}`;
|
||||
|
||||
item.innerHTML = `
|
||||
<div class="event-badge" style="background-color: ${phase.color}"></div>
|
||||
<div class="event-time">${start.toLocaleString('pl-PL', dateOpt)} - ${end.toLocaleTimeString('pl-PL', {hour:'2-digit', minute:'2-digit'})}</div>
|
||||
<div class="event-desc" style="flex-grow: 1;">Faza <strong>${phase.label}</strong>: brak przez ${dur} min.</div>
|
||||
<button class="btn btn-sm btn-outline-info show-event-btn" title="Pokaż 3h wokół zdarzenia"
|
||||
style="padding: 2px 8px; font-size: 11px; height: 24px; color: #58a6ff; border-color: #30363d; min-width: 50px;">
|
||||
Pokaż
|
||||
<div style="display: flex; align-items: center; flex-grow: 1;">
|
||||
<!-- Badge statusu z pierścieniem w kolorze fazy -->
|
||||
<div class="event-badge" style="
|
||||
background-color: ${config.color};
|
||||
box-shadow: 0 0 0 2px ${phase.color};
|
||||
margin-left: 2px;
|
||||
margin-right: 14px;
|
||||
width: 10px; height: 10px;">
|
||||
</div>
|
||||
<div class="event-time">
|
||||
<div>${start.toLocaleDateString('pl-PL', {day:'2-digit', month:'2-digit'})}, ${timeRangeStr}</div>
|
||||
</div>
|
||||
<div class="event-desc">
|
||||
<span style="color: ${phase.color}; font-weight: bold;">${phase.label}</span>:
|
||||
<strong>${config.label}</strong> przez ${dur} min.
|
||||
</div>
|
||||
</div>
|
||||
<button onclick="showEventOnChart('${ev.start}')" class="time-btn" style="padding: 4px 8px !important; margin-left: 10px;">
|
||||
Pokaż 🔍
|
||||
</button>
|
||||
`;
|
||||
|
||||
const btn = item.querySelector('.show-event-btn');
|
||||
btn.addEventListener('click', async () => {
|
||||
const eventCenter = start.getTime() + (end.getTime() - start.getTime()) / 2;
|
||||
const windowMs = 3 * 60 * 60 * 1000;
|
||||
const viewStart = eventCenter - (windowMs / 2);
|
||||
const viewEnd = eventCenter + (windowMs / 2);
|
||||
|
||||
document.querySelectorAll('.time-btn').forEach(b => b.classList.remove('active'));
|
||||
currentTimeRange = 'precise';
|
||||
|
||||
if (voltageChart) {
|
||||
voltageChart.options.scales.x.min = undefined;
|
||||
voltageChart.options.scales.x.max = undefined;
|
||||
}
|
||||
await reloadDataForRange(viewStart, viewEnd, null);
|
||||
|
||||
document.getElementById('voltageChart').scrollIntoView({ behavior: 'smooth' });
|
||||
});
|
||||
|
||||
container.appendChild(item);
|
||||
});
|
||||
} else {
|
||||
container.innerHTML = '<div class="no-events">Brak zarejestrowanych zaników.</div>';
|
||||
container.innerHTML = '<div class="no-events">Brak zarejestrowanych zdarzeń w tym zakresie.</div>';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Funkcja przybliżająca wykres do konkretnego zdarzenia (zakres 3h)
|
||||
*/
|
||||
window.showEventOnChart = function(startTimeStr) {
|
||||
const eventTime = new Date(startTimeStr).getTime();
|
||||
const padding = 1.5 * 60 * 60 * 1000; // 1.5 godziny w milisekundach
|
||||
|
||||
const min = eventTime - padding;
|
||||
const max = eventTime + padding;
|
||||
|
||||
if (voltageChart) {
|
||||
document.querySelectorAll('.time-btn').forEach(b => b.classList.remove('active'));
|
||||
|
||||
voltageChart.options.scales.x.min = min;
|
||||
voltageChart.options.scales.x.max = max;
|
||||
|
||||
currentTimeRange = 'precise';
|
||||
reloadDataForRange(min, max);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
function setupGauges() {
|
||||
const config = {
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
<!-- Informacja o normie napięcia -->
|
||||
<div class="text-center mb-3">
|
||||
<span class="badge bg-dark border border-secondary text-muted" style="font-size: 0.65rem; font-weight: 400; opacity: 0.8;">
|
||||
Norma PN-EN 50160: <span class="text-success">230V ±10% (207V - 253V)</span>
|
||||
Norma PN-EN 50160: <span class="text-success"><strong>230V ±10% (207V - 253V)</strong></span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
|
||||
<div class="events-card mb-3">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<h6 class="mb-0">Dziennik zdarzeń (zaniki faz)</h6>
|
||||
<h6 class="mb-0">Dziennik zdarzeń</h6>
|
||||
<span id="eventRangeLabel" class="text-muted" style="font-size: 0.75rem;">Ładowanie...</span>
|
||||
</div>
|
||||
<div id="eventLogContainer">
|
||||
|
||||
Reference in New Issue
Block a user