Appearance
The energy balance
Once a second, for every zone, the daemon turns the currents it has collected into one published payload. This chapter explains what each number in that payload means and how it was obtained, because several of them are easy to misread — chargeOther in particular is not an error term, and autonomy is deliberately not the number a simple division would give.
The one-second cycle
- A timer fires at 1 Hz.
- For each zone, every device and every variable that has reported within the last five seconds is read; anything older is skipped entirely, as if it were not configured.
- The readings are summed per family into charge and discharge accumulators.
- The residual is computed so the balance closes.
- The battery current is pushed into the zone's history and filtered.
- The payload is published, retained, on
app/energy/<zone>.
Step 2 is the important one. A device that stops reporting does not keep contributing its last value; it drops out of the sum. A zone whose bus has gone quiet therefore reports zeros, not a frozen snapshot — and because the topic is retained, a stopped daemon does the opposite and leaves its last payload on the broker indefinitely. Read metadata.rxdate to tell the two apart.
Bank state
| Field | Unit | How it is obtained |
|---|---|---|
capacity | Ah | sum of the Capacity parameters of the batteries bound to the zone. From the configuration file, never from the bus |
voltage | V | maximum across the batteries that reported in time |
temperature | °C | maximum across the batteries that reported in time |
soc | % | maximum across the batteries that reported in time |
socValid | bool | see below |
voltage, temperature and soc are maxima, not averages. For a bank of series-connected modules reporting the same pack that is the intended reading; for a zone containing genuinely independent banks it means the figure describes the healthiest one.
Only battery devices feed these four. A battery current supplied as a variable moves the balance but leaves capacity, voltage, temperature and soc untouched.
socValid
soc is meaningless in two situations, and socValid is how the daemon says so:
- A battery reports
255, the value a module uses when it has no state-of-charge to give. One such battery in the zone makes the whole zone'ssocValidfalse. - No battery reported at all within the five-second window. Without this guard the untouched
socof0would publish as valid, and a silent CAN bus would be indistinguishable from a flat bank.
soc still carries a number when socValid is false. Consumers must check the flag rather than the value.
Charge and discharge
Each source and load contributes to one of two accumulators, always as a positive magnitude:
| Field | Unit | Source |
|---|---|---|
chargeGroup | A | generator sets (function 4) |
chargeSolar | A | solar (function 10) |
chargeWindTurbine | A | wind turbines (function 11) |
chargeHydro | A | hydrogenerators (function 2, variables only) |
chargeConverter | A | converters charging this zone |
dischargeConverter | A | converters drawing from this zone |
chargeMotor | A | motors regenerating |
dischargeMotor | A | motors under power |
chargeBattery | A | the bank taking current |
dischargeBattery | A | the bank giving current |
chargeOther | A | the residual, generation side |
dischargeOther | A | the residual, consumption side |
charge | A | total in |
discharge | A | total out |
The battery is the reference, not a term of the totals: chargeBattery and dischargeBattery report what the bank is doing, and charge and discharge are the sources and loads around it. That is what makes the identity below meaningful.
Motors and batteries report a signed current on the wire — positive into the battery — and are split into the two magnitudes above. A motor at −80 A publishes dischargeMotor: 80, chargeMotor: 0.
Converters are the awkward case, because the two sides of the same device use opposite conventions: on the output side (life) positive means charging, on the input side (state) negative means charging. The daemon normalises both, so whichever side of a converter a zone sits on, its contribution appears as a positive number in the correct accumulator.
The residual: chargeOther and dischargeOther
Real boats have loads nobody metered. The daemon computes what its known equipment fails to account for:
otherLoad = -(charge - discharge - battery)and publishes it as chargeOther when it is positive — unexplained generation — or dischargeOther when it is negative — unexplained consumption. At most one of the two is ever non-zero.
Folding the residual back into the totals makes this hold on every payload, whatever the inputs were:
charge - discharge == battery currentThe consequence is that the summary can never contradict the battery. It also means dischargeOther is the honest measure of how much of the boat is instrumented: a large steady value is not a fault, it is the lighting, the fridges, the autopilot and everything else on the distribution board. It is worth recording at commissioning and watching over time — a step change in dischargeOther with no change to the installation is a real signal.
Bloc 8 outputs and interconnections are collected but do not currently reach the payload, so their consumption lands here too. See Energy zones.
powerUsed
powerUsed = (charge - battery) * voltage when charge - battery > 0The current that goes into the boat rather than into the bank, converted to watts using the zone voltage. It is 0 when the bank is absorbing everything being produced, and 0 in a zone with no battery reporting a voltage — an AC zone, for example, publishes voltage: 0 and therefore powerUsed: 0 no matter how much current flows through it.
autonomy and chargingTime
Both are in hours, both are derived from the filtered battery current, and each is zero whenever the other applies:
autonomy = (capacity * soc / 100) / -batteryMean when discharging
chargingTime = (capacity * (100 - soc) / 100) / batteryMean when chargingThey are 0 when the filtered current is exactly zero, and 0 whenever capacity is 0 — which is what an unconfigured Capacity produces, and the most common reason for an energy page showing no remaining time.
Note that both use the raw soc regardless of socValid. A zone whose socValid is false publishes a remaining time computed from a state of charge that should not be trusted.
The filter
The battery current of a zone is pushed into a rolling history once a second. The estimate uses the last --filter-period samples — 30 by default, clamped to the range 20…60 — and the filter is not a plain average:
- The samples are sorted.
- The five most extreme samples at each end are dropped.
- The remainder is averaged with weights that increase towards the most-discharging end.
The result is deliberately pessimistic: for a bank that spends half its time at −20 A and half at 0 A, the filter returns a figure below the arithmetic mean, so the autonomy shown to the crew errs short. This is a safety choice, not a bug.
The trimming also explains the lower clamp on the period: a period at or below ten leaves no sample after the extremes are dropped, and the filter falls back to the newest reading alone.
A consequence worth knowing at commissioning: after a restart the history starts at zero, so the estimate needs roughly a full filter period — 20 to 60 seconds — before it means anything.
Metadata
json
"metadata": {
"rxdate": "2024-01-16T15:35:56.000Z",
"rxTimestamp": 1705419356,
"expireAfterSec": 5
}| Key | Meaning |
|---|---|
rxdate | the moment the payload was computed, ISO-8601 UTC |
rxTimestamp | the same instant, Unix epoch seconds |
expireAfterSec | 5 — consumers treat the payload as stale past this age |
The timestamp is the daemon's own publication time, not the receive time of any underlying frame. It is the right thing to compare against the clock to decide whether the daemon is alive.
A worked example
A 48 V zone with 280 Ah at 80 %, 28 A of solar coming in, two motors drawing 80.1 A, a converter taking 20 A to feed the service bank, and the bank supplying 100 A:
| Quantity | Value |
|---|---|
chargeSolar | 28.0 |
dischargeMotor | 80.1 |
dischargeConverter | 20.0 |
dischargeBattery | 100.0 |
| known charge − known discharge | 28.0 − 100.1 = −72.1 |
| battery current | −100.0 |
| residual | −27.9 → dischargeOther: 27.9 |
charge | 28.0 |
discharge | 128.0 |
charge - discharge is −100.0, which is the battery current, as guaranteed. The 27.9 A of dischargeOther is the rest of the boat.
