UNPKG

iobroker.ecoflow-mqtt

Version:
838 lines (812 loc) 100 kB
'use strict'; /* * Created with @iobroker/create-adapter v2.5.0 */ // The adapter-core module gives you access to the core ioBroker functions // you need to create an adapter const utils = require('@iobroker/adapter-core'); const myutils = require('./lib/adapter_utils.js'); const ef = require('./lib/ecoflow_utils.js'); const ha = require('./lib/ha_utils.js'); const mqtt = require('mqtt'); // Load your modules here, e.g.: // const fs = require("node:fs"); let recon_timer = null; let lastQuotInterval = null; let haLoadInterval = null; let msgCalcInterval = null; const version = require('./io-package.json').common.version; class EcoflowMqtt extends utils.Adapter { /** * @param options - Optionen */ constructor(options) { super({ ...options, name: 'ecoflow-mqtt', }); this.mqttClient = null; this.msgReconnects = 0; this.mqttUserId = ''; this.mqttUserName = ''; this.mqttPwd = ''; this.mqttClientId = ''; this.mqttPort = 8883; this.mqttProtocol = 'mqtts://'; this.mqttUrl = 'mqtt-e.ecoflow.com'; this.pdevices = {}; this.pdevicesStates = {}; this.pdevicesStatesDict = {}; this.pdevicesStates = {}; this.pdevicesCmd = {}; this.protoSource = {}; this.protoMsg = {}; this.storeProtoPayload = {}; this.prepareProtoCmd = {}; this.quotas = {}; this.unknownPBmsg = {}; this.haDevices = null; this.haCounter = 0; this.haCountMem = 0; this.efCounter = 0; this.efCountMem = 0; this.onlineDevices = []; this.specialPoweroceanZero = false; this.on('ready', this.onReady.bind(this)); this.on('stateChange', this.onStateChange.bind(this)); // this.on('objectChange', this.onObjectChange.bind(this)); this.on('message', this.onMessage.bind(this)); this.on('unload', this.onUnload.bind(this)); } /** * Is called when databases are connected and adapter received configuration. */ async onReady() { // Initialize your adapter here this.log.info('adapter entered ready'); try { this.mqttUserId = this.config.mqttUserId; this.mqttUserName = this.config.mqttUserName; this.mqttPwd = this.config.mqttPwd; this.mqttClientId = this.config.mqttClientId; this.mqttPort = this.config.mqttPort || 8883; this.mqttProtocol = 'mqtts://'; this.mqttUrl = this.config.mqttUrl || 'mqtt-e.ecoflow.com'; //modify this.pstationStates this.log.info('your configration:'); this.log.info(`powerstream -> ${JSON.stringify(this.config.pstreams)}`); this.log.info(`powerstation -> ${JSON.stringify(this.config.pstations)}`); this.log.info(`smartplug -> ${JSON.stringify(this.config.plugs)}`); this.log.info(`wave -> ${JSON.stringify(this.config.waves)}`); this.log.info(`glacier -> ${JSON.stringify(this.config.glaciers)}`); //this.log.info('blade -> ' + JSON.stringify(this.config.blades)); this.log.info(`generator -> ${JSON.stringify(this.config.generators)}`); this.log.info(`panel -> ${JSON.stringify(this.config.panels)}`); this.log.info(`smartmeter -> ${JSON.stringify(this.config.shellies)}`); this.log.info(`powerkit -> ${JSON.stringify(this.config.powerkits)}`); this.log.info(`powerocean -> ${JSON.stringify(this.config.poweroceans)}`); this.log.info(`alternator -> ${JSON.stringify(this.config.alternators)}`); this.log.info(`rapid -> ${JSON.stringify(this.config.rapids)}`); this.log.info(`unknown -> ${JSON.stringify(this.config.unknowns)}`); //special settings this.specialPoweroceanZero = this.config.specialPoweroceanZero; //loop durch alle Geräte const confdevices = [].concat( this.config.pstreams, this.config.plugs, this.config.pstations, this.config.waves, this.config.glaciers, this.config.generators, this.config.panels, this.config.shellies, this.config.powerkits, this.config.poweroceans, this.config.alternators, this.config.rapids, this.config.unknowns, ); if (confdevices.length > 0) { //loop durch alle devices for (let psta = 0; psta < confdevices.length; psta++) { let devtype = confdevices[psta]['devType']; try { if (devtype !== 'none' && devtype !== '') { const id = confdevices[psta]['devId']; const name = confdevices[psta]['devName']; let haEnable = false; //Testgeräte haben kein haEnable if (confdevices[psta]['haEnable']) { haEnable = confdevices[psta]['haEnable']; } const debugEnable = confdevices[psta]['debugEnable']; this.pdevices[id] = {}; this.pdevices[id]['devType'] = devtype; this.pdevices[id]['devName'] = name; this.pdevices[id]['haEnable'] = haEnable; this.pdevices[id]['debugEnable'] = debugEnable; if (confdevices[psta]['pstationsSlave1']) { this.pdevices[id]['pstationsSlave1'] = confdevices[psta]['pstationsSlave1']; } if (confdevices[psta]['pstationsSlave2']) { this.pdevices[id]['pstationsSlave2'] = confdevices[psta]['pstationsSlave2']; } if (confdevices[psta]['pstationsSlave3']) { this.pdevices[id]['pstationsSlave3'] = confdevices[psta]['pstationsSlave3']; } if (confdevices[psta]['pstationsSlave4']) { this.pdevices[id]['pstationsSlave4'] = confdevices[psta]['pstationsSlave4']; } if (confdevices[psta]['pstationsSlave5']) { this.pdevices[id]['pstationsSlave5'] = confdevices[psta]['pstationsSlave5']; } let devStates = null; if (devtype === 'pstream600' || devtype === 'pstream800') { devStates = require('./lib/dict_data/ef_pstream_data.js').deviceStates; } else if ( devtype === 'powerkitbp2000' || devtype === 'powerkitbp5000' || devtype === 'powerkit' ) { devStates = require('./lib/dict_data/ef_powerkit_data.js').deviceStates; } else if (devtype === 'plug') { devStates = require('./lib/dict_data/ef_plug_data.js').deviceStates; } else { devStates = require(`./lib/dict_data/ef_${devtype}_data.js`).deviceStates; } if (!devStates) { this.log.warn(`no states for ${devtype}`); } //create pdevices objects const origdevtype = devtype; if (devtype === 'pstream600' || devtype === 'pstream800') { devtype = 'pstream'; } if (devtype === 'powerkitbp2000' || devtype === 'powerkitbp5000') { devtype = 'powerkit'; } let pdevicesStatesDict = null; let pdevicesCmd = null; let protoSource = null; let protoMsg = null; let storeProtoPayload = null; let prepareProtoCmd = null; //protobuf devices if ( devtype === 'pstream' || devtype === 'plug' || devtype === 'powerocean' || devtype === 'poweroceanplus' || devtype === 'poweroceanfit' || devtype === 'panel2' || devtype === 'deltaproultra' || devtype === 'alternator' || devtype === 'deltapro3' || devtype === 'delta3' || devtype === 'delta3plus' || devtype === 'river3' || devtype === 'river3plus' || devtype === 'smartmeter' || devtype === 'stream_ac_pro' || devtype === 'stream_pro' || devtype === 'stream_ultra' || devtype === 'wave3' || devtype === 'stream_inverter' || devtype === 'glacier55' || devtype === 'delta3maxplus' || devtype === 'stream_ac' || devtype === 'rapidpro320' || devtype === 'delta3classic' || devtype === 'stream_ultra_x' || devtype === 'unknown' ) { pdevicesStatesDict = require(`./lib/dict_data/ef_${devtype}_data.js`).deviceStatesDict[ devtype ]; pdevicesCmd = require(`./lib/dict_data/ef_${devtype}_data.js`).deviceCmd[origdevtype]; protoSource = require(`./lib/dict_data/ef_${devtype}_data.js`).protoSource; protoMsg = require(`./lib/dict_data/ef_${devtype}_data.js`).protoMsg; storeProtoPayload = require(`./lib/dict_data/ef_${devtype}_data.js`).storeProtoPayload; prepareProtoCmd = require(`./lib/dict_data/ef_${devtype}_data.js`).prepareProtoCmd; } else if (devtype === 'powerkit') { pdevicesStatesDict = require(`./lib/dict_data/ef_${devtype}_data.js`).deviceStatesDict[ devtype ]; pdevicesCmd = require(`./lib/dict_data/ef_${devtype}_data.js`).deviceCmd[devtype]; } else { //JSON devices pdevicesStatesDict = require(`./lib/dict_data/ef_${devtype}_data.js`).deviceStatesDict[ origdevtype ]; pdevicesCmd = require(`./lib/dict_data/ef_${devtype}_data.js`).deviceCmd[origdevtype]; } if (!pdevicesStatesDict) { this.log.warn(`no states dict for ${devtype}`); } if (!pdevicesCmd) { this.log.warn(`no CMD dict for ${devtype}`); } //create device objects //we store only the dict from used components if (!this.pdevicesStatesDict[origdevtype]) { this.pdevicesStatesDict[origdevtype] = pdevicesStatesDict; } if (!this.pdevicesStates[origdevtype]) { this.pdevicesStates[origdevtype] = ef.statesFromDict(devStates, pdevicesStatesDict); } //if protobuf devices store additionally the protoSource and protoMsg if (protoSource) { if (!this.protoSource[devtype]) { this.protoSource[devtype] = protoSource; } } if (protoMsg) { if (!this.protoMsg[devtype]) { this.protoMsg[devtype] = protoMsg; } } // and storing the functions if (storeProtoPayload) { if (!this.storeProtoPayload[devtype]) { this.storeProtoPayload[devtype] = storeProtoPayload; } } if (prepareProtoCmd) { if (!this.prepareProtoCmd[devtype]) { this.prepareProtoCmd[devtype] = prepareProtoCmd; } } //we store only the cmd from used components if (!this.pdevicesCmd[origdevtype]) { this.pdevicesCmd[origdevtype] = pdevicesCmd; } // create devices if (devtype !== 'none' && devStates && pdevicesStatesDict) { this.log.info('=========================='); this.log.info(`start device state creation ->${devtype} for Id ${id}`); try { if (this.config.msgStateCreation) { this.log.debug('____________________________________________'); this.log.debug(`create device ${id}`); } await this.setObjectNotExistsAsync(id, { type: 'device', common: { name: name, role: 'device', }, native: {}, }); for (let part in pdevicesStatesDict) { if ( part !== 'bmsSlave1' && part !== 'bp2' && part !== 'statusReportBattery2' && part !== 'BPInfo1' && part !== 'BMSHeartBeatReport1' && part !== 'bPInfo1' && part !== 'bmsSlave2' && part !== 'bp3' && part !== 'statusReportBattery3' && part !== 'BPInfo2' && part !== 'BMSHeartBeatReport2' && part !== 'bPInfo2' && part !== 'BPInfo3' ) { if (this.config.msgStateCreation) { this.log.debug('____________________________________________'); this.log.debug(`create channel ${part}`); } await myutils.createMyChannel(this, id, part, part, 'channel'); for (let key in pdevicesStatesDict[part]) { let type = pdevicesStatesDict[part][key]['entity']; if (type !== 'icon') { if (devStates[part][type][key]) { if (this.config.msgStateCreation) { this.log.debug(`state creation ${key}`); } await myutils.createMyState( this, id, part, key, devStates[part][type][key], ); } else { this.log.info(`not created/mismatch ->${part} ${key} ${type}`); } } } } else if ( ((part === 'bmsSlave1' || part === 'bp2' || part === 'statusReportBattery2' || part === 'BPInfo1' || part === 'BMSHeartBeatReport1' || part !== 'bPInfo1') && confdevices[psta]['pstationsSlave1']) || ((part === 'bmsSlave2' || part === 'bp3' || part === 'statusReportBattery3' || part === 'BPInfo2' || part === 'BMSHeartBeatReport2' || part !== 'bPInfo2') && confdevices[psta]['pstationsSlave2']) || ((part === 'statusReportBattery4' || part === 'BPInfo3') && confdevices[psta]['pstationsSlave3']) || (part === 'statusReportBattery5' && confdevices[psta]['pstationsSlave4']) || (part === 'statusReportBattery6' && confdevices[psta]['pstationsSlave5']) ) { if (this.config.msgStateCreation) { this.log.debug('____________________________________________'); this.log.debug(`create channel ${part}`); } await myutils.createMyChannel(this, id, part, part, 'channel'); for (let key in pdevicesStatesDict[part]) { let type = pdevicesStatesDict[part][key]['entity']; if (type !== 'icon') { if (devStates[part][type][key]) { if (this.config.msgStateCreation) { this.log.debug(`state creation ${key}`); } await myutils.createMyState( this, id, part, key, devStates[part][type][key], ); } else { this.log.info(`not created/mismatch ->${part} ${key} ${type}`); } } } } } //create timeTask only for certain devices if (devtype === 'pstream' || devtype === 'plug') { let part = 'time_task_config_post'; if (this.config.msgStateCreation) { this.log.debug('____________________________________________'); this.log.debug(`create channel ${part}`); } //createMyChannel(adapter, device, channel, name, role) await myutils.createMyChannel(this, id, part, part, 'channel'); for (let j = 1; j < 12; j++) { const task = `task${j}`; await myutils.createMyChannel(this, `${id}.${part}`, task, task, 'channel'); //taskIndex //createMyState(adapter, device, channel, datapoint, stateObj) await myutils.createMyState( this, id, `${part}.${task}`, 'taskIndex', devStates[part]['tasks']['taskIndex'], ); //type await myutils.createMyState( this, id, `${part}.${task}`, 'type', devStates[part]['tasks']['type'], ); //timerange await myutils.createMyChannel( this, `${id}.${part}.${task}`, 'timeRange', 'timeRange', 'channel', ); for (let key in devStates[part]['tasks']['timeRange']) { if (key === 'startTime' || key === 'stopTime') { for (let key2 in devStates[part]['tasks']['timeRange'][key]) { await myutils.createMyState( this, id, `${part}.${task}.timeRange.${key}`, key2, devStates[part]['tasks']['timeRange'][key][key2], ); } } else { await myutils.createMyState( this, id, `${part}.${task}.timeRange`, key, devStates[part]['tasks']['timeRange'][key], ); } } } } else if (devtype === 'deltamax') { let part = 'timeTask'; if (this.config.msgStateCreation) { this.log.debug('____________________________________________'); this.log.debug(`create channel ${part}`); } //createMyChannel(adapter, device, channel, name, role) await myutils.createMyChannel(this, id, part, part, 'channel'); //"totalTaskNum" await myutils.createMyState( this, id, part, 'totalTaskNum', devStates[part]['totalTaskNum'], ); for (let j = 0; j < 6; j++) { const task = `task${j}`; await myutils.createMyChannel(this, `${id}.${part}`, task, task, 'channel'); for (let type in devStates[part]) { if (type !== 'totalTaskNum') { for (let key in devStates[part][type]) { await myutils.createMyState( this, id, `${part}.${task}`, key, devStates[part][type][key], ); } } } } } else if (devtype === 'panel') { let part = 'timeTask'; if (this.config.msgStateCreation) { this.log.debug('____________________________________________'); this.log.debug(`create channel ${part}`); } //createMyChannel(adapter, device, channel, name, role) await myutils.createMyChannel(this, id, part, part, 'channel'); for (let j = 1; j < 21; j++) { const task = `task${j}`; await myutils.createMyChannel(this, `${id}.${part}`, task, task, 'channel'); await myutils.createMyState( this, id, `${part}.${task}`, 'id', devStates[part]['cfg']['id'], ); await myutils.createMyState( this, id, `${part}.${task}`, 'index', devStates[part]['cfg']['index'], ); //discharge has chSta state omly await myutils.createMyState( this, id, `${part}.${task}`, 'chSta', devStates[part]['cfg']['chSta'], ); //charge has param instead of chSta await myutils.createMyChannel( this, `${id}.${part}.${task}`, 'param', 'param', 'channel', ); for (let key in devStates[part]['cfg']['param']) { await myutils.createMyState( this, id, `${part}.${task}.param`, key, devStates[part]['cfg']['param'][key], ); } //comCfg await myutils.createMyChannel( this, `${id}.${part}.${task}`, 'comCfg', 'comCfg', 'channel', ); for (let key in devStates[part]['cfg']['comCfg']) { if (key === 'setTime') { //setTime await myutils.createMyChannel( this, `${id}.${part}.${task}.comCfg`, 'setTime', 'setTime', 'channel', ); for (let key2 in devStates[part]['cfg']['comCfg'][key]) { await myutils.createMyState( this, id, `${part}.${task}.comCfg.${key}`, key2, devStates[part]['cfg']['comCfg'][key][key2], ); } } else if (key === 'timeRange') { //timerange await myutils.createMyChannel( this, `${id}.${part}.${task}.comCfg`, 'timeRange', 'timeRange', 'channel', ); for (let key2 in devStates[part]['cfg']['comCfg'][key]) { if (key2 === 'startTime' || key2 === 'endTime') { for (let key3 in devStates[part]['cfg']['comCfg'][key][ key2 ]) { await myutils.createMyState( this, id, `${part}.${task}.comCfg.${key}.${key2}`, key3, devStates[part]['cfg']['comCfg'][key][key2][key3], ); } } else { await myutils.createMyState( this, id, `${part}.${task}.comCfg.${key}`, key2, devStates[part]['cfg']['comCfg'][key][key2], ); } } } else { await myutils.createMyState( this, id, `${part}.${task}.comCfg`, key, devStates[part]['cfg']['comCfg'][key], ); } } } } this.setState(`${id}.info.msgCount`, { val: 0, ack: true, }); this.log.info(`device states created for ${id} / ${devtype} / ${name}`); this.log.info('=========================='); } catch (error) { this.log.error(`create states ${error}`); } } else { this.log.error( `something empty ID->${id} states -> ${devStates} dict -> ${ pdevicesStatesDict } type -> ${devtype}`, ); } } } catch (error) { this.log.error(`${devtype} modification or state creation went wrong ->${error}`); } } } } catch (error) { this.log.error(`read config ${error}`); } //Homeassistant connection if (this.config.haMqttEnable) { this.haDevices = ha.defineHaDevices(this.pdevices); this.log.info('HA communication:'); this.log.info(`devices -> ${JSON.stringify(this.config.pstreams)}`); } await myutils.createInfoStates(this, this.config.haMqttEnable); //create subscription topics let topics = []; if (this.mqttUserId.length > 0) { topics = topics.concat(ef.createSubscribeTopics(this.mqttUserId, this.pdevices)); } // this.log.debug('subscription topics EF ' + JSON.stringify(topics)); //connect to Ecoflow const optionsMqtt = { port: this.mqttPort || 8883, clientId: this.mqttClientId, username: this.mqttUserName, password: this.mqttPwd, keepalive: 60, reconnectPeriod: 5000, clean: true, //manualConnect: true }; if (optionsMqtt.clientId.length > 18 && optionsMqtt.username.length > 18 && optionsMqtt.password.length > 18) { try { this.log.info('[EF] ' + 'going to connect to mqtt broker'); this.log.debug('[EF] ' + 'your mqtt configuration:'); //this.log.debug('[EF] ' + 'user -> ' + this.mqttUserId); //this.log.debug('[EF] ' + 'name -> ' + this.mqttUserName); //this.log.debug('[EF] ' + 'client -> ' + this.mqttClientId); this.log.debug(`[EF] ` + `port -> ${this.mqttPort}`); this.log.debug(`[EF] ` + `url -> ${this.mqttUrl}`); this.log.debug(`[EF] ` + `protocol -> ${this.mqttProtocol}`); this.client = mqtt.connect(`${this.mqttUrl}:${this.mqttPort}`, optionsMqtt); this.client.on('connect', async () => { this.log.info('EF connected'); if (topics.length > 0) { if (this.client) { this.client.subscribe(topics, async err => { if (!err) { this.log.debug('EF subscribed the topics'); //initial and interval for requesting last quotas await ef.getLastQuotas(this, this.pdevices); lastQuotInterval = this.setInterval(async () => { await ef.getLastQuotas(this, this.pdevices); }, 300 * 1000); // lastQuot every 5min } else { this.log.warn(`EF could not subscribe to topics ${err}`); } }); } } else { this.log.debug('EF no topics for subscription'); } this.setState('info.connection', true, true); //avg load of receiving telegrams from EF cloud msgCalcInterval = this.setInterval(() => { const msgcnt = this.efCounter - this.efCountMem; this.efCountMem = this.efCounter; this.setState('info.efConnAvgLoad', { val: msgcnt, ack: true }); }, 10 * 1000); }); this.client.on('message', async (topic, message) => { const msgtop = ef.getIdFromTopic(topic, this.mqttUserId); const msgtype = msgtop.msg; //this topic only contains the id of device topic = msgtop.topic; this.efCounter++; let devtype = ''; let logged = false; let devicelogged = false; if (this.pdevices) { if (this.pdevices[topic]) { devtype = this.pdevices[topic]['devType']; if (this.pdevices[topic]['debugEnable'] === true) { devicelogged = true; if (this.config.msgUpdate && msgtype === 'update') { logged = true; } else if ( this.config.msgSetGet && msgtype !== 'update' && msgtype !== 'unknown msgtype' ) { logged = true; } if (msgtype == 'unknown msgtype') { logged = true; } } } else { this.log.debug(`${topic} not part of configured devices`); } } //protobuf msg const origdevtype = devtype; if (devtype === 'pstream600' || devtype === 'pstream800') { devtype = 'pstream'; } if ( devtype === 'pstream' || devtype === 'plug' || devtype === 'deltaproultra' || devtype === 'powerocean' || devtype === 'poweroceanplus' || devtype === 'poweroceanfit' || devtype === 'panel2' || devtype === 'alternator' || devtype === 'deltapro3' || devtype === 'delta3' || devtype === 'delta3plus' || devtype === 'river3' || devtype === 'river3plus' || devtype === 'smartmeter' || devtype === 'stream_ac_pro' || devtype === 'stream_pro' || devtype === 'stream_ultra' || devtype === 'stream_inverter' || devtype === 'glacier55' || devtype === 'wave3' || devtype === 'devtype === ' || devtype === 'stream_ac' || devtype === 'delta3maxplus' || devtype === 'rapidpro320' || devtype === 'delta3classic' || devtype === 'stream_ultra_x' || devtype === 'unknown' ) { if (this.pdevicesStatesDict && this.pdevicesStates) { let msgdecode = null; if (devtype === 'unknown') { this.log.debug( `[PROTOBUF unknown] ${topic} [${devtype}/${msgtype}] raw (hex): ${message.toString( 'hex', )}`, ); } else { try { msgdecode = ef.pstreamDecode( this, message, '', topic, msgtype, this.protoSource[devtype], this.protoMsg[devtype], logged, ); } catch (error) { this.log.debug(`pstreamDecode call (${devtype}) ->${error}`); } } if ( msgtype === 'update' || msgtype === 'get_reply' || msgtype === 'set_reply' || msgtype === 'set' ) { if (msgdecode !== null && typeof msgdecode === 'object') { if (Object.keys(msgdecode).length > 0) { //storeStreamPayload handles multiple objects const haupdate = await this.storeProtoPayload[devtype]( this, this.pdevicesStatesDict[origdevtype], this.pdevicesStates[origdevtype], topic, msgdecode, devtype, this.pdevices[topic]['haEnable'], logged, ); if (haupdate.length > 0) { for (let i = 0; i < haupdate.length; i++) { if (haupdate[i]) { if (typeof haupdate[i].payload === 'string') { ha.publish( this, topic, haupdate[i].topic, haupdate[i].payload, { qos: 1 }, devicelogged && this.config.msgHaOutgoing, 'HA EF PB UPDATE RCV', ); } else { this.log.warn( `not a string! : ${haupdate[i].topic} ${ haupdate[i].payload }`, ); } } } } } } //msg counter, only when receiving telegrams if (msgtype === 'update' || msgtype === 'get_reply' || msgtype === 'set_reply') { try { let countobj = await this.getStateAsync(`${topic}.info.msgCount`); if (countobj) { if (countobj.val !== null) { await this.setState(`${topic}.info.msgCount`, { val: parseInt(countobj.val) + 1, ack: true, }); } } else { this.log.debug(`did not get count info ${topic} ${countobj}`);