DCENT_OS Notifications Reference: MQTT, Home Assistant and Webhooks
Home » DCENT_OS — Open-Source Antminer Firmware » DCENT_OS Notifications Reference: MQTT, Home Assistant and Webhooks
Two ways DCENT_OS talks to something other than you: an MQTT publisher with Home Assistant auto-discovery, and an HTTP webhook dispatcher that speaks Discord, Slack and Telegram natively. Both ship switched off. This page is the reference for what each one emits, what it will accept back, and where the limits are.
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.tomlthat 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.
Both features ship disabled
[mqtt] enabled = false (dcentrald.toml:161) and
[webhook] enabled = false (dcentrald.toml:173).
Verified (shipped config). Nothing described here happens on a stock
configuration until you turn it on.
MQTT: DCENT_OS is a client, not a broker
This is worth stating plainly because the opposite is easy to assume. DCENT_OS does not run a
broker. It connects to one you already have — the broker key is an address it
dials. The module is titled "MQTT publisher with Home Assistant auto-discovery"
(dcentrald-api/src/mqtt.rs:1). Verified (in source).
The shipped configuration block, verbatim
(Verified (shipped config) — dcentrald.toml:160-167):
[mqtt]
enabled = false # master switch for the MQTT publisher + subscriber
broker = "mqtt://203.0.113.100:1883" # broker URL (mqtt:// or mqtts://)
topic_prefix = "dcentrald" # base topic; entities live under <prefix>/<name>
discovery = true # publish HA MQTT-discovery configs on connect
# username = "homeassistant" # optional broker credentials
# password = "secret"
publish_interval_s = 10 # stats publish cadence (seconds)
Note the key is topic_prefix and its shipped value is dcentrald. If you
have seen topicPrefix or a dcentos prefix written down somewhere, that is a
different spelling from the dashboard layer and it will not match the daemon's topic tree.
Two numbers exist for the publish cadence and both are real: the code default is
5 s (default_mqtt_interval() in config.rs), and the
dcentrald.toml that ships in the repository overrides it to 10 s
(dcentrald.toml:167). A stock image therefore publishes every 10 s. We print both
so that seeing "5" somewhere else does not read as a contradiction.
What auto-discovery creates in Home Assistant
On connect, with discovery = true, the daemon publishes retained discovery configs to
homeassistant/<domain>/dcentrald_<mac>/<object>/config. All entities
share one device block: manufacturer "D-Central Technologies",
model from the unit's platform stamp (falling back to "Antminer"), and a
configuration_url resolved from the live management IP, then the hostname, then
http://dcentos.local (mqtt.rs:760-767, :98-107).
| HA domain | Object | Entity name | Unit | Defined at |
|---|---|---|---|---|
| sensor | hashrate | Hashrate | TH/s | :779-786 |
| sensor | temperature | Temperature | °C | :787-798 |
| sensor | power | Power | W | :799-806 |
| sensor | btu | BTU/h | BTU/h | :807-814 |
| sensor | fan_rpm | Fan RPM | RPM | :815-822 |
| sensor | efficiency | Efficiency | J/TH | :823-830 |
| sensor | accepted | Accepted Shares | — | :831-838 |
| sensor | rejected | Rejected Shares | — | :839-846 |
| sensor | uptime | Uptime | s | :847-854 |
| sensor | energy | Energy | kWh | :907-923 |
| sensor | power_provenance | Power Provenance | — (diagnostic) | :930-942 |
| binary_sensor | mining | Mining Active | — | :946-952 |
| binary_sensor | pool | Pool Connected | — | :953-962 |
The energy sensor carries device_class: energy and
state_class: total_increasing in kWh — the exact shape Home Assistant's Energy
dashboard requires, which is what makes a mining heater's kWh/day and cost/day graphable without a
template sensor (mqtt.rs:894-906).
The part worth copying: it refuses to show a modelled number as a measured one
Three of those sensors — power, BTU/h and efficiency — are derived from wall wattage, and wall wattage can be modelled rather than measured. The firmware handles this the hard way:
- They fail closed. Each value template resolves to
noneunless the daemon has publishedlive_power_available: e.g.{{ (value_json.wall_watts | round(0)) if value_json.live_power_available else none }}(mqtt.rs:802, and the same guard at:810,:826). A blank reading, not a plausible invented one. Verified (in source). - Provenance rides along as attributes. Power, BTU and efficiency each get
json_attributes_templatecarryingpower_source,power_source_detail,power_modeled,power_calibratedandpower_note(mqtt.rs:742,:883-886) — in the source's own words, "so a modeled reading is never displayed as if it were a wall-meter measurement" (mqtt.rs:879-882). - There is a sensor whose only job is to explain another sensor. "Power
Provenance" is a diagnostic-category text entity whose value is the human-readable reason a power
reading may be unknown (
mqtt.rs:925-942). - The heater's current temperature is the measured chain temperature, not an
invented room temperature — stated as such at
mqtt.rs:1031-1033.
The four provenance notes the daemon can emit are fixed strings
(dcentrald-api/src/lib.rs:706-737): "Miner load is sourced from live measured power
telemetry." · "Miner load is modeled from live runtime state with an operator wall-meter
calibration." · "Miner load is modeled from the live dispatcher estimate." ·
"Live miner power has not published a positive wall-power reading."
Letting Home Assistant write back — three setpoints, hard-clamped
The command surface is exactly three values, and they only appear when the daemon supplies a validated-setter sink. There is no profile switch, no night-mode toggle and no reboot over MQTT on the Antminer side.
| HA domain | Entity | Command topic | Range enforced |
|---|---|---|---|
| number | Fan PWM | <prefix>/fan_pwm/set | 0 – 30 % |
| number | Target Power | <prefix>/target_watts/set | 100 – 6000 W |
| climate | Space Heater | <prefix>/target_temp/set | 40 – 60 °C |
Each has a matching <prefix>/…/state topic where the daemon echoes the value it
actually applied after clamping — not the value you asked for (mqtt.rs:989).
The clamp behaviour, all Verified (in source):
- Commands are routed through the same validated setters the local REST API uses;
the subscriber "NEVER opens a new unclamped path" (
mqtt.rs:144-148). - The fan ceiling of 30 % is the home safety cap and is "pinned equal" to
dcentrald_hal::fan::PWM_SAFETY_MAXby a test (mqtt.rs:125-127). Publishing a higher number does not raise it — it is clamped (mqtt.rs:127-128). - The 60 °C temperature ceiling is chosen so a remote setpoint "can never park the PID target
at or above the danger line" (
mqtt.rs:138-141). - Non-numeric and non-finite payloads are rejected before dispatch (
mqtt.rs:232-247). - Retained messages on a
/settopic are ignored (mqtt.rs:561-568) — otherwise a retained setpoint would silently re-apply itself on every reconnect.
Availability: a dead miner shows as dead
The daemon registers offline as the broker-side Last Will on
<prefix>/availability, the same topic every discovery entity gates on, and
publishes a retained online when it connects (mqtt.rs:28-38,
:392-410). A crash or a power cut therefore flips every Home Assistant entity to
unavailable rather than freezing them on the last good reading. Verified (in source).
The connect sequence is deliberate and documented at mqtt.rs:328-336: subscribe
first, then retained discovery configs at QoS 1, then retained online, then the latest
state at QoS 0 and not retained.
Broker URLs are passed through the pool-URL sanitiser before they reach any log line or error
string (mqtt.rs:441, :668), so credentials embedded in a broker URL are
masked.
Webhooks: 14 events from two producers, four delivery formats
The shipped configuration block, verbatim
(Verified (shipped config) — dcentrald.toml:172-176):
[webhook]
enabled = false
url = "https://example.com/dcentos-hook"
# events = ["mining_started", "pool_failover", "lucky_share", "share_milestone", "ota"]
miner_name = "dcentos" # label included in every payload
An empty or absent events list means all events
(webhook.rs:463-465).
| Event name | Fires when | Payload fields |
|---|---|---|
mining_started | the daemon began dispatching work | pool |
mining_stopped | clean shutdown, curtailment, or an operator stop | reason |
pool_failover | the Stratum client moved to a backup pool | from, to |
pool_disconnected | a pool connection dropped, no backup engaged yet | pool |
thermal_safety | an emergency-shutdown / dangerous-temperature event | temp_c, chain_id (0 = all) |
share_milestone | a milestone count of accepted shares | accepted |
lucky_share | a share whose achieved difficulty far exceeds the pool target | difficulty (optional) |
ota | a firmware-update lifecycle step | phase |
fan_failure | a fan stopped or fell below its safe RPM floor | rpm |
hashboard_offline | a board enumerated chips but produces no hashrate | chain_id |
thermal_restart | mining resumed after a recoverable thermal event | none |
hashrate_degraded | total hashrate stayed below the operator's floor (detection only) | observed_ghs, floor_ghs |
hashrate_recovery_exhausted | the auto-recovery ladder gave up on a degraded episode | observed_ghs, floor_ghs, attempts |
The table above is one of two event producers. An older alert path,
AlertEvent (dcentrald/src/runtime/notifications.rs:395-446), emits
8 names of its own — seven of which the table above also covers, plus one it does
not: emergency_shutdown.
Two producers, one filter — and the arithmetic closes exactly
WebhookEvent emits 13 names
(dcentrald-api/src/webhook.rs:182-198). AlertEvent emits 8
(notifications.rs:434-445). They overlap on seven — fan_failure,
hashboard_offline, hashrate_degraded,
hashrate_recovery_exhausted, mining_stopped,
pool_disconnected, thermal_restart. Their union is
14, set-identical to WEBHOOK_SUPPORTED_EVENTS, the 14-name list the
API validates your events array against
(dcentrald-api/src/rest.rs:5629-5650).
So the number to configure against is 14. Only
emergency_shutdown comes from the alert path alone; only
mining_started, pool_failover, thermal_safety,
share_milestone, lucky_share and ota come from the webhook
path alone. A configuration naming anything outside those 14 is rejected
(rest.rs:5836). Verified (in source) — the set arithmetic is
extracted by script from both enum definitions, not counted by eye.
| Event name | Fires when | Payload fields |
|---|---|---|
emergency_shutdown | the thermal loop performs an emergency shutdown | temp_c, chain_id |
One event has two spellings, and you need both
Thirteen of the fourteen names are the same string wherever you meet them. One is
not. Both producers are declared
#[serde(tag = "event", content = "data", rename_all = "snake_case")]
(webhook.rs:94, notifications.rs:394), and serde's snake_case of the Rust
variant HashBoardOffline is hash_board_offline. But the explicit
event_name() arm returns hashboard_offline
(webhook.rs:193, notifications.rs:440,
rest.rs:5635). So:
- In your
[webhook] eventsfilter, and anywhere the API validates a name, writehashboard_offline. - In the JSON that actually arrives, the
alert.eventvalue for that one event ishash_board_offline.
If you match on the event string in a receiver, match both. Verified (in source).
| Format | Body posted |
|---|---|
generic (default) | { "miner": …, "timestamp": …, "alert": { "event": …, "data": … } } |
discord | { "content": "<one-line summary>" } |
slack | { "text": "<one-line summary>" } |
telegram | { "chat_id": …, "text": … } to https://api.telegram.org/bot<token>/sendMessage |
The three chat formats are delivered natively, with no relay or proxy in between
(webhook.rs:286-287).
Your wallet address does not leave the miner in a webhook
Stratum pool strings routinely carry the worker — often a wallet address — and the password as URL components. The dispatcher handles this at a single chokepoint, all Verified (in source):
- Redaction runs inside the dispatcher, before serialization, so no caller can
skip it (
webhook.rs:606-607); it is documented as idempotent (:210). - Pool strings are reduced to host and port: scheme dropped, then any
user:pass@, then any path or query — and the remainder is still run through a wallet-address scanner as a second pass (webhook.rs:252-275). - Free-text fields — the
mining_stoppedreason and theotaphase — are wallet-scanned too (:217,:227). - A Telegram bot token lives in the URL path by Telegram's design, so the dispatcher
logs the event name and HTTP status and never the URL (
webhook.rs:305-309).
A dead endpoint cannot slow your miner down
| Bound | Value | Constant |
|---|---|---|
| Per-attempt HTTP timeout | 5 s | DEFAULT_WEBHOOK_TIMEOUT |
| Retries | 2 (3 attempts total) | DEFAULT_WEBHOOK_RETRIES |
| Backoff between attempts | 500 ms, fixed | DEFAULT_WEBHOOK_RETRY_BACKOFF |
| Queue depth | 64 events | WEBHOOK_QUEUE_DEPTH |
Enqueue never blocks: a full queue drops the event and logs it at debug level
(webhook.rs:490-491, :502-520). The enqueue result is explicitly documented
as informational, with the instruction that callers "should never branch mining behaviour on
it" (:500-501). Configuration changes — enable, URL, event list — take effect
through a watch channel with no daemon restart (:526-529). With webhooks disabled or no
URL set, events are dropped before any network I/O happens at all (:596-600).
The Bitaxe side is a second implementation, not the same one
DCENT_axe carries its own Home Assistant discovery builder. The source is explicit that this is
deliberate: "This is a fresh esp-idf-runtime implementation — the TOPIC/PAYLOAD shape is shared,
the code is not" (DCENT_OS_ESP/dcentaxe/src/mqtt_ha.rs:22-25).
Verified (in source). Do not assume a behaviour transfers between them.
| Antminer (dcentrald) | Bitaxe (DCENT_axe) | |
|---|---|---|
| Hashrate unit | TH/s (mqtt.rs:783) | GH/s (mqtt_ha.rs:187) |
| Always-published entities | 13 | 9 |
| BTU/h, Efficiency, Power Provenance, Pool Connected | present | not published |
| Command entities | Fan PWM, Target Power, Space Heater | Target Power, Autotuner Mode, Space Heater |
| Target-power clamp | 100 – 6000 W (mqtt.rs:132, :135) | 5 – 200 W (mqtt_ha.rs:559, :564) |
| Target-temperature clamp | 40 – 60 °C (mqtt.rs:137, :142) | 40 – 95 °C (mqtt_ha.rs:567, :572) |
Device manufacturer | "D-Central Technologies" (mqtt.rs:763) | "D-Central" (mqtt_ha.rs:45) |
| Outbound notification vocabulary | the 14-name union above | four kinds: share_milestone, thermal, failover, ota (DCENT_OS_ESP/dcentaxe/src/notifications.rs:11-16) |
The 60 °C and 95 °C ceilings are not an inconsistency — different silicon, different thermal ladders. They are listed side by side precisely so nobody merges them into one number.
The two notification vocabularies are also not disjoint, and it would be easy to assume they are:
share_milestone and ota appear in both, while thermal and
failover are ESP-only spellings with no exact counterpart in the Antminer union (the
Antminer names for the same territory are thermal_safety / thermal_restart
and pool_failover). If you are writing one receiver for both platforms, treat the two
name sets as overlapping-but-different, not as one vocabulary. Verified (in source)
— the intersection is computed by script from both enum definitions.
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.
What this page does not tell you
- Whether any of it works against a real broker. No bench verification. This is a reading of the source and of the shipped configuration file.
- The REST endpoint schemas. The repository publishes no OpenAPI specification. We
name
GET /api/mqtt/statusonly because the shipped configuration file names it (dcentrald.toml:159); we do not print a route table we have not resolved against the router. See API & ports for what is published. - The full thermal ladder. Two layers of the source name different hot/dangerous thresholds, so we state only the clamp that is actually asserted: a remote Home Assistant setpoint cannot exceed 60 °C on an Antminer.
Which control boards this has evidence on is a separate question from what the firmware is written to do; see hardware status for the per-lane picture. Nearby: all documentation.
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: DCENT_Toolbox command reference.
Related products, repair, and setup paths
- how D-Central diagnoses ASIC repairs
- ASIC troubleshooting library
- ASIC manuals and repair guides
- replacement hashboards
- ASIC control boards
- ASIC power supplies
- S19 family replacement hashboard
- C52 replacement control board
- APW12 S19 power supply
- compare specs in the ASIC miner database
- compare ASIC miner specs
- ASIC miner database
- ASIC repair services
- Antminer S19 specs and profitability
- buy a tested Antminer S19
- Antminer S19 maintenance guide
- Antminer S19 repair service
- Antminer S21 specs
- Bitmain Antminer S21
- Antminer S21 maintenance guide
- BM1370BC S21 Pro chip
- Antminer S9 specs
- Bitmain Antminer S9
- Antminer S9 maintenance guide
- S9 hashboard repair parts bundle
Last reviewed August 19, 2026.
