240 lines
8.3 KiB
Python
240 lines
8.3 KiB
Python
|
|
"""
|
||
|
|
OPC UA Server for Calejo Control Adapter.
|
||
|
|
|
||
|
|
Provides OPC UA interface for SCADA systems to access setpoints and status.
|
||
|
|
"""
|
||
|
|
|
||
|
|
import asyncio
|
||
|
|
from typing import Dict, Optional
|
||
|
|
from datetime import datetime
|
||
|
|
import structlog
|
||
|
|
from asyncua import Server, Node
|
||
|
|
from asyncua.common.methods import uamethod
|
||
|
|
|
||
|
|
from src.core.setpoint_manager import SetpointManager
|
||
|
|
|
||
|
|
logger = structlog.get_logger()
|
||
|
|
|
||
|
|
|
||
|
|
class OPCUAServer:
|
||
|
|
"""OPC UA Server for Calejo Control Adapter."""
|
||
|
|
|
||
|
|
def __init__(
|
||
|
|
self,
|
||
|
|
setpoint_manager: SetpointManager,
|
||
|
|
endpoint: str = "opc.tcp://0.0.0.0:4840",
|
||
|
|
server_name: str = "Calejo Control OPC UA Server"
|
||
|
|
):
|
||
|
|
self.setpoint_manager = setpoint_manager
|
||
|
|
self.endpoint = endpoint
|
||
|
|
self.server_name = server_name
|
||
|
|
self.server = None
|
||
|
|
self.namespace_idx = None
|
||
|
|
|
||
|
|
# Node references
|
||
|
|
self.objects_node = None
|
||
|
|
self.station_nodes = {}
|
||
|
|
self.pump_nodes = {}
|
||
|
|
|
||
|
|
async def start(self):
|
||
|
|
"""Start the OPC UA server."""
|
||
|
|
try:
|
||
|
|
# Create server
|
||
|
|
self.server = Server()
|
||
|
|
await self.server.init()
|
||
|
|
|
||
|
|
# Configure server
|
||
|
|
self.server.set_endpoint(self.endpoint)
|
||
|
|
self.server.set_server_name(self.server_name)
|
||
|
|
self.server.set_security_policy([
|
||
|
|
"http://opcfoundation.org/UA/SecurityPolicy#None"
|
||
|
|
])
|
||
|
|
|
||
|
|
# Setup namespace
|
||
|
|
uri = "http://calejo-control.com/OPCUA/"
|
||
|
|
self.namespace_idx = await self.server.register_namespace(uri)
|
||
|
|
|
||
|
|
# Create object structure
|
||
|
|
await self._create_object_structure()
|
||
|
|
|
||
|
|
# Start server
|
||
|
|
await self.server.start()
|
||
|
|
|
||
|
|
logger.info(
|
||
|
|
"opcua_server_started",
|
||
|
|
endpoint=self.endpoint,
|
||
|
|
namespace_idx=self.namespace_idx
|
||
|
|
)
|
||
|
|
|
||
|
|
# Start background task to update setpoints
|
||
|
|
asyncio.create_task(self._update_setpoints_loop())
|
||
|
|
|
||
|
|
except Exception as e:
|
||
|
|
logger.error("failed_to_start_opcua_server", error=str(e))
|
||
|
|
raise
|
||
|
|
|
||
|
|
async def stop(self):
|
||
|
|
"""Stop the OPC UA server."""
|
||
|
|
if self.server:
|
||
|
|
await self.server.stop()
|
||
|
|
logger.info("opcua_server_stopped")
|
||
|
|
|
||
|
|
async def _create_object_structure(self):
|
||
|
|
"""Create the OPC UA object structure."""
|
||
|
|
# Get objects node
|
||
|
|
self.objects_node = self.server.get_objects_node()
|
||
|
|
|
||
|
|
# Create Calejo Control folder
|
||
|
|
calejo_folder = await self.objects_node.add_folder(
|
||
|
|
self.namespace_idx,
|
||
|
|
"CalejoControl"
|
||
|
|
)
|
||
|
|
|
||
|
|
# Create stations and pumps structure
|
||
|
|
stations = self.setpoint_manager.discovery.get_stations()
|
||
|
|
|
||
|
|
for station in stations:
|
||
|
|
station_id = station['station_id']
|
||
|
|
|
||
|
|
# Create station folder
|
||
|
|
station_folder = await calejo_folder.add_folder(
|
||
|
|
self.namespace_idx,
|
||
|
|
f"Station_{station_id}"
|
||
|
|
)
|
||
|
|
|
||
|
|
# Add station info variables
|
||
|
|
station_name_var = await station_folder.add_variable(
|
||
|
|
self.namespace_idx,
|
||
|
|
"StationName",
|
||
|
|
station.get('station_name', station_id)
|
||
|
|
)
|
||
|
|
await station_name_var.set_writable(False)
|
||
|
|
|
||
|
|
# Create pumps for this station
|
||
|
|
pumps = self.setpoint_manager.discovery.get_pumps(station_id)
|
||
|
|
|
||
|
|
for pump in pumps:
|
||
|
|
pump_id = pump['pump_id']
|
||
|
|
|
||
|
|
# Create pump object
|
||
|
|
pump_obj = await station_folder.add_object(
|
||
|
|
self.namespace_idx,
|
||
|
|
f"Pump_{pump_id}"
|
||
|
|
)
|
||
|
|
|
||
|
|
# Add pump variables
|
||
|
|
pump_name_var = await pump_obj.add_variable(
|
||
|
|
self.namespace_idx,
|
||
|
|
"PumpName",
|
||
|
|
pump.get('pump_name', pump_id)
|
||
|
|
)
|
||
|
|
await pump_name_var.set_writable(False)
|
||
|
|
|
||
|
|
control_type_var = await pump_obj.add_variable(
|
||
|
|
self.namespace_idx,
|
||
|
|
"ControlType",
|
||
|
|
pump.get('control_type', 'UNKNOWN')
|
||
|
|
)
|
||
|
|
await control_type_var.set_writable(False)
|
||
|
|
|
||
|
|
# Add setpoint variable (writable for SCADA override)
|
||
|
|
setpoint_var = await pump_obj.add_variable(
|
||
|
|
self.namespace_idx,
|
||
|
|
"Setpoint_Hz",
|
||
|
|
0.0
|
||
|
|
)
|
||
|
|
await setpoint_var.set_writable(True)
|
||
|
|
|
||
|
|
# Add safety status variable
|
||
|
|
safety_status_var = await pump_obj.add_variable(
|
||
|
|
self.namespace_idx,
|
||
|
|
"SafetyStatus",
|
||
|
|
"normal"
|
||
|
|
)
|
||
|
|
await safety_status_var.set_writable(False)
|
||
|
|
|
||
|
|
# Add timestamp variable
|
||
|
|
timestamp_var = await pump_obj.add_variable(
|
||
|
|
self.namespace_idx,
|
||
|
|
"LastUpdate",
|
||
|
|
datetime.now().isoformat()
|
||
|
|
)
|
||
|
|
await timestamp_var.set_writable(False)
|
||
|
|
|
||
|
|
# Store node references
|
||
|
|
self.pump_nodes[(station_id, pump_id)] = {
|
||
|
|
'object': pump_obj,
|
||
|
|
'setpoint': setpoint_var,
|
||
|
|
'safety_status': safety_status_var,
|
||
|
|
'timestamp': timestamp_var
|
||
|
|
}
|
||
|
|
|
||
|
|
self.station_nodes[station_id] = station_folder
|
||
|
|
|
||
|
|
# Add server status variables
|
||
|
|
server_status_folder = await calejo_folder.add_folder(
|
||
|
|
self.namespace_idx,
|
||
|
|
"ServerStatus"
|
||
|
|
)
|
||
|
|
|
||
|
|
server_status_var = await server_status_folder.add_variable(
|
||
|
|
self.namespace_idx,
|
||
|
|
"Status",
|
||
|
|
"running"
|
||
|
|
)
|
||
|
|
await server_status_var.set_writable(False)
|
||
|
|
|
||
|
|
uptime_var = await server_status_folder.add_variable(
|
||
|
|
self.namespace_idx,
|
||
|
|
"Uptime",
|
||
|
|
0
|
||
|
|
)
|
||
|
|
await uptime_var.set_writable(False)
|
||
|
|
|
||
|
|
total_pumps_var = await server_status_folder.add_variable(
|
||
|
|
self.namespace_idx,
|
||
|
|
"TotalPumps",
|
||
|
|
len(self.pump_nodes)
|
||
|
|
)
|
||
|
|
await total_pumps_var.set_writable(False)
|
||
|
|
|
||
|
|
async def _update_setpoints_loop(self):
|
||
|
|
"""Background task to update setpoints periodically."""
|
||
|
|
while True:
|
||
|
|
try:
|
||
|
|
await self._update_setpoints()
|
||
|
|
await asyncio.sleep(5) # Update every 5 seconds
|
||
|
|
except Exception as e:
|
||
|
|
logger.error("failed_to_update_setpoints", error=str(e))
|
||
|
|
await asyncio.sleep(10) # Wait longer on error
|
||
|
|
|
||
|
|
async def _update_setpoints(self):
|
||
|
|
"""Update all setpoint values in OPC UA server."""
|
||
|
|
for (station_id, pump_id), nodes in self.pump_nodes.items():
|
||
|
|
try:
|
||
|
|
# Get current setpoint
|
||
|
|
setpoint = self.setpoint_manager.get_current_setpoint(station_id, pump_id)
|
||
|
|
|
||
|
|
if setpoint is not None:
|
||
|
|
# Update setpoint variable
|
||
|
|
await nodes['setpoint'].write_value(float(setpoint))
|
||
|
|
|
||
|
|
# Update safety status
|
||
|
|
safety_status = "normal"
|
||
|
|
if self.setpoint_manager.emergency_stop_manager.is_emergency_stop_active(station_id, pump_id):
|
||
|
|
safety_status = "emergency_stop"
|
||
|
|
elif self.setpoint_manager.watchdog.is_failsafe_active(station_id, pump_id):
|
||
|
|
safety_status = "failsafe"
|
||
|
|
|
||
|
|
await nodes['safety_status'].write_value(safety_status)
|
||
|
|
|
||
|
|
# Update timestamp
|
||
|
|
await nodes['timestamp'].write_value(datetime.now().isoformat())
|
||
|
|
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(
|
||
|
|
"failed_to_update_pump_setpoint",
|
||
|
|
station_id=station_id,
|
||
|
|
pump_id=pump_id,
|
||
|
|
error=str(e)
|
||
|
|
)
|