Skip to content
Small team, full backlog, zero orders dropped. Support replies are slower than we’d like. Read our status update → Zero orders dropped. Status → 📬 Check your spam folder — most of our replies land there. We do answer. Status update → 📬 Check your spam folder. Status →

DCENT_OS Off-Grid and Solar Reference: Battery Zones and Inverter Providers

DCENT_OS carries a voltage-driven off-grid controller and a solar-telemetry client with seven providers. Both are real Rust in the public release, and neither appears in any configuration file the project ships. This page documents exactly that: what the code does, what the daemon will parse, what you would have to add to the miner to use it, and where the evidence stops.

How to read this page

Nothing on this page has been verified against a miner on a bench. Every statement here is a statement about the published GPL-3.0 source, or about the configuration file that ships with it. Each claim carries one of four grades:

  • Verified (in source) — read in the public release tree; the file and line are printed beside it.
  • Verified (shipped config) — a real key and value in the dcentrald.toml that ships in the repository.
  • Inferred — arithmetic, or a consequence that follows from Verified facts. Labelled, never dressed as an observation.
  • Unknown — the source does not say, or two files say different things. We box it rather than pick one.

There is deliberately no Supported grade on this page. That grade would mean tested on hardware, and we have not done that for anything described here. When we have, this page changes.

Read this before the tables — the honest state of play

We searched every TOML file in the release tree. There is no [power.offgrid] section and no [solar] section in any of them, and the daemon's own default for off-grid is None (dcentrald/src/config.rs:5479). Compare the LED, MQTT and webhook features, which all ship with a worked example in dcentrald.toml.

So the key names and defaults below are the schema the daemon parses, read out of config.rs — not a configuration we have run, and not a configuration the project publishes an example of. We have not connected DCENT_OS to a battery bank, an inverter, or a gateway. If you are planning a build around this, plan around the source being real and the integration being unproven.

Before anything else: this feature needs a part your miner does not have

Off-grid control reads DC bus voltage, and no stock Antminer wires its DC bus into an ADC channel. You have to add the sensing yourself. The daemon accepts exactly three backends (dcentrald-hal/src/adc.rs:28-63): a TI INA226 I2C power monitor — the one the source calls "recommended for off-grid", defaulting to I2C bus 0, address 0x40, a 10 mΩ shunt and a 1.0 divider (adc.rs:65-76); a Linux IIO sysfs ADC channel whose path you name; or a simulated source, which is for testing and reports numbers nothing measured.

Setting [power.offgrid] enabled = true without an adc backend does not fall back to normal mining — it stops the miner hashing. The daemon detects the missing backend at start-up, puts the controller straight into its sensor-fault state and puts the curtailment controller into sleep (dcentrald/src/daemon.rs:6466-6484). Sensor-fault sets target_freq_mhz = 0 (dcentrald-thermal/src/offgrid.rs:482-485), and the curtailment sleep state is documented as "hash boards de-energized, fans dropped to the controller's sleep_fan_pwm", about 25 W (dcentrald/src/config.rs:5296-5300).

The firmware is right to do this. Without a voltage reading there is no deep-discharge protection, and the alternative — carrying on at full frequency against an unknown battery — is the dangerous one. But it means the config key below is not a switch you can try. The daemon says so itself, in the telemetry it emits at that moment:

"Off-grid mode requires an explicit ADC backend. Configure INA226, Sysfs ADC, or an intentional simulated source before enabling battery protection."

Verified (in source)adc.rs:28-63, :65-76; daemon.rs:6466-6484; offgrid.rs:482-485; config.rs:5296-5300. Read on a bench: no. This is what the code is written to do.

Credit where it is due

The off-grid controller's own header names its predecessor: "Inspired by Gridless Compute's Jua-Kali project (Go-based battery voltage control loop for solar-powered mining in Africa). Reimplemented from scratch in Rust with tighter DCENT_OS integration and 5-zone state machine." (dcentrald-thermal/src/offgrid.rs:19-21). Gridless solved this problem first, in the field, where it mattered most. We are standing on their work.

Five voltage zones

The controller reads DC bus voltage and sorts it into one of five zones, then acts. It never jumps: it moves frequency one step at a time.

Zone behaviour. Verified (in source) — classification offgrid.rs:390-411, state machine offgrid.rs:248 onward
ZoneConditionWhat the controller does
CriticalV < critical_vTarget frequency 0, enter deep sleep. This is deep-discharge protection.
LowV < low_vRamp frequency down one step. If already asleep, stay asleep until recovery_v.
NormalV <= high_vHold. No change.
HighV <= full_vRamp frequency up one step — the source's comment reads "solar surplus available".
Fullabove full_vMine at the configured maximum frequency.

Precision point: normal_v is not a threshold

The presets below list a normal_v, but classify_zone() never reads it — only critical_v, low_v, high_v and full_v decide a zone (offgrid.rs:390-411). normal_v is the nominal/display value. The Normal zone is the closed interval from low_v to high_v.

The edges are strict where it matters: a bus sitting at exactly critical_v is Low, not Critical, because that comparison is <. The repository pins that boundary in its own test (offgrid.rs:696-718).

