From 956c686d53175d0e49c273f6e5a15958c2aaa6fe Mon Sep 17 00:00:00 2001 From: drazere Date: Fri, 8 May 2026 16:18:56 +0530 Subject: [PATCH] fixed readme, added nix files, added multiple instance support --- Makefile | 30 ++--- README.md | 324 +++++++++++++++++++++++++++++++++++++++--------- ocppsim.ini | 9 +- ocppsim.py | 18 ++- python-ocpp.nix | 40 ++++++ shell.nix | 22 ++++ 6 files changed, 365 insertions(+), 78 deletions(-) create mode 100644 python-ocpp.nix create mode 100644 shell.nix diff --git a/Makefile b/Makefile index ffaa3e7..476786a 100644 --- a/Makefile +++ b/Makefile @@ -5,32 +5,32 @@ ifeq ($(OS), Windows_NT) copy=copy FixPath=$(subst /,\,$1) cmdsep=& - IS_POETRY := $(shell pip freeze | find "poetry==") PLATFORM=Windows else copy=cp FixPath=$1 cmdsep=; - IS_POETRY := $(shell pip freeze | grep "poetry==") PLATFORM=Linux endif -.install-poetry: -ifndef IS_POETRY - @echo Installing Poetry... - pip install poetry -endif +POETRY ?= poetry + +update: + $(POETRY) update + +install: + $(POETRY) install -update: .install-poetry - poetry update +format: + $(POETRY) run isort $(wildcard *.py) + $(POETRY) run black --exclude .venv . +# $(POETRY) run flake8 $(wildcard *.py) -install: .install-poetry - poetry install +run: + $(POETRY) run python main.py -format: .install-poetry - poetry run isort $(wildcard *.py) - poetry run black --exclude .venv . -# poetry run flake8 $(wildcard *.py) +shell: + $(POETRY) shell docker: @docker build -t ocppsim . diff --git a/README.md b/README.md index 75cb26c..81348d8 100644 --- a/README.md +++ b/README.md @@ -1,86 +1,277 @@ -# ocpp-simulator +# OCPP Simulator -OCPP v1.6 Charge Point (CP) Simulator used to test OCPP Central Systems (CS). +Python-based OCPP 1.6 Charge Point (CP) Simulator used for testing OCPP Central Systems (CSMS). -## Build +This repository has been extended with: -Clone the repository: +- Configurable INI-based charger profiles +- WebSocket command/control interface +- Custom CSMS endpoint support +- Nix / NixOS development environment support +- Docker support +- Interactive testing workflow +- Local OCPP backend testing + +--- + +# Features + +- OCPP 1.6 charge point simulator +- Multiple charger profiles via config files +- WebSocket-based command interface +- Remote CSMS connectivity +- Easy testing of: + - BootNotification + - StartTransaction + - StopTransaction + - MeterValues + - Authorize + - RemoteStartTransaction + - RemoteStopTransaction +- Scriptable automation support +- Works on Linux and NixOS + +--- + +# Repository Structure ```text +. +├── Makefile +├── ocppsim.py +├── ocppsim.ini +├── python-ocpp.nix +├── shell.nix +└── README.md +``` + +--- + +# Clone Repository + +```bash git clone https://github.com/ocpp-balanz/ocppsim.git cd ocppsim ``` -Install dependencies: +--- -```text +# Running on NixOS / Nix + +This repository includes Nix support for reproducible environments. + +## Enter Development Shell + +```bash +nix-shell +``` + + + +The shell automatically provides: + +- Python +- OCPP dependencies +- WebSocket dependencies +- Build tools + +--- + +# Installing Dependencies (non-Nix) + +```bash make install ``` -## Start +or manually: + +```bash +pip install -r requirements.txt +``` -Run `python ocppsim.py -h` to retrieve the following help message: +--- + +# Configuration File + +The simulator now supports external configuration through: ```text -usage: ocppsim.py [-h] [--version] [--config CONFIG] [--port PORT] [--id ID] +ocppsim.ini +``` + +This allows configuring: + +- Charger ID +- CSMS WebSocket endpoint +- Authentication settings +- Logging +- Meter values +- Timing behavior +- Default RFID tags + +Example: + +```ini +[logging] +ocppsim = DEBUG +ocpp = DEBUG + +[host] +addr = 0.0.0.0 +port = 1234 + +[server] +url = wss://your-csms.example.com/charger-01 +http_auth = False + +[charger] +id = charger-01 +vendor = ExampleVendor +model = ExampleModel +``` + +--- + +# Running the Simulator + +Basic usage: + +```bash +python ocppsim.py +``` + +Using a custom config: + +```bash +python ocppsim.py --config ocppsim.ini +``` + +Override charger ID: + +```bash +python ocppsim.py --config ocppsim.ini --id ELECT1 +``` -OCCPSIM - OCCP v1.6 Charge Point Simulator with Websocket Control Interface +Override command interface port: -options: - -h, --help show this help message and exit - --version show program's version number and exit - --config CONFIG Configuration file (INI format). Default ocpp.ini - --port PORT Command Interface Port. Default in config file. - --id ID Charger Id. Default in config file. +```bash +python ocppsim.py --port 1234 ``` -The configuration file is intended to configure a _type_ of charger, with the actual charger id supplied at startup. Review the file in detail to understand how to configure occpsim. +Show help: -Start simulating a charger. +```bash +python ocppsim.py -h +``` + +--- + +# Command Interface + +The simulator exposes a WebSocket control interface. + +Example: + +```bash +websocat ws://localhost:1234 +``` + +You can then issue commands interactively. + +--- - python ocppsim.py --id TACW6243111G2672 --port 1234 +# Supported Commands -Next, connect to the port using e.g. [websocat](https://github.com/vi/websocat) to interact with the simulator through the simplistic command interface. +| Command | Description | +| -------------- | -------------------------- | +| `wait [sec]` | Wait some seconds | +| `status` | Get simulator status | +| `jsonstatus` | Return JSON status | +| `plugin` | Plug in EV | +| `unplug` | Disconnect EV | +| `tag [id_tag]` | Simulate RFID scan | +| `full` | Simulate EV battery full | +| `delay` | Delay charging | +| `nodelay` | Remove charging delay | +| `suspend` | Suspend charging | +| `resume` | Resume charging | +| `max [amps]` | Set charging current limit | +| `reset` | Reset simulator state | +| `shutdown` | Shutdown simulator | - websocat ws://localhost:1234 +--- -Alternatively, use the included test driver `ocppsim_test.py` in interactive mode as follows: +# Connecting to a CSMS - python ocppsim_test.py --url ws://localhost:321 +Set the backend URL in `ocppsim.ini`: -`occpsim_test.py` may also be imported as a module and used in test scripts, possibly connecting to multiple simulators. +```ini +[server] +url = wss://your-csms.example.com/charger-01 +``` + +Examples: -## Supported commands +```ini +url = ws://192.168.1.50:8080/ocpp/charger-01 +``` -The command interface is very simply. Enter a command and retrieve a one line response. The simulator will then execute the appropriate logic. +```ini +url = wss://csms.example.com/ws/ELECT1 +``` + +Most production systems use: + +- `wss://` +- Port `443` + +--- + +# Docker Support + +Build image: + +```bash +docker build -t ocppsim . +``` + +Run container: + +```bash +docker run --network host ocppsim +``` -Commands | Description ------------------- | ---------------------------------------------------------------- -`wait [sec]` | Wait some seconds. Default 5 sec -`status` | Get the internal status (does not send/receive anything) -`jsonstatus` | Receive JSON object with charger attributes. Useful for automation. -`full` | Emulate that EV is full/does not want to charge more -`fullafter [wh]` | Emulate that EV is full after having received at least [wh] -`delay` | Set delayed charing. EV will not start charging -`nodelay` | Charging no longer delayed -`delay_notrans` | Set delayed charging mode to NOT start a transaction until time is up -`delay_trans` | Restore to normal mode, i.e. DO start transaction if offer and delay -`plugin` | Cable is plugged into EV -`unplug` | Unplug cable/disconnect EV -`tag [id_tag]` | RFID tag is scanned. Default tag in config file -`clock [offset]` | Adjust timestamps send by offset seconds, e.g. clock -3600 -`suspend` | EV suspends charging (for some reason) -`resume` | EV restarts charging (for some reason) -`max [Amps]` | Set maximum charging usage in Amps. Reset without argument. -`reset` | Reset the simulator (clear all state) -`exit` | Exit the command session -`shutdown` | Shutdown the simulator (process will stop) +Using host networking is recommended for local CSMS testing. -## Automating Tests +--- -The simulator can be used to automate tests. For example, you could write a script that sends commands and checks the responses. See a `pytest` based example in the `test_ocppsim_example.py` file. +# Example Local Testing Setup -## Supported CP to CS messages +## Start simulator + +```bash +python ocppsim.py --config ocppsim.ini +``` + +## Connect command interface + +```bash +websocat ws://localhost:1234 +``` + +## Simulate charging flow + +```text +plugin +tag TESTTAG +status +``` + +--- + +# Supported OCPP Messages + +## CP -> CSMS - BootNotification - StatusNotification @@ -90,19 +281,38 @@ The simulator can be used to automate tests. For example, you could write a scri - StopTransaction - TriggerMessage -## Supported CS to Charger (CP) messages +## CSMS -> CP - SetChargingProfile - ClearChargingProfile - Reset -- ChangeConfiguration (only actively uses AuthorizationKey during http authentication if set) +- ChangeConfiguration - RemoteStartTransaction - RemoteStopTransaction - GetConfiguration -- ChangeConfiguration -## Limitations +--- + +# Notes + +- Single connector currently supported +- Some advanced smart charging features are limited +- TLS support depends on backend configuration +- Use `wss://` for secure production deployments + +--- + +# Useful Tools + +- websocat +- Wireshark +- SteVe +- CitrineOS +- Open Charging Cloud + +--- + +# License + +See upstream repository license information. -- Charging profile evaluation assuming timing "forever" -- Currently only chargers with a single connector/outlet is supported. -- Some messages not supported diff --git a/ocppsim.ini b/ocppsim.ini index 690c48c..2cc5abb 100644 --- a/ocppsim.ini +++ b/ocppsim.ini @@ -10,9 +10,11 @@ port = 1234 [server] ; OCCP URL to connnect to (charger_id will be appended) +; input the URL without the charger id, but with token url = ws://localhost:9999/ -; HTTP Basic Auth login. -http_auth = True + +; HTTP Basic Auth login. Leave it false +http_auth = False [charger] ; Charger Id default Can/should be overwritten using --id argument @@ -30,8 +32,11 @@ default_profiles = True ; Various charger model information (will be sent in boot_notification) info_charge_point_vendor = ABB info_charge_point_model = CDT_TACW22::NET_WIFI + +;You might want to edit these info_charge_point_serial_number = avt.001.13.1 info_charge_box_serial_number = avt.001.13.1.02 + info_firmware_version = TAC3Z91180710247::V1.8.21 info_iccid = info_imsi = diff --git a/ocppsim.py b/ocppsim.py index 415c6c7..679cfff 100644 --- a/ocppsim.py +++ b/ocppsim.py @@ -914,6 +914,9 @@ async def main(): host = config.get("host", "addr") port = args.port if args.port else config.get("host", "port") charger_id = args.id if args.id else config.get("charger", "charger_id") + if args.id: + config.set("charger", "info_charge_point_serial_number", args.id) + config.set("charger", "info_charge_box_serial_number", args.id) # Connect to OCPP Server and start headers = {} @@ -925,10 +928,17 @@ async def main(): # BALANZ_SERVER_URL environment variable - if set - takes precedence env_url = os.getenv("BALANZ_SERVER_URL", None) - if env_url: - url = env_url + charger_id - else: - url = config.get("server", "url") + charger_id + from urllib.parse import urlparse, urlunparse + + base_url = env_url if env_url else config.get("server", "url") + + parsed = urlparse(base_url) + + path = parsed.path.rstrip("/") + f"/{charger_id}" + + url = urlunparse(parsed._replace(path=path)) + print(url) + logging.info(f"Connecting to {url}") try: ws = await websockets.connect(url, subprotocols=["ocpp1.6"], additional_headers=headers) diff --git a/python-ocpp.nix b/python-ocpp.nix new file mode 100644 index 0000000..6215420 --- /dev/null +++ b/python-ocpp.nix @@ -0,0 +1,40 @@ +{ + lib, + buildPythonPackage, + fetchPypi, + setuptools, + wheel, + poetry-core, + jsonschema, +}: + +buildPythonPackage rec { + pname = "ocpp"; + version = "2.1.0"; + + src = fetchPypi { + inherit pname version; + hash = "sha256-0pv6+3Fh91p4MU9c/RuC/fFVCCzQ9N5IxNyOV0FCLPA="; + }; + + pyproject = true; + + build-system = [ + setuptools + wheel + poetry-core + ]; + + dependencies = [ + jsonschema + ]; + + + doCheck = false; + + meta = with lib; { + description = "Python OCPP implementation"; + homepage = "https://github.com/mobilityhouse/ocpp"; + license = licenses.mit; + }; +} diff --git a/shell.nix b/shell.nix new file mode 100644 index 0000000..a15a4da --- /dev/null +++ b/shell.nix @@ -0,0 +1,22 @@ +{ pkgs ? import {} }: +let + python-ocpp = pkgs.python313Packages.callPackage ./python-ocpp.nix {}; +in +pkgs.mkShell { + packages = with pkgs; [ + python313 + python313Packages.pip + #python313Packages.poetry-core + gcc + poetry + python313Packages.websockets + (python313.withPackages (ps: [ + python-ocpp + ])) + ]; + + shellHook = '' + export POETRY_VIRTUALENVS_IN_PROJECT=true + export POETRY_VIRTUALENVS_CREATE=true + ''; +}