This file provides guidance to LLM Agents when working with code in this repository.
Detailed technical documentation for each subsystem is in doc/:
| Doc | Subsystem |
|---|---|
| doc/00_overview.md | Global architecture, startup flow, dependencies |
| doc/01_core_data_model.md | IOBase, Room, ListeRoom, IOFactory, Params |
| doc/02_io_drivers.md | All IO drivers: Wago, KNX, MQTT, GPIO, Hue, MySensors… |
| doc/03_rules_engine.md | Rules, Conditions, Actions |
| doc/04_scenarios.md | AutoScenario, time ranges, timers |
| doc/05_audio.md | AudioPlayer (Squeezebox, Roon), AVReceiver (Denon, Yamaha…) |
| doc/06_ipcam.md | IP cameras (IPCam, Axis, Foscam, Synology…) |
| doc/07_remoteui.md | Embedded RemoteUI devices, WebSocket, HMAC auth, OTA |
| doc/08_http_api.md | JSON HTTP/WebSocket API, real-time events |
| doc/09_lua_scripting.md | Lua engine, calaos:* API, script examples |
| doc/10_events_notifications.md | EventManager, push notifications, email, HistLogger |
| doc/11_config_persistence.md | local_config.xml, rules.xml, state cache |
| doc/12_extern_proc.md | ExternProc IPC framework (C++ subprocess drivers) |
| doc/13_utility_lib.md | Utils, Params, Timer, Logger, bundled libraries |
| doc/14_python_extern_proc.md | Python drivers: calaos_extern_proc lib, Reolink, Roon |
| doc/15_mcp_server.md | MCP sidecar: calaos_mcp Python process, reverse proxy, tools, auth |
Calaos Server is a home automation daemon that controls lights, shutters, sensors, music, cameras, and more through a JSON/WebSocket API. It reads configuration from three XML files and runs an event-driven loop based on libuv.
The main binary is calaos_server. Several drivers run as separate subprocesses (C++ or Python) that communicate with the server over Unix sockets using the ExternProc IPC framework. Subprocess drivers: Wago, KNX, MQTT, OneWire, OLA, Reolink (Python), Roon (Python), Lua scripts.
An additional Python subprocess, calaos_mcp, exposes the home to LLM clients (Claude Desktop, etc.) via the Model Context Protocol. Unlike the ExternProc drivers it does not use the binary IPC framing: it runs a FastMCP/uvicorn HTTP server on a Unix domain socket and is reverse-proxied through the /mcp path of the main HTTP port (5454). See doc/15_mcp_server.md.
# Bootstrap the build system (first time or after configure.ac changes)
./autogen.sh
# Configure and build
./configure && make
# Install
sudo make install
# Run tests (requires gtest to be installed)
make check
# Build with optional hardware support
./configure --with-owfs --with-knx --with-mqtt --with-olaThe project uses GNU Autotools. If dependencies are missing, check configure.ac for the optional --with-* flags.
src/bin/calaos_server/main.cpp initializes the system, loads XML config, and starts the libuv event loop.
Startup sequence:
Config::LoadConfigIO()— parsesio.xml, instantiates IOs viaIOFactory, fillsListeRoomConfig::LoadConfigRule()— parsesrules.xml, instantiatesRule/Condition/ActionobjectsHttpServer::Instance(port)— starts the HTTP/WebSocket server (JSONAPI_PORT= 5454 by default, overridable via theport_apiconfig option)McpServerManager::Instance().start()— spawns thecalaos_mcpsidecar (no-op if the wrapper is not installed)ListeRule::ExecuteStartRules()— fires rules withConditionStartonce- libuv event loop runs indefinitely
Every controllable device is an IOBase subclass. IOs self-register via a factory macro placed at file scope in the .cpp:
REGISTER_IO(WagoOutputLight)
REGISTER_IO_USERTYPE("WagoOutputLight", WODigital) // XML type name differs from class nameIOFactory reads the type attribute from local_config.xml and instantiates the matching class. Subdirectories group IOs by protocol:
| Directory | Protocol |
|---|---|
IO/Wago/ |
Modbus TCP (Wago 750 PLCs) — ExternProc |
IO/KNX/ |
KNX bus via knxd — ExternProc |
IO/Mqtt/ |
MQTT — ExternProc |
IO/Gpio/ |
Linux GPIO sysfs |
IO/Hue/ |
Philips Hue REST API |
IO/LAN/ |
Ping / Wake-On-LAN |
IO/MySensors/ |
MySensors RF network |
IO/OLA/ |
DMX512 via OLA — ExternProc |
IO/OneWire/ |
1-Wire via owfs — ExternProc |
IO/RemoteUI/ |
IOs hosted on embedded RemoteUI devices |
IO/Reolink/ |
Reolink cameras — Python ExternProc |
IO/Web/ |
HTTP-based IOs |
Each Rule holds a list of Condition objects (all must be true) and a list of Action objects (executed when conditions pass).
Condition types: ConditionStd (compare IO value), ConditionOutput (compare output state), ConditionStart (once at boot), ConditionScript (Lua).
Action types: ActionStd (set IO value), ActionMail, ActionPush, ActionScript (Lua), ActionTouchscreen.
Framework for subprocess drivers. The server side uses ExternProcServer; subprocess drivers (C++ or Python) use ExternProcClient. Communication is via Unix socket with a simple binary framing (type + size + JSON payload). Python drivers use the calaos_extern_proc package (src/lib/calaos-python/).
Central pub/sub using sigc++ signals. When an IO changes state it calls EmitSignalIO() → ListeRule::ExecuteRuleSignal(id) (triggers rules) and EventManager::create(EventIOChanged, ...) (notifies WebSocket clients). See doc/10_events_notifications.md.
Singleton HttpServer listens on port 5454 (JSONAPI_PORT). Each connection becomes a WebSocket which delegates to JsonApiHandlerHttp (single request) or JsonApiHandlerWS (persistent connection with real-time event push). WebSocket::ProcessData also sniffs the first request line: requests targeting /mcp are handed to McpProxyHandler instead (see MCP section below). See doc/08_http_api.md.
MCP Sidecar (src/bin/calaos_server/McpServerManager.{h,cpp}, McpProxyHandler.{h,cpp}, src/bin/calaos_mcp/)
McpServerManager (singleton) spawns the calaos_mcp Python sidecar at boot via uvw::ProcessHandle, auto-generates the mcp_token and mcp_service_token in local_config.xml, and supervises it (restart with backoff, SIGTERM on shutdown). The sidecar listens on a Unix domain socket ($CALAOS_CACHE_PATH/mcp.sock, mode 0660). McpProxyHandler splices /mcp/* TCP connections to that socket as raw bytes, so the existing HTTPS-terminating reverse proxy (haproxy on Calaos OS) covers MCP without any extra port. The sidecar authenticates back to the JsonApi over WebSocket using login_service (a scope-restricted session, serviceScope=true) rather than admin credentials. See doc/15_mcp_server.md.
Manages embedded devices (ESP32-S3 wall panels, etc.) that connect via WebSocket with HMAC-SHA256 authentication. Handles IO state synchronization, command dispatch, and OTA firmware updates. See doc/07_remoteui.md.
AudioPlayer subclasses: Squeezebox (Logitech Media Server), RoonPlayer (Python ExternProc). AV receivers (AVReceiver subclasses): Denon, Marantz, Onkyo, Pioneer, Rose, Yamaha. See doc/05_audio.md.
IPCam base class with implementations for Axis, Foscam, Gadspot, Planet, StandardMjpeg, SynoSurveillanceStation. See doc/06_ipcam.md.
Scripts run in an isolated subprocess (ScriptExtern_main.cpp via ExternProc). ScriptManager sends the script text to the subprocess; Lua_Calaos (via Lunar) exposes calaos:get_io(), calaos:set_io(), etc. See doc/09_lua_scripting.md.
Config singleton loads/saves local_config.xml (IOs) and rules.xml (rules). A state cache (SQLite via sqlite_modern_cpp) persists last known IO values across restarts. See doc/11_config_persistence.md.
Utils, Logger, Params (string→string map), Timer (libuv-based), ColorUtils, FileUtils, UrlDownloader (libcurl async), ExpressionEvaluator (exprtk), NTPClock. Bundled third-party libs: nlohmann/json, TinyXML+XPath, sole (UUID), sqlite_modern_cpp, llhttp, exprtk, libquickmail, uvw. See doc/13_utility_lib.md.
| File | Constant | Content |
|---|---|---|
io.xml |
IO_CONFIG |
Rooms and IO declarations (type, id, name, protocol params) |
rules.xml |
RULES_CONFIG |
Automation rules (conditions + actions) |
local_config.xml |
LOCAL_CONFIG |
Server settings: credentials, SMTP, InfluxDB, NTP, push tokens, misc options |
Default path: /etc/calaos/. Override with --config <dir> (sets _configBase and the CALAOS_CONFIG_PATH env var), or fall back to ~/.config/calaos/. Cache path defaults to ~/.cache/calaos/, overridable with --cache <dir>. Constants are defined in src/lib/Utils.h.
Important: the type attribute in io.xml must exactly match a registered IOFactory key (case-insensitive). Unknown types are silently skipped at load time.
Config path gotchas (learned the hard way):
Utils::getConfigFile(name)honours--config/CALAOS_CONFIG_PATH(it reads_configBase);Utils::getConfigPath()does not — it re-derives the directory from$HOMEevery time. When you need the active config directory (e.g. to pass to a subprocess), usegetConfigFile("")and strip the trailing slash, notgetConfigPath().getCachePath()does respect--cache.local_config.xmloptions are namespaced elements:<calaos:option name="..." value="..."/>withxmlns:calaos="http://www.calaos.fr". A Pythonxml.etreeparser sees the tag as{http://www.calaos.fr}option, soroot.iter("option")matches nothing — match on the local name after the}instead.- MCP keys in
local_config.xml:mcp_token(Bearer token for MCP clients) andmcp_service_token(sidecar→JsonApi service login). Both are auto-generated (64 hex chars) on first boot if absent.
- C++: camelCase for variables/functions/methods, PascalCase for classes
- C: snake_case for variables/functions/methods, ALL_CAPS for
#defineconstants - Braces: opening
{on a new line; single-line bodies omit braces; multi-line bodies always use braces - Constructor initialization lists:
:at end of the declaration line, each initializer on its own line with,at the end:
MyClass::MyClass():
MyBaseClass(a, b),
member_1(3),
member_2(nullptr)
{
}- Namespace: all server-side business logic is in namespace
Calaos - Singletons: expose a static
Instance()method - IO IDs: unique string, format
id-<uuid4>, generated byget_new_id(prefix) - Comments: English only, clear and concise
- Error handling: always log errors with contextual information using
cErrorDom("domain") << "message" - Python (subprocess drivers): follow the same domain-based logging via
calaos_extern_proc.logger
Tests live in tests/ and use Google Test. Built and run with make check (only if gtest is detected at configure time). Current coverage: ColorValue, ExpressionEvaluator, WebSocketClient.
Required: gcc > 5 (C++14), libuv > 1.10, jansson > 2.5, libcurl > 7.20, luajit, sigc++ > 2.4, sqlite3
Optional (enabled via ./configure --with-<name>):
| Flag | Library | Enables |
|---|---|---|
--with-owfs |
owfs | 1-Wire sensors (OneWire driver) |
--with-knx |
knxd | KNX bus (KNX driver) |
--with-mqtt |
libmosquitto | MQTT broker (MQTT driver) |
--with-ola |
OLA | DMX512 lighting (OLA driver) |
Python drivers (installed separately): reolink_aio (Reolink), roonapi (Roon), colorama (logging)
MCP sidecar (enabled by default, disable with ./configure --without-mcp): requires the Python packages mcp (with mcp[cli]), uvicorn, and fastapi. configure probes for them; if missing it warns and skips building the sidecar (HAVE_PYTHON_MCP). Install with pip3 install "mcp[cli]" uvicorn fastapi.
A multi-stage Dockerfile is provided with dev, builder, and runner targets. Images are published to ghcr.io/calaos/calaos_base.