This document specifies the behavior and feature set of the vcontrold Docker container.
A containerized wrapper around vcontrold (Viessmann heating controller interface) that provides:
- Serial communication with Viessmann heating systems via Optolink/FTDI USB adapter
- Automated periodic polling of heating controller parameters
- MQTT bridge for remote query and control
- JSON response formatting
| Feature | Status |
|---|---|
| vcontrold daemon management | Done |
| Native TCP client (persistent connection) | Done |
| Command batching algorithm | Done |
| Periodic polling loop | Done |
| MQTT v5 publishing | Done |
| Request/response bridge | Done |
| TLS support (rustls) | Done |
| Client certificate auth | Done |
| Insecure TLS mode | Done |
| Environment variable parsing | Done |
| Debug logging | Done |
| Unit tests | Done |
| Health check endpoint | Done |
| Production Dockerfile | Done |
| Devcontainer | Done |
| Variable | Description |
|---|---|
MQTT_HOST |
Broker hostname/IP |
MQTT_TOPIC |
Base topic prefix (e.g., vcontrold) |
| Variable | Default | Description |
|---|---|---|
USB_DEVICE |
/dev/vitocal |
Serial device path inside container |
MAX_LENGTH |
512 |
Max character length per command batch |
MQTT_SUBSCRIBE |
false |
Enable request/response bridge |
MQTT_PORT |
1883 |
Broker TCP port |
MQTT_USER |
"" |
Username (empty = anonymous) |
MQTT_PASSWORD |
"" |
Password |
MQTT_CLIENT_ID_PREFIX |
vcontrold |
Prefix for MQTT client IDs |
MQTT_TIMEOUT |
10 |
Publish timeout in seconds |
MQTT_TLS |
false |
Enable TLS encryption |
MQTT_CAFILE |
"" |
CA certificate file path |
MQTT_CAPATH |
"" |
CA certificate directory path |
MQTT_CERTFILE |
"" |
Client certificate path |
MQTT_KEYFILE |
"" |
Private key path |
MQTT_TLS_VERSION |
"" |
TLS version hint (e.g., tlsv1.2) |
MQTT_TLS_INSECURE |
false |
Skip certificate validation |
INTERVAL |
60 |
Seconds between polling cycles |
COMMANDS |
"" |
Comma-separated list of command names to poll |
DEBUG |
false |
Enable verbose logging |
HEALTHCHECK_PORT |
8080 |
TCP port for the health check HTTP endpoint |
Runs vcontrold with the user-provided XML configuration:
- Normal:
vcontrold -n -x /config/vcontrold.xml - Debug (
DEBUG=true):vcontrold -n -x /config/vcontrold.xml --verbose --debug
The container exits if vcontrold dies.
When COMMANDS is set:
Topic: ${MQTT_TOPIC}/command/<command_name>
Payload: Numeric or string value only
Retained: Yes
Protocol: MQTT v5
Example:
Topic: vcontrold/command/getTempWWObenIst
Payload: 48.1
When MQTT_SUBSCRIBE=true:
Request Topic: ${MQTT_TOPIC}/request
Response Topic: ${MQTT_TOPIC}/response
Response Retained: Yes
Single command:
getTempWWObenIst
Multiple commands (comma-separated):
getTempWWObenIst,getTempWWsoll
Write command (space-separated value):
setTempWWsoll 50
Multiple mixed operations:
set1xWW 2,setTempWWsoll 50,getTempA
JSON with flat structure (vclient -j style):
{"getTempWWObenIst":48.1}{"getTempWWObenIst":48.1,"getTempWWsoll":50}{"setTempWWsoll":"OK"}Errors are included with error message as value.
The Rust implementation uses direct TCP communication to vcontrold instead of shelling out to vclient:
Client -> Server: command\n
Server -> Client: value unit\n
Server -> Client: vctrld>
Error responses start with ERR:.
- Single persistent connection (reduces latency)
- No process spawning overhead per command
- Better error handling and automatic reconnection
- Reduced resource usage
- Parse
COMMANDSas comma-separated list - Batch commands into groups respecting
MAX_LENGTHcharacter limit - For each batch:
- Execute commands via persistent TCP connection
- Parse responses
- Publish each value to
${MQTT_TOPIC}/command/<name>
- Sleep
INTERVALseconds - Repeat
batch = ""
for each command in COMMANDS:
if length(batch + "," + command) > MAX_LENGTH:
execute_batch(batch)
batch = command
else:
batch = batch + "," + command
execute_batch(batch)
vcontrold returns responses in format:
value unit
The parser extracts:
- Numeric values (float or integer)
- String values (for status/error responses)
- Unit information (for logging)
- Connect to MQTT broker
- Subscribe to
${MQTT_TOPIC}/request - For each message:
- Skip empty payloads
- Parse comma-separated commands
- Execute each command via TCP connection
- Build JSON response
- Publish response to
${MQTT_TOPIC}/response
- On disconnect: automatic reconnection via rumqttc
| Condition | Behavior |
|---|---|
Missing /config/vcontrold.xml |
Exit code 1, log error |
| vcontrold crashes on startup | Exit code 1, log error |
| vcontrold fails readiness probe (30s) | Exit code 1, log error |
Missing MQTT_HOST or MQTT_TOPIC |
Exit code 1, log error |
| Condition | Behavior |
|---|---|
| vcontrold process dies | Exit container immediately |
| TCP connection lost | Automatic reconnect on next command |
| Command execution fails | Log warning, continue polling |
| MQTT connection lost | Automatic reconnect via rumqttc |
When DEBUG=true:
- vcontrold:
--verbose --debugflags (protocol-level output) - Polling: logs each command batch and response
- Publishing: logs topic and payload
- Subscriber: logs received payloads
- TCP: logs connection and command details
Example:
Debug mode enabled
Starting vcontrold with config: /config/vcontrold.xml (debug mode)
Polling 5 commands in 2 batches every 60 seconds
Executing batch 1: getTempWWObenIst,getTempWWsoll
Command getTempWWObenIst returned: 48.1
Publishing to vcontrold/command/getTempWWObenIst: 48.1
Main vcontrold configuration. Must define:
- Serial device:
<tty>/dev/vitocal</tty> - TCP port:
<port>3002</port> - Allow localhost:
<allow ip='127.0.0.1'/> - Device ID matching your hardware
- Command definitions (or include vito.xml)
Device-specific command definitions:
<command name="getTempWWObenIst" protocmd="getaddr">
<addr>010D</addr>
<len>2</len>
<unit>UT</unit>
</command>Generated client IDs to avoid collisions:
- Publisher:
${MQTT_CLIENT_ID_PREFIX}-${hostname}-${timestamp}
TLS is implemented using rustls (not OpenSSL) for:
- Smaller binary size
- Easier cross-compilation
- No system OpenSSL dependency
- If
MQTT_CAFILEset: Load specific CA certificate - If
MQTT_CAPATHset: Load all.crt/.pemfiles from directory - Otherwise: Use webpki-roots (Mozilla's root certificates)
When both MQTT_CERTFILE and MQTT_KEYFILE are set:
- Supports PKCS#1, PKCS#8, and SEC1 (EC) key formats
- PEM-encoded certificates and keys
When MQTT_TLS_INSECURE=true:
- Skips server certificate validation
- Useful for self-signed certificates in development
- Not recommended for production
Runtime (in container):
- vcontrold daemon
- libxml2 (vcontrold dependency)
Build-time:
- Rust toolchain
- See
Cargo.tomlfor crate dependencies
vcontrold-mqttd (PID 1, Rust binary)
├── Spawns: vcontrold daemon
├── Task: vcontrold process monitor
├── Task: MQTT event loop
├── Task: Polling loop (if COMMANDS set)
├── Task: Subscriber (if MQTT_SUBSCRIBE=true)
└── Task: Health check HTTP server
All tasks run concurrently via tokio. If any critical task fails, the container exits.
An HTTP health endpoint runs on HEALTHCHECK_PORT (default 8080) and reports
the status of all critical components.
URL: http://0.0.0.0:<HEALTHCHECK_PORT>/health (any path is accepted)
| Status | Condition |
|---|---|
200 OK |
All components healthy |
503 Service Unavailable |
One or more components unhealthy |
Body (JSON):
{
"healthy": true,
"vcontrold_process": true,
"vcontrold_connection": true,
"mqtt_connected": true
}| Component | What is checked |
|---|---|
vcontrold_process |
vcontrold daemon is still running |
vcontrold_connection |
Persistent TCP connection to vcontrold is alive |
mqtt_connected |
MQTT broker connection is active |
The Dockerfile includes:
HEALTHCHECK --interval=30s --timeout=5s --start-period=40s --retries=3 \
CMD ["/usr/local/bin/vcontrold-mqttd", "--healthcheck"]The --healthcheck flag makes the binary act as a lightweight probe client:
it connects to the health endpoint on 127.0.0.1:<HEALTHCHECK_PORT>, checks
for a 200 response, and exits 0 (healthy) or 1 (unhealthy).
- Serial device access (dialout group)
- Volume mount for
/config - Network access to MQTT broker
- Optional: TLS certificate mounts