Struggling with MQTT (tasmota) device that contain multiple entity types - what's your solution?

Hi all,

I don't know if this is the right place to ask, but ...

Anyhoo. I have moved my entire system into Node-RED, and I built an MQTT structure that looks like this:

[building]/[floor]/[room]/[thing type]/[name of thing]/[command or status]

So for example, a light in the kitchen would be:

home/downstairs/kitchen/light/island/status

Over time, I have begun to transition all my devices over to Tasmota, and I'm about halfway there. I created a translation subflow on the MQTT input that reformats the Tasmota MQTT into the format that the rest of the logic flows use internally.

Using the example above, home/downstairs/kitchen/light/island/status/POWER with payload ON would get translated into home/downstairs/kitchen/light/island/status with payload {"state": true}.

There's a translation layer on the output, too, to ensure that the Tasmota device gets the instruction it's expecting. It looks up the topicname in a global object, and converts the payload as needed.

Everything worked well until I got to a single device that has multiple [thing type] entities in it. For example, a Sonoff iFan has the fan controller AND a light. For my structure, that's a /fan/ topic and a /light/ topic, but Tasmota has that has that as one topic, with the devices separated by POWER1 for the relay and {"FanSpeed":<number>}, both on the same topic.

Does anyone have a better method of doing this than a detailed lookup table of devices? Like, is it possible to specify multiple topics in a single Tasmota device?

Why not just use pseudo names, i.e. fan-light becomes a "thing" and fan-speed becomes a "thing".

Hi @gregorius, thank you for your response.

I'm not sure I 100% follow your suggestion because I'm not sure what pseudo names are, but at the moment I'm able to filter and route messages by virtue of whether or not their topic contains a particular /thingtype/.

For example, if a topic contains /fan/, I can send that message to a flow called Fans to do stuff with it. A topic that contains /light/ would get sent somewhere else. I use those forward slashes to protect against device names that might contain the word "light" or "fan".

On the face of it (and please correct me), it seems like pseudo-names would not work with MQTT structures like mine.

Ah ok, because you're using type first, it would be:

  • home/downstairs/kitchen/light/fan/status
  • home/downstairs/kitchen/fan/light/status

what I was trying to say, use:

  • home/downstairs/kitchen/fan/fan-speed/status
  • home/downstairs/kitchen/light/fan-light/status

as topics to distinguish the dual functionality of the fan.

EDIT: or make it more generic by using the convention name:functionality as the "[name of thing]", i.e. fan:light and fan:speed (the colon being a random character choice on my part).

What specifically are you actually struggling with in this sonoff example ?

Personally, I use a lookup table to normalise all data. That is for several reasons. I don't typically use Tasmota but do have a wild mix of different device types and connections. So I have to be able to cope with that anyway. I also pick up data for devices that aren't mine so I need a list of devices that are.

1 Like

It's looking more and more like this is the thing. It removes the need to have gore in the code every time there's a new exception.

1 Like

Hi @bakman2, a Tasmota device (as far as I know) only has one topic, but may have many functions. In this case, the iFan has the fan controller, but also a light.

But if I make the topic to do with the fan, then I have to have an exception of some kind to address the fact that I'll need to use the fan topic when switching the light. It also prevents me from switching all lights simply by addressing .../light/...

I already use a kind of lookup table. It seems like the solution is just to expand it.

The fan light is turned on by POWER 1/On/True (as it is a relay)
The fanspeed is controlled by fanspeed 1/2/3/4
With mqtt you can use wildcards.

In addition you can setup rules in tasmota. ie. if this then that. if this topic, then do that topic or switch that relay.

I think your main problem is, is that you try to capture everything in a subflow, but tasmota is filled with these weird quircks, you can use rules and/or backlog to workaround them, but I have yet to see a generalised solution that actually works :') I find that creating specific flows for specific topics/actions makes it easier. Optimising can always be done later.

Or switch to ESPHome which lets you easily configure everything. :smiley:

Are you talking about sending commands to Tasmota or getting sensor data back?

You send a command by means of MQTT using eg topic CMND/bedroom/light/POWER and payload TOGGLE where bedroom/light is the device's configured topic.

The device will respond with
topic: "stat/bedroom/light/RESULT" , payload {"POWER":"ON"}
and topic: "stat/bedroom/light/POWER", payload "ON"

A slightly more complicated example can use BACKLOG

{
topic: "cmnd/gosund/4/backlog",
payload: "pulsetime 1000; power ON"
}

