A lightweight BDEW API server that receives power control commands and publishes them to MQTT in CLS.EEDI format.
- BDEW API endpoints for limit and reset commands
- Command scheduling for future execution
- MQTT publishing in CLS.EEDI format
- ACK message handling with 30-second timeout
- Webhook callbacks (preliminary and final responses based on ACK)
- Environment-based configuration
Set these environment variables (or create a .env file):
MQTT_URL: MQTT broker URL (default: mqtt://localhost:1883)WEBHOOK_BASE_URL: Base URL for webhook callbacks (required)SERVER_HOST: Server bind address (default: 0.0.0.0)SERVER_PORT: Server port (default: 3000)
cargo runSend a limit command:
curl -X POST 'http://localhost:3000/[Post]/steuerbefehl/konfiguration/?locationId=C1234848431&commandControl=%7B%22maximumPowerValue%22%3A%2210.5%22%2C%22executionTimeFrom%22%3A%222026-01-10T12%3A00%3A00Z%22%7D' \
-H 'transactionId: f81d4fae-7dec-11d0-a765-00a0c91e6bf6' \
-H 'creationDateTime: 2026-01-09T10:00:00Z'Send a reset command:
curl -X POST 'http://localhost:3000/[Post]/steuerbefehl/initialZustand/?locationId=C1234848431&commandRegular=%7B%22executionTimeFrom%22%3A%222026-01-10T12%3A00%3A00Z%22%7D' \
-H 'transactionId: a2c3d4e5-1234-5678-9abc-def012345678' \
-H 'creationDateTime: 2026-01-09T10:00:00Z'- Receive Command: BDEW API endpoint receives limit or reset command
- Validation: Reject commands only if entirely in the past (both start and end times are past). Commands with past start times but future end times (or no end time) are accepted and execute immediately with a warning.
- Cancel Existing Commands: Any existing pending command for the same
location_idis cancelled. This ensures only one active command per location (per BDEW specification). - Preliminary Response: Send preliminary positive webhook immediately
- Schedule: Schedule command for future execution based on
executionTimeFrom - Execute: At scheduled time, publish CLS.EEDI message to MQTT topic
clseedi/to-localdevice/{locationId} - Wait for ACK: Subscribe to
clseedi/from-localdevice/#topic and wait up to 30 seconds - Final Response:
- If ACK errorNumber == 0: Send final positive webhook
- If ACK errorNumber != 0: Send final negative webhook (reason: "communicationFailure")
- If ACK timeout or MQTT error: Send final negative webhook (reason: "unable")
According to the BDEW specification, sending a new command for a location automatically cancels any existing pending command:
- Reset command (
/[Post]/steuerbefehl/initialZustand/): Cancels any pending limit command and schedules a reset - New limit command (
/[Post]/steuerbefehl/konfiguration/): Replaces any existing pending limit command with the new one
The service follows the CLS.EEDI MQTT topic recommendations:
- Command (publish):
clseedi/to-localdevice/{locationId}- Control messages from backend to device - ACK (subscribe):
clseedi/from-localdevice/#- ACK responses from devices to backend
Location IDs are percent-encoded according to the CLS.EEDI specification. Special characters (unprintable characters, #, $, +, /, , %) are replaced with %XX hex encoding.
BDEW API uses kilowatts (kW), CLS.EEDI uses watts (W). The service automatically converts by multiplying by 1000.
ACK messages must match the control message ID (transaction ID) and include an errorNumber:
errorNumber == 0: SuccesserrorNumber != 0: Failure
Example ACK message published to clseedi/from-localdevice/C1234848431:
{
"type": "de.keo-connectivity.clseedi.ack",
"source": "device",
"id": "f81d4fae-7dec-11d0-a765-00a0c91e6bf6",
"specversion": "1.0",
"data": {
"protocol": "1.2.0",
"errorNumber": 0
}
}To manually test with mosquitto:
# Publish ACK for location C1234848431
mosquitto_pub -t 'clseedi/from-localdevice/C1234848431' -m '{"type":"de.keo-connectivity.clseedi.ack","source":"device","id":"f81d4fae-7dec-11d0-a765-00a0c91e6bf6","specversion":"1.0","data":{"protocol":"1.2.0","errorNumber":0}}'