Six chemistry presets, plus a custom fallback

Chemistry presets and their voltage thresholds, in volts. Six chemistries and the custom slot. Verified (in source)dcentrald-thermal/src/battery.rs:47-108; labels at :111-121
Config stringLabel in the firmwarecriticallownormalhighfullrecovery
lifepo4_48v (default)LiFePO4 48V (16S)40.047.051.253.654.449.0
lifepo4_24vLiFePO4 24V (8S)20.023.525.626.827.224.5
lifepo4_12vLiFePO4 12V (4S)10.011.812.813.413.612.2
lead_acid_48vLead Acid 48V (24 cells)42.046.048.050.457.648.0
lead_acid_24vLead Acid 24V (12 cells)21.023.024.025.228.824.0
lead_acid_12vLead Acid 12V (6 cells)10.511.512.012.614.412.0
custom (not a chemistry — the fallback slot)Custom10.011.512.013.014.012.0

The 48 V LiFePO4 row carries its per-cell derivation in the source comments (battery.rs:52-57): 40.0 V is 2.50 V/cell across 16S, 51.2 V is the 3.20 V/cell nominal, 54.4 V is 3.40 V/cell near absorption. The module names its chemistry references (battery.rs:9-11): LiFePO4 practice from BattleBorn and EG4, lead-acid from Trojan and Crown.

A typo in the preset name does not raise an error

Verified (in source): the daemon matches six exact lowercase strings and anything else falls through to Custom without warning (dcentrald/src/config.rs:546-554). Custom's thresholds are the 12 V set, and they are internally consistent — so every validation rule below still passes.

Inferred, and stated as inference: a 48 V bank configured with a mistyped preset name would then sit permanently above full_v, classify as Full, and never curtail. Unknown: whether the dashboard or the REST layer rejects the typo before it reaches the daemon — we did not resolve that, so we are not telling you this happens, only that the fallback is silent and the six accepted strings are the ones in the table above.

The controller loop

Loop parameters. Verified (in source)offgrid.rs:157-230; defaults config.rs:5436-5447
ParameterValueWhere
Tick interval2000 ms (accepted range 500 – 600000)config.rs:5445, :605
Voltage filterexponential moving average, alpha 0.3offgrid.rs:216
History for rate-of-changelast 30 samplesoffgrid.rs:228-230
Frequency step25 MHz (floored at 5)config.rs:5439, offgrid.rs:180
Minimum frequency200 MHz (floored at 100, and clamped down to your ceiling)config.rs:5442, offgrid.rs:179
Maximum frequencyyour mining target frequencyoffgrid.rs:160

Waking is gentle by design: whichever way the bus recovers, the controller restarts at the minimum frequency and ramps from there rather than snapping back to the previous target (offgrid.rs:243, :272, :302, :325).

What happens when the voltage sensor lies

This is the best-engineered part of the module and it is worth understanding, because the obvious implementation fails in the dangerous direction.

If an ADC returns a non-finite reading — NaN, or an infinity — then every < comparison against it is false, so a naive threshold ladder falls through to its last branch. Here that last branch is Full, which would ramp frequency up on a broken sensor and switch off the very protection the controller exists to provide. A NaN would also poison an exponential moving average permanently, staying stuck even after the sensor recovers.

DCENT_OS guards this twice, and the source says why in both places (Verified (in source)):

  • tick() catches a non-finite reading before it reaches the filter or the classifier, enters the same sensor-fault state described in the hardware box above, and returns Sleep (offgrid.rs:198-209).
  • classify_zone() independently maps any non-finite voltage to Critical, so the fail-open "can't be reintroduced by a caller that forgets to pre-filter" (offgrid.rs:391-399).

Both are pinned by a test covering NaN and both infinities (offgrid.rs:729-749). A sensor that fails while running therefore lands in exactly the same place as a sensor that was never fitted: frequency target zero, boards asleep, waiting for a real reading.

The daemon refuses to start on an unsafe off-grid configuration

With off-grid enabled, these are checked at load and each one aborts startup (Verified (in source)dcentrald/src/config.rs:542-614):

Fail-closed load-time validation
RuleWhy, in the source's words
critical_v < low_v < high_v < full_vordering must be sane (:572-592)
recovery_v > critical_v"to prevent permanent sleep" (:594)
critical_v >= 5.0"no battery can safely discharge below 5V" (:597)
500 <= loop_interval_ms <= 600000the value is passed straight to a timer that panics on zero, and the release build aborts on panic (:599-613)

Note what is not on that list: a missing adc backend does not abort start-up. The daemon boots and goes to sleep instead — see the hardware box above.

The battery percentage is a straight line, and you should treat it as one

Verified (formula) · Unknown (accuracy)

The state-of-charge figure in the off-grid telemetry is a linear interpolation between critical_v and full_v: SoC% = (V − critical_v) / (full_v − critical_v) × 100, clamped to 0–100 (offgrid.rs:414-421). It is not coulomb counting and it is not a chemistry curve.

