From a681c6da6b563cd0cf43ce04914267370615e107 Mon Sep 17 00:00:00 2001 From: "m.heisig" Date: Sat, 8 Nov 2025 13:56:28 +0100 Subject: [PATCH] Initial commit --- .gitignore | 219 ++++++++++++++++++++++++++++++++++++++++ globalConfigs.py | 28 ++++++ globalData.py | 12 +++ main.py | 115 +++++++++++++++++++++ models.py | 14 +++ nodeList.txt | 235 +++++++++++++++++++++++++++++++++++++++++++ oldCode.py | 45 +++++++++ opcuaHandler.py | 191 +++++++++++++++++++++++++++++++++++ requirements.txt | 0 routers/__init__.py | 0 routers/websocket.py | 62 ++++++++++++ 11 files changed, 921 insertions(+) create mode 100644 .gitignore create mode 100644 globalConfigs.py create mode 100644 globalData.py create mode 100644 main.py create mode 100644 models.py create mode 100644 nodeList.txt create mode 100644 oldCode.py create mode 100644 opcuaHandler.py create mode 100644 requirements.txt create mode 100644 routers/__init__.py create mode 100644 routers/websocket.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2157908 --- /dev/null +++ b/.gitignore @@ -0,0 +1,219 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[codz] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py.cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +# Pipfile.lock + +# UV +# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# uv.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +# poetry.lock +# poetry.toml + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python. +# https://pdm-project.org/en/latest/usage/project/#working-with-version-control +# pdm.lock +# pdm.toml +.pdm-python +.pdm-build/ + +# pixi +# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control. +# pixi.lock +# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one +# in the .venv directory. It is recommended not to include this directory in version control. +.pixi + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# Redis +*.rdb +*.aof +*.pid + +# RabbitMQ +mnesia/ +rabbitmq/ +rabbitmq-data/ + +# ActiveMQ +activemq-data/ + +# SageMath parsed files +*.sage.py + +# Environments +.env +.envrc +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +# .idea/ + +# Abstra +# Abstra is an AI-powered process automation framework. +# Ignore directories containing user credentials, local state, and settings. +# Learn more at https://abstra.io/docs +.abstra/ + +# Visual Studio Code +# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore +# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore +# and can be added to the global gitignore or merged into this file. However, if you prefer, +# you could uncomment the following to ignore the entire vscode folder +.vscode/ + +# Ruff stuff: +.ruff_cache/ + +# PyPI configuration file +.pypirc + +# Marimo +marimo/_static/ +marimo/_lsp/ +__marimo__/ + +# Streamlit +.streamlit/secrets.toml + +static/* +certificates/* \ No newline at end of file diff --git a/globalConfigs.py b/globalConfigs.py new file mode 100644 index 0000000..f098d7f --- /dev/null +++ b/globalConfigs.py @@ -0,0 +1,28 @@ +from pathlib import Path +import socket + +# Certificate settings +CERT_BASE = Path(__file__).parent +CERT = Path(CERT_BASE / f"certificates/peer-certificate.der") +PRIVATE_KEY = Path(CERT_BASE / f"certificates/peer-private-key.pem") +HOST_NAME = socket.gethostname() +CLIENT_APP_URI = f"urn:{HOST_NAME}:client:hmi" + +# OPC UA settings +OPC_UA_URL = "opc.tcp://192.168.42.1:4840" +OPC_UA_USER = "hmi" +OPC_UA_PW = "hmi" +#OPC_UA_URL = "opc.tcp://192.168.56.101:4840" +#OPC_UA_USER = "twincat" +#OPC_UA_PW = "twincat" +NAMESPACE = "urn:BeckhoffAutomation:Ua:PLC1" +NS_IDX = 0 + +CHECK_INTERVAL = 1.0 # seconds +RECONNECT_INTERVAL = 5.0 # seconds + +# Publishing interval to the clients (rate limiting) +PUBLISH_INTERVAL = 0.1 # 200 ms + +# Path to the nodes list +NODE_IDS_FILE_PATH = Path(CERT_BASE / f"nodeList.txt") \ No newline at end of file diff --git a/globalData.py b/globalData.py new file mode 100644 index 0000000..a9f686e --- /dev/null +++ b/globalData.py @@ -0,0 +1,12 @@ +import asyncio +from asyncua import Client +from asyncua.common.subscription import Subscription + +opc_ua_client: Client +clients = {} # {WebSocket: set(subscribed_tags)} +node_storage = {} +event_storage = {} +queue = asyncio.Queue(maxsize=10000) +event_queue = asyncio.Queue(maxsize=1000) +subscription_handler: Subscription +subscribed_nodes = set() \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..5943979 --- /dev/null +++ b/main.py @@ -0,0 +1,115 @@ +from fastapi import FastAPI #, HTTPException +from routers import websocket +from fastapi.middleware.cors import CORSMiddleware +#from starlette import status +#from fastapi.staticfiles import StaticFiles +#from models import SemiAutoControl, SemiAutoBatteryStatus + +import asyncio +import msgpack + +#from globalData import node_storage, clients +import globalData as gd +import globalConfigs as gc +from opcuaHandler import initOPCUAConnection, connection_watcher + +update_buffer = {} + +async def data_consumer(queue): + """Asynchronous consumer that stores the latest values.""" + while True: + nodeid, val = await queue.get() + gd.node_storage[nodeid] = val + update_buffer[nodeid] = val + queue.task_done() + +async def event_consumer(queue): + while True: + node_id, event_data = await queue.get() + print("Got new event", event_data) + if node_id in gd.event_storage: + event_data.Message = gd.event_storage[node_id].Message + gd.event_storage[node_id].update(event_data) + else: + gd.event_storage[node_id] = event_data + + print(gd.event_storage) + + for ws in list(gd.clients): + print("Sending event:", {"e": {node_id: event_data}}) + await ws.send_bytes(msgpack.packb({"e": {node_id: event_data}})) + queue.task_done() + +async def broadcast_deltas(changes: dict): + payload = msgpack.packb({"u": changes}) + for ws, subscribed_tags in list(gd.clients.items()): + relevant = {tag: val for tag, val in changes.items() if tag in subscribed_tags} + if relevant: + await ws.send_bytes(msgpack.packb({"u": relevant})) + +async def broadcast_loop(): + global update_buffer + while True: + await asyncio.sleep(gc.PUBLISH_INTERVAL) + + if not update_buffer: + continue + + changes = dict(update_buffer) + update_buffer.clear() + + for ws, subscribed_tags in list(gd.clients.items()): + relevant = {tag: val for tag, val in changes.items() if tag in subscribed_tags} + if relevant: + await ws.send_bytes(msgpack.packb({"u": relevant})) + + +async def lifespan(app: FastAPI): + """ + FastAPI lifespan context — creates the client and starts the watcher.loading="warning" + """ + await initOPCUAConnection() + + # Launch the background watcher task for the opc ua connection + watcher_task = asyncio.create_task(connection_watcher()) + + # Lunch data consumer task for node value changed events + consumer_task = asyncio.create_task(data_consumer(gd.queue)) + event_consumer_task = asyncio.create_task(event_consumer(gd.event_queue)) + broadcast_task = asyncio.create_task(broadcast_loop()) + + + yield # Application runs while this yields + + # Cleanup on shutdown + watcher_task.cancel() + broadcast_task.cancel() + consumer_task.cancel() + event_consumer_task.cancel() + try: + await watcher_task + await consumer_task + await broadcast_task + await event_consumer_task + except asyncio.CancelledError: + pass + + +app = FastAPI(lifespan=lifespan) +origins = [ + "http://localhost:3000", + "http://127.0.0.1:3000", + # Fügen Sie hier weitere erforderlich Ursprünge hinzu + ] +app.add_middleware( + CORSMiddleware, + allow_origins=origins, + allow_credentials=True, + allow_methods=["*"], # Erlaubt alle HTTP-Methoden (GET, POST, etc.) + allow_headers=["*"], # Erlaubt alle Header + ) +app.include_router(websocket.router) +#app.include_router(semiAuto.router) + +# Serve the built frontend +#app.mount("/assets", StaticFiles(directory="static/assets"), name="assets") diff --git a/models.py b/models.py new file mode 100644 index 0000000..508f755 --- /dev/null +++ b/models.py @@ -0,0 +1,14 @@ +from pydantic import BaseModel +from asyncua import ua +from typing import Any + +class SemiAutoControl(BaseModel): + string: int + module: int + unit: int + enable: bool + +class WriteNodeCommand(BaseModel): + nodeId: str + nodeValue: Any + nodeType: ua.VariantType diff --git a/nodeList.txt b/nodeList.txt new file mode 100644 index 0000000..50e4798 --- /dev/null +++ b/nodeList.txt @@ -0,0 +1,235 @@ +# Semi auto mode control string 1 +ns=4;s=GVL_SCADA.stSemiAutoBatteryEnable[0].stSemiAutoModul1.xSemiAutoEnableUnit1 +ns=4;s=GVL_SCADA.stSemiAutoBatteryEnable[0].stSemiAutoModul1.xSemiAutoEnableUnit2 +ns=4;s=GVL_SCADA.stSemiAutoBatteryEnable[0].stSemiAutoModul1.xSemiAutoEnableUnit3 +ns=4;s=GVL_SCADA.stSemiAutoBatteryEnable[0].stSemiAutoModul1.xSemiAutoEnableUnit4 +ns=4;s=GVL_SCADA.stSemiAutoBatteryEnable[0].stSemiAutoModul2.xSemiAutoEnableUnit1 +ns=4;s=GVL_SCADA.stSemiAutoBatteryEnable[0].stSemiAutoModul2.xSemiAutoEnableUnit2 +ns=4;s=GVL_SCADA.stSemiAutoBatteryEnable[0].stSemiAutoModul2.xSemiAutoEnableUnit3 +ns=4;s=GVL_SCADA.stSemiAutoBatteryEnable[0].stSemiAutoModul2.xSemiAutoEnableUnit4 +ns=4;s=GVL_SCADA.stSemiAutoBatteryEnable[0].stSemiAutoModul3.xSemiAutoEnableUnit1 +ns=4;s=GVL_SCADA.stSemiAutoBatteryEnable[0].stSemiAutoModul3.xSemiAutoEnableUnit2 +ns=4;s=GVL_SCADA.stSemiAutoBatteryEnable[0].stSemiAutoModul3.xSemiAutoEnableUnit3 +ns=4;s=GVL_SCADA.stSemiAutoBatteryEnable[0].stSemiAutoModul3.xSemiAutoEnableUnit4 + +# Semi auto mode control string 2 +ns=4;s=GVL_SCADA.stSemiAutoBatteryEnable[1].stSemiAutoModul1.xSemiAutoEnableUnit1 +ns=4;s=GVL_SCADA.stSemiAutoBatteryEnable[1].stSemiAutoModul1.xSemiAutoEnableUnit2 +ns=4;s=GVL_SCADA.stSemiAutoBatteryEnable[1].stSemiAutoModul1.xSemiAutoEnableUnit3 +ns=4;s=GVL_SCADA.stSemiAutoBatteryEnable[1].stSemiAutoModul1.xSemiAutoEnableUnit4 +ns=4;s=GVL_SCADA.stSemiAutoBatteryEnable[1].stSemiAutoModul2.xSemiAutoEnableUnit1 +ns=4;s=GVL_SCADA.stSemiAutoBatteryEnable[1].stSemiAutoModul2.xSemiAutoEnableUnit2 +ns=4;s=GVL_SCADA.stSemiAutoBatteryEnable[1].stSemiAutoModul2.xSemiAutoEnableUnit3 +ns=4;s=GVL_SCADA.stSemiAutoBatteryEnable[1].stSemiAutoModul2.xSemiAutoEnableUnit4 +ns=4;s=GVL_SCADA.stSemiAutoBatteryEnable[1].stSemiAutoModul3.xSemiAutoEnableUnit1 +ns=4;s=GVL_SCADA.stSemiAutoBatteryEnable[1].stSemiAutoModul3.xSemiAutoEnableUnit2 +ns=4;s=GVL_SCADA.stSemiAutoBatteryEnable[1].stSemiAutoModul3.xSemiAutoEnableUnit3 +ns=4;s=GVL_SCADA.stSemiAutoBatteryEnable[1].stSemiAutoModul3.xSemiAutoEnableUnit4 + +# Auto control mode +ns=4;s=GVL_SCADA.eCurrentControlMode +ns=4;s=GVL_SCADA.xCanChangeControlMode + +# String 2 - Modul 2 - Unit 1 +# Spannungsmessung +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit1.stE31.rValue +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit1.stE31.sUnit + +# Druck segment +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit1.stP11.rValue +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit1.stP11.sUnit +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit1.stP21.rValue +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit1.stP21.sUnit + +# Druck tank +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit1.stP12.rValue +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit1.stP12.sUnit +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit1.stP22.rValue +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit1.stP22.sUnit + +# Temperatur +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit1.stT11.rValue +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit1.stT11.sUnit +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit1.stT21.rValue +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit1.stT21.sUnit + +# Ventile +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit1.stNS12.stOpenButton.eFeedback +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit1.stNS12.stOpenButton.xRelease +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit1.stNS12.stCloseButton.eFeedback +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit1.stNS12.stCloseButton.xRelease + +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit1.stNS22.stOpenButton.eFeedback +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit1.stNS22.stOpenButton.xRelease +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit1.stNS22.stCloseButton.eFeedback +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit1.stNS22.stCloseButton.xRelease + +# Pumpen +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit1.stNS11.stSetpoint.rValue +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit1.stNS11.stProcessValue.rValue +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit1.stNS11.stProcessValue.sUnit +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit1.stNS11.stStartButton.eFeedback +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit1.stNS11.stStartButton.xRelease +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit1.stNS11.stStopButton.eFeedback +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit1.stNS11.stStopButton.xRelease + +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit1.stNS21.stSetpoint.rValue +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit1.stNS21.stProcessValue.rValue +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit1.stNS21.stProcessValue.sUnit +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit1.stNS21.stStartButton.eFeedback +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit1.stNS21.stStartButton.xRelease +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit1.stNS21.stStopButton.eFeedback +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit1.stNS21.stStopButton.xRelease + +# String 2 - Modul 2 - Unit 2 +# Spannungsmessung +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit2.stE31.rValue +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit2.stE31.sUnit + +# Druck segment +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit2.stP11.rValue +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit2.stP11.sUnit +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit2.stP21.rValue +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit2.stP21.sUnit + +# Druck tank +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit2.stP12.rValue +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit2.stP12.sUnit +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit2.stP22.rValue +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit2.stP22.sUnit + +# Temperatur +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit2.stT11.rValue +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit2.stT11.sUnit +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit2.stT21.rValue +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit2.stT21.sUnit + +# Ventile +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit2.stNS12.stOpenButton.eFeedback +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit2.stNS12.stOpenButton.xRelease +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit2.stNS12.stCloseButton.eFeedback +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit2.stNS12.stCloseButton.xRelease + +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit2.stNS22.stOpenButton.eFeedback +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit2.stNS22.stOpenButton.xRelease +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit2.stNS22.stCloseButton.eFeedback +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit2.stNS22.stCloseButton.xRelease + +# Pumpen +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit2.stNS11.stSetpoint.rValue +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit2.stNS11.stProcessValue.rValue +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit2.stNS11.stProcessValue.sUnit +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit2.stNS11.stStartButton.eFeedback +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit2.stNS11.stStartButton.xRelease +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit2.stNS11.stStopButton.eFeedback +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit2.stNS11.stStopButton.xRelease + +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit2.stNS21.stSetpoint.rValue +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit2.stNS21.stProcessValue.rValue +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit2.stNS21.stProcessValue.sUnit +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit2.stNS21.stStartButton.eFeedback +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit2.stNS21.stStartButton.xRelease +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit2.stNS21.stStopButton.eFeedback +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit2.stNS21.stStopButton.xRelease + +# String 2 - Modul 2 - Unit 3 +# Spannungsmessung +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit3.stE31.rValue +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit3.stE31.sUnit + +# Druck segment +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit3.stP11.rValue +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit3.stP11.sUnit +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit3.stP21.rValue +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit3.stP21.sUnit + +# Druck tank +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit3.stP12.rValue +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit3.stP12.sUnit +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit3.stP22.rValue +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit3.stP22.sUnit + +# Temperatur +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit3.stT11.rValue +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit3.stT11.sUnit +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit3.stT21.rValue +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit3.stT21.sUnit + +# Ventile +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit3.stNS12.stOpenButton.eFeedback +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit3.stNS12.stOpenButton.xRelease +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit3.stNS12.stCloseButton.eFeedback +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit3.stNS12.stCloseButton.xRelease + +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit3.stNS22.stOpenButton.eFeedback +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit3.stNS22.stOpenButton.xRelease +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit3.stNS22.stCloseButton.eFeedback +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit3.stNS22.stCloseButton.xRelease + +# Pumpen +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit3.stNS11.stSetpoint.rValue +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit3.stNS11.stProcessValue.rValue +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit3.stNS11.stProcessValue.sUnit +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit3.stNS11.stStartButton.eFeedback +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit3.stNS11.stStartButton.xRelease +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit3.stNS11.stStopButton.eFeedback +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit3.stNS11.stStopButton.xRelease + +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit3.stNS21.stSetpoint.rValue +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit3.stNS21.stProcessValue.rValue +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit3.stNS21.stProcessValue.sUnit +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit3.stNS21.stStartButton.eFeedback +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit3.stNS21.stStartButton.xRelease +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit3.stNS21.stStopButton.eFeedback +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit3.stNS21.stStopButton.xRelease + +# String 2 - Modul 2 - Unit 4 +# Spannungsmessung +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit4.stE31.rValue +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit4.stE31.sUnit + +# Druck segment +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit4.stP11.rValue +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit4.stP11.sUnit +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit4.stP21.rValue +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit4.stP21.sUnit + +# Druck tank +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit4.stP12.rValue +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit4.stP12.sUnit +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit4.stP22.rValue +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit4.stP22.sUnit + +# Temperatur +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit4.stT11.rValue +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit4.stT11.sUnit +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit4.stT21.rValue +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit4.stT21.sUnit + +# Ventile +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit4.stNS12.stOpenButton.eFeedback +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit4.stNS12.stOpenButton.xRelease +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit4.stNS12.stCloseButton.eFeedback +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit4.stNS12.stCloseButton.xRelease + +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit4.stNS22.stOpenButton.eFeedback +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit4.stNS22.stOpenButton.xRelease +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit4.stNS22.stCloseButton.eFeedback +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit4.stNS22.stCloseButton.xRelease + +# Pumpen +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit4.stNS11.stSetpoint.rValue +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit4.stNS11.stProcessValue.rValue +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit4.stNS11.stProcessValue.sUnit +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit4.stNS11.stStartButton.eFeedback +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit4.stNS11.stStartButton.xRelease +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit4.stNS11.stStopButton.eFeedback +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit4.stNS11.stStopButton.xRelease + +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit4.stNS21.stSetpoint.rValue +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit4.stNS21.stProcessValue.rValue +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit4.stNS21.stProcessValue.sUnit +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit4.stNS21.stStartButton.eFeedback +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit4.stNS21.stStartButton.xRelease +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit4.stNS21.stStopButton.eFeedback +ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit4.stNS21.stStopButton.xRelease \ No newline at end of file diff --git a/oldCode.py b/oldCode.py new file mode 100644 index 0000000..0f4ae6d --- /dev/null +++ b/oldCode.py @@ -0,0 +1,45 @@ +#from fastapi.responses import StreamingResponse, FileResponse + +# @app.get("/events") +# async def sse_endpoint(): +# """SSE endpoint to stream OPC UA updates""" +# return StreamingResponse(event_stream(), media_type="text/event-stream") + +# Serve index.html +#@app.get("/") +#async def serve_frontend(): +# return FileResponse("static/index.html") + +# @app.get("/modes/semiAuto/{strng}") +# async def semiAutoStringEventStream(strng): +# return StreamingResponse(strng_semi_auto_data_stream(), media_type="text/event-stream") + +# async def event_stream(): +# # Send current value immediately if available +# if latest_value["value"] is not None: +# yield f"data: {json.dumps(latest_value)}\n\n" + +# while True: +# await new_value_event.wait() # wait for new value +# data = json.dumps(latest_value) +# yield f"data: {data}\n\n" +# new_value_event.clear() # reset event and wait for next update + +# async def strng_semi_auto_data_stream(): +# while True: +# await new_value_event.wait() +# data + +# @app.get("/getMode") +# async def set_mode(request: Request): +# client = request.app.state.opcua_client +# nsidx = request.app.state.ns_idx +# #node = client.get_node(f"ns={nsidx};s=GVL_SCADA.eRequestedControlMode") +# #node = client.get_node(f"ns={nsidx};s=GVL_SCADA.eCurrentControlMode") +# try: +# node = client.get_node(f"ns={nsidx};s=GVL_SCADA.stHMIInterface[0].stHMIInterfaceModule3.stHMIInterfaceUnit1.stP11.rValue") +# value = await node.read_value() +# print(value) +# return {"node_id": "GVL_SCADA.eCurrentControlMode", "value": value} +# except: +# return {"node_id": "GVL_SCADA.eCurrentControlMode", "value": 0.0} \ No newline at end of file diff --git a/opcuaHandler.py b/opcuaHandler.py new file mode 100644 index 0000000..2fb3765 --- /dev/null +++ b/opcuaHandler.py @@ -0,0 +1,191 @@ +import os + +from asyncua import Client, ua, Node +from asyncua.crypto.security_policies import SecurityPolicyBasic256Sha256 +from asyncua.crypto.cert_gen import setup_self_signed_certificate +from cryptography.x509.oid import ExtendedKeyUsageOID + +import asyncio + +import globalConfigs as gc +import globalData as gd + + +new_value_event = asyncio.Event() # signals when a new value arrives + +class SubHandler: + """ + Subscription Handler. To receive events from server for a subscription + This class is just a sample class. Whatever class having these methods can be used + """ + + def __init__(self, queue, event_queue): + self.queue = queue + self.event_queue = event_queue + + def datachange_notification(self, node: Node, val, data): + """ + called for every datachange notification from server + """ + self.queue.put_nowait((node.nodeid.to_string(), val)) + + def event_notification(self, event: ua.EventNotificationList): + """ + called for every event notification from server + """ + # print("=== Event empfangen ===") + # print("NodeId:", event.NodeId.Identifier) + # print("Severity:", event.Severity) + # print("Message:", event.Message.Text) + # print("ActiveState:", getattr(event, "ActiveState/Id")) + # print("ActiveStateTransTime:", getattr(event, "ActiveState/TransitionTime")) + # print("ConfirmedState:", getattr(event, "ConfirmedState/Id")) + # print("ConfirmedStateTransTime:", getattr(event, "ConfirmedState/TransitionTime")) + # print("Retain:", event.Retain) + # print("------------------------") + + event_data = { + "Severity": event.Severity, + "Message": event.Message.Text, + "ActiveState": getattr(event, "ActiveState/Id"), + "ConfirmedState": getattr(event, "ConfirmedState/Id"), + "ActiveStateTransTime": getattr(event, "ActiveState/TransitionTime").isoformat(), + "ConfirmedStateTransTime": getattr(event, "ConfirmedState/TransitionTime").isoformat(), + "Retain": event.Retain + } + print("Got event") + self.event_queue.put_nowait((event.NodeId.Identifier, event_data)) + + + + def status_change_notification(self, status: ua.StatusChangeNotification): + """ + called for every status change notification from server + """ + print("status_notification %s", status) + +# def load_node_ids(): +# if not gc.NODE_IDS_FILE_PATH.exists(): +# return [] + +# with open(gc.NODE_IDS_FILE_PATH, "r", encoding="utf-8") as f: +# node_ids = [line.strip() for line in f if line.strip() and not line.startswith("#")] + +# if not node_ids: +# raise ValueError(f"No node IDs found in {gc.NODE_IDS_FILE_PATH}") +# return node_ids + +# async def createSubscriptions(): + +# subscription = await gd.opc_ua_client.create_subscription(250, SubHandler(gd.queue)) +# node_ids = load_node_ids() +# nodes = [gd.opc_ua_client.get_node(f"{nid}") for nid in node_ids] +# await subscription.subscribe_data_change(nodes) + +async def connection_watcher2(): + await initOPCUAConnection() + + connected = False + + while not connected: + try: + # In the first round we can use the normal connect + await gd.opc_ua_client.connect() + connected = True + except Exception: + print(f"Reconnecting in {gc.RECONNECT_INTERVAL} seconds") + await asyncio.sleep(gc.RECONNECT_INTERVAL) + + + # Create subscriptions + gd.subscription_handler = await gd.opc_ua_client.create_subscription(250, SubHandler(gd.queue, gd.event_queue)) + + # Create an event subscription + event_logger_node = gd.opc_ua_client.get_node("ns=8;s=eventlogger") + await gd.subscription_handler.subscribe_events(event_logger_node, evtypes=[ua.ObjectIds.BaseEventType, "ns=5;i=4000"]) + + # Periodically check for connection + state = 0 + while True: + match (state): + case 0: + try: + await gd.opc_ua_client.connect_sessionless() + await gd.opc_ua_client.create_session() + except Exception: + + + await asyncio.sleep(gc.CHECK_INTERVAL) + try: + await gd.opc_ua_client.check_connection() + except ua.uaerrors._auto.BadConnectionClosed: + connected = False + print(f"Reconnecting in {gc.RECONNECT_INTERVAL} seconds") + await asyncio.sleep(gc.RECONNECT_INTERVAL) + + + +async def connection_watcher(): + """ + Background task to keep checking OPC UA client connection. + Reconnects automatically if disconnected. + """ + while True: + try: + gd.opc_ua_client = Client(url=gc.OPC_UA_URL) + + gd.opc_ua_client.application_uri = gc.CLIENT_APP_URI + await gd.opc_ua_client.set_security( + SecurityPolicyBasic256Sha256, + certificate=str(gc.CERT), + private_key=str(gc.PRIVATE_KEY) + ) + gd.opc_ua_client.set_user(gc.OPC_UA_USER) + gd.opc_ua_client.set_password(gc.OPC_UA_PW) + + async with gd.opc_ua_client: + print("Connected") + #print("Creating subscriptions") + #await createSubscriptions() + gd.subscription_handler = await gd.opc_ua_client.create_subscription(250, SubHandler(gd.queue, gd.event_queue)) + event_logger_node = gd.opc_ua_client.get_node("ns=8;s=eventlogger") + await gd.subscription_handler.subscribe_events(event_logger_node, evtypes=[ua.ObjectIds.BaseEventType, "ns=5;i=4000"]) + print("Registered events") + while True: + await asyncio.sleep(gc.CHECK_INTERVAL) + await gd.opc_ua_client.check_connection() # Throws a exception if connection is lost + except ua.uaerrors._auto.BadConnectionClosed: + print("Lost connection") + print(f"Reconnecting in {gc.RECONNECT_INTERVAL} seconds") + await asyncio.sleep(gc.RECONNECT_INTERVAL) + except Exception as e: + print(e) + break + + +async def initOPCUAConnection(): + if (not os.path.isfile(gc.CERT)) or (not os.path.isfile(gc.PRIVATE_KEY)): + await setup_self_signed_certificate( + gc.PRIVATE_KEY, + gc.CERT, + gc.CLIENT_APP_URI, + gc.HOST_NAME, + [ExtendedKeyUsageOID.CLIENT_AUTH], + { + "countryName": "DE", + "stateOrProvinceName": "NRW", + "localityName": "HMI backend", + "organizationName": "Heisig GmbH", + }, + ) + + gd.opc_ua_client = Client(url=gc.OPC_UA_URL) + + gd.opc_ua_client.application_uri = gc.CLIENT_APP_URI + await gd.opc_ua_client.set_security( + SecurityPolicyBasic256Sha256, + certificate=str(gc.CERT), + private_key=str(gc.PRIVATE_KEY) + ) + gd.opc_ua_client.set_user(gc.OPC_UA_USER) + gd.opc_ua_client.set_password(gc.OPC_UA_PW) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..e69de29 diff --git a/routers/__init__.py b/routers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/routers/websocket.py b/routers/websocket.py new file mode 100644 index 0000000..4807e83 --- /dev/null +++ b/routers/websocket.py @@ -0,0 +1,62 @@ +from fastapi import WebSocket, APIRouter +import msgpack +import globalData as gd + +from asyncua import ua +from models import WriteNodeCommand + +router = APIRouter() + +@router.websocket("/ws") +async def websocket_endpoint(ws: WebSocket): + global clients + await ws.accept() + gd.clients[ws] = set() + + try: + while True: + # empfange binäre Daten vom Client + raw = await ws.receive_bytes() + data = msgpack.unpackb(raw, raw=False) + + # Subscription handling + if "subscribe" in data: + gd.clients[ws].update(data["subscribe"]) + diff_set = gd.subscribed_nodes.symmetric_difference(data["subscribe"]) + nodes = [gd.opc_ua_client.get_node(f"{nid}") for nid in diff_set] + + #await gd.subscription_handler.subscribe_data_change(nodes) + + initial_values = { + nid: val + for nid in gd.node_storage + if (val := gd.node_storage.get(nid)) is not None + } + + initial_events = { + nid: val + for nid in gd.event_storage + if (val := gd.event_storage.get(nid)) is not None + } + + #if initial_values: + await ws.send_bytes(msgpack.packb({"initial": {"values": initial_values, "events": initial_events}})) + + elif "unsubscribe" in data: + gd.clients[ws].pop(data["unsubscribe"]) + + elif "command" in data: + await handle_command(data["command"]) + except Exception: + if ws in gd.clients: + del gd.clients[ws] + +async def handle_command(cmd): + #print(f"Received command: {cmd}") + try: + wnc = WriteNodeCommand.model_validate(cmd) + node = gd.opc_ua_client.get_node(cmd["nodeId"]) + dv = ua.DataValue(ua.Variant(wnc.nodeValue, wnc.nodeType)) + await node.write_value(dv) + except Exception as e: + print(e) \ No newline at end of file