private@homelab: ~/latest
local-first guides · privacy-aware · no noisy tracking
private@homelab:~$ cat guides/article.md
· ·
9–13 minutes
read

How to Write a Custom Zigbee2MQTT External Converter

Zigbee2MQTT pairs your device but shows it unsupported? Here's how to write an external converter that fixes it locally, and what stops it loading.

Pair an unsupported Zigbee device and Zigbee2MQTT will usually still let you talk to it — it just won’t know what to do with what it’s saying. The frontend shows a generic device card, the Exposes tab is thin or blank, and Home Assistant sees a handful of entities that don’t map to what the device actually does. A custom Zigbee2MQTT external converter is how you close that gap yourself, entirely on your own network, without waiting for someone else to submit a pull request upstream.

This is a mechanism walkthrough, not a single-device tutorial, built from Zigbee2MQTT’s own external converters reference and its support new devices process page, plus the issue tracker for two current behaviors that make older tutorials silently wrong.

When you actually need a custom converter

Before writing anything, rule out the cheaper options. Update Zigbee2MQTT first: device support lands continuously, and a device that’s unsupported today can be fully supported two releases later with no work from you. That remains the most common fix for this whole error class, as the companion piece on the no converter available error covers in more detail. Next, search the project’s GitHub discussions and issue tracker for your exact model number, since someone may have already published a converter for it. Finally, confirm the device genuinely lacks support rather than throwing a per-attribute diagnostic message about something cosmetic.

If none of that gets you unstuck, writing a converter is the correct move — and, per Zigbee2MQTT’s docs, the first half of the intended path. The second half is submitting it upstream.

Note · compatibility

Devices exposing a manuSpecificTuya cluster need Tuya-specific handling rather than a generic converter approach. If that’s your device, the site’s dedicated TS0601 custom-converter guide covers that path directly.

Before you write anything: pairing and reading Exposes and Clusters

Zigbee2MQTT can pair with essentially any Zigbee device, recognized or not. Pairing and support are separate concerns. Once the device is paired, the frontend’s Exposes tab shows whatever capabilities were auto-discovered, which for an unsupported device is often incomplete or generic. The Clusters tab beside it shows the raw ZCL (Zigbee Cluster Library) data the device is actually reporting: clusters, attributes, and their current values. That’s the ground truth a converter gets built from.

Read both tabs before writing a line of converter code. The official docs are direct about one limit: if the device doesn’t conform properly to the ZCL specification, no amount of correct-looking converter code will make it work fully, or at all. A converter maps what a device reports and accepts; it can’t invent conformance.

Anatomy of a converter

An external converter is a JavaScript file that exports an object, or an array of objects for a file covering more than one device, matching the same DefinitionWithExtend shape Zigbee2MQTT’s built-in converters use internally. There’s no separate, simplified “external” API; you’re writing to the same interface the project’s maintainers write to.

A minimal definition needs five things: zigbeeModel (what the device reports itself as over the Zigbee network), model, vendor and description for identification, and an extend array. Where a modelID is shared across unrelated products, as it is throughout Tuya’s range, matching happens on a fingerprint (modelID plus manufacturerName) instead. The extend array does most of the real work for common device types, because Zigbee2MQTT’s “modern extends” API bundles standard capabilities like battery reporting, temperature or humidity into single reusable building blocks.

For anything outside those patterns, two more fields come into play: fromZigbee converters parse incoming messages from the device into the values Zigbee2MQTT publishes over MQTT, and toZigbee converters do the reverse, turning an outgoing command into the Zigbee command the device expects. The exposes field ties it together — it determines which capabilities show up, and are user-controllable, in the frontend and in Home Assistant. If a capability isn’t in exposes, it doesn’t matter how well fromZigbee parses it. The site’s explainer on Zigbee2MQTT’s topic and attribute structure covers what an exposed capability then looks like on MQTT.

The fast path: generate an external definition

You don’t have to start from a blank file. Zigbee2MQTT’s frontend includes a Dev console with a “generate external definition” option that builds a starting-point converter automatically, mapped from whatever clusters and attributes it read off the paired device. It won’t be correct for anything unusual, but it saves the tedious part: getting the zigbeeModel string exactly right, scaffolding the object shape, and pre-filling whatever the device’s clusters made obvious.

Treat it as a draft. The remaining work is adding extend entries the generator didn’t recognize, writing custom fromZigbee/toZigbee logic for anything genuinely device-specific, and checking the result against real behavior: does reported state match reality, do commands reach the device.

Enabling external converters on a current install

This is the step that defeats otherwise-correct converters, and the reason an older tutorial can walk someone through flawless converter code that then appears to do nothing. Since Zigbee2MQTT 2.11.0, external converters and external extensions are disabled by default on new installations, for security reasons. The setting is enable_external_js, and it lives in the advanced section of your configuration.

configuration.yaml
advanced:
  enable_external_js: true

The scope matters when you’re diagnosing a setup: an install that predates 2.11.0 and never set the option keeps external JavaScript enabled, and the project has said that holds until 3.0. Two machines on the same Zigbee2MQTT version can behave differently here based only on when each was created.

Turning it on lets Zigbee2MQTT execute arbitrary JavaScript from that folder, including converters you didn’t write. Treat one copied from a forum post the way you’d treat any script running alongside your smart home’s credentials.

Warning

On an installation created since 2.11.0, enable_external_js is off and a correctly written converter simply never loads. Set it explicitly rather than relying on the default, and check it first if a converter tutorial written before 2.11.0 appears to produce no effect at all.

The converter file itself still lives in an external_converters folder at the same filesystem level as your configuration.yaml. That part of the mechanism hasn’t changed; what changed is that populating the folder is no longer sufficient on its own.

