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 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.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.

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).

Entities published on every connect — 13. Verified (in source)dcentrald-api/src/mqtt.rs:744-984
HA domainObjectEntity nameUnitDefined at
sensorhashrateHashrateTH/s:779-786
sensortemperatureTemperature°C:787-798
sensorpowerPowerW:799-806
sensorbtuBTU/hBTU/h:807-814
sensorfan_rpmFan RPMRPM:815-822
sensorefficiencyEfficiencyJ/TH:823-830
sensoracceptedAccepted Shares:831-838
sensorrejectedRejected Shares:839-846
sensoruptimeUptimes:847-854
sensorenergyEnergykWh:907-923
sensorpower_provenancePower Provenance— (diagnostic):930-942
binary_sensorminingMining Active:946-952
binary_sensorpoolPool 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 none unless the daemon has published live_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_template carrying power_source, power_source_detail, power_modeled, power_calibrated and power_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.

Operator-writable entities, published only when commands are enabled. Verified (in source)mqtt.rs:990-1055; constants mqtt.rs:124-142; topics mqtt.rs:207-227
HA domainEntityCommand topicRange enforced
numberFan PWM<prefix>/fan_pwm/set0 – 30 %
numberTarget Power<prefix>/target_watts/set100 – 6000 W
climateSpace Heater<prefix>/target_temp/set40 – 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_MAX by 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 /set topic 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).

Events from the WebhookEvent producer — 13. Verified (in source)dcentrald-api/src/webhook.rs:182-198, payload fields at :95-176
Event nameFires whenPayload fields
mining_startedthe daemon began dispatching workpool
mining_stoppedclean shutdown, curtailment, or an operator stopreason
pool_failoverthe Stratum client moved to a backup poolfrom, to
pool_disconnecteda pool connection dropped, no backup engaged yetpool
thermal_safetyan emergency-shutdown / dangerous-temperature eventtemp_c, chain_id (0 = all)
share_milestonea milestone count of accepted sharesaccepted
lucky_sharea share whose achieved difficulty far exceeds the pool targetdifficulty (optional)
otaa firmware-update lifecycle stepphase
fan_failurea fan stopped or fell below its safe RPM floorrpm
hashboard_offlinea board enumerated chips but produces no hashratechain_id
thermal_restartmining resumed after a recoverable thermal eventnone
hashrate_degradedtotal hashrate stayed below the operator's floor (detection only)observed_ghs, floor_ghs
hashrate_recovery_exhaustedthe auto-recovery ladder gave up on a degraded episodeobserved_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.

The eighth alert-path event, absent from the table above. Verified (in source)notifications.rs:395-399, :436
Event nameFires whenPayload fields
emergency_shutdownthe thermal loop performs an emergency shutdowntemp_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] events filter, and anywhere the API validates a name, write hashboard_offline.
  • In the JSON that actually arrives, the alert.event value for that one event is hash_board_offline.

If you match on the event string in a receiver, match both. Verified (in source).

Delivery formats. Verified (in source)webhook.rs:288-312, generic envelope at :628-637
FormatBody 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_stopped reason and the ota phase — 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

Delivery bounds. Verified (in source)webhook.rs:61-76
BoundValueConstant
Per-attempt HTTP timeout5 sDEFAULT_WEBHOOK_TIMEOUT
Retries2 (3 attempts total)DEFAULT_WEBHOOK_RETRIES
Backoff between attempts500 ms, fixedDEFAULT_WEBHOOK_RETRY_BACKOFF
Queue depth64 eventsWEBHOOK_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.

Where the two platforms differ. Verified (in source) — Antminer mqtt.rs, Bitaxe mqtt_ha.rs
 Antminer (dcentrald)Bitaxe (DCENT_axe)
Hashrate unitTH/s (mqtt.rs:783)GH/s (mqtt_ha.rs:187)
Always-published entities139
BTU/h, Efficiency, Power Provenance, Pool Connectedpresentnot published
Command entitiesFan PWM, Target Power, Space HeaterTarget Power, Autotuner Mode, Space Heater
Target-power clamp100 – 6000 W (mqtt.rs:132, :135)5 – 200 W (mqtt_ha.rs:559, :564)
Target-temperature clamp40 – 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 vocabularythe 14-name union abovefour 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/status only 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.