diff --git a/app.py b/app.py
index 956e5ce..ed2d33a 100644
--- a/app.py
+++ b/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'])
\ No newline at end of file
diff --git a/config.py b/config.py
index b3e571b..7934ac9 100644
--- a/config.py
+++ b/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"
+}
diff --git a/requirements.txt b/requirements.txt
index 794dabf..f892d27 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,7 +1,7 @@
Flask
influxdb
-influxdb-client
+#influxdb-client
+simple-websocket
python-socketio
flask-socketio
-gunicorn
eventlet
diff --git a/static/js/monitor.js b/static/js/monitor.js
index 4671dc4..3b7b4da 100644
--- a/static/js/monitor.js
+++ b/static/js/monitor.js
@@ -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);
-}
/**
diff --git a/templates/base.html b/templates/base.html
index eb33d94..f89ebbc 100644
--- a/templates/base.html
+++ b/templates/base.html
@@ -12,9 +12,16 @@
{% block content %}{% endblock %}
+