import eventlet eventlet.monkey_patch(all=True) import os import warnings import logging from flask import Flask, render_template, jsonify, request from flask_socketio import SocketIO from influxdb import InfluxDBClient from datetime import datetime, timedelta 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) 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) def get_file_hash(filename): full_path = os.path.join(app.static_folder, filename) try: with open(full_path, "rb") as f: return hashlib.md5(f.read()).hexdigest()[:8] except FileNotFoundError: return "1" # Klient InfluxDB v1 def get_influx_client(): client = InfluxDBClient( host=config.INFLUXDB_CONFIG['host'], port=config.INFLUXDB_CONFIG['port'], database=config.INFLUXDB_CONFIG['database'] ) if config.INFLUXDB_CONFIG['username']: client.switch_user(config.INFLUXDB_CONFIG['username'], config.INFLUXDB_CONFIG['password']) return client # --- LOGIKA --- def get_current_voltage(phase_id): client = get_influx_client() entity_id = config.PHASES[phase_id]['entity_id'] query = f'SELECT "value" FROM "{config.MEASUREMENT}" WHERE "entity_id" = \'{entity_id}\' ORDER BY time DESC LIMIT 1' try: result = client.query(query) points = list(result.get_points()) if points and points[0].get('value') is not None: return {'voltage': round(float(points[0]['value']), 2), 'timestamp': points[0]['time']} except Exception as e: print(f"Current Error: {e}") finally: client.close() return {'voltage': 0, 'timestamp': None} # --- ENDPOINTY --- @app.context_processor def inject_static_version(): def static_v(filename): full_path = os.path.join(app.static_folder, filename) try: with open(full_path, "rb") as f: v = hashlib.md5(f.read()).hexdigest()[:8] except Exception: v = "1" return f"{request.script_root}/static/{filename}?v={v}" return dict(static_v=static_v) @app.after_request def add_header(response): if request.path.startswith('/static/'): response.cache_control.max_age = 31536000 response.cache_control.public = True else: response.cache_control.no_cache = True response.cache_control.no_store = True response.cache_control.must_revalidate = True return response @app.route('/favicon.ico') def favicon(): return '', 204 @app.route('/') def index(): return render_template('index.html', phases=config.PHASES, time_ranges=config.TIME_RANGES, default_range=config.DEFAULT_TIME_RANGE, footer=config.FOOTER) @app.route('/api/timeseries/') def get_timeseries(phase_id): if phase_id not in config.PHASES: return jsonify({'error': 'Invalid phase'}), 400 client = get_influx_client() range_param = request.args.get('range', config.DEFAULT_TIME_RANGE) start_param = request.args.get('start') end_param = request.args.get('end') entity_id = config.PHASES[phase_id]['entity_id'] if start_param and end_param: time_filter = f"time >= '{start_param}' AND time <= '{end_param}'" interval = "1m" else: clean_range = range_param.replace(" ", "") time_filter = f"time > now() - {clean_range}" mapping = { "1h": "10s", "6h": "30s", "24h": "2m", "7d": "10m", "30d": "30m", "60d": "1h", "120d": "2h", "180d": "4h", "365d": "6h" } interval = mapping.get(range_param, "1m") query = f''' SELECT mean("value") AS voltage FROM "{config.MEASUREMENT}" WHERE "entity_id" = '{entity_id}' AND {time_filter} GROUP BY time({interval}) fill(none) ''' try: result = client.query(query) data = [{"time": p['time'], "voltage": round(p['voltage'], 2)} for p in result.get_points() if p.get('voltage') is not None] return jsonify(data) except Exception as e: app.logger.error(f"Timeseries Error: {e} | Query: {query}") return jsonify([]) 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) 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')) time_filter = f"time >= '{start_p}' - 24h AND time <= '{end_p}'" else: clean_range = range_p.replace(" ", "") 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" all_events = [] try: for p_id, p_cfg in config.PHASES.items(): query = f''' SELECT mean("value") AS volts FROM "{config.MEASUREMENT}" WHERE "entity_id" = '{p_cfg["entity_id"]}' AND {time_filter} GROUP BY time(1m) fill(none) ''' result = client.query(query) points = list(result.get_points()) i = 0 while i < len(points): val = points[i].get('volts') if val is not None and float(val) < 207: 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: 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() / 120 if duration >= 0.5: 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) }) 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}") return jsonify([]) finally: client.close() @app.route('/api/outages/') def api_outages(phase_id): client = get_influx_client() t_range = request.args.get('range', '24h') entity_id = config.PHASES[phase_id]['entity_id'] query = f'SELECT "value" FROM "{config.MEASUREMENT}" WHERE "entity_id" = \'{entity_id}\' AND "value" < 10 AND time > now() - {t_range}' try: result = client.query(query) return jsonify(list(result.get_points())) finally: client.close() def background_voltage_update(): 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'] socketio.emit('voltage_update', voltages) except Exception as e: print(f"Worker Error: {e}") eventlet.sleep(config.CHART_CONFIG['update_interval'] / 1000) 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.run(app, host='0.0.0.0', port=config.FLASK_CONFIG['port'])