Files
FCI_WaterMaker_API/watermaker_plc_api/controllers/system_controller.py
2025-06-08 15:53:25 +00:00

359 lines
14 KiB
Python

"""
System controller for status, configuration, and general API endpoints.
"""
from flask import Blueprint, jsonify, request
from datetime import datetime
from ..config import Config
from ..services.data_cache import get_data_cache
from ..services.plc_connection import get_plc_connection
from ..services.register_reader import RegisterReader
from ..services.register_writer import RegisterWriter
from ..utils.logger import get_logger
from ..utils.error_handler import create_error_response, create_success_response, RegisterWriteError, PLCConnectionError
logger = get_logger(__name__)
# Create blueprint
system_bp = Blueprint('system', __name__)
# Initialize services
cache = get_data_cache()
plc = get_plc_connection()
reader = RegisterReader()
writer = RegisterWriter()
@system_bp.route('/status')
def get_status():
"""Get connection and system status"""
plc_status = plc.get_connection_status()
return jsonify({
"connection_status": cache.get_connection_status(),
"last_update": cache.get_last_update(),
"plc_config": {
"ip": plc_status["ip_address"],
"port": plc_status["port"],
"connected": plc_status["connected"]
},
"timestamp": datetime.now().isoformat()
})
@system_bp.route('/all')
def get_all_data():
"""Get all PLC data in one response"""
all_data = cache.get_all_data()
summary = cache.get_summary_stats()
return jsonify({
"status": {
"connection_status": all_data["connection_status"],
"last_update": all_data["last_update"],
"connected": plc.is_connected
},
"sensors": all_data["sensors"],
"timers": all_data["timers"],
"rtc": all_data["rtc"],
"outputs": all_data["outputs"],
"runtime": all_data["runtime"],
"water_counters": all_data["water_counters"],
"summary": summary
})
@system_bp.route('/select')
def get_selected_data():
"""Get only selected variables by groups and/or keys to reduce bandwidth and PLC traffic"""
# Get query parameters
groups_param = request.args.get('groups', '')
keys_param = request.args.get('keys', '')
# Parse groups and keys
requested_groups = [g.strip() for g in groups_param.split(',') if g.strip()] if groups_param else []
requested_keys = [k.strip() for k in keys_param.split(',') if k.strip()] if keys_param else []
if not requested_groups and not requested_keys:
return create_error_response(
"Bad Request",
"Must specify either 'groups' or 'keys' parameter",
400,
{
"usage": {
"groups": "Comma-separated list: system,pressure,temperature,flow,quality,fwf_timer,dts_timer,rtc,outputs,runtime,water_counters",
"keys": "Comma-separated list of register numbers: 1000,1003,1017,136,138,5014,5024",
"examples": [
"/api/select?groups=temperature,pressure",
"/api/select?keys=1036,1003,1017",
"/api/select?groups=dts_timer&keys=1036",
"/api/select?groups=runtime,water_counters"
]
}
}
)
# Check PLC connection
if not plc.is_connected:
if not plc.connect():
return create_error_response(
"Service Unavailable",
"PLC connection failed",
503,
{"connection_status": cache.get_connection_status()}
)
try:
# Read selective data
result = reader.read_selective_data(requested_groups, requested_keys)
result["timestamp"] = datetime.now().isoformat()
return jsonify(result)
except Exception as e:
logger.error(f"Error reading selective data: {e}")
return create_error_response(
"Internal Server Error",
f"Failed to read selective data: {str(e)}",
500
)
@system_bp.route('/errors')
def get_errors():
"""Get recent errors"""
errors = cache.get_errors(limit=10)
return jsonify({
"errors": errors,
"count": len(errors)
})
@system_bp.route('/write/register', methods=['POST'])
def write_register():
"""Write to a single holding register"""
try:
# Check if request has JSON data
if not request.is_json:
return create_error_response(
"Bad Request",
"Request must be JSON with Content-Type: application/json",
400
)
data = request.get_json()
if not data or 'address' not in data or 'value' not in data:
return create_error_response(
"Bad Request",
"Must provide 'address' and 'value' in JSON body",
400
)
address = int(data['address'])
value = int(data['value'])
# Validate the write operation
is_valid, error_msg = writer.validate_write_operation(address, value)
if not is_valid:
return create_error_response("Bad Request", error_msg, 400)
# Perform the write
success = writer.write_holding_register(address, value)
return create_success_response(
f"Successfully wrote {value} to register {address}",
{
"address": address,
"value": value,
"timestamp": datetime.now().isoformat()
}
)
except ValueError as e:
return create_error_response(
"Bad Request",
f"Invalid address or value: {e}",
400
)
except (RegisterWriteError, PLCConnectionError) as e:
return create_error_response(
"Service Unavailable",
str(e),
503
)
except Exception as e:
logger.error(f"Unexpected error in write_register: {e}")
return create_error_response(
"Internal Server Error",
"An unexpected error occurred",
500
)
@system_bp.route('/config')
def get_config():
"""Get API configuration and available endpoints"""
return jsonify({
"api_version": Config.API_VERSION,
"endpoints": {
"/api/status": "Connection and system status",
"/api/sensors": "All sensor data",
"/api/sensors/category/<category>": "Sensors by category (system, pressure, temperature, flow, quality)",
"/api/timers": "All timer data",
"/api/timers/dts": "DTS timer data",
"/api/timers/fwf": "FWF timer data",
"/api/rtc": "Real-time clock data",
"/api/outputs": "Output control data",
"/api/outputs/active": "Active output controls only",
"/api/runtime": "Runtime hours data (IEEE 754 float)",
"/api/water_counters": "Water production counters (gallon totals)",
"/api/all": "All data in one response",
"/api/select": "Selective data retrieval (groups and/or keys) - BANDWIDTH OPTIMIZED",
"/api/errors": "Recent errors",
"/api/config": "This configuration",
"/api/dts/start": "POST - Start DTS watermaker sequence (async)",
"/api/dts/stop": "POST - Stop watermaker sequence (async, mode-dependent)",
"/api/dts/skip": "POST - Skip current step automatically (async)",
"/api/dts/status": "Get latest DTS operation status",
"/api/dts/status/<task_id>": "Get specific DTS task status",
"/api/dts/cancel/<task_id>": "POST - Cancel running DTS task",
"/api/write/register": "POST - Write single holding register"
},
"control_endpoints": {
"/api/dts/start": {
"method": "POST",
"description": "Start DTS watermaker sequence (ASYNC)",
"parameters": "None required",
"returns": "task_id for status polling",
"response_time": "< 100ms (immediate)",
"sequence": [
"Check R1000 value",
"Set R1000=34 if not already",
"Wait 2 seconds",
"Set R71=256",
"Wait 2 seconds",
"Set R71=0",
"Monitor R138 for valve positioning",
"Set R1000=5 to start DTS mode"
],
"polling": {
"status_endpoint": "/api/dts/status/{task_id}",
"recommended_interval": "1 second",
"total_duration": "~10 seconds"
}
},
"/api/dts/stop": {
"method": "POST",
"description": "Stop watermaker sequence (ASYNC, mode-dependent)",
"parameters": "None required",
"returns": "task_id for status polling",
"response_time": "< 100ms (immediate)",
"mode_sequences": {
"mode_5_dts": "R71=512, wait 1s, R71=0, R1000=8",
"mode_7_service": "R71=513, wait 1s, R71=0, R1000=8",
"mode_8_flush": "R71=1024, wait 1s, R71=0, R1000=2"
},
"note": "Watermaker always ends with flush screen (mode 8)"
},
"/api/dts/skip": {
"method": "POST",
"description": "Skip current step automatically (ASYNC)",
"parameters": "None required - auto-determines next step",
"returns": "task_id for status polling",
"response_time": "< 100ms (immediate)",
"auto_logic": {
"from_mode_5": "Skip step 2 → step 3: R67=32841 (PLC advances to mode 6)",
"from_mode_6": "Skip step 3 → step 4: R67=32968, wait 1s, R1000=7"
},
"valid_from_modes": [5, 6],
"example": "/api/dts/skip"
},
"/api/write/register": {
"method": "POST",
"description": "Write single holding register",
"body": {"address": "register_number", "value": "value_to_write"},
"example": {"address": 1000, "value": 5}
}
},
"variable_groups": {
"system": {
"description": "System status and operational mode",
"keys": ["1000", "1036"],
"count": 2
},
"pressure": {
"description": "Water pressure sensors",
"keys": ["1003", "1007", "1008"],
"count": 3
},
"temperature": {
"description": "Temperature monitoring",
"keys": ["1017", "1125"],
"count": 2
},
"flow": {
"description": "Flow rate meters",
"keys": ["1120", "1121", "1122"],
"count": 3
},
"quality": {
"description": "Water quality (TDS) sensors",
"keys": ["1123", "1124"],
"count": 2
},
"fwf_timer": {
"description": "Fresh water flush timers",
"keys": ["136"],
"count": 1
},
"dts_timer": {
"description": "DTS process step timers",
"keys": ["138", "128", "129", "133", "135", "139"],
"count": 6
},
"rtc": {
"description": "Real-time clock registers",
"keys": ["513", "514", "516", "517", "518", "519"],
"count": 6
},
"outputs": {
"description": "Digital output controls",
"keys": ["257", "258", "259", "260", "264", "265"],
"count": 6
},
"runtime": {
"description": "System runtime hours (IEEE 754 float)",
"keys": ["5014"],
"count": 1,
"note": "32-bit float from register pairs R5014+R5015"
},
"water_counters": {
"description": "Water production counters (gallon totals)",
"keys": ["5024", "5028", "5032", "5034"],
"count": 4,
"note": "32-bit floats from register pairs (Single/Double/DTS Total/Since Last)"
}
},
"selective_api_usage": {
"endpoint": "/api/select",
"description": "Retrieve only specified variables to reduce bandwidth and PLC traffic",
"parameters": {
"groups": "Comma-separated group names (system,pressure,temperature,flow,quality,fwf_timer,dts_timer,rtc,outputs,runtime,water_counters)",
"keys": "Comma-separated register numbers (1000,1003,1017,136,etc.)"
},
"examples": {
"temperature_and_pressure": "/api/select?groups=temperature,pressure",
"specific_sensors": "/api/select?keys=1036,1003,1017,1121",
"dts_monitoring": "/api/select?groups=dts_timer&keys=1036",
"critical_only": "/api/select?keys=1036,1003,1121,1123",
"runtime_and_counters": "/api/select?groups=runtime,water_counters"
}
},
"total_variables": 36,
"update_interval": f"{Config.DATA_UPDATE_INTERVAL} seconds (full scan) / on-demand (selective)",
"plc_config": {
"ip": Config.PLC_IP,
"port": Config.PLC_PORT
}
})