Development Environment
This page describes how to develop a Python application for WinCC OA on a workstation, keep it under source control, and verify it on a build node that has no WinCC OA installation. For running a finished script inside a project, see Getting Started.
Scenario
The typical setup involves three roles. They may be on the same machine, but separating them keeps the repository self-contained.
| Role | WinCC OA installed | Purpose |
|---|---|---|
| Workstation | recommended | Write code in an IDE, run and debug against a local test project |
| Build node | no | Lint, type-check, run unit tests, and package the application |
| Target system | yes | Runs the application as a Python manager |
The steps on this page fall into three categories.
| Step | Category |
|---|---|
| Install the CPython version released for your operating system | mandatory |
Make the etm-wccoa package available in the repository |
mandatory |
Make the etm-wccoa-manager package available in the repository |
recommended |
Make extension packages such as etm-wccoa-msa available to the repository, if the application uses them |
optional |
| Work in a virtual environment | recommended |
| Provide the wheels independently of a local installation | recommended |
| Separate application logic from the manager | recommended |
| Type checking and linting | optional |
| A local WinCC OA project for integration tests | optional |
Core Packages
The core of the API consists of the following distribution packages, which share the etm.wccoa namespace.
| Package | Content | In the repository | Importable without a WinCC OA installation |
|---|---|---|---|
etm-wccoa
|
Data types, exceptions, the OaScriptingApi interface |
mandatory | yes |
etm-wccoa-manager
|
Manager, CtrlScript |
recommended | no |
etm-wccoa is pure Python and has no dependencies.
It can be installed on any machine and covers everything that is not manager-specific.
etm-wccoa-manager contains Python sources only.
The native extension is not part of the wheel.
It belongs to the runtime component PythonEnv
and is loaded from the bin/ directory of the WinCC OA installation
when the package is imported.
Provide the manager package in your repository as well. The entry point of your application is manager-specific code, and without the package, neither the IDE nor a type checker can resolve it. Both analyse the sources without importing them, so they work without an installation.
Executing that code is a different matter.
On a machine without a WinCC OA installation, import etm.wccoa.manager fails
with a ModuleNotFoundError for the native extension, even when the wheel is installed.
Unit tests must therefore not import it — see Writing Testable Code.
Extension Packages
Extensions of the API are separate distribution packages in the same etm.wccoa namespace.
A repository needs them only if the application uses them.
| Package | Content | Depends on | Importable without a WinCC OA installation |
|---|---|---|---|
etm-wccoa-msa
|
Manager Service API, see MSA |
etm-wccoa, etm-wccoa-manager |
yes, but stubs and services need a running manager |
An extension package contains Python sources only.
Its native part is in the extension module of etm-wccoa-manager,
so its version must match that package exactly.
Creating a Virtual Environment
Use the CPython version released for your operating system, see Installation. A virtual environment keeps the dependencies of your application separate from the system interpreter.
python -m venv .venv
.venv\Scripts\Activate.ps1
python3 -m venv .venv
source .venv/bin/activate
Installing the Packages
The wheels are part of the developer component APIPython
and are installed in <WINCCOA_ROOT>/api.python/packages/.
The examples below show the core packages.
An extension package such as etm-wccoa-msa is added in the same way and in the identical version.
There are three ways to get the wheels into your virtual environment. The first is the quickest, the other two make the repository independent of a local WinCC OA installation and are therefore the ones to use as soon as a build node is involved.
From the WinCC OA Installation
pip install <WINCCOA_ROOT>\api.python\packages\etm_wccoa-<version>-py3-none-any.whl
pip install <WINCCOA_ROOT>\api.python\packages\etm_wccoa_manager-<version>-py3-none-any.whl
This works without any further infrastructure, but it ties the repository to a local installation.
From a Private Package Index
Upload the wheels to the package index of your organization — for example Artifactory, Nexus or Azure Artifacts — and depend on them by name and version:
etm-wccoa==<version>
etm-wccoa-manager==<version>
Configure the index in pip.conf or pip.ini, or pass it with --index-url.
This keeps the repository free of binary artifacts
and lets several applications share the same API version.
From the Repository
If no package index is available, commit the wheels to the repository,
for example to a vendor/ directory, and reference them from requirements.txt:
./vendor/etm_wccoa-<version>-py3-none-any.whl
./vendor/etm_wccoa_manager-<version>-py3-none-any.whl
pip install -r requirements.txt
The build node then needs nothing but CPython and the repository.
With either variant, keep the wheel version in sync with the WinCC OA version of the target system.
Writing Testable Code
Manager implements the abstract class OaScriptingApi from the etm-wccoa package.
Write your application logic against that interface and pass the manager in from the outside.
from etm.wccoa.services.protocols import OaScriptingApi
class TemperatureMonitor:
def __init__(self, api: OaScriptingApi) -> None:
self._api = api
def is_critical(self, dp: str, limit: float) -> bool:
return float(self._api.dp_get(dp)) > limit
The entry point is the only place that creates the manager:
from etm.wccoa.manager import Manager
with Manager() as manager:
monitor = TemperatureMonitor(manager)
This keeps the import of etm.wccoa.manager out of the modules under test,
so that the unit tests can run on a build node without a WinCC OA installation.
Unit Testing Without a Running Project
With the logic separated as shown above, a test double is enough:
from unittest.mock import MagicMock
def test_is_critical() -> None:
api = MagicMock()
api.dp_get.return_value = 95.0
assert TemperatureMonitor(api).is_critical("Boiler.Temp", limit=90.0)
Data types such as OaVariant, DpIdentifier or OaQueryResult come from the etm-wccoa package
and can be constructed in tests directly.
Quality Checks on a Build Node
The build node needs CPython, the repository and the wheels of the packages it uses — no WinCC OA installation.
Linters and type checkers analyse the sources without importing them,
so they cover manager-specific code such as your entry point.
Tests are different because they execute the code:
everything that imports etm.wccoa.manager has to be excluded from a build node run.
The API itself is developed with ruff for linting and formatting and with mypy in strict mode.
Using the same tools for your application is recommended but not required.
All packages ship a py.typed marker,
so type checkers and IDEs resolve their annotations without additional stubs.
Running a Script Against a Local Project
The most reliable way to test a script is to register it as a manager in a local project and start it from the Console, as described in Getting Started.
To start a script directly from the development environment instead — for example under a debugger — the environment that PMON normally prepares must be set up manually:
$env:PYTHONPATH = "<WINCCOA_ROOT>\python\libs;<WINCCOA_ROOT>\bin"
$env:WCCOA_BIN_DIRS = "<WINCCOA_ROOT>\bin"
python my_script.py -proj <project> -num 50
The manager number must not collide with a scripting-manager (CTRL, NodeJS, Python) that is already running. Use the interpreter of the version released for your operating system. The native extension does not load with an older version.
Deployment
Two prerequisites are installed on the target system independently of your application:
WinCC OA including the PythonEnv component, and CPython.
See Installation.
Neither of them is part of what you build and publish.
What the Artifact Contains
| Content | Part of the artifact |
|---|---|
| Your scripts and modules | yes |
| Third-party packages your application depends on | yes |
etm-wccoa, etm-wccoa-manager and extension packages such as etm-wccoa-msa |
no, they come with the WinCC OA installation |
| Native extension and WinCC OA libraries | no, they come with the WinCC OA installation |
| CPython | no, it is installed separately |
Leaving the API packages out allows the same artifact to run on any WinCC OA installation of the matching version.
Collecting the Dependencies
PMON puts the python/libs/ directory at every level of the project search path
on the PYTHONPATH of the manager process.
That is where third-party packages belong on the target system.
Build that layout on the build node:
pip install -r requirements.txt --target dist/python/libs
Copy-Item src/*.py dist/python/
The result mirrors the structure of the project:
dist/
└── python/
├── my_script.py
└── libs/
└── <third-party packages>
pip resolves wheels for the platform and CPython version of the machine it runs on.
If a dependency contains native code — numpy for example —
build the artifact on a machine that matches the target system,
or pass --platform, --python-version and --only-binary=:all: to pip.
Pure Python dependencies are not affected.
The layout above shares one directory with every other Python manager on the target system.
Install into dist/python/<application>/libs instead
if the application is to keep its dependencies to itself,
as described in Several Applications on One System.
Publishing
| Channel | Suitable for |
|---|---|
| Artifact repository such as Artifactory, Azure Artifacts or Nexus | Versioned releases, the usual choice where a build pipeline exists |
Operating system package, for example .deb, .rpm or MSI |
Plants that are provisioned through their operating system |
| Archive on a file share | Small installations and manual roll-out |
Whichever channel you use, publish the tree above as one versioned unit and record the WinCC OA version it was built against.
Installing on the Target System
- Install WinCC OA including
PythonEnv, and CPython. This is done once per system, not once per application. - Unpack the artifact so that the scripts end up in
<project>/python/and the dependencies in<project>/python/libs/. A subproject is an alternative target that keeps the application separate from the project configuration. - Add the manager in the Console, as described in Getting Started.
- Restart the manager to activate new code.
Virtual Environments
A virtual environment is the natural choice on the workstation and on the build node. On the target system, it needs more thought.
PMON starts the first python executable it finds in a bin/ directory of the project search path
and otherwise leaves the lookup to the operating system.
It then sets PYTHONPATH and WCCOA_BIN_DIRS for the manager process.
It does not activate a virtual environment.
A virtual environment, therefore, only takes effect if the interpreter that PMON starts belongs to it,
which means the environment has to be active for the process monitor itself.
Deploying the dependencies to <project>/python/libs/ avoids that dependency on the process environment,
which is why it is the approach described above.
Several Applications on One System
Each Python manager runs as its own operating system process.
Two managers can therefore use different versions of the same package at runtime without interfering with each other.
What they share is the search path: PMON adds the same python/libs/ directories to the PYTHONPATH of every Python manager.
A package that exists there is available in exactly one version for all of them.
This matters as soon as applications with independent release cycles run on the same system.
Subprojects do not solve it, because their python/libs/ directory is on the search path of every manager as well
and it only changes the order in which packages are found.
Give each application a directory of its own below python/ and keep its dependencies inside it:
<project>/python/
├── plant_monitor/
│ ├── main.py
│ ├── monitoring.py
│ └── libs/
│ └── <dependencies of plant_monitor>
└── report_export/
├── main.py
└── libs/
└── <dependencies of report_export>
Enter the script as plant_monitor/main.py in the Console.
PMON resolves the path below the python/ directory of every level of the project search path.
CPython puts the directory of the started script at the front of sys.path,
so the modules next to main.py are importable without further setup.
The dependencies require one line at the top of the entry script:
import os
import sys
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "libs"))
from etm.wccoa.manager import Manager # noqa: E402
Keep this line in the entry script only, never in a module, and place it before the first import of a third-party package so that the entries provided by PMON are consulted afterwards. The WinCC OA packages come from the installation and are the same for every manager; importing them after this line is intentional.
Because the directory of the application is the first entry of sys.path,
a module named like one of the standard library — logging.py or queue.py for example —
hides the standard module from the whole process.
Choose names that cannot collide.
Use python/libs/ for packages that all applications on the system share
and the libs/ directory of an application for everything with its own release cycle.
