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