From f0862c12ebefb7514f61856d6f279ab35769986d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Sun, 1 Mar 2026 23:24:54 +0100 Subject: [PATCH] =?UTF-8?q?poprawki,=20zmian=20w=20logice=20eventow,=20pop?= =?UTF-8?q?rawka=20w=20zapytaniach,=20wykres=20live,=20lepsza=20stopla,=20?= =?UTF-8?q?uzuni=C4=99cie=20eventlet?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app.py | 103 ++++++++++++++++++++++++++++++------------- config.py | 12 ++++- requirements.txt | 4 +- static/js/monitor.js | 26 +++++++---- templates/base.html | 11 ++++- 5 files changed, 111 insertions(+), 45 deletions(-) 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 %} +