UNPKG

iobroker.e3oncan

Version:

Collect data on CAN bus for Viessmann E3 devices, e.g. Vitocal, Vitocharge, Energy Meters E380CA and E3100CB

303 lines (286 loc) 12.2 kB
const storage = require('./storage'); /** * Implement relevant set of communication routines to collect data on CAN bus */ class collect { /** * @param {object} config Device UDS worker configuration */ constructor(config) { this.config = config; this.config.statId = 'statCollect'; this.config.worker = 'collect'; this.storage = new storage.storage(this.config); this.ts = {}; this.msDelay = config.delay * 1000; this.timeoutHandle = null; this.maxDid = 3500; this.commBusy = false; // Communication routine running this.data = { len: 0, timestamp: 0, databytes: [], did: 0, collecting: false, D0expected: 0x21, }; this.stat = { state: 'standby', cntCommTotal: 0, // Number collected dids cntCommOk: 0, // Number of ok cntCommStored: 0, // Number of dids stored cntCommTimeout: 0, // Number of timeouts cntCommBadProt: 0, // Number of bad communications cntTooBusy: 0, // Number of conflicting calls of msgCollect() nextTs: 0, // Timestamp for next storage (earliest) tsMinStep: 5000, // Minimum time step between storages }; } /** * Setup collect worker * * @param {object} ctx Caller context * @param {string} opMode Initial mode of operation */ async initStates(ctx, opMode) { await this.storage.initStates(ctx, opMode); this.stat.state = 'standby'; await this.storage.storeStatistics(ctx, this, true); } /** * Handle timeout error * * @param {object} ctxGlobal Adapter context * @param {object} ctxLocal Worker context */ async onTimeout(ctxGlobal, ctxLocal) { ctxGlobal.log.warn( `Collect timeout on 0x${Number(ctxLocal.config.canID[0]).toString(16)}.${String(ctxLocal.data.did)}`, ); ctxLocal.stat.cntCommTimeout += 1; ctxLocal.data.collecting = false; } /** * Start up collect worker * * @param {object} ctx Caller context */ async startup(ctx) { this.stat.state = 'active'; await this.storage.storeStatistics(ctx, this, true); this.data.collecting = false; await this.storage.setOpMode('normal'); await ctx.log.info(`Collect worker started on ${this.config.stateBase}`); ctx.cntWorkersActive += 1; } /** * Stop collect worker * * @param {object} ctx Caller context */ async stop(ctx) { try { await this.storage.setOpMode('standby'); // Stop Timeout: if (this.timeoutHandle) { await ctx.clearTimeout(this.timeoutHandle); } this.timeoutHandle = null; this.stat.state = 'stopped'; await this.storage.storeStatistics(ctx, this, true); // Stop worker: this.data.collecting = false; ctx.log.info(`Collect worker stopped on ${this.config.stateBase}`); } catch (e) { ctx.log.error(`Collect worker on ${this.config.stateBase} could not be stopped. err=${e.message}`); } ctx.cntWorkersActive -= 1; } /** * Returns actual operation mode of uds worker */ async getWorkerOpMode() { return this.storage.getOpMode(); } /** * Handle user request * * @param {object} ctx Adapter context * @param {object} ctxWorker Worker context * @param {Array} id Chaged id * @param {object} state State reference */ async onUdsStateChange(ctx, ctxWorker, id, state) { if (id.includes(this.storage.storageDids.didsSpecId)) { // User requests change of UDS device specific datapoint definition await this.storage.storageDids.readKnownDids(ctx, await this.getWorkerOpMode()); await ctx.setStateAsync(id, { val: state.val, ack: true }); // Acknowlegde user command return; } } /** * Evaluate received CAN message and perform decoding if possible * * @param {object} ctx Adapter context * @param {object} msg CAN frame received */ async msgCollect(ctx, msg) { if ((await this.storage.getOpMode()) == 'standby') { return; } // No communication in mode 'standby' if (this.commBusy) { this.stat.cntTooBusy += 1; if (this.stat.cntTooBusy == 1) { ctx.log.warn(`Collect worker error on ${this.config.stateBase}: Evaluation of messages overloaded.`); } return; } this.commBusy = true; const candata = msg.data.toJSON().data; const canid = msg.id; const msgDlc = candata.length; const tsNow = new Date().getTime(); const D3 = candata[3]; switch (this.config.device) { case 'e380': // Energy meter E380 this.stat.cntCommTotal += 1; this.stat.cntCommOk += 1; if (!(canid in this.ts)) { this.ts[canid] = tsNow; } if (this.config.delay == 0 || tsNow >= this.ts[canid]) { this.stat.cntCommStored += 1; this.storage.decodeDataCAN(ctx, this, msg.id, candata); this.ts[canid] = tsNow + this.msDelay; } break; case 'e3100cb': // Energy meter E3100CB this.stat.cntCommTotal += 1; this.stat.cntCommOk += 1; if (!(canid in this.ts)) { this.ts[canid] = tsNow; } if (this.config.delay == 0 || tsNow >= this.ts[canid]) { this.stat.cntCommStored += 1; const didStr = `00${String(D3)}`; this.storage.decodeDataCAN(ctx, this, `${String(msg.id)}_${didStr.slice(-2)}`, candata.slice(4)); this.ts[canid] = tsNow + this.msDelay; } break; default: // E3 device if (this.data.collecting) { if (candata[0] == this.data.D0expected) { // append next part of data this.data.databytes = this.data.databytes.concat(candata.slice(1)); if (this.data.databytes.length >= this.data.len) { // All data received if (this.timeoutHandle) { await ctx.clearTimeout(this.timeoutHandle); } this.stat.cntCommOk += 1; if (!(this.data.did in this.ts)) { this.ts[this.data.did] = tsNow; } if (this.config.delay == 0 || tsNow >= this.ts[this.data.did]) { this.stat.cntCommStored += 1; this.storage.decodeDataCAN( ctx, this, String(this.data.did), this.data.databytes.slice(0, this.data.len), ); this.ts[this.data.did] = tsNow + this.msDelay; } this.data.collecting = false; } else { // More data to come this.data.D0expected += 1; if (this.data.D0expected > 0x2f) { this.data.D0expected = 0x20; } } } } if (!this.data.collecting && msgDlc > 4 && candata[0] == 0x21 && D3 >= 0xb0 && D3 < 0xc0) { this.data.D0expected = candata[0]; this.data.did = candata[1] + 256 * candata[2]; this.data.timestamp = msg.ts_sec * 1000 + Math.round(msg.ts_usec / 1000); if (this.data.did > 0 && this.data.did < this.maxDid) { switch (D3) { case 0xb1: case 0xb2: case 0xb3: case 0xb4: // Single Frame B1,B2,B3,B4 this.data.len = D3 - 0xb0; this.data.databytes = candata.slice(4); this.stat.cntCommTotal += 1; this.stat.cntCommOk += 1; if (!(this.data.did in this.ts)) { this.ts[this.data.did] = tsNow; } if (this.config.delay == 0 || tsNow >= this.ts[this.data.did]) { this.stat.cntCommStored += 1; this.storage.decodeDataCAN( ctx, this, String(this.data.did), this.data.databytes.slice(0, this.data.len), ); this.ts[this.data.did] = tsNow + this.msDelay; } break; case 0xb0: // Multi Frame B0 this.data.D0expected = candata[0] + 1; this.stat.cntCommTotal += 1; if (candata[4] == 0xc1) { this.data.databytes = candata.slice(6); this.data.len = candata[5]; } else { this.data.databytes = candata.slice(5); this.data.len = candata[4]; } this.timeoutHandle = ctx.setTimeout(this.onTimeout, this.config.timeout, ctx, this); this.data.collecting = true; break; case 0xb5: case 0xb6: case 0xb7: case 0xb8: case 0xb9: case 0xba: case 0xbb: case 0xbc: case 0xbd: case 0xbe: case 0xbf: // Multi Frame B6 .. BF this.stat.cntCommTotal += 1; this.data.D0expected = candata[0] + 1; this.data.databytes = candata.slice(4); this.data.len = D3 - 0xb0; this.timeoutHandle = ctx.setTimeout(this.onTimeout, this.config.timeout, ctx, this); this.data.collecting = true; break; default: ctx.log.debug( `Collect worker error on ${ this.config.stateBase }: Unplausible byte D3: Did=${String(this.data.did)} D3=0x${Number(D3).toString( 16, )} candata=${this.storage.storageDids.arr2Hex(candata)}`, ); } } } } this.commBusy = false; } } module.exports = { collect, };