A Tasmota device sends telemetry messages at defined intervals using topic tele/..., eg
tele/bedroom/light/STATE = {"Time":"2026-03-10T18:45:45","Uptime":"40T05:41:47","UptimeSec":3476507,"Heap":24,"SleepMode":"Dynamic","Sleep":50,"LoadAvg":19,"MqttCount":83,"POWER":"ON","Wifi":{"AP":1,"SSId":"GreyWagtail561","BSSId":"B4:FB:E4:62:00:67","Channel":1,"Mode":"11n","RSSI":80,"Signal":-60,"LinkCount":43,"Downtime":"0T01:43:14"}}
tele/bedroom/light/SENSOR = {"Time":"2026-03-10T18:45:45","ENERGY":{"TotalStartTime":"2025-09-02T16:35:13","Total":7.927,"Yesterday":0.052,"Today":0.041,"Period":0,"Power":2,"ApparentPower":8,"ReactivePower":8,"Factor":0.25,"Voltage":220,"Current":0.036}}

You can also request sensor data, in which case they are sent using the stat/... topic.

If you choose appropriate topics there should be no confusion over which device is meant, and certainly no need to test if topic contains eg /kitchen/

@TotallyInformation That would be a significant step backwards, since the goal is to be entirely rid of all Nabu Casa software. The process began with removing Home Assistant, and will be complete when the last ESPHome device is reflashed or replaced.

I cannot in good conscience recommend Naba Casa software to anyone who values privacy in the home. The ease of configuration you refer to is precisely how they managed to get so evil without people noticing.

Thanks for your detailed response. Mostly the latter, but I solved the problem with a lookup table as @TotallyInformation suggested.

1 Like

Urm, well I've never used ESPHome with Home Assistant! :wink: Personally, I love its balance between simple configuration and power and love the fact that I can create small reusable YAML configuration files that I import as needed for any device. Also love that you can dip into C++ if needing something bespoke but without the normal complexity. And the range of supported hardware, sensors, etc is mahoosive.

I'm not familiar with those issues and, as mentioned, I don't use any cloud with ESPHome and no Home Assistant. As far as I know, ESPHome is as open source as Node-RED?

I'm sure it has improved but back when I was using Tasmota, I kept getting a bunch of crashes on ESP8266's, never had any issues on ESPHome.

I'm glad you have a satisfactory resolution.
Still a bit confused but all is good.

For what it's worth, this is an example of the telemetry payload from one of my devices

{
   "Time":"2026-03-10T18:09:36",
   "BME680":{
      "Temperature":13.9,
      "Humidity":71.6,
      "DewPoint":8.9,
      "Pressure":990.3,
      "SeaPressure":1007.5,
     "Gas":418.32
   },
"PressureUnit":"hPa",
"TempUnit":"C"
}

And since it's in JSON format, I can simply access the Humidity in Node-red as msg.payload.BME680.Humidity

I think that what the OP is doing (which I do too) is translating all the various input values into a standard mqtt structure. In my case, for example, the bedroom temperature is in home/bedroom/temperature. This means that if the temperature sensor changes from a Tasmota device to a Zigbee one, as it has in my case recently, then all one has to do is change the flow that picks it up and publishes it to the standard MQTT topic. Nothing else needs to change.

The concept is called a UNS (Unified Namespace), where it should be possible to determine the topic of a value by knowing what it represents.

1 Like

Hey @TotallyInformation

Been meaning for ages to move towards a lookup table concept - any chance you could publish a screenshot of how you defined yours for inspiration (may as well learn from the master) and then a flow that does the MQTT magic ?

Moved into a new house and just started the rollout so now is as good a time as any to start cleaning stuff up and doing it right

Craig

1 Like

@Colin thank you, you expressed it better than I did.

@TotallyInformation confirmed it's best done with a lookup table. I'm using two global objects for this (one in, one out). It does the thing.

Hi @craigcurtin, I'm adding my voice to your request, if @TotallyInformation has the time.

Well, I've been meaning for a while to simplify mine. It has evolved over time but as you know, adding complexity is easy, removing it is hard! :rofl:

So right now, my 1st flow tab executes first on Node-RED startup and makes sure everything is set up correctly:



Note that I will generally keep an in-memory object for things but I have a process that breaks such an object into separate MQTT topics so that everything is there as well. MQTT being used for event-driven things and the context variable for functional things.

Here is something I was working on a while ago - until, as usual, I got distracted by new uibuilder features! It was an attempt to document a sane schema for IoT and network devices.

