Skip to content

Commit 6df0802

Browse files
committed
add php/laravel example, update README
1 parent c3e5e8b commit 6df0802

File tree

17 files changed

+10406
-3
lines changed

17 files changed

+10406
-3
lines changed

README.md

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ RabbitMQ already excels at message durability and routing, but traditionally the
3535
## Installation
3636

3737
This plugin works only with modern versions of RabbitMQ 4.x based on AMQP 1.0.
38-
You can [build from source](https://www.rabbitmq.com/plugin-development.html) or you can download the latest release build from GitHub. Then, unzip and place the `rabbitmq_web_ocpp-4.x.x.ez` file into your `/etc/rabbitmq/plugins/` folder.
38+
You can [build from source](https://www.rabbitmq.com/plugin-development.html) or you can download the latest release build from GitHub. Unzip and place the `rabbitmq_web_ocpp-4.x.x.ez` file into your `/etc/rabbitmq/plugins/` folder.
3939
Like all plugins, it [must be enabled](https://www.rabbitmq.com/plugins.html) before it can be used:
4040

4141
``` bash
@@ -48,10 +48,23 @@ Detailed instructions on how to install a plugin into RabbitMQ broker can be fou
4848
Note that release branches (`v4.1.x` vs. `main`) and target RabbitMQ version need to be taken into account
4949
when building plugins from source.
5050

51+
## How It Works
52+
53+
The communication flow is straightforward:
54+
1. Messages arriving from the EVSE are sent to the configured exchange (default: `amq.topic`) with `correlation_id` set to the OCPP `messageId` and `reply_to` set to the EVSE ID.
55+
2. Configure backend worker routing on the CSMS side by creating a queue bound to the same exchange. Use routing keys in the format: `protocolver.actionname.req/conf/error`. Examples: `ocpp16.BootNotification.req`, `ocpp16.Heartbeat.conf`, `ocpp201.StatusNotification.req`. Common patterns include `ocpp16.#` for all v1.6 traffic or `*.StartTransaction.#` for billing-specific workers. See the [RabbitMQ Topics tutorial](https://www.rabbitmq.com/tutorials/tutorial-five-python#topic-exchange) for details.
56+
3. After processing and validating the message in your async worker, build a valid OCPP Response (or error) and publish it back to the same exchange with the routing key set to the EVSE ID and `correlation_id` set to the original request's OCPP `messageId`. The plugin handles sending this message back to the EVSE via the correct WebSocket connection.
57+
4. Queues can be consumed by multiple identical, stateless workers written in any programming language. Monitor queues using built-in tools (e.g., Grafana) and configure auto-scaling based on message latency or queue depth.
58+
5. If a worker throws an exception before sending a valid OCPP response, standard AMQP ACK/NACK principles apply: unconfirmed messages return to the queue for processing by another worker. Handle failure scenarios (e.g., database outages) gracefully to avoid infinite retry loops.
59+
5160
## Documentation
5261

5362
For all configuration options, please refer to the nearly identical plugin, [RabbitMQ Web MQTT guide](https://www.rabbitmq.com/web-mqtt.html).
5463

64+
## Screenshots
65+
66+
![RabbitMQ Web OCPP Management Interface](examples/screenshots/Screenshot_RabbitMQ_1.png)
67+
5568
## Enterprise-Grade Hosting & SLA Support
5669

5770
For CPOs or platform operators that need to onboard fleets of tens of thousands of chargers, our team offers cloud-native RabbitMQ HA deployments in AWS, Azure or GCP, complete with 24/7 monitoring, incident response, rolling upgrades, and expert assistance for PKI, Prometheus dashboards and OCPP-specific queue policies; we can also deliver custom feature work; we tailor service levels and cluster topologies so you can scale from pilot projects to nationwide networks without re-architecting.

examples/php/.gitattributes

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
* text=auto eol=lf
2+
3+
*.blade.php diff=html
4+
*.css diff=css
5+
*.html diff=html
6+
*.md diff=markdown
7+
*.php diff=php
8+
9+
/.github export-ignore
10+
CHANGELOG.md export-ignore
11+
.styleci.yml export-ignore

examples/php/.gitignore

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
*.log
2+
.DS_Store
3+
.env
4+
.env.backup
5+
.env.production
6+
.phpactor.json
7+
.phpunit.result.cache
8+
/.fleet
9+
/.idea
10+
/.nova
11+
/.phpunit.cache
12+
/.vscode
13+
/.zed
14+
/auth.json
15+
/node_modules
16+
/public/build
17+
/public/hot
18+
/public/storage
19+
/storage/*.key
20+
/storage/pail
21+
/vendor
22+
Homestead.json
23+
Homestead.yaml
24+
Thumbs.db

examples/php/README.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# OCPP AMQP Server (PHP/Laravel)
2+
3+
This directory contains a PHP/Laravel example for handling OCPP (Open Charge Point Protocol) messages using the RabbitMQ native OCPP to AMQP plugin. It leverages Laravel's queue system and the [solution-forest/ocpp-php](https://github.com/solution-forest/ocpp-php) library.
4+
5+
## Files
6+
7+
- `app/Jobs/OcppMessageProxy.php` - The main entry point (Job) that consumes raw OCPP messages from RabbitMQ and dispatches them to specific action handlers.
8+
- `app/Jobs/Ocpp/v16/` - Directory containing individual Job classes for each OCPP 1.6 action (e.g., `BootNotification`, `Heartbeat`).
9+
- `config/queue.php` - Configuration for the RabbitMQ connection and queue settings.
10+
- `composer.json` - PHP dependencies.
11+
12+
## Dependencies
13+
14+
- [Laravel](https://laravel.com) - The web application framework.
15+
- [laravel-queue-rabbitmq](https://github.com/vyuldashev/laravel-queue-rabbitmq) - RabbitMQ driver for Laravel queues.
16+
- [ocpp-php](https://github.com/solution-forest/ocpp-php) - Library for parsing and validating OCPP messages.
17+
18+
## Key Features
19+
20+
- **Laravel Queue Integration**: Uses the standard `php artisan queue:work` or `rabbitmq:consume` commands to process OCPP messages.
21+
- **Proxy Job Pattern**: `OcppMessageProxy` acts as a router, intercepting raw OCPP JSON arrays from the queue and dispatching them to dedicated classes.
22+
- **Schema Validation**: Incoming messages are validated against OCPP 1.6 JSON schemas using the `ocpp-php` library.
23+
- **Separation of Concerns**: Each OCPP action (like `BootNotification`, `Heartbeat`) is handled by its own Job class.
24+
25+
## Running the Worker
26+
27+
To start consuming messages from the queue:
28+
29+
```bash
30+
# Standard Laravel worker (polling)
31+
php artisan queue:work rabbitmq --queue=ocpp.worker --sleep=0.01
32+
33+
# Or using the RabbitMQ consumer (push-based, recommended for lower latency)
34+
php artisan rabbitmq:consume rabbitmq --queue=ocpp.worker
35+
```
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php
2+
3+
namespace App\Jobs\Ocpp\v16;
4+
5+
use Illuminate\Support\Facades\Log;
6+
use SolutionForest\OcppPhp\Ocpp\v16\CallResults\BootNotification as BootNotificationResponse;
7+
8+
class BootNotification
9+
{
10+
/**
11+
* Handle BootNotification request.
12+
*
13+
* @param string $messageId
14+
* @param array $data
15+
* @return array OCPP CALLRESULT: [3, messageId, payload]
16+
*/
17+
public function handle(string $messageId, array $data): array
18+
{
19+
Log::info('BootNotification received', [
20+
'messageId' => $messageId,
21+
'vendor' => $data['chargePointVendor'] ?? 'unknown',
22+
'model' => $data['chargePointModel'] ?? 'unknown',
23+
'serialNumber' => $data['chargePointSerialNumber'] ?? null,
24+
]);
25+
26+
$response = new BootNotificationResponse($messageId);
27+
$response->status = 'Accepted'; // Options: Accepted, Pending, Rejected
28+
$response->currentTime = now()->toIso8601String();
29+
$response->interval = 60; // Heartbeat interval in seconds
30+
31+
return $response->toArray();
32+
}
33+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php
2+
3+
namespace App\Jobs\Ocpp\v16;
4+
5+
use Illuminate\Support\Facades\Log;
6+
use SolutionForest\OcppPhp\Ocpp\v16\CallResults\Heartbeat as HeartbeatResponse;
7+
8+
class Heartbeat
9+
{
10+
/**
11+
* Handle Heartbeat request.
12+
*
13+
* @param string $messageId
14+
* @param array $data
15+
* @return array OCPP CALLRESULT: [3, messageId, payload]
16+
*/
17+
public function handle(string $messageId, array $data): array
18+
{
19+
Log::debug('Heartbeat received', ['messageId' => $messageId]);
20+
21+
$response = new HeartbeatResponse($messageId);
22+
$response->currentTime = now()->toIso8601String();
23+
24+
return $response->toArray();
25+
}
26+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php
2+
3+
namespace App\Jobs\Ocpp\v16;
4+
5+
use Illuminate\Support\Facades\Log;
6+
use SolutionForest\OcppPhp\Ocpp\v16\CallResults\StatusNotification as StatusNotificationResponse;
7+
8+
class StatusNotification
9+
{
10+
/**
11+
* Handle StatusNotification request.
12+
*
13+
* @param string $messageId
14+
* @param array $data
15+
* @return array OCPP CALLRESULT: [3, messageId, payload]
16+
*/
17+
public function handle(string $messageId, array $data): array
18+
{
19+
Log::info('StatusNotification received', [
20+
'messageId' => $messageId,
21+
'connectorId' => $data['connectorId'] ?? null,
22+
'status' => $data['status'] ?? 'unknown',
23+
'errorCode' => $data['errorCode'] ?? 'NoError',
24+
'timestamp' => $data['timestamp'] ?? null,
25+
]);
26+
27+
$response = new StatusNotificationResponse($messageId);
28+
// StatusNotification response has no additional fields
29+
30+
return $response->toArray();
31+
}
32+
}
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
<?php
2+
3+
namespace App\Jobs;
4+
5+
use Illuminate\Support\Facades\Log;
6+
use SolutionForest\OcppPhp\Ocpp\Exceptions\NotImplementedError;
7+
use SolutionForest\OcppPhp\Ocpp\JsonSchemaValidator;
8+
use SolutionForest\OcppPhp\Ocpp\Messages\CallError;
9+
use VladimirYuldashev\LaravelQueueRabbitMQ\Queue\Jobs\RabbitMQJob;
10+
11+
class OcppMessageProxy extends RabbitMQJob
12+
{
13+
protected $actionName = '';
14+
15+
/**
16+
* Fire the job - dispatches to specific OCPP action jobs.
17+
*
18+
* @return void
19+
*/
20+
public function fire()
21+
{
22+
$payload = $this->payload();
23+
24+
dump('OcppMessageJob received:', $payload);
25+
26+
// OCPP CALL format: [messageTypeId, messageId, action, payload]
27+
if (! is_array($payload) || count($payload) < 3) {
28+
Log::error('Invalid OCPP message format', ['payload' => $payload]);
29+
$this->delete();
30+
return false;
31+
}
32+
33+
[$messageType, $messageId, $action] = $payload;
34+
$data = $payload[3] ?? [];
35+
36+
// Only handle CALL messages (2); ignore CALLRESULT/CALLERROR
37+
if ($messageType !== 2) {
38+
Log::debug('Ignoring non-CALL message', ['messageType' => $messageType]);
39+
$this->delete();
40+
return false;
41+
}
42+
43+
// Resolve Call Class from Library
44+
$callClass = "SolutionForest\\OcppPhp\\Ocpp\\v16\\Calls\\{$action}";
45+
46+
if (!class_exists($callClass)) {
47+
Log::warning("Unknown OCPP Action (Library class not found): {$action}");
48+
$error = new NotImplementedError($messageId);
49+
$response = $error->toArray();
50+
dump('OcppMessageJob response (NotImplemented - Unknown Action):', $response);
51+
Log::info('OCPP Response ready to send', ['response' => $response]);
52+
$this->delete();
53+
return false;
54+
}
55+
56+
// Instantiate and hydrate Call object for validation
57+
/** @var \SolutionForest\OcppPhp\Ocpp\Messages\Call $callObject */
58+
$callObject = new $callClass();
59+
$callObject->messageId = $messageId;
60+
foreach ($data as $key => $value) {
61+
$callObject->$key = $value;
62+
}
63+
64+
// Validate incoming OCPP message using built-in JSON schema validator
65+
$validationResult = JsonSchemaValidator::validate($callObject, 'v1.6', false);
66+
67+
// If validation fails, validator already built a proper CallError
68+
if ($validationResult instanceof CallError) {
69+
Log::warning("OCPP message validation failed for action: {$action}", [
70+
'messageId' => $messageId,
71+
'errorCode' => $validationResult->errorCode,
72+
'errorDescription' => $validationResult->errorDescription,
73+
]);
74+
$response = $validationResult->toArray();
75+
}
76+
77+
// Build job class name: App\Jobs\Ocpp\v16\{Action}
78+
$jobClass = "App\\Jobs\\Ocpp\\v16\\{$action}";
79+
80+
if (!class_exists($jobClass)) {
81+
Log::warning("No job found for action: {$action}, sending NotImplemented");
82+
$error = new NotImplementedError($messageId);
83+
$response = $error->toArray();
84+
}
85+
86+
// Instantiate and execute the specific OCPP job
87+
$job = app($jobClass);
88+
$response = $job->handle($messageId, $data);
89+
90+
if ($response) {
91+
dump('OcppMessageJob response:', $response);
92+
Log::info('OCPP Response ready to send', ['response' => $response]);
93+
94+
$this->respond($response);
95+
}
96+
97+
$this->delete();
98+
return true;
99+
}
100+
101+
public function getName()
102+
{
103+
return $this->actionName;
104+
}
105+
106+
public function respond($response)
107+
{
108+
$broker = $this->getRabbitMQ();
109+
$message = $this->getRabbitMQMessage();
110+
111+
if ($message->has('reply_to')) {
112+
$broker->getChannel()->basic_publish(
113+
new \PhpAmqpLib\Message\AMQPMessage(
114+
json_encode($response),
115+
['correlation_id' => $message->get('correlation_id') ?? '']
116+
),
117+
'amq.topic',
118+
$message->get('reply_to')
119+
);
120+
}
121+
}
122+
}

examples/php/artisan

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
#!/usr/bin/env php
2+
<?php
3+
4+
use Illuminate\Foundation\Application;
5+
use Symfony\Component\Console\Input\ArgvInput;
6+
7+
define('LARAVEL_START', microtime(true));
8+
9+
// Register the Composer autoloader...
10+
require __DIR__.'/vendor/autoload.php';
11+
12+
// Bootstrap Laravel and handle the command...
13+
/** @var Application $app */
14+
$app = require_once __DIR__.'/bootstrap/app.php';
15+
16+
$status = $app->handleCommand(new ArgvInput);
17+
18+
exit($status);

examples/php/bootstrap/app.php

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?php
2+
3+
use Illuminate\Foundation\Application;
4+
use Illuminate\Foundation\Configuration\Exceptions;
5+
use Illuminate\Foundation\Configuration\Middleware;
6+
7+
return Application::configure(basePath: dirname(__DIR__))
8+
->withRouting(
9+
// web: __DIR__.'/../routes/web.php',
10+
// commands: __DIR__.'/../routes/console.php',
11+
health: '/up',
12+
)
13+
->withMiddleware(function (Middleware $middleware): void {
14+
//
15+
})
16+
->withExceptions(function (Exceptions $exceptions): void {
17+
//
18+
})->create();

0 commit comments

Comments
 (0)