359 lines
14 KiB
Python
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
|
|
}
|
|
}) |