const schemas = {

    /** network table
     * A table of network devices (logical and physical)
     * This schema uses a structure suitable for use with VueJS/bootstrap-vue `b-table` options.
     * The table is an object of objects with the ID as the primary key
     * It is periodically updated by an NMAP scan.
     * The hostname, dhcp and description fields can be update from a uibuilder front-end.
     */
    network:  {
        id: {
            key: 'id', 
            primary: true, 
            label: 'ID', 
            dataType: 'string',
            thClass: 'd-none', 
            tdClass: 'd-none', // Don't display this field in tables
            eg: 'D8:6C:63:5E:41:5F'
        },
        mac: {
            key: 'mac', 
            label: 'MAC', 
            dataType: 'string',
            eg: 'D8:6C:63:5E:41:5F'
        },
        ipaddr: { 
            key: 'ipaddr', 
            label: 'IP', 
            dataType: 'text', 
            eg: '192.168.1.1'
        },
        vendor: { 
            key: 'vendor', 
            label: 'Vendor', 
            dataType: 'text', 
            eg: 'Expressif' 
        },
        updated: { 
            key: 'updated', 
            label: 'Updated', 
            dataType: 'date', 
            eg: 1581869007000 
        },
        updatedBy: { 
            key: 'updatedBy', 
            label: 'By', 
            dataType: 'text', 
            eg: 'nmap' 
        },
        srtt: { 
            key: 'srtt', 
            label: 'Latency', 
            dataType: 'number', 
            eg: 97.269 
        },
        host: { 
            key: 'host', 
            label: 'Host Name', 
            dataType: 'text', 
            eg: 'pi2.local' 
        },
        hostname: {
            key: 'hostname', 
            label: 'Host Descr', 
            dataType: 'text', 
            editable: true,
            description: 'Device Description', 
            eg: 'D1M04 - Access Point'
        },
        dhcp: {
            key: 'dhcp', 
            label: 'DHCP', 
            dataType: 'text', 
            editable: true,
            description: 'Is IP address set from DHCP?', 
            eg: 'Yes-Fixed'
        },
        description: {
            key: 'description', 
            label: 'Description', 
            dataType: 'text', 
            editable: true,
            eg: 'Some Text'
        },
    },

    /** Table of physical & logical IoT devices 
     * Used to track operational data for known devices.
     * Used to map logical device names to physical ID's 
     *   e.g. when setting `switch01`, mapping to physical ID and output channel to turn the relay on/off.
     */
    known_devices:  {
        /* Object Key - For MQTT, object key topic also holds online/offline status */
        name: '{String} Unique',
        /* Required Properties */
        location: '{ENUM} Short description - see `locations` global',
        description: '{String} Description of device',
        type: '{String} Description of device type',
        id: '{String} <source>/<unique id>[-<Optional channel #>]  Technical ID including leading protocol to ensure uniqueness',
        source: '{ENUM} ["LAN", "433"] How the device communicates with Node-RED',
        input: '{Boolean} Does this device take input? e.g. a switch/relay/bell sounder',
        output: '{Boolean} Does this device produce output? e.g. a sensor or a controller',
        in_use: '{Boolean} Is this device actively being used?',
        /* Optional Properties */
        controls: '{Integer} Number of controls (buttons or sensors) that produce control output (e.g. to turn things on/off)',
        sensors: '{String Array} Codes for all the sensors on board. See the `sensors` schema',
        info: {
            // For RFX devices (To send to RFX, `protocol/physical_id/id_channel`)
            src_channel: '{ENUM} Input/Output channel from/to source. e.g. `RFX/lights`',
            protocol: '{ENUM} [EasyESP, LIGHTWAVERF, ...]',
            physical_id: '{String} Optional. e.g. hex id for 433mhz devices',
            id_channel: '{Integer} Channel number for those devices that use settable channels (e.g. switches)',
            // For networked devices
            //protocol: '{String} description of protocol, e.g. `Tasmota`, `EasyESP`, `Custom`',
            mac: '{String} Network MAC address of devices network interface. Only for LAN sources.',
            ip: '{IP Address} IP Address of devices network interface. Only for Net or LAN sources.',
            url: '{url} URL of web interface for the device. Only if it has one.',
            host_name: '{String} Network hostname',
            // optional
            aka: '{String} If I/O from device also appears on a different ID',
            geo: '{Array[lat,lon]} Geolocation of device',
            docs: '{url} Link to device documentation',
            /* For devices with MQTT input/output information/control */
            mqtt: {
                in: '{mqtt_topic} Information source, e.g. zigbee2mqtt/Ikea_Bulb_01 or RFX_RAW/0x000001C0:2',
                set: '{mqtt_topic} Topic used to control device (if possible)',
                get: '{mqtt_topic} Topic used to manually get information from device (if possible)',
            }
        },
        /* Operational Data - added at runtime */
        status: {
            online: '{Boolean} Is the device considered to be online?',
            updated: '{Timestamp}',
            updated_by: '{String} What process/service updated the operational data?',

            last_on: '{Timestamp} Last time device turned on (for a switch) or came online (for a network device)',
            last_off: '{Timestamp} Last time device turned off (for a switch) or went offline (for a network device)',
            on_duration: '{Integer ms} Duration device has been turned on for, was last turned on for (or similarly online for if a network device)',
            off_duration: '{Integer ms} Duration device has been turned off for, was last turned off for (or similarly offline for if a network device)',

            rssi: '{Integer} Measure of radio strength',
            rssiMax: '{Integer} Largest RSSI measured',
            rssiMin: '{Integer} Smallest RSSI measured',
            battery: '{Integer %} Measure of battery',
            
            memory: '{Integer %} Memory in use',
            uptime: '{HH:mm:ss} Elapsed tim the device has been up for',
            last_restart: '{Timestamp} Last time device restarted',
            last_restart_reason: '{String} What caused the last restart?',

            // Other device specific entries may be here
        },
        outputs: {
            /* Sensor outputs - matches the sensor_types list */
            '<sensor_name>': '{Number|Boolean|String} The value output from the sensor.'
        },

        /** Allows data/commands to be sent to IoT devices 
         *  TODO: Need a way to define allowed input commands
         */
        inputs: {
            
            '<input_id>': {
                state: '{On|Off} State of switch',
                updated: '{Date} Last update of any kind',
                last_on: '{Date} Last time of On status',
                last_off: '{Date} Last time of Off status',
                on_duration: '{Integer} ms elapsed between last on and last off',
                off_duration: '{Integer} ms elapsed between last off and last on',
            },
        },

        /* Input Switch status & last on/off timestamps - 1 entry for each # controls */
        switches: {
            '<switch_number>': {
                state: '{On|Off} State of switch',
                updated: '{Date} Last update of any kind',
                last_on: '{Date} Last time of On status',
                last_off: '{Date} Last time of Off status',
                on_duration: '{Integer} ms elapsed between last on and last off',
                off_duration: '{Integer} ms elapsed between last off and last on',
            },
        },

        /** Defines max/min thresholds for light, temperature, humidity, power, etc */
        thresholds: {
            light: {
                /** Min permitted light in lux - any load lamps will be turned ON BELOW this level */
                min: 000,
                /** Max permitted light in lux - any load lamps will be turned OFF ABOVE this level */
                max: 999,
            }
        },

    }, // ----- End of known devices schema ----- //

    /** Same as known_devices but key'd by device ID instead of name.
     *  Automatically generated when known_devices changes.
     *  Enables easy matching of incoming data with the device name.
     */
    deviceids:  {},

    /** Controller map
     * Maps controller inputs to controlled devices (e.g. Switches)
     */
    controller_map: {
        input_name: '{String} Name (from known_devices)',
        input_switch: '{Number}',
        output: [ // can be any number of entries
            'Known_Id Name', 
        ],
    },

    /** Index of locations with maps to devices, switches & sensors
     * Used to make it easy to look up available devices, switches & sensors by location name
     */
    locations:  {
        /** See locations json */
    },

    protocols:  {
        /** Actual protocol used */
        'EasyESP': 'ESP',
        'Tasmota': 'ESP',
        'LIGHTWAVERF': '433MHz Siemens Lightwave-RF',
        'AC': '433MHz HomeEasy EU',
        'ti_home2': 'Totally Information`s home2 MQTT schema',
        'Custom': 'Other or unknown protocol/schema',
    },

    sources:  {
        /** Where does this signal come from? */
        'NET': 'General TCP/IP networking, OS service name as ID as this is a logical rather than physical source',
        'LAN': 'Wired or Wi-Fi network, MAC address as ID (upper case, colon separated)',
        '433': '433MHz wireless. e.g. RFXtrx433e, Unique Hex code as ID',
    },

    /** The different sensor types allowed in the `sensors` list property
     *  of the known_devices table.
     */
    sensor_types:  {
        'temperature': 'Temperature (degrees celsius)',
        'humidity': 'Relative humidity (%)',
        'light': 'Light intensity (Lumens)',
        'pressure': 'Sea-level equivalent pressure (mbar)',
        'movement': 'Movement (1,0) e.g. PIR or mag door sensor',
        'switch': 'Switch (On/Off) e.g. push button, latch, etc',
        'sound': 'Sound (1/0)',
        'energy': 'Energy (Watts)',
        'voltage': 'Voltage (Volts)',
        'current': 'Current (amps)',
    },

}

And here is an example that normalises inputs - this from the venerable RFXtrx433e:

Also worth noting that I typically may have multiple MQTT topic structures that repeat some of the same outputs. So the known_devices structure keeps the updated device details in logical device id order. But an environment structure keeps things in logical sensor/room order:


Same data, different structure so that it stays very easy to subscribe whether you want to know things about a device, a room or a sensor type.

2 Likes