Reloading a converter without restarting Zigbee2MQTT

How you load a converter decides whether you need a restart. A .js file you place or edit directly in the external_converters folder is read at startup, so a change made on disk doesn’t take effect until Zigbee2MQTT restarts. There’s also a runtime path that skips it: saving through the frontend’s Dev console (the External converters section, under Settings on current builds), or the equivalent converter/save MQTT request. A matching converter/remove request deletes one the same way.

Zigbee2MQTT publishes every loaded external converter to the zigbee2mqtt/bridge/converters topic at startup, and republishes that list whenever one changes at runtime — useful confirmation that a save actually applied. That’s a faster loop than a service restart per edit while you’re still working out fromZigbee parsing logic.

mosquitto_pub example
mosquitto_pub -h 192.168.10.20 -t "zigbee2mqtt/bridge/request/converter/save" \
  -m '{"name": "my_device.js", "code": "..."}'

The broker address is a representative example; point it at your own broker, ideally on a dedicated IoT VLAN. The code field is the full converter source as a string, which is why the frontend route is easier for hand-editing.

When your converter gets renamed .invalid

This is the second trap, and it’s the kind of thing that produces a confused “it worked yesterday” report. Starting in Zigbee2MQTT 2.2.0, a converter file that fails the loader’s validation is renamed with a .invalid extension so it can’t interfere with the rest of startup, and it stops loading from then on.

Zigbee2MQTT does say so: the log carries “Failed to load external converter” plus a line stating the invalid converter was ignored and renamed. But the log at startup is the only place it announces itself. In the frontend and in Home Assistant the device simply goes back to looking unsupported, and the file you go looking for is no longer under the name you gave it. That combination is what makes this read as a device fault rather than a converter fault.

Warning

A converter that fails validation is renamed to *.invalid and stops loading, on Zigbee2MQTT 2.2.0 and later. If one that previously worked vanishes after an update, check the external_converters folder for the renamed file and read the startup log before assuming the device changed.

The pattern isn’t confined to one release. Issue #27031 describes converters that worked on 2.1.2 being invalidated on 2.2.x; issue #29438 describes the same after 2.6.3. The file didn’t change in either case, the interface it was written against did. Keep a copy of any converter you rely on outside Zigbee2MQTT’s own directory, and expect to revisit it at major upgrades.

Contributing your converter upstream

The recommended workflow doesn’t end at “it works on my install.” Zigbee2MQTT’s docs point authors toward submitting a finished converter as a pull request to the zigbee-herdsman-converters repository, so the device gets built-in support for everyone. It’s optional, and a working external converter is a legitimate permanent setup, but upstreaming also takes your file off the list of things a future validation change can break.

If you’re still choosing between Zigbee2MQTT and ZHA, the mechanisms differ enough to read first: the ZHA custom quirk guide covers the ZHA-side equivalent, and the ZHA vs. Zigbee2MQTT comparison covers platform choice.

FAQ

Does Zigbee2MQTT work with devices it doesn’t officially support?
Yes. Zigbee2MQTT pairs with essentially any Zigbee device regardless of whether it’s officially recognized. Support determines how well the device’s capabilities are parsed and exposed afterward, not whether pairing succeeds.

Where do I put an external converter file in Zigbee2MQTT?
In an external_converters folder at the same filesystem level as your configuration.yaml, per the official external converters documentation.

Why does my external converter file do nothing after I add it?
Check enable_external_js under advanced in your configuration. On installations created since 2.11.0 it’s off by default, and a correctly written converter won’t load until it’s on. If it’s already on, remember that a file edited on disk is only read at startup, so the change needs a restart.

Why did my external converter stop working after a Zigbee2MQTT update?
Since 2.2.0, a converter that fails internal validation is renamed with a .invalid extension and stops loading. It’s logged at startup but nothing else surfaces it, so check the external_converters folder for that suffix and read the log before assuming the device changed behavior.

Do I need to restart Zigbee2MQTT every time I edit a converter?
Only if you edit the file on disk. Saving through the frontend’s Dev console, or the converter/save MQTT request, applies at runtime without a restart.

How is a Zigbee2MQTT external converter different from a ZHA quirk?
Both map an unsupported device’s raw Zigbee behavior into something the platform understands, but they aren’t interchangeable: a converter is JavaScript loaded by Zigbee2MQTT, a quirk is Python loaded by ZHA.

What this article verified, and what to check yourself

The file location, the DefinitionWithExtend shape, the converter fields, the bridge/converters topic, the converter/save and converter/remove requests and the enable_external_js behavior come from Zigbee2MQTT’s official external converters documentation, cross-checked against the 2.11.0 release notes for the new-installations-only scope of that default. The pairing and Exposes workflow, the “generate external definition” option, the ZCL conformance caveat and the pull request path come from the official support new devices page. The .invalid rename and the log lines with it are sourced from the issue tracker: #27031 against 2.2.0, opened 2025-04-07 and since closed, and #29438 reporting the same behavior after 2.6.3.

No device-specific converter was written or tested for this article. It’s a mechanism explainer synthesized from official documentation and cited issues, not a walkthrough of one author’s device.

What stays local: the converter file, the logic it contains and the resulting MQTT traffic all live on your own install, with no cloud dependency introduced. What doesn’t: whether the target device conforms to the ZCL specification closely enough for a converter to work, and whether Zigbee2MQTT’s validation rules or defaults change again in a future release.

After setting one up, check three things. That enable_external_js is actually enabled if nothing seems to be loading. That the external_converters folder holds no unexpected .invalid file after any update. And that the device’s real behavior, not just the absence of errors in the log, matches what the converter’s exposes claims it should do.

local-firstHome Assistantno-cloud