Ok, I finally had time to find a solution. Thanks to chatGPT 🙂
The zigbee2tasmota device is connected to a "regular" mqtt client in iobroker (not the sonoff adapter). When a zigbee message arrives it gets forwarded via mqtt to my iobroker as a json string.
My script parses that json and creates separate states for each of the key:value pairs in the json.
For sending commands I automatically add an additional state "PowerSet" to devices that have a "Power" state. When I update the PowerSet state, a script creates a json string and writes it into the "mqtt.0.cmnd.ZigbeeGateway.ZbSend" state (see https://tasmota.github.io/docs/Zigbee/#sending-device-commands)
The advantage of this approach is that I have nothing to do on the iobroker side when I add new zigbee devices, they are handled automatically.
That's it 🙂 Seems to work well so far. Here's the script in case anyone has a similar problem with zigbee2tasmota:
// ############## user config
// where the mqtt messages arrive
const zigbee2tasmotaSensorObjectId = 'mqtt.0.tele.ZigbeeGateway.SENSOR';
// where the states of the zigbee devices should appear
const zbBaseFolder = '0_userdata.0.zigbee2tasmota';
// ZbSend state, see https://tasmota.github.io/docs/Zigbee/#sending-device-commands
const zbsendObjId = 'mqtt.0.cmnd.ZigbeeGateway.ZbSend';
// ############## end user config
const regexPowerSetStr = `${zbBaseFolder}\\..*PowerSet$`;
const regexPowerSet = new RegExp(regexPowerSetStr);
// receive
on(zigbee2tasmotaSensorObjectId, (obj) => {
const jsonString = obj.state.val;
const jsonData = JSON.parse(jsonString);
// log(`Received JSON string: ${jsonString}`);
function createObjectsRecursively(parent, data) {
for (const key in data) {
const obj = data[key];
const stateName = `${parent}.${key}`;
if (typeof obj === 'object') {
createObjectsRecursively(stateName, obj);
} else {
let value = obj;
// custom handling of special data types
if (key.toLowerCase() == 'time') {
value = new Date(obj);
log("found time")
}
// Check if the state already exists. If it does, just update the value.
// If it does not exist (-> new zigbee device), create the states.
if (!existsState(stateName)) {
// If the state does not exist, create it and set the value
createState(stateName, value, {
name: key,
type: typeof value,
role: "value",
read: true,
write: true
});
log(`Created state ${stateName}`);
// create additional state to set a new value that will be sent to the zigbee device.
// If I use the normal "Power" state, I am afraid it will trigger the zbSend command
// everytime it gets updated from the zigbee device itself.
if (stateName.endsWith("Power")) {
let powerSetStateName = stateName + "Set"
createState(powerSetStateName, !!value, {
name: key + "Set",
type: "boolean",
role: "value",
read: true,
write: true
});
log(`Created state ${powerSetStateName}`);
}
// potentially add more custom handling for dimmer values, etc
} else {
setState(stateName, value);
}
// log(`set state ${parent}.${key} with value ${value}`);
}
}
}
createObjectsRecursively(zbBaseFolder, jsonData);
});
// send power on/off
on(regexPowerSet, (obj) => {
const objId = obj.id;
// probably regex is not needed and we can just take .Device or .Name
// Define a regular expression to match the substring between "ZbReceived" and "PowerSet"
const regex = /ZbReceived\.(.*?)\.PowerSet/;
// Use the regular expression to extract the substring
const match = objId.match(regex);
if (match) {
const zbname = match[1];
let newValue = obj.state.val;
let zbsendstring = "\{\"Device\": \"" + zbname + "\"\,\"Send\"\:\{\"Power\"\: "+newValue+"\}}"
log("zbsend: " + zbsendstring)
setState(zbsendObjId, zbsendstring)
} else {
log(`The substring between "ZbReceived" and "PowerSet" not found in ${objId}`);
}
});
PS: sending zigbee commands is only supported for power on/off. I don't have any zigbee devices that have dimmer or hue values or anything else. I guess the script can be adapted to work with those kind of devices, too.