Zigbee2MQTT External Convertor - problems with binary, numeric & temp in MJS file

I have an ESP32C6 (Arudino Framework) running a zibgee project.

My ESP32C6 is connected to HA via Z2M

I created a folder called external_converters and under that a file called myproj.mjs

First question, should I be using mjs or js for this? Online docs, and online examples that I’ve found are rubbish. If .mjs isn’t the correct way (I was following the guide here → External converters | Zigbee2MQTT)

Anyway… In my ESP32C6 I have 3 switches, 1 temp, 1 numeric and 1 binary endpoints.

I’ve got the switches showing in HA no problems (and I can toggle them in H/A and get the ESP32 to react), but I can’t get the temp to display (just shows null in Z2M) and likewise can’t get the numeric value or the binary value to show in Z2M.

What am I doing wrong please???

My myproj.mjs file is below

import * as m from 'zigbee-herdsman-converters/lib/modernExtend';
import * as fz from 'zigbee-herdsman-converters/converters/fromZigbee';
import * as tz from 'zigbee-herdsman-converters/converters/toZigbee';
import * as exposes from 'zigbee-herdsman-converters/lib/exposes';

const e = exposes.presets;

export default {
    zigbeeModel: ['MyProj'],
    model: 'MyProj',
    vendor: 'Me',
    description: 'ESP32-C6 controller',
    fromZigbee: [
        fz.on_off, 
        fz.temperature
    ],
    toZigbee: [
        tz.on_off
    ],
    exposes: [
        e.switch().withEndpoint('OnBoardLight').withDescription('Onboard status LED'),
        e.switch().withEndpoint('Light1').withDescription('External light 1'),
        e.switch().withEndpoint('Light2').withDescription('External light 2'),

        e.binary('systemInit', exposes.access.STATE, true, false)
            .withEndpoint('SystemInit')
            .withDescription('System Initialised'),

        e.temperature()
            .withEndpoint('InternalTemp')
            .withProperty('internal_temp')
            .withDescription('Internal Air Temp'),

        e.numeric('water_level', exposes.access.STATE)
            .withEndpoint('WaterLevel')
            .withProperty('water_level')               // 👈 consistent property name
            .withUnit('cm')                            // optional: show units
            .withDescription('Water level'),

    ],
    meta: { multiEndpoint: true },
    endpoint: (device) => ({
        OnBoardLight: 10,
        Light1: 9,
        Light2: 8,
        InternalTemp: 26,
        WaterLevel: 31,
        SystemInit: 99,
    }),
};

For completeness this is an extract from my configuration.yaml file

homeassistant:
  enabled: true
advanced:

   # Other stuff here but removed for this post.

  external_converters:
    - test.mjs
    - esp32c6-pond-controller.mjs

In H/A the binary is showing in Z2M as “SystemInit”, Description “System Initialised” value is Null, HA entities its showing as “SystemInit SystemInit”

In H/A the numeric is showing in Z2M as “Water Level”, Description “Water Level” value is Null cm, HA entities its showing as “Water Level WaterLevel”

In H/A the temperature is showing in Z2M as “Temperature”, Description “Internal Air Temp” value is Null C, HA entities its showing as “Temperature”

I know the ESP32C6 is working because for example I can see the binary report sending, see console output

[767484][D][ZigbeeBinary.cpp:70] setBinaryInput(): Setting binary input to 1
[767486][V][ZigbeeBinary.cpp:100] reportBinaryInput(): Binary Input report sent

and temp

[222118][V][ZigbeeTempSensor.cpp:79] setTemperature(): Updating temperature sensor value... 
[222118][D][ZigbeeTempSensor.cpp:81] setTemperature(): Setting temperature to 138

Can anyone please help me fix my mjs file so I can get the numeric, temp and binary showing in H/A

In addition to my post above.

When you look at all the zigbee examples → arduino-esp32/libraries/Zigbee/examples at master · espressif/arduino-esp32 · GitHub

It would be nice if in H/A / Z2M docs there was matching .mjs external convertor files for each zigbee example.

Are you using Z2M add-on for HA? That’s what I use. I had to setup the folder tree as such - config > zigbee2mqtt > data > external_converters > file. My current converter is just a .js file and it works, but I have seen that is something to keep an eye on as it may be deprecating and moving to .mjs formats in the future.

[edit] According to current Z2M docs for the latest release, you don’t have to add external_converters: to the configuration.yaml anymore. Just make sure your folder tree is correct. That is unless of course you’re using an older Z2M version.

Z2M is reading my .mjs file just fine.

and indeed the e.switch() lines work.

I’m having trouble with the correct syntax for temp, numeric and binary, they just all show up as null.

Try this -

import * as fz from 'zigbee-herdsman-converters/converters/fromZigbee';
import * as tz from 'zigbee-herdsman-converters/converters/toZigbee';
import * as exposes from 'zigbee-herdsman-converters/lib/exposes';

const e = exposes.presets;

