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
JavaScript
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,
};