poprawki, zmian w logice eventow, poprawka w zapytaniach, wykres live, lepsza stopla, uzunięcie eventlet
This commit is contained in:
103
app.py
103
app.py
@@ -1,9 +1,7 @@
|
||||
import eventlet
|
||||
eventlet.monkey_patch(all=True)
|
||||
|
||||
import os
|
||||
import warnings
|
||||
import logging
|
||||
import time
|
||||
from flask import Flask, render_template, jsonify, request
|
||||
from flask_socketio import SocketIO
|
||||
from influxdb import InfluxDBClient
|
||||
@@ -11,7 +9,6 @@ from datetime import datetime, timedelta, timezone
|
||||
import config
|
||||
import hashlib
|
||||
|
||||
warnings.filterwarnings("ignore", category=DeprecationWarning, module="eventlet")
|
||||
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
||||
logger = logging.getLogger('werkzeug')
|
||||
logger.setLevel(logging.INFO)
|
||||
@@ -19,7 +16,8 @@ logger.setLevel(logging.INFO)
|
||||
app = Flask(__name__)
|
||||
app.config['SECRET_KEY'] = config.FLASK_CONFIG['secret_key']
|
||||
app.config['SEND_FILE_MAX_AGE_DEFAULT'] = config.FLASK_CONFIG.get('static_cache_timeout', 60)
|
||||
socketio = SocketIO(app, cors_allowed_origins="*", async_mode='eventlet', logger=False)
|
||||
# Bez Eventlet (deprecated) – prosty tryb wątkowy.
|
||||
socketio = SocketIO(app, cors_allowed_origins="*", async_mode='threading', logger=False)
|
||||
|
||||
def get_file_hash(filename):
|
||||
full_path = os.path.join(app.static_folder, filename)
|
||||
@@ -115,7 +113,7 @@ def get_timeseries(phase_id):
|
||||
if range_param in config.TIME_RANGES:
|
||||
interval = config.TIME_RANGES[range_param]['interval']
|
||||
else:
|
||||
interval = config.DEFAULT_INTERVALk
|
||||
interval = config.DEFAULT_INTERVAL
|
||||
|
||||
query = f'''
|
||||
SELECT mean("value") AS voltage
|
||||
@@ -173,33 +171,61 @@ def get_events():
|
||||
all_events = []
|
||||
try:
|
||||
for p_id, p_cfg in config.PHASES.items():
|
||||
# min() + fill(0) dla pewnego wykrywania zaników
|
||||
# min()+max() + fill(null):
|
||||
# - brak danych (np. restart Influxa) NIE udaje 0V
|
||||
# - "wysokie" wykrywamy po vmax, a "niskie/zanik" po vmin
|
||||
query = f'''
|
||||
SELECT min("value") AS volts
|
||||
SELECT min("value") AS vmin, max("value") AS vmax
|
||||
FROM "{config.MEASUREMENT}"
|
||||
WHERE "entity_id" = '{p_cfg["entity_id"]}'
|
||||
AND {time_filter}
|
||||
GROUP BY time({config.EVENT_DETECTION_INTERVAL}) fill(0)
|
||||
GROUP BY time({config.EVENT_DETECTION_INTERVAL}) fill(null)
|
||||
'''
|
||||
result = client.query(query)
|
||||
points = list(result.get_points())
|
||||
|
||||
i = 0
|
||||
while i < len(points):
|
||||
val = points[i].get('volts')
|
||||
if val is None:
|
||||
i += 1
|
||||
vmin_val = points[i].get('vmin')
|
||||
vmax_val = points[i].get('vmax')
|
||||
|
||||
if vmin_val is None and vmax_val is None:
|
||||
start_str = points[i]['time']
|
||||
dt_s = datetime.fromisoformat(start_str.replace('Z', '+00:00'))
|
||||
j = i
|
||||
while j + 1 < len(points):
|
||||
if points[j+1].get('vmin') is None and points[j+1].get('vmax') is None:
|
||||
j += 1
|
||||
else:
|
||||
break
|
||||
|
||||
end_str = points[j]['time']
|
||||
dt_e = datetime.fromisoformat(end_str.replace('Z', '+00:00'))
|
||||
duration = (dt_e - dt_s).total_seconds() / 60 + 1
|
||||
|
||||
if duration >= config.MIN_EVENT_DURATION_MINUTES:
|
||||
if dt_e >= dt_view_start and dt_s <= dt_view_end:
|
||||
all_events.append({
|
||||
"start": start_str,
|
||||
"end": end_str,
|
||||
"phase": p_id,
|
||||
"type": "zanik",
|
||||
"duration": int(round(duration, 0)),
|
||||
"source": "gap"
|
||||
})
|
||||
i = j + 1
|
||||
continue
|
||||
|
||||
v_now = float(val)
|
||||
vmin = float(vmin_val) if vmin_val is not None else None
|
||||
vmax = float(vmax_val) if vmax_val is not None else None
|
||||
ev_type = None
|
||||
|
||||
# Używamy progów z config.VOLTAGE_THRESHOLDS
|
||||
if v_now < config.VOLTAGE_THRESHOLDS['outage']:
|
||||
if vmin is not None and vmin < config.VOLTAGE_THRESHOLDS['outage']:
|
||||
ev_type = "zanik"
|
||||
elif config.VOLTAGE_THRESHOLDS['outage'] <= v_now < config.VOLTAGE_THRESHOLDS['min_safe']:
|
||||
elif vmin is not None and config.VOLTAGE_THRESHOLDS['outage'] <= vmin < config.VOLTAGE_THRESHOLDS['min_safe']:
|
||||
ev_type = "niskie"
|
||||
elif v_now > config.VOLTAGE_THRESHOLDS['max_safe']:
|
||||
elif vmax is not None and vmax > config.VOLTAGE_THRESHOLDS['max_safe']:
|
||||
ev_type = "wysokie"
|
||||
# else: ev_type = None (wartość w zakresie bezpiecznym 207-253V)
|
||||
|
||||
@@ -209,19 +235,19 @@ def get_events():
|
||||
j = i
|
||||
|
||||
while j + 1 < len(points):
|
||||
v_next_val = points[j+1].get('volts')
|
||||
v_next_min = points[j+1].get('vmin')
|
||||
v_next_max = points[j+1].get('vmax')
|
||||
next_type = None
|
||||
if v_next_val is not None:
|
||||
v_next = float(v_next_val)
|
||||
|
||||
# Używamy progów z config.VOLTAGE_THRESHOLDS
|
||||
if v_next < config.VOLTAGE_THRESHOLDS['outage']:
|
||||
next_type = "zanik"
|
||||
elif config.VOLTAGE_THRESHOLDS['outage'] <= v_next < config.VOLTAGE_THRESHOLDS['min_safe']:
|
||||
next_type = "niskie"
|
||||
elif v_next > config.VOLTAGE_THRESHOLDS['max_safe']:
|
||||
next_type = "wysokie"
|
||||
# else: next_type = None (wartość w zakresie bezpiecznym)
|
||||
|
||||
nm = float(v_next_min) if v_next_min is not None else None
|
||||
nx = float(v_next_max) if v_next_max is not None else None
|
||||
if nm is not None and nm < config.VOLTAGE_THRESHOLDS['outage']:
|
||||
next_type = "zanik"
|
||||
elif nm is not None and config.VOLTAGE_THRESHOLDS['outage'] <= nm < config.VOLTAGE_THRESHOLDS['min_safe']:
|
||||
next_type = "niskie"
|
||||
elif nx is not None and nx > config.VOLTAGE_THRESHOLDS['max_safe']:
|
||||
next_type = "wysokie"
|
||||
# else: next_type = None
|
||||
|
||||
if next_type == ev_type:
|
||||
j += 1
|
||||
@@ -266,22 +292,37 @@ def api_outages(phase_id):
|
||||
client.close()
|
||||
|
||||
def background_voltage_update():
|
||||
last_refresh = 0
|
||||
refresh_every_s = config.CHART_CONFIG.get("refresh_interval_seconds", 15)
|
||||
|
||||
while True:
|
||||
try:
|
||||
voltages = {'timestamp': None}
|
||||
|
||||
for pid in config.PHASES.keys():
|
||||
res = get_current_voltage(pid)
|
||||
voltages[f'phase{pid}'] = res['voltage']
|
||||
if res['timestamp']: voltages['timestamp'] = res['timestamp']
|
||||
if res['timestamp']:
|
||||
voltages['timestamp'] = res['timestamp']
|
||||
|
||||
# aktualizacja gauge
|
||||
socketio.emit('voltage_update', voltages)
|
||||
|
||||
# cykliczny refresh wykresu + eventów
|
||||
now = time.time()
|
||||
if now - last_refresh >= refresh_every_s:
|
||||
socketio.emit('refresh_timeseries', {'ts': int(now)})
|
||||
last_refresh = now
|
||||
|
||||
except Exception as e:
|
||||
print(f"Worker Error: {e}")
|
||||
eventlet.sleep(config.CHART_CONFIG['update_interval'] / 1000)
|
||||
|
||||
time.sleep(config.CHART_CONFIG['update_interval'] / 1000.0)
|
||||
|
||||
if __name__ == '__main__':
|
||||
print("\n" + "="*50)
|
||||
print(f"Voltage Monitor API / Port: {config.FLASK_CONFIG['port']}")
|
||||
print("="*50 + "\n")
|
||||
|
||||
eventlet.spawn(background_voltage_update)
|
||||
socketio.start_background_task(background_voltage_update)
|
||||
socketio.run(app, host='0.0.0.0', port=config.FLASK_CONFIG['port'])
|
||||
12
config.py
12
config.py
@@ -65,7 +65,7 @@ DEFAULT_INTERVAL = '1m'
|
||||
|
||||
# Skalowanie
|
||||
GAUGE_CONFIG = {'min': 190, 'max': 270}
|
||||
CHART_CONFIG = {'y_min': 190, 'y_max': 270, 'update_interval': 2000}
|
||||
CHART_CONFIG = {'y_min': 190, 'y_max': 270, 'update_interval': 2000, "refresh_interval_seconds": 15}
|
||||
|
||||
# Flask settings - PORT 8798
|
||||
FLASK_CONFIG = {
|
||||
@@ -75,4 +75,12 @@ FLASK_CONFIG = {
|
||||
'secret_key': os.getenv('SECRET_KEY', 'voltage-monitor-secret-key'),
|
||||
}
|
||||
|
||||
FOOTER = {'author': 'www.linuxiarz.pl', 'year': '2026'}
|
||||
FOOTER = {
|
||||
"year": "2026",
|
||||
"owner": {
|
||||
"name": "Mateusz Gruszczyński | @linuxiarz.pl",
|
||||
"url": "https://www.linuxiarz.pl"
|
||||
},
|
||||
"project": "Voltage Monitor",
|
||||
"version": "1..0"
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
Flask
|
||||
influxdb
|
||||
influxdb-client
|
||||
#influxdb-client
|
||||
simple-websocket
|
||||
python-socketio
|
||||
flask-socketio
|
||||
gunicorn
|
||||
eventlet
|
||||
|
||||
@@ -4,6 +4,7 @@ let phasesConfig = {};
|
||||
const gauges = {};
|
||||
let voltageChart = null;
|
||||
const THRESHOLDS = { min: 207, max: 253 };
|
||||
let disableChartAnimationOnce = false;
|
||||
|
||||
/**
|
||||
* Inicjalizacja monitora
|
||||
@@ -31,6 +32,13 @@ function initMonitor(phases, defaultRange) {
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('refresh_timeseries', async () => {
|
||||
if (currentTimeRange === 'precise') return;
|
||||
|
||||
disableChartAnimationOnce = true;
|
||||
await reloadDataForRange(null, null, currentTimeRange);
|
||||
});
|
||||
|
||||
window.changeTimeRange(currentTimeRange);
|
||||
}
|
||||
|
||||
@@ -200,16 +208,18 @@ async function reloadDataForRange(min, max, rangeName = null) {
|
||||
}
|
||||
|
||||
voltageChart.data.datasets = newDatasets;
|
||||
if (rangeName) {
|
||||
voltageChart.update();
|
||||
|
||||
if (disableChartAnimationOnce) {
|
||||
voltageChart.update('none');
|
||||
disableChartAnimationOnce = false;
|
||||
} else {
|
||||
voltageChart.update('none');
|
||||
if (rangeName) voltageChart.update();
|
||||
else voltageChart.update('none');
|
||||
}
|
||||
const finalMin = rangeName ? voltageChart.scales.x.min : (min || voltageChart.scales.x.min);
|
||||
const finalMax = rangeName ? voltageChart.scales.x.max : (max || voltageChart.scales.x.max);
|
||||
updateRangeLabel(finalMin, finalMax);
|
||||
}
|
||||
|
||||
const finalMin = rangeName ? voltageChart.scales.x.min : (min || voltageChart.scales.x.min);
|
||||
const finalMax = rangeName ? voltageChart.scales.x.max : (max || voltageChart.scales.x.max);
|
||||
updateRangeLabel(finalMin, finalMax);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
|
||||
@@ -12,9 +12,16 @@
|
||||
{% block content %}{% endblock %}
|
||||
<footer class="text-center mt-4 py-3">
|
||||
<small class="text-muted">
|
||||
© {{ footer.year }} <a href="https://{{ footer.author }}" target="_blank" class="text-decoration-none">{{ footer.author }}</a>
|
||||
© {{ footer.year }} {{ footer.project }} |
|
||||
Autor:
|
||||
<a href="{{ footer.owner.url }}" target="_blank" class="text-decoration-none">
|
||||
{{ footer.owner.name }}
|
||||
</a>
|
||||
{% if footer.version %}
|
||||
| v{{ footer.version }}
|
||||
{% endif %}
|
||||
</small>
|
||||
</footer>
|
||||
</footer>
|
||||
</div>
|
||||
<!-- Chart.js -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
|
||||
Reference in New Issue
Block a user