// Custom fromZigbee converters
const fzLocal = {
    internal_temp: {
        cluster: 'msTemperatureMeasurement',
        type: ['attributeReport', 'readResponse'],
        convert: (model, msg, publish, options, meta) => {
            if (msg.data.measuredValue !== undefined) {
                return { internal_temp: msg.data.measuredValue / 100.0 }; // Zigbee reports in 0.01°C
            }
        },
    },
    water_level: {
        cluster: 'genAnalogInput',
        type: ['attributeReport', 'readResponse'],
        convert: (model, msg, publish, options, meta) => {
            if (msg.data.presentValue !== undefined) {
                return { water_level: msg.data.presentValue };
            }
        },
    },
    system_init: {
        cluster: 'genBinaryInput',
        type: ['attributeReport', 'readResponse'],
        convert: (model, msg, publish, options, meta) => {
            if (msg.data.presentValue !== undefined) {
                return { systemInit: msg.data.presentValue === 1 };
            }
        },
    },
};

export default {
    zigbeeModel: ['MyProj'],
    model: 'MyProj',
    vendor: 'Me',
    description: 'ESP32-C6 controller',
    fromZigbee: [
        fz.on_off,
        fzLocal.internal_temp,
        fzLocal.water_level,
        fzLocal.system_init,
    ],
    toZigbee: [
        tz.on_off,
    ],
    exposes: [
        e.switch().withEndpoint('OnBoardLight').withDescription('Onboard status LED'),
        e.switch().withEndpoint('Light1').withDescription('External light 1'),
        e.switch().withEndpoint('Light2').withDescription('External light 2'),

        e.binary('systemInit', exposes.access.STATE, true, false)
            .withEndpoint('SystemInit')
            .withDescription('System Initialised'),

        e.temperature()
            .withEndpoint('InternalTemp')
            .withProperty('internal_temp')
            .withDescription('Internal Air Temp'),

        e.numeric('water_level', exposes.access.STATE)
            .withEndpoint('WaterLevel')
            .withProperty('water_level')
            .withUnit('cm')
            .withDescription('Water level'),
    ],
    meta: { multiEndpoint: true },
    endpoint: (device) => ({
        OnBoardLight: 10,
        Light1: 9,
        Light2: 8,
        InternalTemp: 26,
        WaterLevel: 31,
        SystemInit: 99,
    }),
};

This is according to ChatGPT - The reason they show as null is because Zigbee2MQTT doesn’t know how to map the raw Zigbee attribute reports into those expose properties.

Right now your fromZigbee only has fz.on_off and fz.temperature, but those don’t automatically know about your custom endpoints (InternalTemp, WaterLevel, SystemInit).

Sadly that didn’t work, still null values.

Hot dog. That’s too bad. In any regard, what ChatGPT is saying is you have to map the attributes to the expose properties in the converter. ChatGPT got the logic wrong in it’s attempt to do this, but it says you have to use fzLocal to accomplish that.

Yay!

Resolved my issue. Instead of using fromZigbee, toZigbee and exposes, I changed to using modernExtend

import * as m from 'zigbee-herdsman-converters/lib/modernExtend';

export default {
    zigbeeModel: ['MyProj'],
    model: 'MyProj',
    vendor: 'Me',
    description: 'ESP32-C6 controller',
    meta: { multiEndpoint: true },
    extend: [
        m.deviceEndpoints({
            "endpoints":{
                "light_onboard":10,
                "light_one":9,
                "light_two":8,
                "internal_temp":26,
                "water_level":31,
                "system_init":99}
        }), 
        m.onOff({
            "powerOnBehavior":false,
            "endpointNames":["light_onboard"],
            "description": "On Board LED Light",
        }), 
        m.onOff({
            "powerOnBehavior":false,
            "endpointNames":["light_one"],
            "description": "Light1",
        }), 
        m.onOff({
            "powerOnBehavior":false,
            "endpointNames":["light_two"],
            "description": "Light2",
        }), 
        m.temperature({
            "endpointNames": ["internal_temp"],
            "description": "Internal Air Temp",
        }),
        m.binary({
            "name":"system_init",
            "cluster":"genBinaryInput",
            "attribute":"presentValue",
            "reporting":{"attribute":"presentValue","min":"MIN","max":"MAX","change":1},
            "valueOn":["ON",1],
            "valueOff":["OFF",0],
            "description":"System Initialised",
            "access":"STATE_GET",
            "endpointName":"system_init"
        }),
        m.numeric({
            "name": "water_level",
            "unit": "cm",
            "description": "Water level",
            "cluster": "genAnalogInput",
            "attribute": "presentValue",
            "reporting": {attribute: "presentValue", min: 10, max: 3600, change: 1},
            "endpointName": "water_level",
        }),        
    ],    
};

I know I should be able to use the other way as well with fromZigbee, toZigbee and exposes but I just couldn’t work out what was missing in my mjs file.

I still think documentation could be better for the external convertors and also .mjs example files that match the arduino-esp32 Zigbee examples with maybe an addition to the zigbee2mqtt example external converters that match the arduino examples.

Having to dig deep in the source of modernExtend.ts looking to see what properties onOff(), binary(), numeric() supported was a pita.

Also had to rename my end points to match the names I wanted in Home Assistant as the modernExtend didn’t seem to support the naming of a property like you could with exposes (.withProperty). ChatGPT suggested I could mix modernExtend with exposes but I couldn’t get that working. So just renamed everything to how I needed it in HA. Sorted…