UNPKG

iobroker.zigbee2mqtt

Version:
180 lines (159 loc) 6.99 kB
/** * Übersetzt ioBroker-State-Änderungen in Zigbee2MQTT-MQTT-Nachrichten * und leitet Z2M-Log-Nachrichten in den ioBroker-Logger weiter. */ class Z2mController { /** * Erstellt eine neue Z2mController-Instanz. * * @param {object} adapter Die ioBroker-Adapter-Instanz * @param {Array} deviceCache Gemeinsamer Cache aller bekannten Geräte * @param {Array} groupCache Gemeinsamer Cache aller bekannten Gruppen * @param {object} logCustomizations Debug/Filter-Einstellungen (debugDevices, logfilter) */ constructor(adapter, deviceCache, groupCache, logCustomizations) { this.adapter = adapter; this.groupCache = groupCache; this.deviceCache = deviceCache; this.logCustomizations = logCustomizations; } /** * Erzeugt aus einer ioBroker-State-Änderung eine Zigbee2MQTT-Nachricht. * Gibt null zurück wenn kein Gerät/State gefunden wurde oder der Setter undefined liefert. * * @param {string} id Vollständige State-ID (z.B. "zigbee2mqtt.0.0xAABB.state") * @param {ioBroker.State} state Das neue State-Objekt mit val und ack * @returns {Promise<{topic: string, payload: object}|undefined>} Die Z2M-Nachricht oder undefined */ async createZ2MMessage(id, state) { const splitedID = id.split('.'); if (splitedID.length < 4) { this.adapter.log.warn(`state ${id} not valid`); return; } if (id.endsWith('info.coordinator_check')) { // Fix 1: Z2M erwartet JSON-Objekt {} nicht leeren String '' return { topic: 'bridge/request/coordinator_check', payload: {} }; } const ieee_address = splitedID[2]; const stateName = splitedID[3]; const device = this.groupCache.concat(this.deviceCache).find((d) => d.ieee_address === ieee_address); if (!device) { return; } const deviceState = device.states.find((s) => s.id === stateName); if (!deviceState) { return; } let stateVal = state.val; if (deviceState.setter) { // Fix 3: setter kann crashen (z.B. ungültiger Farbwert) → try/catch try { stateVal = deviceState.setter(state.val); } catch (err) { this.adapter.log.warn(`${device.ieee_address} state: ${stateName} setter error: ${err.message || err}`); return; } } // Fix 5: wenn setter undefined zurückgibt (z.B. Toggle-Release) → nichts senden if (stateVal === undefined) { this.adapter.log.debug(`${device.ieee_address} state: ${stateName} setter returned undefined, skipping`); return; } let stateID = deviceState.id; if (deviceState.prop) { stateID = deviceState.prop; } if (deviceState.setattr) { stateID = deviceState.setattr; } const controlObj = { payload: { [stateID]: stateVal, }, topic: `${device.id}/set`, }; if (stateID === 'send_payload') { try { controlObj.payload = JSON.parse(stateVal); this.adapter.setState(id, state.val, true); } catch (error) { // Fix 9: rawStr als String direkt als payload – wird in main.js via // JSON.stringify() nochmals gewrappt → würde doppelt quoten. // Stattdessen: Fehler loggen und NICHT senden (ungültiges JSON bleibt ungültig) this.adapter.log.warn( `${device.ieee_address} state: ${stateID} error: value passed is not a valid JSON – not sent` ); this.adapter.log.debug(`${device.ieee_address} raw value: ${stateVal}`); return; } } // if available read option and set payload if (deviceState.options) { for (const option of deviceState.options) { // Fix: "in"-Prüfung statt === undefined, damit null als gültiger gecachter Wert behandelt wird if (!(option in device.optionsValues)) { const optState = await this.adapter.getStateAsync(`${splitedID[0]}.${splitedID[1]}.${splitedID[2]}.${option}`); device.optionsValues[option] = optState ? optState.val : null; } if (option === 'transition' && device.optionsValues[option] === -1) { continue; } controlObj.payload[option] = device.optionsValues[option]; } } // If an option datapoint has been set, it does not have to be sent. // This is confirmed directly by the adapter (ack = true) if (deviceState.isOption) { // set optionsValues 'Cache' device.optionsValues[stateName] = state.val; this.adapter.setState(id, state.val, true); return; } // States die nicht von Z2M zurückgemeldet werden → sofort ack=true setzen const immediateAckRole = ['button'].includes(deviceState.role); const immediateAckId = ['brightness_move', 'colortemp_move', 'brightness_step', 'effect'].includes(deviceState.id); if (immediateAckRole || immediateAckId) { this.adapter.setState(id, state.val, true); } if (this.logCustomizations.debugDevices) { const debugList = String(this.logCustomizations.debugDevices).split(',').map((s) => s.trim()); if (debugList.includes(device.ieee_address)) { this.adapter.log.warn(`<<<--- toZ2M -> ${device.ieee_address} states: ${JSON.stringify(controlObj)}`); } } return controlObj; } /** * Leitet eine Z2M-Log-Nachricht (bridge/logging) an den ioBroker-Logger weiter. * Nachrichten die dem konfigurierten Logfilter entsprechen werden unterdrückt. * * @param {{ payload: { message: string, level: string } }} messageObj Die Log-Nachricht */ async proxyZ2MLogs(messageObj) { const logMessage = messageObj.payload && messageObj.payload.message; if (!logMessage) { return; } if (this.logCustomizations.logfilter.some((x) => logMessage.includes(x))) { return; } const logLevel = messageObj.payload.level; switch (logLevel) { case 'debug': case 'info': case 'error': this.adapter.log[logLevel](logMessage); break; case 'warning': this.adapter.log.warn(logMessage); break; default: this.adapter.log.debug(`Z2M [${logLevel}]: ${logMessage}`); break; } } } module.exports = { Z2mController: Z2mController, };