UNPKG

iobroker.ecoflow-mqtt

Version:
1,064 lines (1,032 loc) 169 kB
const protobuf = require('protobufjs'); const https = require('node:https'); const protoSource = ` syntax = "proto3"; message Header { optional bytes pdata = 1; optional int32 src = 2; optional int32 dest = 3; optional int32 d_src= 4; optional int32 d_dest = 5; optional int32 enc_type = 6; optional int32 check_type = 7; optional int32 cmd_func = 8; optional int32 cmd_id = 9; optional int32 data_len = 10; optional int32 need_ack = 11; optional int32 is_ack = 12; optional int32 seq = 14; optional int32 product_id = 15; optional int32 version = 16; optional int32 payload_ver = 17; optional int32 time_snap = 18; optional int32 is_rw_cmd = 19; optional int32 is_queue = 20; optional int32 ack_type= 21; optional string code = 22; optional string from = 23; optional string module_sn = 24; optional string device_sn = 25; } message HeaderMessage { repeated Header header = 1; } message EventRecordItem { optional uint32 timestamp = 1; optional uint32 sys_ms = 2; optional uint32 event_no = 3; repeated float event_detail = 4; } message EventRecordReport { optional uint32 event_ver = 1; optional uint32 event_seq = 2; repeated EventRecordItem event_item = 3; } message EventInfoReportAck { optional uint32 result = 1; optional uint32 event_seq = 2; optional uint32 event_item_num = 3; } message ProductNameSet { optional string name = 1; } message ProductNameSetAck { optional uint32 result = 1; } message ProductNameGet {} message ProductNameGetAck { optional string name = 3; } message RTCTimeGet {} message RTCTimeGetAck { optional uint32 timestamp = 1; optional int32 timezone = 2; } message RTCTimeSet { optional uint32 timestamp = 1; optional int32 timezone = 2 [(nanopb).default = 0]; } message RTCTimeSetAck { optional uint32 result = 1; } message Send_Header_Msg { optional Header msg = 1; } message SendMsgHart { optional int32 link_id = 1; optional int32 src = 2; optional int32 dest = 3; optional int32 d_src = 4; optional int32 d_dest = 5; optional int32 enc_type = 6; optional int32 check_type = 7; optional int32 cmd_func = 8; optional int32 cmd_id = 9; optional int32 data_len = 10; optional int32 need_ack = 11; optional int32 is_ack = 12; optional int32 ack_type = 13; optional int32 seq = 14; optional int32 time_snap = 15; optional int32 is_rw_cmd = 16; optional int32 is_queue = 17; optional int32 product_id = 18; optional int32 version = 19; } message setMessage { setHeader header = 1; } message setHeader { setValue pdata = 1; int32 src = 2; int32 dest = 3; int32 d_src = 4; int32 d_dest = 5; int32 enc_type = 6; int32 check_type = 7; int32 cmd_func = 8; int32 cmd_id = 9; int32 data_len = 10; int32 need_ack = 11; int32 is_ack = 12; int32 seq = 14; int32 product_id = 15; int32 version = 16; int32 payload_ver = 17; int32 time_snap = 18; int32 is_rw_cmd = 19; int32 is_queue = 20; int32 ack_type = 21; string code = 22; string from = 23; string module_sn = 24; string device_sn = 25; } message setValue { optional int32 value = 1; optional int32 value2 = 2; } `; function decodeMsg(hexString, msgtype, protoSource) { try { const root = protobuf.parse(protoSource).root; const PowerMessage = root.lookupType(msgtype); //const message = PowerMessage.decode(hexString); // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore const message = PowerMessage.decode(Buffer.from(hexString, 'hex')); const object = PowerMessage.toObject(message, { defaults: false }); return object; } catch (error) { throw new Error(`decodeMsg -> ${error}`); } } /** * @param {object} stateDictObj - dictionary of datapoints * @param {string} channel - upper struct * @param {string} state - the datapoint * @param {string | number | boolean} value - the value of datapoint * @param {object} stateObj - datapoint definitions */ function convertData(stateDictObj, channel, state, value, stateObj) { let key = stateDictObj[channel][state]['entity']; if (value !== '' && value != null) { let val = value.toString(); switch (key) { case 'number': case 'level': //convert number if (state !== 'brightness') { if (typeof value === 'string' || value instanceof String) { value = (parseFloat(value) * stateObj[channel][key][state]['mult']).toFixed(2); } else if (Number.isInteger(value)) { value = value * stateObj[channel][key][state]['mult']; } else { value = Number((value * stateObj[channel][key][state]['mult']).toFixed(2)); } } break; case 'string': value = value.toString(); break; case 'switch': //convert false/true if (value == 0 || value == false || value == '0') { value = false; } else { value = true; } break; case 'diagnostic': //convert string to item if (Object.prototype.hasOwnProperty.call(stateObj[channel][key][state][state], val)) { value = stateObj[channel][key][state][state][val].toString(); } else { value = `${val} not part of array`; } break; case 'array': value = JSON.stringify(value); break; default: break; } } return value; } /* pstream umbauen, damit Antworten mit mehr als einer Nachricht auch verarbeitet werden Rückgabe der decodierung als Object abspeichern mit loop durch Objekt */ /** * @param adapter - THIS transfer * @param {import("node:buffer").WithImplicitCoercion<string>} payload - payload of MQTT msg * @param {any} usage - switch to use it for other cases * @param {string | number} topic - serial# * @param {string} msgtype - set/get/update * @param {any} protoSourceDevice - dictionary * @param protoMsg - proto definition * @param {boolean} log - log enable */ function pstreamDecode(adapter, payload, usage, topic, msgtype, protoSourceDevice, protoMsg, log) { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore const buf = new Buffer.from(payload, 'base64'); try { if (log === true) { adapter.log.debug(`[PROTOBUF decode] ${topic} [${msgtype}] raw (hex): ${payload.toString('hex')}`); } let msgobj = decodeMsg(buf, 'HeaderMessage', protoSource); if (msgobj) { if (Array.isArray(msgobj.header)) { let returnobj = []; let dbgmsg = {}; if (log === true) { adapter.log.debug( `[PROTOBUF decode] ${topic} [${msgtype}] stream has ${msgobj.header.length} message(s)`, ); } for (let i = 0; i < msgobj.header.length; i++) { for (let info in msgobj.header[i]) { if (info !== 'pdata') { dbgmsg[info] = msgobj.header[i][info]; } } if (log === true) { adapter.log.debug( `[PROTOBUF decode] ${topic} [${msgtype}] msg#${i} header ${JSON.stringify(dbgmsg)}`, ); } //evaluate cmdFunc and cmd Id let cmdFunc = msgobj.header[i].cmdFunc; let cmdId = msgobj.header[i].cmdId; let src = msgobj.header[i].src; const seq = msgobj.header[i].seq; let prototyp = ''; if (protoMsg && protoSourceDevice) { if (cmdFunc) { if (Object.prototype.hasOwnProperty.call(protoMsg['cmdFunc'], cmdFunc)) { if (cmdId) { if ( Object.prototype.hasOwnProperty.call( protoMsg['cmdFunc'][cmdFunc]['cmdId'], cmdId, ) ) { prototyp = protoMsg['cmdFunc'][cmdFunc]['cmdId'][cmdId]; } else { prototyp = 'undef'; //no decode possible; if (log === true) { adapter.log.debug( `[PROTOBUF decode] ${topic} [${msgtype}] msg#${i} unknown cmdId `, ); } //continue; } } else { prototyp = 'undef'; //message without cmdId; if (log === true) { adapter.log.debug( `[PROTOBUF decode] ${topic} [${msgtype}] msg#${i} no cmdId in Message`, ); } } } else { prototyp = 'undef'; //no decodei possible; if (log === true) { adapter.log.debug( `[PROTOBUF decode] ${topic} [${msgtype}] msg#${i} unknown cmdFunc`, ); } } } else { prototyp = 'undef'; //message without packageType; if (log === true) { adapter.log.debug( `[PROTOBUF decode] ${topic} [${msgtype}] msg#${i} no cmdFunc in message`, ); } if (msgobj.header[i].code) { if (msgobj.header[i].code == '-2') { returnobj.push({ info: { status: 'offline' } }); } } } } else { prototyp = 'undef'; //no objects adapter.log.debug( `[PROTOBUF decode] no protoMsg or protoSourceDevice objects ${topic} [${msgtype}] protoMsg ${protoMsg}protoSourceDevice ${ protoSourceDevice }`, ); } // go to msg itself let msg = null; if (msgobj.header[i].pdata) { if (msgobj.header[i].encType) { if (msgobj.header[i].encType == 1 && src !== 32) { //alternator && src !== 32 //shp2 && src == 11 if (log === true) { adapter.log.debug(`[PROTOBUF decode] ${topic} [${msgtype}] msg#${i} Encrypted`); } const typedArray = new Uint8Array(msgobj.header[i].pdata); const array = Array.from(typedArray); let modarray = []; for (let i = 0; i < array.length; i++) { modarray.push(array[i] ^ seq); } const newbuf = Buffer.from(modarray); msgobj.header[i].pdata = newbuf; } } if (log === true) { if (msgobj.header[i].pdata) { adapter.log.debug( `[PROTOBUF decode] ${topic} [${msgtype}] msg#${i} pdata: ${msgobj.header[ i ].pdata.toString('hex')}`, ); } } if (prototyp !== 'undef') { try { if (log === true && !msgobj.header[i].pdata) { adapter.log.debug(`[PROTOBUF decode] ${topic} [${msgtype}] msg#${i} => no pdata `); } msg = decodeMsg(msgobj.header[i].pdata, prototyp, protoSourceDevice); if (log === true) { adapter.log.debug( `[PROTOBUF decode] ${topic} [${msgtype}] msg#${i} => ${ prototyp } ${JSON.stringify(msg)}`, ); } returnobj.push({ [prototyp]: msg }); } catch (error) { adapter.log.debug( `[PROTOBUF decode] ${topic} [${msgtype}] msg#${i} => ` + ` id ${prototyp} error at: ${error}`, ); } } else { writeUnknownMsg(adapter, { device: String(topic), topic: msgtype, cmdFunc: String(cmdFunc), cmdId: String(cmdId), buffer: msgobj.header[i].pdata.toString('hex'), src: src, }); } } else { if (log === true) { adapter.log.debug(` [PROTOBUF decode] ${topic} [${msgtype}] msg#${i} => no pdata`); } } //Umschreiben, damit das für get_reply nutzbar wird if (msgobj.header[i].pdata) { //msgtype === 'get_reply' && msgobj.header[i].pdata = msgobj.header[i].pdata.toString('hex'); } /* if (cmdId === 32 && cmdFunc !== 12) { if (usage === 'noderedchart') { try { let msgobj32 = msg; //adapter.log.debug('energy:' + JSON.stringify(msgobj32)); let chart = [ { label: 'no', series: [], data: [], labels: [ '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13', '14', '15', '16', '17', '18', '19', '20', '21', '22', '23', '24' ] } ]; for (let i = 0; i < msgobj32.sysEnergyStream.length; i++) { let date = new Date(msgobj32.sysEnergyStream[i].timestamp * 1000); chart[0].label = date.toString(); chart[0].series.push( String(msgobj32.sysEnergyStream[i].watthType) + ' ' + msgobj32.sysEnergyStream[i].watth.reduce((a, b) => a + b, 0) ); chart[0].data.push(msgobj32.sysEnergyStream[i].watth); } return chart; } catch (error) { adapter.log.debug('id 32 error at: ' + error); } } else { let msgobj32 = msg; let watthType; let energy; let timeStamp; let energyobj = {}; try { for (let i = 0; i < msgobj32.sysEnergyStream.length; i++) { if ( msgobj32.sysEnergyStream[i].watthType && msgobj32.sysEnergyStream[i].watth && msgobj32.sysEnergyStream[i].timestamp ) { //adapter.log.debug('complete ' + i); //we assume all timestamps are equal and the last is used for historical message timeStamp = msgobj32.sysEnergyStream[i].timestamp; watthType = msgobj32.sysEnergyStream[i].watthType; energy = msgobj32.sysEnergyStream[i].watth.reduce((a, b) => a + b, 0); energyobj['watth' + watthType] = energy; } else { //adapter.log.debug('incomplete'); //incomplete telegram } } } catch (error) { adapter.log.debug('id 32 error at: ' + error); } if (msgobj32.sysSeq == 65535) { returnobj.push({'energy': energyobj}); } else { returnobj.push({'hist_energy':{ time: timeStamp, energy: energyobj }}); } } } */ } if (msgtype === 'get_reply') { adapter.quotas[topic] = msgobj; } return returnobj; } } } catch (error) { adapter.log.debug(`[PROTOBUF decode] error -> payload was: ${payload.toString('hex')}`); throw new Error(`[PROTOBUF decode] -> ${error}`); } } function writeUnknownMsg(adapter, msg) { if (!adapter.unknownPBmsg[msg.device]) { adapter.unknownPBmsg[msg.device] = {}; } if (!adapter.unknownPBmsg[msg.device][msg.topic]) { adapter.unknownPBmsg[msg.device][msg.topic] = {}; } if (!adapter.unknownPBmsg[msg.device][msg.topic][msg.cmdFunc]) { adapter.unknownPBmsg[msg.device][msg.topic][msg.cmdFunc] = {}; } if (!adapter.unknownPBmsg[msg.device][msg.topic][msg.cmdFunc][msg.cmdId]) { adapter.unknownPBmsg[msg.device][msg.topic][msg.cmdFunc][msg.cmdId] = { pbmsg: msg.buffer, src: msg.src, dst: msg.dst, }; } } /** * @param adapter - THIS transfer * @param {any} topic - serial# of device */ async function setOnlineStatus(adapter, topic) { let status = await adapter.getStateAsync(`${topic}.info.status`).catch(e => { adapter.log.warn(`did not get old value .info.status ->${e}`); }); if (status && status.val === 'offline') { await adapter.setStateAsync(`${topic}.info.status`, { val: 'online', ack: true }); adapter.log.info(`[EF] ${topic} is online with cloud`); } } /** * @param adapter - THIS transfer * @param {any} topic - serial# of device */ async function setOfflineStatus(adapter, topic) { let status = await adapter.getStateAsync(`${topic}.info.status`).catch(e => { adapter.log.warn(`did not get old value .info.status ->${e}`); }); if (status && status.val === 'online') { await adapter.setStateAsync(`${topic}.info.status`, { val: 'offline', ack: true, }); adapter.log.info(`[EF] ${topic} is OFFLINE with cloud`); } } /** * @param adapter - THIS transfer * @param {object} stateDictObj - dictionary of states * @param {object} stateObj - datapoint definition * @param {string} topic - serial# * @param {string} msgtype - set/get/update * @param payload - payload * @param {boolean} haenable - HA enabled for this device * @param {boolean} logged - log enable */ async function storeStationPayload(adapter, stateDictObj, stateObj, topic, msgtype, payload, haenable, logged) { let haUpdate = []; if (payload) { /* if (adapter.config.msgUpdatePstation) { adapter.log.debug( 'got topic ' + topic + ' ( stateDictObj ' + stateDictObj + ' stateObj: ' + stateObj + ' )' ); } */ try { let payloadobj = null; //station cyclically receives params messages, so we can detect from here the online status await setOnlineStatus(adapter, topic); /* let onlinestatus = await adapter.getStateAsync(topic + '.info.status').catch((e) => { adapter.log.warn('did not get old value .info.status ->' + e); }); */ if (payload['params']) { //skipping the incomming data when not valid if (payload['params']['ems.openBmsIdx']) { //only valid when payload ems.openBmsIdx=1 @Deltamax if (payload['params']['ems.openBmsIdx'] !== 0) { payloadobj = payload['params']; } } else if (payload['params']['bmsMaster.bqSysStatReg']) { //only valid when payload bmsMaster.bqSysStatReg = 128 @Deltamax if (payload['params']['bmsMaster.bqSysStatReg'] !== 0) { payloadobj = payload['params']; } } else if (payload['params']['bms_emsStatus.openBmsIdx']) { //only valid when payload ems.openBmsIdx=1 @Deltamax if (payload['params']['bms_emsStatus.openBmsIdx'] !== 0) { payloadobj = payload['params']; } } else if (payload['params']['bms_bmsStatus.bqSysStatReg']) { //only valid when payload bmsMaster.bqSysStatReg = 128 @Deltamax if (payload['params']['bms_bmsStatus.bqSysStatReg'] !== 0) { payloadobj = payload['params']; } } else { payloadobj = payload['params']; } /* if (onlinestatus && onlinestatus.val === 'offline') { await adapter.setStateAsync(topic + '.info.status', { val: 'online', ack: true }); adapter.log.info('[EF] ' + topic + ' is online with cloud'); } */ } else if (payload['data']) { if (payload['data']['quotaMap']) { let status = 'OK'; //skipping the incomming data when not valid if (payload['data']['quotaMap']['ems.openBmsIdx']) { if (payload['data']['quotaMap']['ems.openBmsIdx'] === 0) { status = 'unreliable ems'; } } if (payload['data']['quotaMap']['bms_emsStatus.openBmsIdx']) { if (payload['data']['quotaMap']['bms_emsStatus.openBmsIdx'] === 0) { status = 'unreliable ems'; } } if (payload['data']['quotaMap']['bmsMaster.bqSysStatReg']) { if (payload['data']['quotaMap']['bmsMaster.bqSysStatReg'] === 0) { status = 'unreliable bmsMaster'; } } if (payload['data']['quotaMap']['bms_bmsStatus.bqSysStatReg']) { if (payload['data']['quotaMap']['bms_bmsStatus.bqSysStatReg'] === 0) { status = 'unreliable bms'; } } if (status === 'OK') { payloadobj = payload['data']['quotaMap']; } /* if (onlinestatus && onlinestatus.val === 'offline' && payload['data']['online'] === 1) { await adapter.setStateAsync(topic + '.info.status', { val: 'online', ack: true }); adapter.log.info('[EF] ' + topic + ' is online with cloud'); } */ adapter.log.debug(`[${topic}] processing get_reply latest quotas !`); //storage of latest quotas to object, can then be called from sendTo if (msgtype === 'get_reply') { adapter.quotas[topic] = payload; } if (payload['data']['quotaMap']['timeTask']) { //logging //totalTaskNum als Kennung für Stations (bei SHP1 nicht dabei) if (payload['data']['timeTask']['totalTaskNum']) { await adapter.setStateAsync(`${topic}.timeTask.totalTaskNum`, { val: payload['data']['quotaMap']['timeTask']['totalTaskNum'], ack: true, }); let taskarr = payload['data']['quotaMap']['timeTask']['taskCfgs']; for (let i = 0; i < taskarr.length; i++) { const task = `task${i}`; if (taskarr[i]['taskIndex'] !== 255) { for (let key in taskarr[i]) { if (key !== 'timeScale') { await adapter.setStateAsync(`${topic}.timeTask.${task}.${key}`, { val: taskarr[i][key], ack: true, }); } else { //convert fehlt nich await adapter.setStateAsync(`${topic}.timeTask.${task}.${key}`, { val: JSON.stringify(taskarr[i][key]), ack: true, }); } } } } adapter.log.debug('processed get_reply timeTask !'); } } } else if (payload['data']['online'] === 0) { //station reports the data.online=0 as offline //it is ony part of messages with data (rquested by latestQuotas) /* let status = await adapter.getStateAsync(`${topic}.info.status`).catch(e => { adapter.log.warn(`did not get old value .info.status ->${e}`); }); */ await setOfflineStatus(adapter, topic); /* if (status && status.val === 'online') { await adapter.setStateAsync(topic + '.info.status', { val: 'offline', ack: true }); adapter.log.info('[EF] ' + topic + ' is OFFLINE with cloud'); } */ } } else { adapter.log.debug(`no params or data in payload ->${JSON.stringify(payload)}`); } if (payloadobj) { //stations with dotted syntax for (let comportion in payloadobj) { let dotted = comportion.split('.'); let channel = dotted[0]; let state = dotted[1]; if ( channel === 'timestamp' || channel === 'latestTimeStamp' || state === null || channel === 'kit' || channel === 'bms_kitInfo' || //to avoid crashing when undefined datapoint is uncomming, better the way around to check the known and there is leftover channel === 'bms_bmsInfo' || channel === 'bms_slave_bmsSlaveInfo_1' || channel === 'bms_slave_bmsSlaveInfo_2' ) { //exceptions } else { // manipulation of delta2 and delta2max if (channel === 'bms_bmsStatus' || channel === 'bms') { channel = 'bmsMaster'; } else if (channel === 'bms_emsStatus') { channel = 'ems'; } else if (channel === 'bms_slave_bmsSlaveStatus_1') { channel = 'bmsSlave1'; } else if (channel === 'bms_slave_bmsSlaveStatus_2') { channel = 'bmsSlave2'; } else if (channel === 'bms_slave') { //delta2 has only one port issue #264 channel = 'bmsSlave1'; } const channelsave = channel; // for handling the slaves when converting the value if (channel === 'bmsSlave1' || channel === 'bmsSlave2') { channel = 'bmsMaster'; } if (stateDictObj[channel][state]) { if (stateDictObj[channel][state]['entity'] !== 'icon') { let value = convertData(stateDictObj, channel, state, payloadobj[comportion], stateObj); //adapter.log.debug('converted value ' + value); let old = await adapter.getStateAsync(`${topic}.${channelsave}.${state}`).catch(e => { adapter.log.warn(`did not get old value ${topic}.${channelsave}.${state} ->${e}`); }); //adapter.log.debug('old ' + JSON.stringify(old)); if (!old) { await adapter.setStateAsync(`${topic}.${channelsave}.${state}`, { val: value, ack: true, }); if (haenable && stateDictObj[channel][state]['entity'] !== 'array') { let havalue; const type = stateDictObj[channel][state]['entity']; const enttype = stateObj[channel][type][state]['entity_type']; switch (enttype) { case 'sensor': havalue = String(value); break; case 'switch': if (value === true) { havalue = stateObj[channel][type][state]['payload_on']; } else if (value === false) { havalue = stateObj[channel][type][state]['payload_off']; } break; case 'select': try { havalue = stateObj[channel][type][state]['states'][value]; } catch (error) { adapter.log.warn( `select problem ${value} not in ${ stateObj[channel][type][state]['states'] } error -> ${error}`, ); } break; case 'text': havalue = value; break; default: havalue = String(value); break; } haUpdate.push({ topic: `${adapter.config.haTopic}/${topic}_${channelsave}/${state}`, payload: havalue, }); } } else { if (old.val !== value) { if (logged) { adapter.log.debug( `station update ` + `.${channelsave}.${state} ${old.val} -> ${value}`, ); } await adapter.setStateAsync(`${topic}.${channelsave}.${state}`, { val: value, ack: true, }); if (haenable && stateDictObj[channel][state]['entity'] !== 'array') { let havalue; const type = stateDictObj[channel][state]['entity']; const enttype = stateObj[channel][type][state]['entity_type']; switch (enttype) { case 'sensor': havalue = String(value); break; case 'switch': if (value === true) { havalue = stateObj[channel][type][state]['payload_on']; } else if (value === false) { havalue = stateObj[channel][type][state]['payload_off']; } break; case 'select': try { havalue = stateObj[channel][type][state]['states'][value]; } catch (error) { adapter.log.warn( `select problem ${value} not in ${ stateObj[channel][type][state]['states'] } error -> ${error}`, ); } break; case 'text': havalue = value; break; default: havalue = String(value); break; } haUpdate.push({ topic: `${adapter.config.haTopic}/${topic}_${channelsave}/${state}`, payload: havalue, }); } } else { //even the same value switches get an update //strange behaviour on HA side, where the switch returns to previous position // achtung macht aber ordentlich last, da immer mitgeschickt if (haenable && stateDictObj[channel][state]['entity'] === 'switch') { let havalue; const type = stateDictObj[channel][state]['entity']; const enttype = stateObj[channel][type][state]['entity_type']; switch (enttype) { case 'switch': if (value === true) { havalue = stateObj[channel][type][state]['payload_on']; } else if (value === false) { havalue = stateObj[channel][type][state]['payload_off']; } break; default: break; } haUpdate.push({ topic: `${adapter.config.haTopic}/${topic}_${channel}/${state}`, payload: havalue, }); } } } } } else { adapter.log.debug( `[store station] ${topic} not processed ${channelsave} state: ${state} value ${ payloadobj[comportion] }`, ); } } } } } catch (error) { adapter.log.debug(`store JSON payload ${error}`); adapter.log.debug(`payload from ${topic} was: ${JSON.stringify(payload)}`); } } else { adapter.log.debug('nothing to process'); } return haUpdate; } /** * @param adapter - THIS transfer * @param {object} stateDictObj - dictionary of states * @param {object} stateObj - datapoint definition * @param {boolean} haenable - HA enabled for this device * @param {string} topic - serial# * @param {string} channel - upper struct * @param {string} state - datapoint name * @param {any} val - value of state * @param {string} origchannel - the channel where references work * @param {boolean} logged - log enable */ async function compareUpdate( adapter, stateDictObj, stateObj, haenable, topic, channel, state, val, origchannel, logged, ) { let haUpdate = {}; try { if (stateDictObj) { if (stateDictObj[origchannel]) { if (stateDictObj[origchannel][state]) { if (stateDictObj[origchannel][state]['entity'] !== 'icon') { let value = null; try { value = convertData(stateDictObj, origchannel, state, val, stateObj); } catch (error) { adapter.log.debug( `CONVERT went wrong ${topic}.${channel}.${state} with ${val} error: ${error}`, ); } //adapter.log.debug('converted value ' + value); let old = await adapter.getStateAsync(`${topic}.${channel}.${state}`).catch(e => { adapter.log.warn(`[Compare] did not get old value ${topic}.${channel}.${state} ->${e}`); }); // adapter.log.debug(`old ${JSON.stringify(old)}`); if (!old) { if (logged) { adapter.log.debug( `[Compare] force update ` + `${topic}.${channel}.${state} -> ${value}`, ); } await adapter.setStateAsync(`${topic}.${channel}.${state}`, { val: value, ack: true, }); if (haenable && stateDictObj[origchannel][state]['entity'] !== 'array') { let havalue; const type = stateDictObj[origchannel][state]['entity']; const enttype = stateObj[origchannel][type][state]['entity_type']; switch (enttype) { case 'sensor': case 'number': havalue = String(value); break; case 'switch': //always a cmd if (value === true) { havalue = stateObj[origchannel][type][state]['payload_on']; } else if (value === false) { havalue = stateObj[origchannel][type][state]['payload_off']; } break; case 'select': try { havalue = stateObj[origchannel][type][state]['states'][val]; } catch (error) { adapter.log.warn( `[Compare] select problem ${value} not in ${ stateObj[origchannel][type][state]['states'] } error -> ${error}`, ); } break; case 'text': havalue = value; break; default: havalue = String(value); break; } haUpdate = { topic: `${adapter.config.haTopic}/${topic}_${channel}/${state}`, payload: havalue, }; } } else { if (Object.prototype.hasOwnProperty.call(old, 'val')) { if (old.val !== value) { if (logged) { adapter.log.debug( `[Compare] update ` + `${topic}.${channel}.${state} ${old.val} -> ${value}`, ); } await adapter.setStateAsync(`${topic}.${channel}.${state}`, { val: value, ack: true, }); if (haenable && stateDictObj[origchannel][state]['entity'] !== 'array') { let havalue; const type = stateDictObj[origchannel][state]['entity']; const enttype = stateObj[origchannel][type][state]['entity_type']; switch (enttype) { case 'sensor': case 'number': havalue = String(value); break; case 'switch': //always a cmd, no need to check the specific type if (value === true) { havalue = stateObj[origchannel][type][state]['payload_on']; } else if (value === false) { havalue = stateObj[origchannel][type][state]['payload_off']; } else { adapter.log.warn( `[Compare] switch problem ${value} not convertable `, ); } break; case 'select': //always a cmd, no need to check the specific type try { havalue = stateObj[origchannel][type][state]['states'][val]; } catch (error) { adapter.log.warn( `[Compare] select problem ${value} not in ${ stateObj[origchannel][type][state]['states'] } error -> ${error}`, ); } break; case 'text': havalue = value; break; default: havalue = String(value); break; } haUpdate = { topic: `${adapter.config.haTopic}/${topic}_${channel}/${state}`, payload: havalue, }; } } else { // even the same value switches get an update // strange behaviour on HA side, where the switch returns to previous position // achtung macht aber ordentlich last, da immer mitgeschickt // erweitert um die anderen CMD typen if (haenable) { let havalue; const type = stateDictObj[origchannel][state]['entity']; const enttype = stateObj[origchannel][type][state]['entity_type']; switch (enttype) { case 'switch': if (value === true) { havalue = stateObj[origchannel][type][state]['payload_on']; haUpdate = { topic: `${adapter.config.haTopic}/${topic}_${channel}/${state}`, payload: havalue, }; } else if (value === false) { havalue = stateObj[origchannel][type][state]['payload_off']; haUpdate = { topic: `${adapter.config.haTopic}/${topic}_${channel}/${state}`, payload: havalue, }; } else { adapter.log.warn(