Appearance
Energy zones
An energy zone is one electrical parc on the boat: a battery bank and everything wired to it. A catamaran with two 48 V banks, a 24 V service bank and an AC circuit has four zones. Each zone gets its own line on the energy screen and its own MQTT topic.
Zones are not discovered from the bus. Nothing in a CAN frame says which bank a battery belongs to, and a converter sitting between two banks is a load on one and a source on the other at the same instant. The zone membership comes entirely from the boat's deployment file, and getting it right is the substance of commissioning the energy page.
The daemon reads the file once, at startup. A change takes effect when the daemon restarts.
What the daemon reads
The deployment file is the shared MUXEN boat configuration — /etc/muxen/deploy.json by default. muxen-energy reads three sections of it and ignores everything else:
| Section | Used for |
|---|---|
settings[] | zone names, via EnergyZoneName<N> |
devices[] | bus devices and their zone binding |
sensors[] | analogue variables that carry a current |
The root of the file must be a JSON object. Anything else — a bare array, a truncated file, invalid JSON — is refused at startup with zone: failed to parse the JSON, and the daemon exits.
Zone numbers and names
A zone is identified by an integer. That integer is what appears in the MQTT topic (app/energy/3) and in the id field of the payload.
The name comes from a setting whose name is EnergyZoneName followed by the zone number:
json
{
"settings": [
{ "name": "EnergyZoneName1", "value": "48V BD" },
{ "name": "EnergyZoneName2", "value": "48V TD" },
{ "name": "EnergyZoneName3", "value": "24V" }
]
}If no such setting exists, the zone is named Zone <N> — literally, with the number substituted. A screen showing Zone 3 is telling you the setting is missing or misspelled.
Zones are created on demand: a zone exists because at least one device or variable claims it, not because it was declared anywhere. There is no list of zones in the configuration, and nothing has to be created to add one.
Binding a device to a zone
Every entry in devices[] is examined. The binding is a parameter named BindToParc, whose string value is the zone number:
json
{
"function": 5,
"functionName": "Power source battery",
"instance": 0,
"parameters": [
{ "name": "Capacity", "value": "280" },
{ "name": "BindToParc", "value": "1" }
]
}Three rules govern the result:
functionandinstancemust both be present and be integers. Otherwise the entry is skipped silently.- The
BindToParcvalue must be a string. A JSON number is ignored and the device falls back to zone 1. - A device with no
BindToParc, or with a negative one, joins zone 1.
That last rule is worth dwelling on. Every device in the file is processed, not only the electrical ones, so on a typical boat zone 1 collects every button panel, lighting controller and HVAC unit that has no BindToParc. They contribute nothing to the balance — the daemon has no handler for those functions — but each of them produces one line at startup:
can: not supported device type 21That message is expected on a real boat, and is not a fault. It is only a symptom when it names a function you expected to be counted.
Converters belong to two zones
A converter — function 6 — sits between two parcs, and the daemon subscribes it on both sides:
| Parameter | Side | Frame read | Sign on the wire |
|---|---|---|---|
BindToParc | output | life | positive = charging |
InputBindToParc | input | state | negative = charging |
json
{
"function": 6,
"instance": 2,
"parameters": [
{ "name": "BindToParc", "value": "3" },
{ "name": "InputBindToParc", "value": "1" }
]
}This converter appears in zone 3 as a source, reading its output current, and in zone 1 as a load, reading its input current. The daemon normalises both into positive magnitudes, so chargeConverter and dischargeConverter are never negative on either side.
InputBindToParc is only honoured when it is greater than zero; the input-side membership is simply not created otherwise.
A converter that carries no BindToParc falls into zone 1 like any other unbound device, and is read on its input side there — the output side is selected only when BindToParc names the zone being built. Bind your converters explicitly.
Battery capacity
The capacity of a zone is the sum of the Capacity parameters of the batteries — function 5 — bound to it, in amp-hours:
json
{ "name": "Capacity", "value": "280" }The value is accepted as a JSON number or as a string. It is read at startup from the configuration, never from the bus, and it is the only device parameter besides the bindings that the daemon consumes.
A zone with no capacity publishes capacity: 0, and both autonomy and chargingTime stay at 0 — see The energy balance.
Binding a variable to a zone
Some currents do not come from a bus device. A shunt read through an analogue input is published by the sensor layer as a plain value on app/sensor/<name>, and muxen-energy can pull it into a zone.
An entry in sensors[] joins a zone through its energy block:
json
{
"name": "hydro-portside",
"energy": {
"enable": true,
"type": 2,
"zone": 1
}
}| Key | Type | Meaning |
|---|---|---|
enable | boolean | must be true; the entry is skipped otherwise |
type | integer | the MUXEN function code that says what this current is |
zone | integer | the zone number, must not be negative |
The daemon then subscribes to app/sensor/hydro-portside and reads data.value out of the payload, treating it as a current in amperes with the sign convention of the declared type.
Two consequences of this design:
- Variables are always taken from MQTT, even when the daemon is running against a CAN interface. A boat with no
sensors[]entry ever reachingapp/sensor/…gets nothing from this path. - The name is the topic.
energy.typeselects only how the value is counted, so a variable can stand in for any of the supported functions.
Variables carry no capacity: a battery current supplied as a variable contributes to the balance but adds nothing to capacity, voltage, temperature or soc, which come from battery devices only.
What each function contributes
The function code — of a device, or of a variable's energy.type — decides which accumulator the current lands in.
| Code | Function | As a device | As a variable | Contributes to |
|---|---|---|---|---|
| 1 | Bloc 8 | yes | yes | accumulated, not published (see below) |
| 2 | Hydro | no | yes | chargeHydro |
| 3 | Interconnection | yes | yes | accumulated, not published (see below) |
| 4 | Power source genset | yes | yes | chargeGroup |
| 5 | Battery | yes | yes | the zone battery current; devices also feed voltage, temperature, soc, capacity |
| 6 | Converter | yes | yes | chargeConverter / dischargeConverter |
| 7 | Motor | yes | yes | chargeMotor / dischargeMotor |
| 10 | Solar | yes | yes | chargeSolar |
| 11 | Wind turbine | yes | yes | chargeWindTurbine |
Any other function code is subscribed as a variable but counts for nothing, and is refused outright as a device with a not supported device type line at startup.
Bloc 8 and interconnection currents are collected but do not reach the published payload. The source carries an explicit note about this: it is undecided whether they are loads in their own right or already covered by the residual. Until that is settled, their consumption shows up inside dischargeOther like any other uninstrumented load, which keeps the balance correct but attributes it to "other".
Hydro has no device path. A hydrogenerator is only counted when it is declared as a variable; a function-2 device in devices[] is refused with not supported device type 2.
A worked configuration
A boat with two banks and a 48 V → 24 V converter between them:
json
{
"settings": [
{ "name": "EnergyZoneName1", "value": "48V main" },
{ "name": "EnergyZoneName2", "value": "24V service" }
],
"devices": [
{
"function": 5,
"instance": 0,
"parameters": [
{ "name": "Capacity", "value": "280" },
{ "name": "BindToParc", "value": "1" }
]
},
{
"function": 5,
"instance": 1,
"parameters": [
{ "name": "Capacity", "value": "150" },
{ "name": "BindToParc", "value": "2" }
]
},
{
"function": 10,
"instance": 0,
"parameters": [
{ "name": "BindToParc", "value": "1" }
]
},
{
"function": 6,
"instance": 0,
"parameters": [
{ "name": "BindToParc", "value": "2" },
{ "name": "InputBindToParc", "value": "1" }
]
}
],
"sensors": [
{
"name": "hydro-portside",
"energy": { "enable": true, "type": 2, "zone": 1 }
}
]
}This publishes app/energy/1 — 280 Ah, one solar array, the hydro variable, and the converter as a load — and app/energy/2 — 150 Ah, with the same converter as its source.
Check it with the zone tree:
sh
muxen-energy --interface can0 --energy /etc/muxen/deploy.json -vzones: 2 zones
zones: 48V main
zones: ├╴Variables
zones: │ └╴hydro-portside, type: Power source hydro (2)
zones: └╴Devices
zones: └╴instance: 1, type: Power source battery (5)
zones: └╴instance: 1, type: Power source solar (10)
zones: └╴instance: 1, type: Power source converter (6)
zones: Connected to: input
zones: 24V service
...The Connected to: line under a converter is the check that matters: output means this zone reads the converter's life frame, input means it reads the state frame.
Note that the tree prints instances one-based, while the configuration file, the MQTT topics and the CAN frames are all zero-based. Device instance: 0 in the file appears as instance: 1 in this listing.