That matters most on LiFePO4, whose discharge curve is famously flat through the middle of its range — a small voltage change covers a large slice of real capacity. The field is named "estimated" in the firmware and we are repeating the word deliberately. Use it as a coarse indicator; do not size a bank against it.

Solar: seven providers, graded by the firmware itself

The interesting thing here is not the provider list. It is that DCENT_OS ships a maturity grade per provider, in code, and one of them is deliberately marked down.

Solar providers and their firmware-declared stage. Verified (in source) — list dcentrald-api/src/lib.rs:770-780, grading lib.rs:782-818, dispatch dcentrald/src/solar.rs:1176-1223
Provider stringStage the firmware declaresWhat it means here
manualliveNumbers you type in. Explicitly not telemetry-backed (lib.rs:820-822).
victronliveA backend exists in the dispatcher.
bridgeliveGeneric normalised payload contract.
enphaseliveA backend exists in the dispatcher.
solaredgeliveCloud endpoints, including current power flow and overview.
teslaliveThe local Gateway API — see below.
ecoflowlimitedThe firmware narrows its own claim — see below.

Tesla — say exactly what it is

DCENT_OS implements a client for the local Tesla Gateway HTTP API. It requires an http:// or https:// gateway address, and reads /api/meters/aggregates and /api/system_status/soe (solar.rs:1109-1136), mapping solar.instant_power, load.instant_power and site.instant_power to production, consumption and net grid (solar.rs:486-488). If the state-of-energy endpoint is unavailable because of authentication, it degrades to "no battery SoC" rather than failing (solar.rs:1123-1133).

That is the whole claim. We have read the client. We have never pointed it at a physical Powerwall. "DCENT_OS supports Tesla Powerwall" is a sentence we are not entitled to write, and we are not writing it.

EcoFlow — the firmware's own words, unedited

The stage reason is a string in the source, and it is a better disclaimer than anything we would draft (lib.rs:799):

"EcoFlow support is intentionally narrow: DCENT_OS only accepts validated, normalized EcoFlow HTTP payloads that can be mapped safely to production, consumption or net-grid, optional battery SoC, and optional sample age/timestamp metadata. It does not claim direct EcoFlow cloud/local authentication coverage across device families."

Three payload shapes are accepted — bridge-contract, site-summary and power-summary (lib.rs:802-806, field patterns at solar.rs:18-76). The transport resolves by endpoint scheme to an HTTP bridge, an MQTT bridge, or ecoflow-unsupported (lib.rs:846-869).

Two more things the solar layer tracks that are unusual and worth knowing about:

  • It records which fields it recognised. A verification sample carries matched_fields, alongside sample age, a staleness flag, consecutive failures and the last success timestamp (lib.rs:741-756). That is how you debug a bridge without guessing what the far end sent.
  • Commissioning is a state, not a boolean. Exactly four values: pending_restart, manual_runtime, telemetry_live, telemetry_degraded (lib.rs:824-844).

The schema the daemon parses

These are field names and defaults read out of config.rs. There is no shipped example to copy, and we have not run either section. They are printed so you can read the source with the same map we used, not as a configuration to paste — and [power.offgrid] enabled = true in particular is not a switch you can safely try without first reading the hardware box at the top of this page.

[power.offgrid]dcentrald/src/config.rs:5245-5285: enabled (false) · battery_preset ("lifepo4_48v") · adc (no default — absent unless you supply it. This is the key that decides whether the feature can run at all; see the hardware box above) · freq_step_mhz (25) · min_frequency_mhz (200) · loop_interval_ms (2000) · custom_critical_v · custom_low_v · custom_high_v · custom_full_v · custom_recovery_v.

[solar]dcentrald/src/config.rs:5398-5434: enabled (false) · inverter_brand ("manual" — this is the provider string from the table above) · api_endpoint · api_key · solar_only_mode (false) · base_load_watts (500) · battery_threshold_pct (20) · battery_wake_hysteresis_pct (3) · provider_max_sample_age_ms (60000) · provider_failure_hysteresis_samples (1) · hybrid_import_deadband_watts (75) · manual_production_watts (0) · manual_site_load_watts (0) · manual_battery_soc_pct.

Read this as source, not as a datasheet. These values are what the firmware is written to do. We have not confirmed them on a miner.

Planning an off-grid or solar build

This page is about the firmware. For the build itself, D-Central's off-grid and solar material is the better starting point:

What this page does not tell you

  • How to turn either feature on. No shipped configuration example exists, and we will not invent a procedure. When there is one, this page gains a how-to link.
  • Whether it works. No battery, no inverter, no gateway has been connected to DCENT_OS by us.
  • Which miners this applies to. Off-grid frequency control presumes a platform DCENT_OS can already drive; see hardware status for which lanes have evidence.

Source of record: the public GPL-3.0 release tree at github.com/DCentralTech/DCENT_OS, commit 6f61603 (2026-08-16). Line references are to that commit. Code moves; if a line number has drifted, the file and the symbol name still find it. Reviewed 2026-08-19.


Companion tool — DCENT_Toolbox: Audit an off-grid miner read-only before changing anything.