iobroker.e3oncan
Version:
Collect data on CAN bus for Viessmann E3 devices, e.g. Vitocal, Vitocharge, Energy Meters E380CA and E3100CB
994 lines (940 loc) • 61.6 kB
JavaScript
'use strict';
/*
* Unveil life data of Viessmann E3 series devices via CAN bus
*
* Based on project open3e: https://github.com/open3e/open3e
*
*/
/*
* 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');
// Loading modules:
let can = null;
let canLoadError = null;
try {
can = require('socketcan');
} catch (e) {
canLoadError = e;
}
const storage = require('./lib/storage');
const E3DidsDict = require('./lib/didsE3.json');
const E3DidsVarDict = require('./lib/didsE3var.json');
const E380DidsDict = require('./lib/didsE380.json');
const E3100CBDidsDict = require('./lib/didsE3100CB.json');
const E3DidsWritable = require('./lib/didsE3Writables.json');
const collect = require('./lib/canCollect');
const uds = require('./lib/canUds');
const udsScan = require('./lib/udsScan');
class E3oncan extends utils.Adapter {
/**
* @param {object} options Adapter options
*/
constructor(options) {
super({
...options,
name: 'e3oncan',
});
this.stoppingInstance = false; // true during unLoad()
this.suppressStateStorage = false;
this.defaultDelayEM = 5;
this.e380Active = false;
this.e3100cbActive = false;
this.e380Delay = 5;
this.e3100cbDelay = 5;
this.e380Collect97 = null;
this.e380Collect97Channel = 'ext';
this.e380Collect98 = null;
this.e380Collect98Channel = 'ext';
this.e3100cbCollect = null;
this.e3100cbCollectChannel = 'ext';
this.E3CollectInt = {}; // Dict of collect devices on internal bus
this.E3CollectExt = {}; // Dict of collect devices on external bus
this.collectTimeout = 2000; // Timeout (ms) for collecting data
this.E3UdsWorkers = {}; // Dict of standard uds workers
this.E3UdsSID77Workers = {}; // Dict of uds workers for service 77
this.cntWorkersActive = 0; // Total number of active workers (collect + UDS)
this.channelExt = null;
this.channelExtName = '';
this.channelInt = null;
this.channelIntName = '';
this.cntCanConnDesired = 0; // Number of activated CAN connections in config
this.cntCanConnActual = 0; // Number if actualy connected CAN buses
this.udsWorkers = {};
this.udsTimeout = 7500; // Timeout (ms) for normal UDS communication
this.udsDevices = []; // Confirmed & edited UDS devices
this.udsTimeDelta = 50; // Time delta (ms) between UDS schedules
this.udsTimeoutHandles = [];
this.udsMasterDevAddr = 0x680; // Address of master device
this.udsMasterDevUnits = 'n/a'; // Units and Formats of master device (0x680)
this.didsVersionTC = '20240309'; // Change of type of numerical dids to Number at this version
this.udsDidForScan = 256; // Busidentification is in this id
this.udsDidForUnits = 382; // UnitsAndFormats
this.udsDidsVarLength = [257, 258, 259, 260, 261, 262, 263, 264, 265, 266]; // Dids have content of variable length dependend of number of list elements
this.udsScanWorker = new udsScan.udsScan();
this.detectedEnergyMeters = { e380_97: '', e380_98: '', e3100cb: '' };
this.detectedCollectCanIds = new Set();
this.collectScanDone = false; // true if info.collect exists in v1.x format (detectedIds array)
this.udsScanDevices = []; // UDS devices found during scan
this.udsDevAddrs = [];
this.udsDevStateNames = [];
//this.udsDidsLimits = { min: 256, max: 268 }; // Min. and max. numerical value of dids for scan of data points. Should meet range defined in didsE3,json
this.udsDidsLimits = { min: 256, max: 3338 }; // Min. and max. numerical value of dids for scan of data points. Should meet range defined in didsE3,json
//this.on('install', this.onInstall.bind(this));
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
if (!can) {
if (
canLoadError &&
/compiled against a different Node\.js version|did not self-register/i.test(canLoadError.message)
) {
this.log.error('Native module socketcan was compiled for a different Node.js version.');
this.log.error(
'Fix: upgrade the adapter to version 1.0.3 or later. From that version, socketcan uses N-API and no longer needs to be rebuilt after a Node.js upgrade.',
);
} else {
this.log.error(
`Failed to load native module socketcan: ${canLoadError ? canLoadError.message : 'unknown error'}`,
);
}
return;
}
await this.log.info(`Startup of instance ${this.namespace}: Starting.`);
//await this.log.debug('this.config:');
//await this.log.debug(JSON.stringify(this.config));
// Reset the connection indicator during startup
this.setState('info.connection', false, true);
// Read energy meter configuration from info.energyMeter JSON (with migration fallback):
try {
const sEm = await this.getStateAsync('info.energyMeter');
if (sEm && sEm.val) {
const em = JSON.parse(String(sEm.val));
const toChannel = v => (v && typeof v === 'string' ? v : '');
this.detectedEnergyMeters = {
e380_97: toChannel(em.e380_97),
e380_98: toChannel(em.e380_98),
e3100cb: toChannel(em.e3100cb),
};
this.e380Active = em.e380Active != null ? !!em.e380Active : false;
this.e380Delay = em.e380Delay != null ? Number(em.e380Delay) : this.defaultDelayEM;
this.e3100cbActive = em.e3100cbActive != null ? !!em.e3100cbActive : false;
this.e3100cbDelay = em.e3100cbDelay != null ? Number(em.e3100cbDelay) : this.defaultDelayEM;
} else {
// Migration: read from old individual states (pre-v1.0.0 installations)
const s97 = await this.getStateAsync('info.e380_97');
const s98 = await this.getStateAsync('info.e380_98');
const sCb = await this.getStateAsync('info.e3100cb');
const toChannel = s => (s && s.val ? (typeof s.val === 'string' ? s.val : 'ext') : '');
this.detectedEnergyMeters = {
e380_97: toChannel(s97),
e380_98: toChannel(s98),
e3100cb: toChannel(sCb),
};
const sDelay380 = await this.getStateAsync('info.e380_delay');
this.e380Delay = sDelay380 && sDelay380.val != null ? Number(sDelay380.val) : this.defaultDelayEM;
const sDelayCb = await this.getStateAsync('info.e3100cb_delay');
this.e3100cbDelay = sDelayCb && sDelayCb.val != null ? Number(sDelayCb.val) : this.defaultDelayEM;
const sActive380 = await this.getStateAsync('info.e380_active');
if (sActive380 == null) {
// @ts-expect-error AdapterConfig
this.e380Active = !!this.config.e380Active; // migrate from very old config
if (this.e380Active) {
this.detectedEnergyMeters.e380_98 = 'ext';
this.log.warn(
'Upgrade migration: E380 configured for CAN address 98 on UDS CAN channel. ' +
'If your E380 uses CAN address 97, please run a device scan to reconfigure.',
);
}
} else {
this.e380Active = !!sActive380.val;
}
const sActiveCb = await this.getStateAsync('info.e3100cb_active');
if (sActiveCb == null) {
// @ts-expect-error AdapterConfig
this.e3100cbActive = !!this.config.e3100cbActive; // migrate from very old config
if (this.e3100cbActive) {
this.detectedEnergyMeters.e3100cb = 'ext';
this.log.info('Upgrade migration: E3100CB configured for UDS CAN channel.');
}
} else {
this.e3100cbActive = !!sActiveCb.val;
}
}
} catch {
// states not yet available, keep defaults
}
// Read detected Collect CAN IDs from info.collect JSON (with migration fallback):
try {
const sCo = await this.getStateAsync('info.collect');
if (sCo && sCo.val) {
const co = JSON.parse(String(sCo.val));
if (Array.isArray(co.detectedIds)) {
// Current format: array of numeric CAN IDs
this.collectScanDone = true;
for (const id of co.detectedIds) {
this.detectedCollectCanIds.add(Number(id));
}
} else {
// Migration: old format before dynamic Collect IDs (pre-v1.0.0)
if (co.detected451) {
this.detectedCollectCanIds.add(0x451);
}
if (co.detected693) {
this.detectedCollectCanIds.add(0x693);
}
}
} else {
// Migration: read from old individual states (pre-v1.0.0 installations)
for (const canId of [0x451, 0x693]) {
const s = await this.getStateAsync(`info.detectedCollect${canId.toString(16)}`);
if (s && s.val) {
this.detectedCollectCanIds.add(canId);
}
}
}
} catch {
// states not yet available, keep default
}
// Collect known devices adresses:
for (const dev of Object.values(this.config.tableUdsDevices)) {
// @ts-expect-error AdapterConfig
this.udsDevAddrs.push(dev.devAddr);
// @ts-expect-error AdapterConfig
this.udsDevStateNames.push(dev.devStateName);
}
// Check for updates of list of datapoints and perform update if needed:
await this.updateDatapointsSpecificNumTypeChange(this.config.tableUdsDevices); // UDS devices, specific dids, possible change of type of numerical values
await this.updateDatapointsSpecificVariants(this.config.tableUdsDevices); // UDS devices, specific dids, update for dids having variant length of structure
await this.updateDatapointsCommon(this.config.tableUdsDevices); // UDS devices, common dids
// Update datapoints for detected energy meters (auto-named):
if (this.detectedEnergyMeters.e380_98) {
const name98 = this.detectedEnergyMeters.e380_98 === 'int' ? 'e380_98' : 'e380';
await this.updateDatapointsCommon([{ devStateName: name98, device: 'e380' }]);
}
if (this.detectedEnergyMeters.e380_97) {
await this.updateDatapointsCommon([{ devStateName: 'e380_97', device: 'e380' }]);
}
if (this.detectedEnergyMeters.e3100cb) {
await this.updateDatapointsCommon([{ devStateName: 'e3100cb', device: 'e3100cb' }]);
}
// Setup external CAN bus if required
// ==================================
// @ts-expect-error AdapterConfig
if (this.config.canExtActivated) {
this.cntCanConnDesired++;
[this.channelExt, this.channelExtName] = await this.connectToCan(
this.channelExt,
// @ts-expect-error AdapterConfig
this.config.canExtName,
this.onCanMsgExt,
this.onCanExtStopped,
);
}
// Setup internal CAN bus if required
// ==================================
// @ts-expect-error AdapterConfig
if (this.config.canIntActivated) {
this.cntCanConnDesired++;
[this.channelInt, this.channelIntName] = await this.connectToCan(
this.channelInt,
// @ts-expect-error AdapterConfig
this.config.canIntName,
this.onCanMsgInt,
this.onCanIntStopped,
);
}
if (this.cntCanConnActual == this.cntCanConnDesired) {
// All configured CAN connections are established
await this.setState('info.connection', true, true);
}
// Setup energy meter collect workers for detected and activated meters:
if (this.channelExt || this.channelInt) {
if (this.e380Active) {
if (this.detectedEnergyMeters.e380_97) {
this.e380Collect97Channel = this.detectedEnergyMeters.e380_97;
this.e380Collect97 = await this.setupE380Collect97Worker(this.e380Delay);
}
if (this.detectedEnergyMeters.e380_98) {
this.e380Collect98Channel = this.detectedEnergyMeters.e380_98;
this.e380Collect98 = await this.setupE380Collect98Worker(this.e380Delay);
}
}
if (this.e3100cbActive && this.detectedEnergyMeters.e3100cb) {
this.e3100cbCollectChannel = this.detectedEnergyMeters.e3100cb;
this.e3100cbCollect = await this.setupE3100cbCollectWorker(this.e3100cbDelay);
}
}
// Setup all configured devices for collect:
if (this.channelExt) {
// @ts-expect-error AdapterConfig
await this.setupE3CollectWorkers(this.config.tableCollectCanExt, this.E3CollectExt, this.channelExt);
}
if (this.channelInt) {
// @ts-expect-error AdapterConfig
await this.setupE3CollectWorkers(this.config.tableCollectCanInt, this.E3CollectInt, this.channelInt);
}
// Initial setup all configured devices for UDS:
if (this.channelExt) {
await this.setupUdsWorkers();
}
this.log.debug(`Total number of active workers: ${String(this.cntWorkersActive)}`);
this.log.info(`Startup of instance ${this.namespace}: Done.`);
}
// Check for updates:
async updateDatapointsCommon(devices) {
// Update list of common datapoints of all devices during startup of adapter
for (const dev of Object.values(devices)) {
let didsDictNew = null;
let didsWritable = null;
switch (dev.device) {
case 'e380':
didsDictNew = E380DidsDict;
didsWritable = {};
break;
case 'e3100cb':
didsDictNew = E3100CBDidsDict;
didsWritable = {};
break;
default:
didsDictNew = E3DidsDict;
didsWritable = E3DidsWritable;
}
const devDids = new storage.storageDids({ stateBase: dev.devStateName, device: dev.device });
await devDids.initStates(this, 'standby');
await devDids.readKnownDids(this, 'standby');
if (devDids.didsDevSpecAvail) {
if (
devDids.didsDictDevCom.Version === undefined ||
Number(didsDictNew.Version) > Number(devDids.didsDictDevCom.Version)
) {
this.log.info(
`Updating common datapoints to version ${didsDictNew.Version} for device ${dev.devStateName}`,
);
for (const did of Object.keys(devDids.didsDictDevCom)) {
if (did != 'Version' && did in didsDictNew) {
// Check for changes in datapoint structure
const didStateName = `${await devDids.getDidStr(did)}_${await devDids.didsDictDevCom[did].id}`;
const devStruct = await devDids.getDidStruct(this, [], devDids.didsDictDevCom[did]);
const E3Struct = await devDids.getDidStruct(this, [], didsDictNew[did]);
if (JSON.stringify(devStruct) != JSON.stringify(E3Struct)) {
// Structure of datapoint has changed
// Replace .json and .tree state(s) based on raw data of did
this.log.info(` > Structure of datapoint ${didStateName} has changed. Updating.`);
// Delete tree states based on old structure:
await this.delObjectAsync(
`${this.namespace}.${dev.devStateName}.tree.${didStateName}`,
{ recursive: true },
);
const raw = await devDids.getObjectVal(this, `${dev.devStateName}.raw.${didStateName}`);
if (raw != null) {
// Create states based on new structure if raw data is available:
const cdi = await didsDictNew[did];
const res = await devDids.decodeDid(
this,
{ config: { stateBase: dev.devStateName, devUnits: dev.devUnits } },
did,
cdi,
devDids.toByteArray(raw),
);
await devDids.storeObjectJson(
this,
did,
res.idStr,
`${this.namespace}.${dev.devStateName}.json.${didStateName}`,
res.val,
);
await devDids.storeObjectTree(
this,
did,
res.idStr,
`${this.namespace}.${dev.devStateName}.tree.${didStateName}`,
res.val,
);
}
} else {
// No change of structure of datapoint
// Check for change of data type for numerical values
if (Number(devDids.didsDictDevCom.Version) < Number(this.didsVersionTC)) {
// Make sure, data type and role of tree objects are correct
// Force update of .tree state(s) based on raw data of did
if (this.udsDidsVarLength.includes(Number(did))) {
// Did with variable length has to be deleted to avoid type confilct, when length gets larger in future
this.log.silly(
` > Delete datapoint ${didStateName} to secure change of data type`,
);
await this.delObjectAsync(
`${this.namespace}.${dev.devStateName}.tree.${didStateName}`,
{ recursive: true },
);
}
const raw = await devDids.getObjectVal(
this,
`${dev.devStateName}.raw.${didStateName}`,
);
if (raw != null) {
// Update .tree states:
this.log.silly(` > Update type and role of datapoint ${didStateName}`);
const cdi = await didsDictNew[did];
const res = await devDids.decodeDid(
this,
{ config: { stateBase: dev.devStateName, devUnits: dev.devUnits } },
did,
cdi,
devDids.toByteArray(raw),
);
await devDids.storeObjectTree(
this,
did,
res.idStr,
`${this.namespace}.${dev.devStateName}.tree.${didStateName}`,
res.val,
true,
);
}
}
}
// Update datapoint description:
devDids.didsDictDevCom[did] = await didsDictNew[did];
// Update list of writable datapoints:
if (did in didsWritable && !(did in devDids.didsWritable)) {
this.log.silly(` > Add ${didStateName} to list of writable datapoints`);
devDids.didsWritable[did] = await didsWritable[did];
}
}
}
devDids.didsDictDevCom['Version'] = didsDictNew.Version;
}
}
await devDids.storeKnownDids(this);
}
}
async updateDatapointsSpecificNumTypeChange(devices) {
// Update list of device-specific datapoints of all devices during startup of adapter
// Take care about possible change of type of numerical values
for (const dev of Object.values(devices)) {
const didsDictNew = E3DidsDict;
const devDids = new storage.storageDids({ stateBase: dev.devStateName, device: dev.device });
await devDids.initStates(this, 'standby');
await devDids.readKnownDids(this, 'standby');
if (devDids.didsDevSpecAvail) {
if (
devDids.didsDictDevCom.Version === undefined ||
(Number(didsDictNew.Version) > Number(devDids.didsDictDevCom.Version) &&
Number(devDids.didsDictDevCom.Version) < Number(this.didsVersionTC))
) {
this.log.info(
`Fixing numerical type handling for device specific datapoints to version ${didsDictNew.Version} for device ${
dev.devStateName
}`,
);
for (const did of Object.keys(devDids.didsDictDevSpec)) {
if (did.length <= 4) {
try {
const didNo = Number(did);
// Make sure, data type and role of tree objects are correct
// Force update of .tree state(s) based on raw data of did
const didStateName = `${await devDids.getDidStr(did)}_${await devDids.didsDictDevSpec[did].id}`;
if (this.udsDidsVarLength.includes(didNo)) {
// Did with variable length of content has to be deleted to avoid type confilct, when length gets larger in future
this.log.silly(
` > Delete datapoint ${didStateName} to secure change of data type`,
);
await this.delObjectAsync(
`${this.namespace}.${dev.devStateName}.tree.${didStateName}`,
{ recursive: true },
);
}
const raw = await devDids.getObjectVal(this, `${dev.devStateName}.raw.${didStateName}`);
if (raw != null) {
// Update .tree states:
this.log.silly(` > Update type and role of datapoint ${didStateName}`);
const cdi = await devDids.didsDictDevSpec[did];
const res = await devDids.decodeDid(
this,
{ config: { stateBase: dev.devStateName, devUnits: dev.devUnits } },
did,
cdi,
devDids.toByteArray(raw),
);
await devDids.storeObjectTree(
this,
did,
res.idStr,
`${this.namespace}.${dev.devStateName}.tree.${didStateName}`,
res.val,
true,
);
}
} catch {
this.log.warn(
` > Could not update did ${did} because of wrong format (expected a number).`,
);
continue;
}
}
}
}
}
}
}
async updateDatapointsSpecificVariants(devices) {
// Update list of device-specific datapoints of all devices during startup of adapter
// Take care about dids having variant length of structure (introduced with open3e 0.6.0)
for (const dev of Object.values(devices)) {
const didsDictVar = E3DidsVarDict;
const devDids = new storage.storageDids({ stateBase: dev.devStateName, device: dev.device });
await devDids.initStates(this, 'standby');
await devDids.readKnownDids(this, 'standby');
if (devDids.didsDevSpecAvail) {
if (
devDids.didsDictDevSpec.Version === undefined ||
Number(didsDictVar.Version) > Number(devDids.didsDictDevSpec.Version)
) {
this.log.info(
`Updating device specific datapoints to version ${didsDictVar.Version} for device ${
dev.devStateName
}`,
);
if (!('Backup' in devDids.didsDictDevSpec)) {
devDids.didsDictDevSpec['Backup'] = {};
}
for (const did of Object.keys(devDids.didsDictDevSpec)) {
if (did.length <= 4 && did in didsDictVar) {
// Check if matching did is available in list of variant dids
// Skip entries for 'Version' and 'Backup'
const didLen = devDids.didsDictDevSpec[did].len;
if (!(didLen in didsDictVar[did])) {
// No variant definition for stored length available, skip this did
if (devDids.didsDictDevCom[did]?.len !== didLen) {
this.log.warn(
` > Variant did ${did}: no definition for length ${String(didLen)} found in didsE3var. Skipping.`,
);
}
continue;
}
// Up to now, only RawCodec was known for this did or an user defined version or an older version of variant did => Use newly defined variant did
// Replace .json and .tree state(s) based on raw data of did
const didStateName = `${await devDids.getDidStr(did)}_${await didsDictVar[did][didLen].id}`;
// Check for changes in datapoint structure
const devStruct = await devDids.getDidStruct(this, [], devDids.didsDictDevSpec[did]);
const E3Struct = await devDids.getDidStruct(this, [], didsDictVar[did][didLen]);
if (JSON.stringify(devStruct) != JSON.stringify(E3Struct)) {
if (devDids.didsDictDevSpec[did].protected) {
const reason = devDids.didsDictDevSpec[did].reason
? ` Reason: "${devDids.didsDictDevSpec[did].reason}"`
: '';
this.log.info(
` > Variant datapoint ${didStateName} is protected by user. Update skipped.${reason}`,
);
continue;
}
this.log.info(
` > New or updated defintion of variant datapoint ${didStateName} is available. Updating.`,
);
// Delete tree states based on old structure:
await this.delObjectAsync(
`${this.namespace}.${dev.devStateName}.tree.${didStateName}`,
{ recursive: true },
);
const raw = await devDids.getObjectVal(this, `${dev.devStateName}.raw.${didStateName}`);
if (raw != null) {
// Create states based on new structure if raw data is available:
const cdi = await didsDictVar[did][didLen];
const res = await devDids.decodeDid(
this,
{ config: { stateBase: dev.devStateName, devUnits: dev.devUnits } },
did,
cdi,
devDids.toByteArray(raw),
);
await devDids.storeObjectJson(
this,
did,
res.idStr,
`${this.namespace}.${dev.devStateName}.json.${didStateName}`,
res.val,
);
await devDids.storeObjectTree(
this,
did,
res.idStr,
`${this.namespace}.${dev.devStateName}.tree.${didStateName}`,
res.val,
);
}
// Create Backup of user defined data point definition:
if (
(devDids.didsDictDevSpec[did].codec != 'RawCodec' && // It's not a RawCodec
!('source' in devDids.didsDictDevSpec[did])) || // and info about source of definition is missing OR
('source' in devDids.didsDictDevSpec[did] && // the source of definition is known
!devDids.didsDictDevSpec[did].source.includes('didsE3var_')) // and the source is NOT the list of variant codecs
) {
this.log.info(
` > Creating backup of actual definition of data point ${didStateName} - see section "Backup"`,
);
await (devDids.didsDictDevSpec['Backup'][did] = await devDids.didsDictDevSpec[did]);
}
// Update datapoint description:
await (devDids.didsDictDevSpec[did] = await didsDictVar[did][didLen]);
// Remember version of source:
devDids.didsDictDevSpec[did]['source'] = `didsE3var_${didsDictVar.Version}`;
}
}
}
devDids.didsDictDevSpec['Version'] = didsDictVar.Version;
}
}
await devDids.storeKnownDids(this);
}
}
// Setup CAN busses
async connectToCan(channel, name, onMsg, onStop) {
let chName = name;
if (!channel) {
try {
channel = can.createRawChannel(name, true);
await channel.addListener('onMessage', onMsg, this);
await channel.addListener('onStopped', onStop, this);
await channel.start();
this.cntCanConnActual++;
await this.log.info(`CAN-Adapter connected: ${name}`);
} catch (e) {
await this.log.error(`Could not connect to CAN-Adapter "${name}" - err=${e.message}`);
channel = null;
chName = '';
}
}
return [channel, chName];
}
disconnectFromCan(channel, name) {
if (channel) {
try {
channel.stop();
this.log.info(`CAN-Adapter disconnected: ${name}`);
channel = null;
} catch (e) {
this.log.error(`Could not disconnect from CAN "${name}" - err=${e.message}`);
channel = null;
}
}
}
// Setup E380 collect worker for CAN address 97 (odd CAN IDs):
async setupE380Collect97Worker(delay) {
const worker = new collect.collect({
canID: [0x251, 0x253, 0x255, 0x257, 0x259, 0x25b, 0x25d],
stateBase: 'e380_97',
device: 'e380',
delay: delay,
active: true,
});
await worker.initStates(this, 'standby');
await worker.startup(this);
return worker;
}
// Setup E380 collect worker for CAN address 98 (even CAN IDs).
// Backward compat: state name is 'e380' on ext channel, 'e380_98' on int channel.
async setupE380Collect98Worker(delay) {
const stateName = this.e380Collect98Channel === 'int' ? 'e380_98' : 'e380';
const worker = new collect.collect({
canID: [0x250, 0x252, 0x254, 0x256, 0x258, 0x25a, 0x25c],
stateBase: stateName,
device: 'e380',
delay: delay,
active: true,
});
await worker.initStates(this, 'standby');
await worker.startup(this);
return worker;
}
// Setup E3100CB collect worker:
async setupE3100cbCollectWorker(delay) {
const worker = new collect.collect({
canID: [0x569],
stateBase: 'e3100cb',
device: 'e3100cb',
delay: delay,
active: true,
});
await worker.initStates(this, 'standby');
await worker.startup(this);
return worker;
}
// Setup E3 collect workers:
async setupE3CollectWorkers(conf, workers) {
if (conf && conf.length > 0) {
for (const workerConf of Object.values(conf)) {
if (workerConf.collectActive) {
const devInfo = this.config.tableUdsDevices.filter(
// @ts-expect-error AdapterConfig
item => item.collectCanId == workerConf.collectCanId,
);
if (devInfo.length > 0) {
const worker = new collect.collect({
canID: [Number(workerConf.collectCanId)],
stateBase: devInfo[0].devStateName,
devUnits: await (devInfo[0].devUnits ? devInfo[0].devUnits : 'n/a'),
device: 'common',
timeout: this.collectTimeout,
delay: workerConf.collectDelayTime,
});
await worker.initStates(this, 'standby');
if (worker) {
await worker.startup(this);
}
workers[Number(workerConf.collectCanId)] = worker;
}
}
}
}
}
// Setup workers for collecting data and for communication via UDS
async setupUdsWorkers() {
// Create an UDS worker for each device
// This is to allow writing of data points even when no schedule for reading is defined
for (const dev of Object.values(this.config.tableUdsDevices)) {
// @ts-expect-error AdapterConfig
const devTxAddr = Number(dev.devAddr);
const devRxAddr = devTxAddr + 16;
// @ts-expect-error AdapterConfig
this.log.silly(`New UDS worker on ${String(dev.devStateName)}`);
this.E3UdsWorkers[devRxAddr] = new uds.uds({
canID: devTxAddr,
// @ts-expect-error AdapterConfig
stateBase: dev.devStateName,
// @ts-expect-error AdapterConfig
devUnits: await (dev.devUnits ? dev.devUnits : 'n/a'),
device: 'common',
delay: 0,
active: false,
channel: this.channelExt,
timeout: this.udsTimeout,
});
await this.E3UdsWorkers[devRxAddr].initStates(this, 'standby');
if (devTxAddr == this.udsMasterDevAddr) {
// Initialize units of master device
this.udsMasterDevUnits = await this.E3UdsWorkers[devRxAddr].config.devUnits;
}
}
// @ts-expect-error AdapterConfig
if (this.config.tableUdsSchedules && this.config.tableUdsSchedules.length > 0) {
// @ts-expect-error AdapterConfig
for (const dev of Object.values(this.config.tableUdsSchedules)) {
if (dev.udsScheduleActive) {
const devTxAddr = Number(dev.udsSelectDevAddr);
const devRxAddr = devTxAddr + 16;
await this.E3UdsWorkers[devRxAddr].addSchedule(this, dev.udsSchedule, dev.udsScheduleDids);
this.log.silly(
`New Schedule (${String(dev.udsSchedule)}s) UDS device on ${String(dev.udsSelectDevAddr)}`,
);
}
}
}
for (const worker of Object.values(this.E3UdsWorkers)) {
await worker.startup(this, 'normal');
this.subscribeStates(`${this.namespace}.${worker.config.stateBase}.*`, this.onStateChange);
await this.udsScanWorker.sleep(this, this.udsTimeDelta);
}
}
/**
* Is called when adapter shuts down - callback has to be called under any circumstances!
*
* @param {object} callback Callback
*/
async onUnload(callback) {
try {
const tStart = new Date().getTime();
this.stoppingInstance = true;
// Stop UDS workers:
for (const toh of Object.values(this.udsTimeoutHandles)) {
await this.clearTimeout(toh);
}
for (const worker of Object.values(this.E3UdsWorkers)) {
await worker.stop(this);
}
for (const worker of Object.values(this.E3UdsSID77Workers)) {
await worker.stop(this);
}
for (const worker of Object.values(this.udsScanWorker.workers)) {
await worker.stop(this);
}
// Stop Collect workers:
if (this.e380Collect97) {
await this.e380Collect97.stop(this);
}
if (this.e380Collect98) {
await this.e380Collect98.stop(this);
}
if (this.e3100cbCollect) {
await this.e3100cbCollect.stop(this);
}
for (const worker of Object.values(this.E3CollectExt)) {
await worker.stop(this);
}
for (const worker of Object.values(this.E3CollectInt)) {
await worker.stop(this);
}
if (this.cntWorkersActive > 0) {
// Timeout - there are still unstopped workers
this.log.warn(
`Not all workers could be stopped during onOnload(). Number of still active workers: ${String(
this.cntWorkersActive,
)}`,
);
}
// Stop CAN communication:
// @ts-expect-error AdapterConfig
this.disconnectFromCan(this.channelExt, this.config.canExtName);
// @ts-expect-error AdapterConfig
this.disconnectFromCan(this.channelInt, this.config.canIntName);
this.setState('info.connection', false, true);
this.log.debug(`onUnload() took ${String(new Date().getTime() - tStart)} ms to complete.`);
callback();
} catch (e) {
this.log.error(`unLoad() could not be completed. err=${e.message}`);
callback();
}
}
// If you need to react to object changes, uncomment the following block and the corresponding line in the constructor.
// You also need to subscribe to the objects with `this.subscribeObjects`, similar to `this.subscribeStates`.
// /**
// * Is called if a subscribed object changes
// * @param {string} id
// * @param {ioBroker.Object | null | undefined} obj
// */
/*
onObjectChange(id, obj) {
if (obj) {
// The object was changed
this.log.info(`object ${id} changed: ${JSON.stringify(obj)}`);
} else {
// The object was deleted
this.log.info(`object ${id} deleted`);
}
}
*/
/**
* Is called if a subscribed state changes
*
* @param {string} id State id
* @param {ioBroker.State | null | undefined} state State
*/
onStateChange(id, state) {
if (state && !state.ack) {
// The state was changed and ack == false
this.log.silly(`state ${id} changed: ${state.val} (ack = ${state.ack})`);
// Check for necessary measures for all UDS workers
for (const worker of Object.values(this.E3UdsWorkers)) {
if (id.includes(`${this.namespace}.${worker.config.stateBase}`)) {
this.log.silly(`Call UDS worker for ${worker.config.stateBase}`);
worker.onUdsStateChange(this, worker, id, state);
}
}
// Check for necessary measures for all collect workers on external CAN
for (const worker of Object.values(this.E3CollectExt)) {
if (id.includes(`${this.namespace}.${worker.config.stateBase}`)) {
this.log.silly(`Call internal collect worker for ${worker.config.stateBase}`);
worker.onUdsStateChange(this, worker, id, state);
}
}
// Check for necessary measures for all collect workers on internal CAN
for (const worker of Object.values(this.E3CollectInt)) {
if (id.includes(`${this.namespace}.${worker.config.stateBase}`)) {
this.log.silly(`Call internal collect worker for ${worker.config.stateBase}`);
worker.onUdsStateChange(this, worker, id, state);
}
}
}
}
// If you need to accept messages in your adapter, uncomment the following block and the corresponding line in the constructor.
// /**
// * Some message was sent to this instance over message box. Used by email, pushover, text2speech, ...
// * Using this method requires "common.messagebox" property to be set to true in io-package.json
// * @param {ioBroker.Message} obj
// */
async onMessage(obj) {
//await this.log.debug('this.config:');
//await this.log.debug(JSON.stringify(this.config));
if (typeof obj === 'object' && obj.message) {
this.log.silly(`command received ${obj.command}`);
if (obj.command === 'getUdsDevices') {
if (obj.callback) {
if (!this.udsDevScanIsRunning) {
this.udsDevScanIsRunning = true;
await this.log.silly(`Received data - ${JSON.stringify(obj)}`);
await this.udsScanWorker.scanUdsDevices(this);
await this.log.silly(
`Data to send - ${JSON.stringify({ native: { tableUdsDevices: this.udsDevices } })}`,
);
const em = this.detectedEnergyMeters;
const emChan = ch => (ch === 'int' ? '2nd CAN' : 'UDS CAN');
const emParts = [
em.e380_97 ? `E380 (CAN addr 97, ${emChan(em.e380_97)})` : null,
em.e380_98 ? `E380 (CAN addr 98, ${emChan(em.e380_98)})` : null,
em.e3100cb ? `E3100CB (${emChan(em.e3100cb)})` : null,
].filter(Boolean);
await this.sendTo(
obj.from,
obj.command,
{
native: {
tableUdsDevices: this.udsDevices,
energyMeterDetectionResult:
emParts.length > 0 ? emParts.join(', ') : 'None detected',
},
},
obj.callback,
);
this.udsDevScanIsRunning = false;
} else {
await this.log.debug('Request "getUdsDevice" during running UDS scan!');
this.sendTo(
obj.from,
obj.command,
{ native: { tableUdsDevices: this.udsDevices } },
obj.callback,
);
}
} else {
this.sendTo(obj.from, obj.command, { native: { tableUdsDevices: [] } }, obj.callback);
}
}
if (obj.command === 'getUdsDeviceSelect') {
if (obj.callback) {
this.log.silly(`Received data - ${JSON.stringify(obj)}`);
if (Array.isArray(obj.message)) {
const selUdsDevices = obj.message.map(item => ({
label: item.devStateName,
value: item.devAddr,
}));
this.log.silly(`Data to send - ${JSON.stringify(selUdsDevices)}`);
if (selUdsDevices) {
this.sendTo(obj.from, obj.command, selUdsDevices, obj.callback);
}
} else {
this.sendTo(obj.from, obj.command, [{ label: 'Not available', value: '' }], obj.callback);
}
} else {
this.sendTo(obj.from, obj.command, [{ label: 'Not available', value: '' }], obj.callback);
}
}
if (obj.command === 'getExtColDeviceSelect') {
if (obj.callback) {
this.log.silly(`Received data - ${JSON.stringify(obj)}`);
if (Array.isArray(obj.message)) {
const selUdsDevices = obj.message
.filter(item => item.collectCanId != '')
.map(item => ({ label: item.devStateName, value: item.collectCanId }));
this.log.silly(`Data to send - ${JSON.stringify(selUdsDevices)}`);
if (selUdsDevices) {
this.sendTo(obj.from, obj.command, selUdsDevices, obj.callback)