feat(backend): implemented the backend of station dashboard web app
commit
ea1e8a9266
|
|
@ -0,0 +1,15 @@
|
||||||
|
# --- Flask Application Settings ---
|
||||||
|
# This is a secret key used by Flask for session management.
|
||||||
|
# You can generate a new one with: python -c 'import os; print(os.urandom(24).hex())'
|
||||||
|
SECRET_KEY="80473e17c5707e19252ef3736fba32805be21a9b3e914190"
|
||||||
|
|
||||||
|
# --- PostgreSQL Database Connection ---
|
||||||
|
# Replace with your actual database credentials.
|
||||||
|
# Format: postgresql://<user>:<password>@<host>:<port>/<dbname>
|
||||||
|
DATABASE_URL="postgresql://swap_app_user:2004@localhost:5432/swap_station_db"
|
||||||
|
|
||||||
|
# --- MQTT Broker Connection ---
|
||||||
|
MQTT_BROKER="mqtt-dev.upgrid.in"
|
||||||
|
MQTT_PORT="1883"
|
||||||
|
MQTT_USER="guest"
|
||||||
|
MQTT_PASSWORD="password"
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -0,0 +1,75 @@
|
||||||
|
import paho.mqtt.client as mqtt
|
||||||
|
import uuid
|
||||||
|
import time
|
||||||
|
import threading
|
||||||
|
import socket
|
||||||
|
|
||||||
|
class MqttClient:
|
||||||
|
"""
|
||||||
|
Handles the connection and message processing for a single MQTT station.
|
||||||
|
This is a standard Python class, with no GUI dependencies.
|
||||||
|
"""
|
||||||
|
def __init__(self, broker, port, user, password, station_id, on_message_callback):
|
||||||
|
super().__init__()
|
||||||
|
self.broker = broker
|
||||||
|
self.port = port
|
||||||
|
self.user = user
|
||||||
|
self.password = password
|
||||||
|
self.station_id = station_id
|
||||||
|
self.on_message_callback = on_message_callback
|
||||||
|
|
||||||
|
# Generate a unique client ID to prevent connection conflicts
|
||||||
|
unique_id = str(uuid.uuid4())
|
||||||
|
self.client_id = f"WebApp-Backend-{self.station_id}-{unique_id}"
|
||||||
|
|
||||||
|
self.client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, self.client_id)
|
||||||
|
|
||||||
|
# Assign callback functions
|
||||||
|
self.client.on_connect = self.on_connect
|
||||||
|
self.client.on_message = self.on_message
|
||||||
|
self.client.on_disconnect = self.on_disconnect
|
||||||
|
|
||||||
|
if self.user and self.password:
|
||||||
|
self.client.username_pw_set(self.user, self.password)
|
||||||
|
|
||||||
|
def on_connect(self, client, userdata, flags, rc, properties):
|
||||||
|
"""Callback for when the client connects to the broker."""
|
||||||
|
if rc == 0:
|
||||||
|
print(f"Successfully connected to MQTT broker for station: {self.station_id}")
|
||||||
|
# Subscribe to all topics for this station using a wildcard
|
||||||
|
topic_base = f"VEC/batterySmartStation/v100/{self.station_id}/#"
|
||||||
|
self.client.subscribe(topic_base)
|
||||||
|
print(f"Subscribed to: {topic_base}")
|
||||||
|
else:
|
||||||
|
print(f"Failed to connect to MQTT for station {self.station_id}, return code {rc}")
|
||||||
|
|
||||||
|
def on_disconnect(self, client, userdata, rc, properties):
|
||||||
|
"""Callback for when the client disconnects."""
|
||||||
|
print(f"Disconnected from MQTT for station {self.station_id}. Will attempt to reconnect...")
|
||||||
|
# Paho-MQTT's loop_start() handles automatic reconnection.
|
||||||
|
|
||||||
|
def on_message(self, client, userdata, msg):
|
||||||
|
"""Callback for when a message is received from the broker."""
|
||||||
|
try:
|
||||||
|
# Pass the relevant data to the main application's handler
|
||||||
|
self.on_message_callback(self.station_id, msg.topic, msg.payload)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error processing message in callback for topic {msg.topic}: {e}")
|
||||||
|
|
||||||
|
def connect(self):
|
||||||
|
"""Connects the client to the MQTT broker."""
|
||||||
|
print(f"Attempting to connect to {self.broker}:{self.port} with client ID: {self.client_id}")
|
||||||
|
try:
|
||||||
|
self.client.connect(self.broker, self.port, 60)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error connecting to MQTT for station {self.station_id}: {e}")
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
"""Starts the MQTT client's network loop in a separate thread."""
|
||||||
|
self.connect()
|
||||||
|
self.client.loop_start()
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
"""Stops the MQTT client's network loop."""
|
||||||
|
print(f"Stopping MQTT client for station: {self.station_id}")
|
||||||
|
self.client.loop_stop()
|
||||||
|
|
@ -0,0 +1,157 @@
|
||||||
|
import json
|
||||||
|
from google.protobuf.json_format import MessageToDict
|
||||||
|
from google.protobuf.message import DecodeError
|
||||||
|
|
||||||
|
# Import the specific message types from your generated protobuf file
|
||||||
|
# Make sure the path 'proto.your_proto_file_pb2' matches your file structure
|
||||||
|
from proto.vec_payload_chgSt_pb2 import (
|
||||||
|
mainPayload as PeriodicData,
|
||||||
|
eventPayload as EventData,
|
||||||
|
rpcRequest as RpcRequest,
|
||||||
|
eventType_e,
|
||||||
|
jobType_e,
|
||||||
|
languageType_e
|
||||||
|
)
|
||||||
|
|
||||||
|
class ProtobufDecoder:
|
||||||
|
"""
|
||||||
|
Handles the decoding of different Protobuf message types with robust error handling.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def decode_periodic(self, payload: bytes) -> dict | None:
|
||||||
|
"""
|
||||||
|
Decodes a binary payload into a PeriodicData dictionary.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
message = PeriodicData()
|
||||||
|
message.ParseFromString(payload)
|
||||||
|
return MessageToDict(message, preserving_proto_field_name=True)
|
||||||
|
except DecodeError as e:
|
||||||
|
print(f"Error decoding PeriodicData: {e}")
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
print(f"An unexpected error occurred during periodic decoding: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def decode_event(self, payload_bytes: bytes) -> dict | None:
|
||||||
|
"""
|
||||||
|
Decodes an event payload robustly, ensuring the correct eventType is used.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# 1. Standard parsing to get a base dictionary
|
||||||
|
msg = EventData()
|
||||||
|
msg.ParseFromString(payload_bytes)
|
||||||
|
d = MessageToDict(msg, preserving_proto_field_name=True)
|
||||||
|
|
||||||
|
# 2. Manually extract the true enum value from the raw bytes
|
||||||
|
wire_num = self._extract_field3_varint(payload_bytes)
|
||||||
|
wire_name = None
|
||||||
|
if wire_num is not None:
|
||||||
|
try:
|
||||||
|
wire_name = eventType_e.Name(wire_num)
|
||||||
|
except ValueError:
|
||||||
|
wire_name = f"UNKNOWN_ENUM_VALUE_{wire_num}"
|
||||||
|
|
||||||
|
# 3. Always prefer the manually extracted "wire value"
|
||||||
|
if wire_name:
|
||||||
|
d["eventType"] = wire_name
|
||||||
|
|
||||||
|
# 4. Ensure consistent structure with default values
|
||||||
|
ed = d.setdefault("eventData", {})
|
||||||
|
ed.setdefault("nfcData", None)
|
||||||
|
ed.setdefault("batteryIdentification", "")
|
||||||
|
ed.setdefault("activityFailureReason", 0)
|
||||||
|
ed.setdefault("swapAbortReason", "ABORT_UNKNOWN")
|
||||||
|
ed.setdefault("swapTime", 0)
|
||||||
|
ed.setdefault("faultCode", 0)
|
||||||
|
ed.setdefault("doorStatus", 0)
|
||||||
|
ed.setdefault("slotId", 0)
|
||||||
|
|
||||||
|
# 5. Reorder for clean logs and return as a dictionary
|
||||||
|
return {
|
||||||
|
"ts": d.get("ts"),
|
||||||
|
"deviceId": d.get("deviceId"),
|
||||||
|
"eventType": d.get("eventType"),
|
||||||
|
"sessionId": d.get("sessionId"),
|
||||||
|
"eventData": d.get("eventData"),
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
print(f"An unexpected error occurred during event decoding: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def decode_rpc_request(self, payload_bytes: bytes) -> dict | None:
|
||||||
|
"""
|
||||||
|
Decodes an RPC request payload robustly, ensuring the correct jobType is used.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# 1. Standard parsing
|
||||||
|
msg = RpcRequest()
|
||||||
|
msg.ParseFromString(payload_bytes)
|
||||||
|
d = MessageToDict(msg, preserving_proto_field_name=True)
|
||||||
|
|
||||||
|
# 2. Manually extract the true enum value for jobType (field 3)
|
||||||
|
wire_num = self._extract_field3_varint(payload_bytes)
|
||||||
|
wire_name = None
|
||||||
|
if wire_num is not None:
|
||||||
|
try:
|
||||||
|
wire_name = jobType_e.Name(wire_num)
|
||||||
|
except ValueError:
|
||||||
|
wire_name = f"UNKNOWN_ENUM_VALUE_{wire_num}"
|
||||||
|
|
||||||
|
# 3. Prefer the manually extracted value
|
||||||
|
if wire_name:
|
||||||
|
d["jobType"] = wire_name
|
||||||
|
|
||||||
|
# 4. Ensure consistent structure
|
||||||
|
d.setdefault("rpcData", None)
|
||||||
|
d.setdefault("slotInfo", None)
|
||||||
|
|
||||||
|
return d
|
||||||
|
except Exception as e:
|
||||||
|
print(f"An unexpected error occurred during RPC request decoding: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
# --- Helper methods for manual byte parsing ---
|
||||||
|
def _read_varint(self, b: bytes, i: int):
|
||||||
|
"""Helper to read a varint from a raw byte buffer."""
|
||||||
|
shift = 0
|
||||||
|
val = 0
|
||||||
|
while True:
|
||||||
|
if i >= len(b): raise ValueError("truncated varint")
|
||||||
|
c = b[i]
|
||||||
|
i += 1
|
||||||
|
val |= (c & 0x7F) << shift
|
||||||
|
if not (c & 0x80): break
|
||||||
|
shift += 7
|
||||||
|
if shift > 64: raise ValueError("varint too long")
|
||||||
|
return val, i
|
||||||
|
|
||||||
|
def _skip_field(self, b: bytes, i: int, wt: int):
|
||||||
|
"""Helper to skip a field in the buffer based on its wire type."""
|
||||||
|
if wt == 0: # VARINT
|
||||||
|
_, i = self._read_varint(b, i)
|
||||||
|
return i
|
||||||
|
if wt == 1: # 64-BIT
|
||||||
|
return i + 8
|
||||||
|
if wt == 2: # LENGTH-DELIMITED
|
||||||
|
ln, i = self._read_varint(b, i)
|
||||||
|
return i + ln
|
||||||
|
if wt == 5: # 32-BIT
|
||||||
|
return i + 4
|
||||||
|
raise ValueError(f"unsupported wire type to skip: {wt}")
|
||||||
|
|
||||||
|
def _extract_field3_varint(self, b: bytes):
|
||||||
|
"""Manually parses the byte string to find the integer value of field number 3 (e.g., eventType, jobType)."""
|
||||||
|
i = 0
|
||||||
|
n = len(b)
|
||||||
|
while i < n:
|
||||||
|
key, i2 = self._read_varint(b, i)
|
||||||
|
wt = key & 0x7
|
||||||
|
fn = key >> 3
|
||||||
|
i = i2
|
||||||
|
if fn == 3 and wt == 0:
|
||||||
|
v, _ = self._read_varint(b, i)
|
||||||
|
return v
|
||||||
|
i = self._skip_field(b, i, wt)
|
||||||
|
return None
|
||||||
|
|
@ -0,0 +1,436 @@
|
||||||
|
# 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)
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
from flask_sqlalchemy import SQLAlchemy
|
||||||
|
from sqlalchemy.dialects.postgresql import JSONB
|
||||||
|
|
||||||
|
# Create a SQLAlchemy instance. This will be linked to the Flask app in main.py.
|
||||||
|
db = SQLAlchemy()
|
||||||
|
|
||||||
|
class User(db.Model):
|
||||||
|
"""Represents a user in the database."""
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
username = db.Column(db.String(80), unique=True, nullable=False)
|
||||||
|
password_hash = db.Column(db.String(120), nullable=False)
|
||||||
|
|
||||||
|
class Station(db.Model):
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
station_id = db.Column(db.String(120), unique=True, nullable=False)
|
||||||
|
name = db.Column(db.String(120), nullable=True)
|
||||||
|
location = db.Column(db.String(200), nullable=True)
|
||||||
|
|
||||||
|
# --- ADD THESE NEW FIELDS ---
|
||||||
|
mqtt_broker = db.Column(db.String(255), nullable=False)
|
||||||
|
mqtt_port = db.Column(db.Integer, nullable=False)
|
||||||
|
mqtt_user = db.Column(db.String(120), nullable=True)
|
||||||
|
mqtt_password = db.Column(db.String(120), nullable=True)
|
||||||
|
|
||||||
|
# --- Table for MQTT Logs (without raw payload) ---
|
||||||
|
class MqttLog(db.Model):
|
||||||
|
"""Represents a single MQTT message payload for historical logging."""
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
timestamp = db.Column(db.DateTime, server_default=db.func.now())
|
||||||
|
station_id = db.Column(db.String(120), nullable=False, index=True)
|
||||||
|
topic = db.Column(db.String(255), nullable=False)
|
||||||
|
# --- NEW: Added topic_type for efficient filtering ---
|
||||||
|
topic_type = db.Column(db.String(50), nullable=False, index=True)
|
||||||
|
# JSONB is a highly efficient way to store JSON data in PostgreSQL
|
||||||
|
payload = db.Column(JSONB)
|
||||||
Binary file not shown.
Binary file not shown.
|
|
@ -0,0 +1,162 @@
|
||||||
|
syntax = "proto2";
|
||||||
|
|
||||||
|
enum eventType_e {
|
||||||
|
EVENT_SWAP_START = 0x200;
|
||||||
|
EVENT_BATTERY_ENTRY = 0x201;
|
||||||
|
EVENT_BATTERY_EXIT = 0x202;
|
||||||
|
EVENT_ACTIVITY_FAILED = 0x203;
|
||||||
|
EVENT_SWAP_ABORTED = 0x204;
|
||||||
|
EVENT_BATFAULT_ALARM = 0x205;
|
||||||
|
EVENT_SLOT_LOCK_ENEGAGED = 0x206;
|
||||||
|
EVENT_SWAP_ENDED = 0x207;
|
||||||
|
EVENT_CHGFAULT_ALARM = 0x208;
|
||||||
|
EVENT_NFC_SCAN = 0x209;
|
||||||
|
EVENT_SLOT_LOCK_DISENEGAGED = 0x20A;
|
||||||
|
EVENT_REVERSE_SWAP = 0x20B;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum jobType_e {
|
||||||
|
JOBTYPE_NONE = 0;
|
||||||
|
JOBTYPE_GET_STATUS_OF_A_JOB = 0x01;
|
||||||
|
JOBTYPE_SWAP_START = 0x100;
|
||||||
|
JOBTYPE_CHARGER_ENABLE_DISABLE = 0x101;
|
||||||
|
JOBTYPE_GATE_OPEN_CLOSE = 0x102;
|
||||||
|
JOBTYPE_TRANSACTION_ABORT = 0x103;
|
||||||
|
JOBTYPE_REBOOT = 0x104;
|
||||||
|
JOBTYPE_SWAP_DENY = 0x105;
|
||||||
|
JOBTYPE_LANGUAGE_UPDATE = 0x106;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum jobResult_e {
|
||||||
|
JOB_RESULT_UNKNOWN = 0;
|
||||||
|
JOB_RESULT_SUCCESS = 1;
|
||||||
|
JOB_RESULT_REJECTED = 2;
|
||||||
|
JOB_RESULT_TIMEOUT = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum jobStatus_e {
|
||||||
|
JOB_STATUS_IDLE = 0;
|
||||||
|
JOB_STATUS_PENDING = 1;
|
||||||
|
JOB_STATUS_EXECUTING = 2;
|
||||||
|
JOB_STATUS_EXECUTED = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message slotLevelPayload{
|
||||||
|
optional uint32 batteryPresent = 1;
|
||||||
|
optional uint32 chargerPresent = 2;
|
||||||
|
optional uint32 doorLockStatus = 3;
|
||||||
|
optional uint32 doorStatus = 4;
|
||||||
|
optional uint32 voltage = 5;
|
||||||
|
optional int32 current = 6;
|
||||||
|
optional uint32 batteryFaultCode = 7;
|
||||||
|
optional uint32 chargerFaultCode = 8;
|
||||||
|
optional int32 batteryMaxTemp = 9;
|
||||||
|
optional int32 chargerMaxTemp= 10;
|
||||||
|
optional string batteryIdentification = 11;
|
||||||
|
optional uint32 batteryMode = 12;
|
||||||
|
optional uint32 chargerMode = 13;
|
||||||
|
optional int32 slotTemperature = 14;
|
||||||
|
optional uint32 gasSensor = 15;
|
||||||
|
optional uint32 soc=16;
|
||||||
|
optional uint32 ts = 17;
|
||||||
|
}
|
||||||
|
|
||||||
|
message mainPayload{
|
||||||
|
required uint32 ts = 1;
|
||||||
|
required string deviceId = 2;
|
||||||
|
required string sessionId = 3;
|
||||||
|
repeated slotLevelPayload slotLevelPayload = 4;
|
||||||
|
optional uint32 backupSupplyStatus = 5;
|
||||||
|
repeated uint32 switchStatus = 6;
|
||||||
|
optional uint32 stationStatus = 7;
|
||||||
|
optional uint32 stationDiagnosticCode = 8;
|
||||||
|
repeated float coordinates = 9;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum swapAbortReason_e{
|
||||||
|
ABORT_UNKNOWN=0;
|
||||||
|
ABORT_BAT_EXIT_TIMEOUT=1;
|
||||||
|
ABORT_BAT_ENTRY_TIMEOUT=2;
|
||||||
|
ABORT_DOOR_CLOSE_TIMEOUT=3;
|
||||||
|
ABORT_DOOR_OPEN_TIMEOUT=4;
|
||||||
|
ABORT_INVALID_PARAM=5;
|
||||||
|
ABORT_REMOTE_REQUESTED=6;
|
||||||
|
ABORT_INVALID_BATTERY=7;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum swapDenyReason_e{
|
||||||
|
SWAP_DENY_INSUFFICIENT_BAL=1;
|
||||||
|
SWAP_DENY_INVALID_NFC=2;
|
||||||
|
SWAP_DENY_BATTERY_UNAVAILABLE=3;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum languageType_e{
|
||||||
|
LANGUAGE_TYPE_ENGLISH = 1;
|
||||||
|
LANGUAGE_TYPE_HINDI = 2;
|
||||||
|
LANGUAGE_TYPE_KANNADA = 3;
|
||||||
|
LANGUAGE_TYPE_TELUGU = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message nfcPayload_s{
|
||||||
|
required string manufacturingData = 1;
|
||||||
|
required string customData = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message eventData_s {
|
||||||
|
optional nfcPayload_s nfcData = 1;
|
||||||
|
optional string batteryIdentification = 2;
|
||||||
|
optional uint32 activityFailureReason = 3;
|
||||||
|
optional swapAbortReason_e swapAbortReason = 4;
|
||||||
|
optional uint32 swapTime = 5;
|
||||||
|
optional uint32 faultCode = 6;
|
||||||
|
optional uint32 doorStatus = 7;
|
||||||
|
optional uint32 slotId = 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
message eventPayload {
|
||||||
|
required uint32 ts = 1;
|
||||||
|
required string deviceId = 2;
|
||||||
|
required eventType_e eventType = 3;
|
||||||
|
required string sessionId = 4;
|
||||||
|
optional eventData_s eventData = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
message rpcData_s {
|
||||||
|
optional string sessionId = 1;
|
||||||
|
repeated uint32 slotsData = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message slotControl_s {
|
||||||
|
required uint32 slotId = 1;
|
||||||
|
required uint32 state = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message getJobStatusByJobId_s{
|
||||||
|
required string jobId = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message rpcRequest {
|
||||||
|
required uint32 ts = 1;
|
||||||
|
required string jobId = 2;
|
||||||
|
required jobType_e jobType = 3;
|
||||||
|
optional rpcData_s rpcData = 4;
|
||||||
|
optional slotControl_s slotInfo = 5;
|
||||||
|
optional swapDenyReason_e swapDeny = 8;
|
||||||
|
optional getJobStatusByJobId_s getJobStatusByJobId = 9;
|
||||||
|
optional languageType_e languageType = 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
message jobStatusByJobIdResponse_s{
|
||||||
|
required string jobId = 1;
|
||||||
|
required jobStatus_e jobStatus = 2;
|
||||||
|
required jobResult_e jobResult = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message rpcResponse {
|
||||||
|
required uint32 ts = 1;
|
||||||
|
required string deviceId = 2;
|
||||||
|
required string jobId = 3;
|
||||||
|
required jobStatus_e jobStatus = 4;
|
||||||
|
required jobResult_e jobResult = 5;
|
||||||
|
optional jobStatusByJobIdResponse_s jobStatusByJobIdResponse = 6;
|
||||||
|
}
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1,287 @@
|
||||||
|
from google.protobuf.internal import containers as _containers
|
||||||
|
from google.protobuf.internal import enum_type_wrapper as _enum_type_wrapper
|
||||||
|
from google.protobuf import descriptor as _descriptor
|
||||||
|
from google.protobuf import message as _message
|
||||||
|
from collections.abc import Iterable as _Iterable, Mapping as _Mapping
|
||||||
|
from typing import ClassVar as _ClassVar, Optional as _Optional, Union as _Union
|
||||||
|
|
||||||
|
DESCRIPTOR: _descriptor.FileDescriptor
|
||||||
|
|
||||||
|
class eventType_e(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
|
||||||
|
__slots__ = ()
|
||||||
|
EVENT_SWAP_START: _ClassVar[eventType_e]
|
||||||
|
EVENT_BATTERY_ENTRY: _ClassVar[eventType_e]
|
||||||
|
EVENT_BATTERY_EXIT: _ClassVar[eventType_e]
|
||||||
|
EVENT_ACTIVITY_FAILED: _ClassVar[eventType_e]
|
||||||
|
EVENT_SWAP_ABORTED: _ClassVar[eventType_e]
|
||||||
|
EVENT_BATFAULT_ALARM: _ClassVar[eventType_e]
|
||||||
|
EVENT_SLOT_LOCK_ENEGAGED: _ClassVar[eventType_e]
|
||||||
|
EVENT_SWAP_ENDED: _ClassVar[eventType_e]
|
||||||
|
EVENT_CHGFAULT_ALARM: _ClassVar[eventType_e]
|
||||||
|
EVENT_NFC_SCAN: _ClassVar[eventType_e]
|
||||||
|
EVENT_SLOT_LOCK_DISENEGAGED: _ClassVar[eventType_e]
|
||||||
|
EVENT_REVERSE_SWAP: _ClassVar[eventType_e]
|
||||||
|
|
||||||
|
class jobType_e(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
|
||||||
|
__slots__ = ()
|
||||||
|
JOBTYPE_NONE: _ClassVar[jobType_e]
|
||||||
|
JOBTYPE_GET_STATUS_OF_A_JOB: _ClassVar[jobType_e]
|
||||||
|
JOBTYPE_SWAP_START: _ClassVar[jobType_e]
|
||||||
|
JOBTYPE_CHARGER_ENABLE_DISABLE: _ClassVar[jobType_e]
|
||||||
|
JOBTYPE_GATE_OPEN_CLOSE: _ClassVar[jobType_e]
|
||||||
|
JOBTYPE_TRANSACTION_ABORT: _ClassVar[jobType_e]
|
||||||
|
JOBTYPE_REBOOT: _ClassVar[jobType_e]
|
||||||
|
JOBTYPE_SWAP_DENY: _ClassVar[jobType_e]
|
||||||
|
JOBTYPE_LANGUAGE_UPDATE: _ClassVar[jobType_e]
|
||||||
|
|
||||||
|
class jobResult_e(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
|
||||||
|
__slots__ = ()
|
||||||
|
JOB_RESULT_UNKNOWN: _ClassVar[jobResult_e]
|
||||||
|
JOB_RESULT_SUCCESS: _ClassVar[jobResult_e]
|
||||||
|
JOB_RESULT_REJECTED: _ClassVar[jobResult_e]
|
||||||
|
JOB_RESULT_TIMEOUT: _ClassVar[jobResult_e]
|
||||||
|
|
||||||
|
class jobStatus_e(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
|
||||||
|
__slots__ = ()
|
||||||
|
JOB_STATUS_IDLE: _ClassVar[jobStatus_e]
|
||||||
|
JOB_STATUS_PENDING: _ClassVar[jobStatus_e]
|
||||||
|
JOB_STATUS_EXECUTING: _ClassVar[jobStatus_e]
|
||||||
|
JOB_STATUS_EXECUTED: _ClassVar[jobStatus_e]
|
||||||
|
|
||||||
|
class swapAbortReason_e(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
|
||||||
|
__slots__ = ()
|
||||||
|
ABORT_UNKNOWN: _ClassVar[swapAbortReason_e]
|
||||||
|
ABORT_BAT_EXIT_TIMEOUT: _ClassVar[swapAbortReason_e]
|
||||||
|
ABORT_BAT_ENTRY_TIMEOUT: _ClassVar[swapAbortReason_e]
|
||||||
|
ABORT_DOOR_CLOSE_TIMEOUT: _ClassVar[swapAbortReason_e]
|
||||||
|
ABORT_DOOR_OPEN_TIMEOUT: _ClassVar[swapAbortReason_e]
|
||||||
|
ABORT_INVALID_PARAM: _ClassVar[swapAbortReason_e]
|
||||||
|
ABORT_REMOTE_REQUESTED: _ClassVar[swapAbortReason_e]
|
||||||
|
ABORT_INVALID_BATTERY: _ClassVar[swapAbortReason_e]
|
||||||
|
|
||||||
|
class swapDenyReason_e(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
|
||||||
|
__slots__ = ()
|
||||||
|
SWAP_DENY_INSUFFICIENT_BAL: _ClassVar[swapDenyReason_e]
|
||||||
|
SWAP_DENY_INVALID_NFC: _ClassVar[swapDenyReason_e]
|
||||||
|
SWAP_DENY_BATTERY_UNAVAILABLE: _ClassVar[swapDenyReason_e]
|
||||||
|
|
||||||
|
class languageType_e(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
|
||||||
|
__slots__ = ()
|
||||||
|
LANGUAGE_TYPE_ENGLISH: _ClassVar[languageType_e]
|
||||||
|
LANGUAGE_TYPE_HINDI: _ClassVar[languageType_e]
|
||||||
|
LANGUAGE_TYPE_KANNADA: _ClassVar[languageType_e]
|
||||||
|
LANGUAGE_TYPE_TELUGU: _ClassVar[languageType_e]
|
||||||
|
EVENT_SWAP_START: eventType_e
|
||||||
|
EVENT_BATTERY_ENTRY: eventType_e
|
||||||
|
EVENT_BATTERY_EXIT: eventType_e
|
||||||
|
EVENT_ACTIVITY_FAILED: eventType_e
|
||||||
|
EVENT_SWAP_ABORTED: eventType_e
|
||||||
|
EVENT_BATFAULT_ALARM: eventType_e
|
||||||
|
EVENT_SLOT_LOCK_ENEGAGED: eventType_e
|
||||||
|
EVENT_SWAP_ENDED: eventType_e
|
||||||
|
EVENT_CHGFAULT_ALARM: eventType_e
|
||||||
|
EVENT_NFC_SCAN: eventType_e
|
||||||
|
EVENT_SLOT_LOCK_DISENEGAGED: eventType_e
|
||||||
|
EVENT_REVERSE_SWAP: eventType_e
|
||||||
|
JOBTYPE_NONE: jobType_e
|
||||||
|
JOBTYPE_GET_STATUS_OF_A_JOB: jobType_e
|
||||||
|
JOBTYPE_SWAP_START: jobType_e
|
||||||
|
JOBTYPE_CHARGER_ENABLE_DISABLE: jobType_e
|
||||||
|
JOBTYPE_GATE_OPEN_CLOSE: jobType_e
|
||||||
|
JOBTYPE_TRANSACTION_ABORT: jobType_e
|
||||||
|
JOBTYPE_REBOOT: jobType_e
|
||||||
|
JOBTYPE_SWAP_DENY: jobType_e
|
||||||
|
JOBTYPE_LANGUAGE_UPDATE: jobType_e
|
||||||
|
JOB_RESULT_UNKNOWN: jobResult_e
|
||||||
|
JOB_RESULT_SUCCESS: jobResult_e
|
||||||
|
JOB_RESULT_REJECTED: jobResult_e
|
||||||
|
JOB_RESULT_TIMEOUT: jobResult_e
|
||||||
|
JOB_STATUS_IDLE: jobStatus_e
|
||||||
|
JOB_STATUS_PENDING: jobStatus_e
|
||||||
|
JOB_STATUS_EXECUTING: jobStatus_e
|
||||||
|
JOB_STATUS_EXECUTED: jobStatus_e
|
||||||
|
ABORT_UNKNOWN: swapAbortReason_e
|
||||||
|
ABORT_BAT_EXIT_TIMEOUT: swapAbortReason_e
|
||||||
|
ABORT_BAT_ENTRY_TIMEOUT: swapAbortReason_e
|
||||||
|
ABORT_DOOR_CLOSE_TIMEOUT: swapAbortReason_e
|
||||||
|
ABORT_DOOR_OPEN_TIMEOUT: swapAbortReason_e
|
||||||
|
ABORT_INVALID_PARAM: swapAbortReason_e
|
||||||
|
ABORT_REMOTE_REQUESTED: swapAbortReason_e
|
||||||
|
ABORT_INVALID_BATTERY: swapAbortReason_e
|
||||||
|
SWAP_DENY_INSUFFICIENT_BAL: swapDenyReason_e
|
||||||
|
SWAP_DENY_INVALID_NFC: swapDenyReason_e
|
||||||
|
SWAP_DENY_BATTERY_UNAVAILABLE: swapDenyReason_e
|
||||||
|
LANGUAGE_TYPE_ENGLISH: languageType_e
|
||||||
|
LANGUAGE_TYPE_HINDI: languageType_e
|
||||||
|
LANGUAGE_TYPE_KANNADA: languageType_e
|
||||||
|
LANGUAGE_TYPE_TELUGU: languageType_e
|
||||||
|
|
||||||
|
class slotLevelPayload(_message.Message):
|
||||||
|
__slots__ = ("batteryPresent", "chargerPresent", "doorLockStatus", "doorStatus", "voltage", "current", "batteryFaultCode", "chargerFaultCode", "batteryMaxTemp", "chargerMaxTemp", "batteryIdentification", "batteryMode", "chargerMode", "slotTemperature", "gasSensor", "soc", "ts")
|
||||||
|
BATTERYPRESENT_FIELD_NUMBER: _ClassVar[int]
|
||||||
|
CHARGERPRESENT_FIELD_NUMBER: _ClassVar[int]
|
||||||
|
DOORLOCKSTATUS_FIELD_NUMBER: _ClassVar[int]
|
||||||
|
DOORSTATUS_FIELD_NUMBER: _ClassVar[int]
|
||||||
|
VOLTAGE_FIELD_NUMBER: _ClassVar[int]
|
||||||
|
CURRENT_FIELD_NUMBER: _ClassVar[int]
|
||||||
|
BATTERYFAULTCODE_FIELD_NUMBER: _ClassVar[int]
|
||||||
|
CHARGERFAULTCODE_FIELD_NUMBER: _ClassVar[int]
|
||||||
|
BATTERYMAXTEMP_FIELD_NUMBER: _ClassVar[int]
|
||||||
|
CHARGERMAXTEMP_FIELD_NUMBER: _ClassVar[int]
|
||||||
|
BATTERYIDENTIFICATION_FIELD_NUMBER: _ClassVar[int]
|
||||||
|
BATTERYMODE_FIELD_NUMBER: _ClassVar[int]
|
||||||
|
CHARGERMODE_FIELD_NUMBER: _ClassVar[int]
|
||||||
|
SLOTTEMPERATURE_FIELD_NUMBER: _ClassVar[int]
|
||||||
|
GASSENSOR_FIELD_NUMBER: _ClassVar[int]
|
||||||
|
SOC_FIELD_NUMBER: _ClassVar[int]
|
||||||
|
TS_FIELD_NUMBER: _ClassVar[int]
|
||||||
|
batteryPresent: int
|
||||||
|
chargerPresent: int
|
||||||
|
doorLockStatus: int
|
||||||
|
doorStatus: int
|
||||||
|
voltage: int
|
||||||
|
current: int
|
||||||
|
batteryFaultCode: int
|
||||||
|
chargerFaultCode: int
|
||||||
|
batteryMaxTemp: int
|
||||||
|
chargerMaxTemp: int
|
||||||
|
batteryIdentification: str
|
||||||
|
batteryMode: int
|
||||||
|
chargerMode: int
|
||||||
|
slotTemperature: int
|
||||||
|
gasSensor: int
|
||||||
|
soc: int
|
||||||
|
ts: int
|
||||||
|
def __init__(self, batteryPresent: _Optional[int] = ..., chargerPresent: _Optional[int] = ..., doorLockStatus: _Optional[int] = ..., doorStatus: _Optional[int] = ..., voltage: _Optional[int] = ..., current: _Optional[int] = ..., batteryFaultCode: _Optional[int] = ..., chargerFaultCode: _Optional[int] = ..., batteryMaxTemp: _Optional[int] = ..., chargerMaxTemp: _Optional[int] = ..., batteryIdentification: _Optional[str] = ..., batteryMode: _Optional[int] = ..., chargerMode: _Optional[int] = ..., slotTemperature: _Optional[int] = ..., gasSensor: _Optional[int] = ..., soc: _Optional[int] = ..., ts: _Optional[int] = ...) -> None: ...
|
||||||
|
|
||||||
|
class mainPayload(_message.Message):
|
||||||
|
__slots__ = ("ts", "deviceId", "sessionId", "slotLevelPayload", "backupSupplyStatus", "switchStatus", "stationStatus", "stationDiagnosticCode", "coordinates")
|
||||||
|
TS_FIELD_NUMBER: _ClassVar[int]
|
||||||
|
DEVICEID_FIELD_NUMBER: _ClassVar[int]
|
||||||
|
SESSIONID_FIELD_NUMBER: _ClassVar[int]
|
||||||
|
SLOTLEVELPAYLOAD_FIELD_NUMBER: _ClassVar[int]
|
||||||
|
BACKUPSUPPLYSTATUS_FIELD_NUMBER: _ClassVar[int]
|
||||||
|
SWITCHSTATUS_FIELD_NUMBER: _ClassVar[int]
|
||||||
|
STATIONSTATUS_FIELD_NUMBER: _ClassVar[int]
|
||||||
|
STATIONDIAGNOSTICCODE_FIELD_NUMBER: _ClassVar[int]
|
||||||
|
COORDINATES_FIELD_NUMBER: _ClassVar[int]
|
||||||
|
ts: int
|
||||||
|
deviceId: str
|
||||||
|
sessionId: str
|
||||||
|
slotLevelPayload: _containers.RepeatedCompositeFieldContainer[slotLevelPayload]
|
||||||
|
backupSupplyStatus: int
|
||||||
|
switchStatus: _containers.RepeatedScalarFieldContainer[int]
|
||||||
|
stationStatus: int
|
||||||
|
stationDiagnosticCode: int
|
||||||
|
coordinates: _containers.RepeatedScalarFieldContainer[float]
|
||||||
|
def __init__(self, ts: _Optional[int] = ..., deviceId: _Optional[str] = ..., sessionId: _Optional[str] = ..., slotLevelPayload: _Optional[_Iterable[_Union[slotLevelPayload, _Mapping]]] = ..., backupSupplyStatus: _Optional[int] = ..., switchStatus: _Optional[_Iterable[int]] = ..., stationStatus: _Optional[int] = ..., stationDiagnosticCode: _Optional[int] = ..., coordinates: _Optional[_Iterable[float]] = ...) -> None: ...
|
||||||
|
|
||||||
|
class nfcPayload_s(_message.Message):
|
||||||
|
__slots__ = ("manufacturingData", "customData")
|
||||||
|
MANUFACTURINGDATA_FIELD_NUMBER: _ClassVar[int]
|
||||||
|
CUSTOMDATA_FIELD_NUMBER: _ClassVar[int]
|
||||||
|
manufacturingData: str
|
||||||
|
customData: str
|
||||||
|
def __init__(self, manufacturingData: _Optional[str] = ..., customData: _Optional[str] = ...) -> None: ...
|
||||||
|
|
||||||
|
class eventData_s(_message.Message):
|
||||||
|
__slots__ = ("nfcData", "batteryIdentification", "activityFailureReason", "swapAbortReason", "swapTime", "faultCode", "doorStatus", "slotId")
|
||||||
|
NFCDATA_FIELD_NUMBER: _ClassVar[int]
|
||||||
|
BATTERYIDENTIFICATION_FIELD_NUMBER: _ClassVar[int]
|
||||||
|
ACTIVITYFAILUREREASON_FIELD_NUMBER: _ClassVar[int]
|
||||||
|
SWAPABORTREASON_FIELD_NUMBER: _ClassVar[int]
|
||||||
|
SWAPTIME_FIELD_NUMBER: _ClassVar[int]
|
||||||
|
FAULTCODE_FIELD_NUMBER: _ClassVar[int]
|
||||||
|
DOORSTATUS_FIELD_NUMBER: _ClassVar[int]
|
||||||
|
SLOTID_FIELD_NUMBER: _ClassVar[int]
|
||||||
|
nfcData: nfcPayload_s
|
||||||
|
batteryIdentification: str
|
||||||
|
activityFailureReason: int
|
||||||
|
swapAbortReason: swapAbortReason_e
|
||||||
|
swapTime: int
|
||||||
|
faultCode: int
|
||||||
|
doorStatus: int
|
||||||
|
slotId: int
|
||||||
|
def __init__(self, nfcData: _Optional[_Union[nfcPayload_s, _Mapping]] = ..., batteryIdentification: _Optional[str] = ..., activityFailureReason: _Optional[int] = ..., swapAbortReason: _Optional[_Union[swapAbortReason_e, str]] = ..., swapTime: _Optional[int] = ..., faultCode: _Optional[int] = ..., doorStatus: _Optional[int] = ..., slotId: _Optional[int] = ...) -> None: ...
|
||||||
|
|
||||||
|
class eventPayload(_message.Message):
|
||||||
|
__slots__ = ("ts", "deviceId", "eventType", "sessionId", "eventData")
|
||||||
|
TS_FIELD_NUMBER: _ClassVar[int]
|
||||||
|
DEVICEID_FIELD_NUMBER: _ClassVar[int]
|
||||||
|
EVENTTYPE_FIELD_NUMBER: _ClassVar[int]
|
||||||
|
SESSIONID_FIELD_NUMBER: _ClassVar[int]
|
||||||
|
EVENTDATA_FIELD_NUMBER: _ClassVar[int]
|
||||||
|
ts: int
|
||||||
|
deviceId: str
|
||||||
|
eventType: eventType_e
|
||||||
|
sessionId: str
|
||||||
|
eventData: eventData_s
|
||||||
|
def __init__(self, ts: _Optional[int] = ..., deviceId: _Optional[str] = ..., eventType: _Optional[_Union[eventType_e, str]] = ..., sessionId: _Optional[str] = ..., eventData: _Optional[_Union[eventData_s, _Mapping]] = ...) -> None: ...
|
||||||
|
|
||||||
|
class rpcData_s(_message.Message):
|
||||||
|
__slots__ = ("sessionId", "slotsData")
|
||||||
|
SESSIONID_FIELD_NUMBER: _ClassVar[int]
|
||||||
|
SLOTSDATA_FIELD_NUMBER: _ClassVar[int]
|
||||||
|
sessionId: str
|
||||||
|
slotsData: _containers.RepeatedScalarFieldContainer[int]
|
||||||
|
def __init__(self, sessionId: _Optional[str] = ..., slotsData: _Optional[_Iterable[int]] = ...) -> None: ...
|
||||||
|
|
||||||
|
class slotControl_s(_message.Message):
|
||||||
|
__slots__ = ("slotId", "state")
|
||||||
|
SLOTID_FIELD_NUMBER: _ClassVar[int]
|
||||||
|
STATE_FIELD_NUMBER: _ClassVar[int]
|
||||||
|
slotId: int
|
||||||
|
state: int
|
||||||
|
def __init__(self, slotId: _Optional[int] = ..., state: _Optional[int] = ...) -> None: ...
|
||||||
|
|
||||||
|
class getJobStatusByJobId_s(_message.Message):
|
||||||
|
__slots__ = ("jobId",)
|
||||||
|
JOBID_FIELD_NUMBER: _ClassVar[int]
|
||||||
|
jobId: str
|
||||||
|
def __init__(self, jobId: _Optional[str] = ...) -> None: ...
|
||||||
|
|
||||||
|
class rpcRequest(_message.Message):
|
||||||
|
__slots__ = ("ts", "jobId", "jobType", "rpcData", "slotInfo", "swapDeny", "getJobStatusByJobId", "languageType")
|
||||||
|
TS_FIELD_NUMBER: _ClassVar[int]
|
||||||
|
JOBID_FIELD_NUMBER: _ClassVar[int]
|
||||||
|
JOBTYPE_FIELD_NUMBER: _ClassVar[int]
|
||||||
|
RPCDATA_FIELD_NUMBER: _ClassVar[int]
|
||||||
|
SLOTINFO_FIELD_NUMBER: _ClassVar[int]
|
||||||
|
SWAPDENY_FIELD_NUMBER: _ClassVar[int]
|
||||||
|
GETJOBSTATUSBYJOBID_FIELD_NUMBER: _ClassVar[int]
|
||||||
|
LANGUAGETYPE_FIELD_NUMBER: _ClassVar[int]
|
||||||
|
ts: int
|
||||||
|
jobId: str
|
||||||
|
jobType: jobType_e
|
||||||
|
rpcData: rpcData_s
|
||||||
|
slotInfo: slotControl_s
|
||||||
|
swapDeny: swapDenyReason_e
|
||||||
|
getJobStatusByJobId: getJobStatusByJobId_s
|
||||||
|
languageType: languageType_e
|
||||||
|
def __init__(self, ts: _Optional[int] = ..., jobId: _Optional[str] = ..., jobType: _Optional[_Union[jobType_e, str]] = ..., rpcData: _Optional[_Union[rpcData_s, _Mapping]] = ..., slotInfo: _Optional[_Union[slotControl_s, _Mapping]] = ..., swapDeny: _Optional[_Union[swapDenyReason_e, str]] = ..., getJobStatusByJobId: _Optional[_Union[getJobStatusByJobId_s, _Mapping]] = ..., languageType: _Optional[_Union[languageType_e, str]] = ...) -> None: ...
|
||||||
|
|
||||||
|
class jobStatusByJobIdResponse_s(_message.Message):
|
||||||
|
__slots__ = ("jobId", "jobStatus", "jobResult")
|
||||||
|
JOBID_FIELD_NUMBER: _ClassVar[int]
|
||||||
|
JOBSTATUS_FIELD_NUMBER: _ClassVar[int]
|
||||||
|
JOBRESULT_FIELD_NUMBER: _ClassVar[int]
|
||||||
|
jobId: str
|
||||||
|
jobStatus: jobStatus_e
|
||||||
|
jobResult: jobResult_e
|
||||||
|
def __init__(self, jobId: _Optional[str] = ..., jobStatus: _Optional[_Union[jobStatus_e, str]] = ..., jobResult: _Optional[_Union[jobResult_e, str]] = ...) -> None: ...
|
||||||
|
|
||||||
|
class rpcResponse(_message.Message):
|
||||||
|
__slots__ = ("ts", "deviceId", "jobId", "jobStatus", "jobResult", "jobStatusByJobIdResponse")
|
||||||
|
TS_FIELD_NUMBER: _ClassVar[int]
|
||||||
|
DEVICEID_FIELD_NUMBER: _ClassVar[int]
|
||||||
|
JOBID_FIELD_NUMBER: _ClassVar[int]
|
||||||
|
JOBSTATUS_FIELD_NUMBER: _ClassVar[int]
|
||||||
|
JOBRESULT_FIELD_NUMBER: _ClassVar[int]
|
||||||
|
JOBSTATUSBYJOBIDRESPONSE_FIELD_NUMBER: _ClassVar[int]
|
||||||
|
ts: int
|
||||||
|
deviceId: str
|
||||||
|
jobId: str
|
||||||
|
jobStatus: jobStatus_e
|
||||||
|
jobResult: jobResult_e
|
||||||
|
jobStatusByJobIdResponse: jobStatusByJobIdResponse_s
|
||||||
|
def __init__(self, ts: _Optional[int] = ..., deviceId: _Optional[str] = ..., jobId: _Optional[str] = ..., jobStatus: _Optional[_Union[jobStatus_e, str]] = ..., jobResult: _Optional[_Union[jobResult_e, str]] = ..., jobStatusByJobIdResponse: _Optional[_Union[jobStatusByJobIdResponse_s, _Mapping]] = ...) -> None: ...
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
Flask
|
||||||
|
Flask-SocketIO
|
||||||
|
Flask-SQLAlchemy
|
||||||
|
psycopg2-binary
|
||||||
|
paho-mqtt
|
||||||
|
protobuf
|
||||||
|
python-dotenv
|
||||||
Loading…
Reference in New Issue