Appearance
Reference
Everything the daemon exposes, in tables.
Command line
muxen-energy [OPTION]| Short | Long | Argument | Default | Meaning |
|---|---|---|---|---|
-h | --help | — | — | print the usage block on stdout and exit 0 |
-v | --verbose | — | off | more output; repeatable |
-V | --version | — | — | print the version and exit 0 |
-i | --interface | canX | none | CAN interface to bind to |
-m | --mqtt | — | off | take device data from MQTT instead of CAN |
-e | --energy | path | none | the configuration file. Required |
-f | --filter-period | seconds | 30 | window of the battery-current filter |
| — | --json-pretty | — | off | indent the published JSON, for debugging; compact otherwise |
Rules enforced at startup:
--energyis mandatory. Without it:config: configuration file is missing, usage on stderr, exit 1.- Either
--interfaceor--mqttis mandatory. Without either:config: select a CAN interface, or the MQTT source, exit 1. --filter-periodis parsed withstrtoland must consume the whole argument; a bad value givesconfig: invalid filter period '<value>'and exit 1. A valid value outside 20…60 is clamped, withconfig: filter period set to <n> secondson stdout.--interfaceis truncated to the platform interface-name length.
Verbosity levels:
| Level | Effect |
|---|---|
| none | configuration summary, zone list, dispatcher tables, connection state |
-v | adds the zone tree: variables and devices per zone, with function names and converter side |
-vv | adds a dump of every CAN frame and every MQTT message received |
--version is accepted but is not listed in the usage block, and is not offered by the bash completion.
Exit codes
| Code | Meaning |
|---|---|
| 0 | normal termination (SIGINT, SIGTERM), or --help / --version |
| 1 | command-line error |
| 255 | startup failure after the command line was accepted: configuration file unreadable or not a JSON object, dispatcher construction failed, MQTT connection failed, CAN interface not found |
The daemon exits on SIGINT and SIGTERM by leaving its main loop cleanly; it closes the CAN socket and disconnects from the broker.
Environment
Read by the systemd unit, not by the binary:
| Variable | Default in the unit | Used as |
|---|---|---|
MUXEN_DEPLOY | /etc/muxen/deploy.json | --energy |
MUXEN_DEPLOY is overridable with a drop-in. The CAN interface is literal in ExecStart and in the unit's device dependency — see Getting started.
Files
| Path | Content |
|---|---|
/usr/bin/muxen-energy | the daemon |
/usr/lib/systemd/system/muxen-energy.service | the systemd unit |
/usr/share/bash-completion/completions/muxen-energy | bash completion |
/etc/muxen/deploy.json | the boat configuration, read once at startup. Not shipped by this package |
The daemon creates no files, no sockets and no runtime directory.
systemd
Unit muxen-energy.service:
| Directive | Value |
|---|---|
PartOf | muxen-deploy.target |
WantedBy | muxen-deploy.target |
After | mosquitto.service |
User / Group | muxen |
Restart | always, RestartSec=30 |
ConfigurationDirectory | muxen, mode 0755 |
Hardening: PrivateTmp, PrivateDevices, ProtectSystem=strict, ProtectKernelModules, ProtectKernelTunables, ProtectControlGroups, NoNewPrivileges; CAP_SYS_ADMIN, CAP_SYS_TIME and CAP_NET_ADMIN dropped from the bounding set; the @clock, @debug, @module, @mount, @raw-io, @reboot, @swap, @privileged and @resources system-call sets denied with EPERM.
Restarting muxen-deploy.target restarts the daemon. That is the mechanism by which a change to the deployment file is picked up.
Packaging
| Field | Value |
|---|---|
| Package | muxen-energy |
| Architecture | any, Multi-Arch: foreign |
| Depends | muxen-systemd (>= 1.2.0), muxen-sensors (>= 5.0.0) |
| Recommends | muxen-boat (>= 5.0.0) |
| Trigger | activates muxen-restart-target |
The muxen-restart-target trigger is what makes dependent services restart when the package is upgraded.
MQTT
Connection
| Parameter | Value |
|---|---|
| Host | 127.0.0.1 |
| Port | 1883 |
| Client ID | muxen-energy |
| Keepalive | 5 s |
| Clean session | yes |
| Authentication | none |
| Subscription | # — every topic, filtered by regular expression in the daemon |
The connection is made at startup and the daemon exits if it fails. Once established, a lost connection is retried every second.
Published
| Topic | QoS | Retain | Period |
|---|---|---|---|
app/energy/<zone> | 0 | yes | 1 s per zone |
app/energy/info | 1 | yes | on each connection; online: false as last will and on clean exit |
<zone> is the integer zone number. Payload:
json
{
"id": 1,
"name": "48V BD",
"data": { },
"metadata": { "rxdate": "", "rxTimestamp": 0, "expireAfterSec": 5 }
}data key | Unit | Meaning |
|---|---|---|
capacity | Ah | sum of the configured battery capacities in the zone |
voltage | V | maximum battery voltage in the zone |
temperature | °C | maximum battery temperature in the zone |
soc | % | maximum battery state of charge in the zone |
socValid | bool | false if any battery reports 255, or if no battery reported |
powerUsed | W | (charge - battery) * voltage, floored at 0 |
autonomy | h | time to empty, from the filtered battery current. 0 when charging |
chargingTime | h | time to full, from the filtered battery current. 0 when discharging |
charge | A | total current in, residual included |
discharge | A | total current out, residual included |
chargeBattery | A | battery current when positive |
dischargeBattery | A | magnitude of the battery current when negative |
chargeGroup | A | generator sets |
chargeMotor | A | motor current when positive |
dischargeMotor | A | magnitude of the motor current when negative |
chargeSolar | A | solar |
chargeWindTurbine | A | wind turbines |
chargeHydro | A | hydrogenerators |
chargeConverter | A | converters charging the zone |
dischargeConverter | A | converters drawing from the zone |
chargeOther | A | unexplained generation |
dischargeOther | A | unexplained consumption |
charge - discharge == chargeBattery - dischargeBattery holds on every payload. The JSON is compact, on one line; --json-pretty indents it for reading by hand.
app/energy/info
The daemon identity, the same shape every MUXEN daemon publishes on app/<daemon>/info. Published retained at QoS 1 on every (re)connection with online: true; the same payload with online: false is the last will and is also published on a clean exit, so a subscriber always gets the daemon's last known state.
json
{
"name": "muxen-energy",
"version": "v6.1.0",
"hostname": "brain-3",
"features": ["energy-zones", "source-can"],
"online": true,
"metadata": { "rxdate": "2026-09-22T07:55:22.552Z", "rxTimestamp": 1790063722, "expireAfterSec": 3124137600 }
}version is the git describe of the build and is for display only; clients test features and ignore the ones they do not know. expireAfterSec is about 99 years: the info never goes stale, online carries liveness.
| Feature | Meaning |
|---|---|
energy-zones | app/energy/<zone> is published |
source-can | device data is read from the CAN interface (--interface) |
source-mqtt | device data is read from the device/… MQTT topics (--mqtt) |
Subscribed
Matched by anchored regular expression against every topic on the broker.
| Topic pattern | When | Read |
|---|---|---|
app/sensor/<name> | always | data.value |
device/1/<instance>/life | --mqtt | Bloc 8 total current |
device/3/<instance>/life | --mqtt | interconnection output current |
device/4/<instance>/state | --mqtt | power source genset current |
device/5/<instance>/life | --mqtt | battery voltage, current, state of charge, temperature |
device/6/<instance>/life | --mqtt | converter output current |
device/6/<instance>/state | --mqtt | converter input current |
device/7/<instance>/life | --mqtt | motor current |
device/10/<instance>/life | --mqtt | solar current |
device/11/<instance>/life | --mqtt | wind turbine current |
<name> is the sensor's name from the configuration; <instance> is the device instance as written in the configuration, zero-based.
Receive time is taken from metadata.rxdate in the payload when present, and from the local clock otherwise.
app/sensor/<name> payloads are read as:
json
{ "data": { "value": 12.5 }, "metadata": { "rxdate": "…" } }Only integer and floating-point value fields are accepted.
CAN
Used when --mqtt is not given. The daemon binds a raw AF_CAN socket to the interface, subscribes to one broadcast frame per configured device, and never transmits.
| Function | Frame | Broadcast ID | DLC | Field consumed |
|---|---|---|---|---|
| 1 Bloc 8 | life | 1 | 5 | currentTotal |
| 3 Interconnection | life | 1 | 8 | outputCurrent |
| 4 Power source genset | state | 3 | 8 | current |
| 5 Battery | life | 1 | 8 | voltage, current, soc, temperature |
| 6 Converter | life | 1 | 7 | current (output side) |
| 6 Converter | state | 3 | 7 | current (input side) |
| 7 Motor | life | 1 | 8 | current |
| 10 Solar | life | 1 | 6 | current |
| 11 Wind turbine | life | 1 | 8 | current |
The matched CAN identifier is composed as
canId = (broadcastId << 12) | (function << 6) | instancewith the mask 0xFFFFFF, so a battery at instance 0 is matched at 0x001140. The dispatcher table printed at startup lists the exact identifier of every subscription.
There is no hydro (function 2) CAN or MQTT device handler; a hydrogenerator is counted only as a variable.
Configuration keys
Read from the file given to --energy. Everything not listed here is ignored.
| Location | Key | Type | Meaning |
|---|---|---|---|
settings[] | EnergyZoneName<N> | object with value string | display name of zone N. Default Zone <N> |
devices[] | function | int | MUXEN function code. Required |
devices[] | instance | int | device instance, zero-based. Required |
devices[].parameters[] | BindToParc | string | zone number. Default 1 |
devices[].parameters[] | InputBindToParc | string | for converters: zone of the input side. Only honoured when > 0 |
devices[].parameters[] | Capacity | number or string | battery capacity in Ah. Default 0 |
sensors[] | name | string | the variable name, and the MQTT topic suffix. Required |
sensors[].energy | enable | bool | must be true |
sensors[].energy | type | int | MUXEN function code the value represents |
sensors[].energy | zone | int | zone number, must be ≥ 0 |
Constants
| Constant | Value | Meaning |
|---|---|---|
| publication period | 1 s | one payload per zone per tick |
| staleness window | 5 s | a device or variable older than this is dropped from the sum |
expireAfterSec | 5 | published in the payload metadata |
| filter period | 20…60 s, default 30 | number of battery-current samples used by the estimator |
| filter trim | 5 samples each end | dropped before weighting |
| unavailable state of charge | 255 | makes socValid false |
| fallback zone | 1 | for a device with no usable BindToParc |
Building from source
sh
meson setup build
meson compile -C build
meson test -C buildThe makefile wraps the common cases: make deb builds the Debian package, make dev a sanitiser build with the tests, make lint and make lint-check run clang-format.
Dependencies: GLib, libmosquitto, json-c, and the MUXEN libcanmqtt, libstdmuxen and libmuxenfile, which are resolved as installed packages if present and as Meson subprojects otherwise.
