SwapStation_WebApp/backend/main.py

437 lines
15 KiB
Python

# import os
# import sys
# import threading
# import json
# import csv
# import io
# from datetime import datetime
# from flask import Flask, jsonify, request, Response
# from flask_socketio import SocketIO
# from dotenv import load_dotenv
# # Import your custom core modules and the new models
# from core.mqtt_client import MqttClient
# from core.protobuf_decoder import ProtobufDecoder
# from models import db, Station, User, MqttLog
# # --- Load Environment Variables ---
# load_dotenv()
# # --- Pre-startup Check for Essential Configuration ---
# DATABASE_URL = os.getenv("DATABASE_URL")
# if not DATABASE_URL:
# print("FATAL ERROR: DATABASE_URL is not set in .env file.")
# sys.exit(1)
# # --- Application Setup ---
# app = Flask(__name__)
# app.config['SQLALCHEMY_DATABASE_URI'] = DATABASE_URL
# app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
# app.config['SECRET_KEY'] = os.getenv("SECRET_KEY", "a_very_secret_key")
# db.init_app(app)
# socketio = SocketIO(app, cors_allowed_origins="*")
# # --- Global instances ---
# decoder = ProtobufDecoder()
# mqtt_clients = {}
# # --- MQTT Message Handling ---
# def on_message_handler(station_id, topic, payload):
# """
# Handles incoming MQTT messages, decodes them, writes to PostgreSQL,
# and emits to WebSockets.
# """
# print(f"Main handler received message for station {station_id} on topic {topic}")
# decoded_data = None
# message_type = topic.split('/')[-1]
# if message_type == 'PERIODIC':
# decoded_data = decoder.decode_periodic(payload)
# elif message_type == 'EVENTS':
# decoded_data = decoder.decode_event(payload)
# elif message_type == 'REQUEST':
# decoded_data = decoder.decode_rpc_request(payload)
# if decoded_data:
# # 1. Write the data to PostgreSQL for historical storage
# try:
# with app.app_context():
# log_entry = MqttLog(
# station_id=station_id,
# topic=topic,
# payload=decoded_data
# )
# db.session.add(log_entry)
# db.session.commit()
# print(f"Successfully wrote data for {station_id} to PostgreSQL.")
# except Exception as e:
# print(f"Error writing to PostgreSQL: {e}")
# # 2. Emit the data to the frontend for real-time view
# socketio.emit('dashboard_update', {
# 'stationId': station_id,
# 'topic': topic,
# 'data': decoded_data
# }, room=station_id)
# # --- (WebSocket and API routes remain the same) ---
# @socketio.on('connect')
# def handle_connect():
# print('Client connected to WebSocket')
# @socketio.on('disconnect')
# def handle_disconnect():
# print('Client disconnected')
# @socketio.on('join_station_room')
# def handle_join_station_room(data):
# station_id = data.get('station_id')
# if station_id:
# from flask import request
# socketio.join_room(station_id, request.sid)
# @socketio.on('leave_station_room')
# def handle_leave_station_room(data):
# station_id = data.get('station_id')
# if station_id:
# from flask import request
# socketio.leave_room(station_id, request.sid)
# @app.route('/api/stations', methods=['GET'])
# def get_stations():
# try:
# stations = Station.query.all()
# return jsonify([{"id": s.station_id, "name": s.name} for s in stations])
# except Exception as e:
# return jsonify({"error": f"Database query failed: {e}"}), 500
# # --- (CSV Export route remains the same) ---
# @app.route('/api/logs/export', methods=['GET'])
# def export_logs():
# # ... (existing implementation)
# pass
# # --- Main Application Logic (UPDATED) ---
# def start_mqtt_clients():
# """
# Initializes and starts an MQTT client for each station found in the database,
# using the specific MQTT credentials stored for each station.
# """
# try:
# with app.app_context():
# # Get the full station objects, not just the IDs
# stations = Station.query.all()
# except Exception as e:
# print(f"CRITICAL: Could not query stations from the database in MQTT thread: {e}")
# return
# for station in stations:
# if station.station_id not in mqtt_clients:
# print(f"Creating and starting MQTT client for station: {station.name} ({station.station_id})")
# # Use the specific details from each station object in the database
# client = MqttClient(
# broker=station.mqtt_broker,
# port=station.mqtt_port,
# user=station.mqtt_user,
# password=station.mqtt_password,
# station_id=station.station_id,
# on_message_callback=on_message_handler
# )
# client.start()
# mqtt_clients[station.station_id] = client
# if __name__ == '__main__':
# try:
# with app.app_context():
# db.create_all()
# if not Station.query.first():
# print("No stations found. Adding a default station with default MQTT config.")
# # Add a default station with MQTT details for first-time setup
# default_station = Station(
# station_id="V16000868210069259709",
# name="Test Station 2",
# mqtt_broker="mqtt-dev.upgrid.in",
# mqtt_port=1883,
# mqtt_user="guest",
# mqtt_password="password"
# )
# db.session.add(default_station)
# db.session.commit()
# except Exception as e:
# print(f"FATAL ERROR: Could not connect to PostgreSQL: {e}")
# sys.exit(1)
# mqtt_thread = threading.Thread(target=start_mqtt_clients, daemon=True)
# mqtt_thread.start()
# print(f"Starting Flask-SocketIO server on http://localhost:5000")
# socketio.run(app, host='0.0.0.0', port=5000)
import os
import sys
import threading
import json
import csv
import io
from datetime import datetime
from flask import Flask, jsonify, request, Response
from flask_socketio import SocketIO
from dotenv import load_dotenv
# Import your custom core modules and the new models
from core.mqtt_client import MqttClient
from core.protobuf_decoder import ProtobufDecoder
from models import db, Station, User, MqttLog
# --- Load Environment Variables ---
load_dotenv()
# --- Pre-startup Check for Essential Configuration ---
DATABASE_URL = os.getenv("DATABASE_URL")
if not DATABASE_URL:
print("FATAL ERROR: DATABASE_URL is not set in .env file.")
sys.exit(1)
# --- Application Setup ---
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = DATABASE_URL
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
app.config['SECRET_KEY'] = os.getenv("SECRET_KEY", "a_very_secret_key")
db.init_app(app)
socketio = SocketIO(app, cors_allowed_origins="*")
# --- Global instances ---
decoder = ProtobufDecoder()
mqtt_clients = {}
# --- MQTT Message Handling (UPDATED) ---
def on_message_handler(station_id, topic, payload):
"""
Handles incoming MQTT messages, decodes them, writes to PostgreSQL,
and emits to WebSockets.
"""
print(f"Main handler received message for station {station_id} on topic {topic}")
decoded_data = None
message_type = topic.split('/')[-1]
if message_type == 'PERIODIC':
decoded_data = decoder.decode_periodic(payload)
elif message_type == 'EVENTS':
decoded_data = decoder.decode_event(payload)
elif message_type == 'REQUEST':
decoded_data = decoder.decode_rpc_request(payload)
if decoded_data:
# 1. Write the data to PostgreSQL for historical storage
try:
with app.app_context():
log_entry = MqttLog(
station_id=station_id,
topic=topic,
topic_type=message_type, # <-- Save the new topic_type
payload=decoded_data
)
db.session.add(log_entry)
db.session.commit()
print(f"Successfully wrote data for {station_id} to PostgreSQL.")
except Exception as e:
print(f"Error writing to PostgreSQL: {e}")
# 2. Emit the data to the frontend for real-time view
socketio.emit('dashboard_update', {
'stationId': station_id,
'topic': topic,
'data': decoded_data
}, room=station_id)
# --- (WebSocket and API routes remain the same) ---
@socketio.on('connect')
def handle_connect():
print('Client connected to WebSocket')
# ... (other socketio handlers)
@app.route('/api/stations', methods=['GET'])
def get_stations():
try:
stations = Station.query.all()
return jsonify([{"id": s.station_id, "name": s.name} for s in stations])
except Exception as e:
return jsonify({"error": f"Database query failed: {e}"}), 500
# --- CSV Export route (UPDATED) ---
def _format_periodic_row(payload, num_slots=9):
"""
Flattens a periodic payload dictionary into a single list for a CSV row.
"""
row = [
datetime.fromtimestamp(payload.get("ts")).strftime("%Y-%m-%d %H:%M:%S"),
payload.get("deviceId", ""),
payload.get("stationDiagnosticCode", "")
]
slots_data = payload.get("slotLevelPayload", [])
slot_map = {s.get('slotId', i+1): s for i, s in enumerate(slots_data)}
slot_fields_keys = [
"batteryIdentification", "batteryPresent", "chargerPresent", "doorStatus", "doorLockStatus",
"voltage", "current", "soc", "batteryMaxTemp", "slotTemperature",
"batteryFaultCode", "chargerFaultCode", "batteryMode", "chargerMode"
]
for i in range(1, num_slots + 1):
slot = slot_map.get(i)
if slot:
row.extend([
slot.get('batteryIdentification', ''),
slot.get("batteryPresent", 0),
slot.get("chargerPresent", 0),
slot.get("doorStatus", 0),
slot.get("doorLockStatus", 0),
slot.get('voltage', 0) / 1000.0,
slot.get('current', 0) / 1000.0,
slot.get('soc', 0),
slot.get('batteryMaxTemp', 0) / 10.0,
slot.get('slotTemperature', 0) / 10.0,
slot.get('batteryFaultCode', 0),
slot.get('chargerFaultCode', 0),
slot.get('batteryMode', 0),
slot.get('chargerMode', 0)
])
else:
row.extend([''] * len(slot_fields_keys))
row.append('') # Placeholder for RawHexPayload
return row
@app.route('/api/logs/export', methods=['GET'])
def export_logs():
station_id = request.args.get('station_id')
start_date_str = request.args.get('start_date')
end_date_str = request.args.get('end_date')
log_type = request.args.get('log_type', 'PERIODIC')
if not all([station_id, start_date_str, end_date_str]):
return jsonify({"error": "Missing required parameters: station_id, start_date, end_date"}), 400
try:
start_date = datetime.strptime(start_date_str, '%Y-%m-%d')
end_date = datetime.strptime(end_date_str, '%Y-%m-%d').replace(hour=23, minute=59, second=59)
except ValueError:
return jsonify({"error": "Invalid date format. Use YYYY-MM-DD."}), 400
# UPDATED QUERY: Filter by the new 'topic_type' column for better performance
query = MqttLog.query.filter(
MqttLog.station_id == station_id,
MqttLog.timestamp.between(start_date, end_date),
MqttLog.topic_type == log_type
)
logs = query.order_by(MqttLog.timestamp.asc()).all()
output = io.StringIO()
writer = csv.writer(output)
if log_type == 'PERIODIC':
base_header = ["Timestamp", "DeviceID", "StationDiagnosticCode"]
slot_fields = [
"BatteryID", "BatteryPresent", "ChargerPresent", "DoorStatus", "DoorLockStatus",
"Voltage_V", "Current_A", "SOC_Percent", "BatteryTemp_C", "SlotTemp_C",
"BatteryFaultCode", "ChargerFaultCode", "BatteryMode", "ChargerMode"
]
slot_header = [f"Slot{i}_{field}" for i in range(1, 10) for field in slot_fields]
header = base_header + slot_header + ["RawHexPayload"]
writer.writerow(header)
for log in logs:
writer.writerow(_format_periodic_row(log.payload))
else: # For EVENTS_RPC
header = ["Timestamp", "Topic", "Payload_JSON"]
writer.writerow(header)
for log in logs:
writer.writerow([log.timestamp, log.topic, json.dumps(log.payload)])
output.seek(0)
return Response(
output,
mimetype="text/csv",
headers={"Content-Disposition": f"attachment;filename=logs_{station_id}_{log_type}_{start_date_str}_to_{end_date_str}.csv"}
)
# --- Main Application Logic ---
def start_mqtt_clients():
"""
Initializes and starts an MQTT client for each station found in the database,
using the specific MQTT credentials stored for each station.
"""
try:
with app.app_context():
stations = Station.query.all()
except Exception as e:
print(f"CRITICAL: Could not query stations from the database in MQTT thread: {e}")
return
for station in stations:
if station.station_id not in mqtt_clients:
print(f"Creating and starting MQTT client for station: {station.name} ({station.station_id})")
client = MqttClient(
broker=station.mqtt_broker,
port=station.mqtt_port,
user=station.mqtt_user,
password=station.mqtt_password,
station_id=station.station_id,
on_message_callback=on_message_handler
)
client.start()
mqtt_clients[station.station_id] = client
if __name__ == '__main__':
try:
with app.app_context():
db.create_all()
if not Station.query.first():
print("No stations found. Adding a default station with default MQTT config.")
default_station = Station(
station_id="V16000868210069259709",
name="Test Station 2",
mqtt_broker="mqtt-dev.upgrid.in",
mqtt_port=1883,
mqtt_user="guest",
mqtt_password="password"
)
db.session.add(default_station)
db.session.commit()
except Exception as e:
print(f"FATAL ERROR: Could not connect to PostgreSQL: {e}")
sys.exit(1)
mqtt_thread = threading.Thread(target=start_mqtt_clients, daemon=True)
mqtt_thread.start()
print(f"Starting Flask-SocketIO server on http://localhost:5000")
socketio.run(app, host='0.0.0.0', port=5000)