iobroker.e3dc-rscp
Version:
Control E3/DC power station via RSCP
1,172 lines (1,115 loc) • 133 kB
JavaScript
/**
* ioBroker adapter for E3/DC devices.
*
* Control your E3/DC power station using the proprietary RSCP protocol which allows for reading state values and also setting control parameters, e.g. the charge power limit. This is the advantage of RSCP compared to the standard Modbus, which is only for reading values. If you have no need to write values, have a look at the (simpler) Modbus adapter.
*
* @file This files defines the E3dcRscp class.
* @author git-kick.
* @since 1.0.0
*/
'use strict';
// System dictionary
const fs = require('fs');
const path = require('path');
let systemDictionary = {};
//let ad = {};
const helper = require(path.join(__dirname, '/helper.js'));
let wb = {};
const wallbox = require(path.join(__dirname, '/wallbox.js'));
const dayOfWeek = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'];
// RSCP constants & lookup tables
const rscpTag = require(path.join(__dirname, '/lib/RscpTags.json'));
const rscpTagCode = {}; // maps string to code
for (const i in rscpTag) {
rscpTagCode[rscpTag[i].TagNameGlobal] = i;
}
const rscpType = {
0x00: 'None',
0x01: 'Bool',
0x02: 'Char8',
0x03: 'UChar8',
0x04: 'Int16',
0x05: 'UInt16',
0x06: 'Int32',
0x07: 'UInt32',
0x08: 'Int64',
0x09: 'UInt64',
0x0a: 'Float32',
0x0b: 'Double64',
0x0c: 'Bitfield',
0x0d: 'CString',
0x0e: 'Container',
0x0f: 'Timestamp',
0x10: 'ByteArray',
0xff: 'Error',
};
const rscpTypeCode = {}; // maps string to code
for (const i in rscpType) {
rscpTypeCode[rscpType[i]] = i;
}
// Mapping RSCP tag data type to state.common.type
const rscpTypeMap = {
None: 'undefined',
Bool: 'boolean',
Char8: 'number',
UChar8: 'number',
Int16: 'number',
UInt16: 'number',
Int32: 'number',
UInt32: 'number',
Int64: 'number',
UInt64: 'number',
Float32: 'number',
Double64: 'number',
Bitfield: 'string',
CString: 'string',
Container: 'undefined',
Timestamp: 'string',
ByteArray: 'string',
Error: 'number',
};
const rscpReturnCode = {
'-2': 'could not set, try later',
'-1': 'value out of range',
0: 'success',
1: 'success, but below recommendation',
};
const rscpError = {
1: 'RSCP_ERR_NOT_HANDLED',
2: 'RSCP_ERR_ACCESS_DENIED',
3: 'RSCP_ERR_FORMAT',
4: 'RSCP_ERR_AGAIN',
5: 'RSCP_ERR_OUT_OF_BOUNDS',
6: 'RSCP_ERR_NOT_AVAILABLE',
7: 'RSCP_ERR_UNKNOWN_TAG',
8: 'RSCP_ERR_ALREADY_IN_USE',
};
const rscpGeneralError = {
1: 'NOT_HANDLED',
2: 'ACCESS_DENIED',
3: 'FORMAT',
4: 'AGAIN',
};
const rscpAuthLevel = {
0: 'NO_AUTH',
10: 'USER',
20: 'INSTALLER',
30: 'PARTNER',
40: 'E3DC',
50: 'E3DC_ADMIN',
60: 'E3DC_ROOT',
};
const rscpBatTrainingMode = {
0: 'Not training',
1: 'Training, discharging',
2: 'Training, charging',
};
const rscpPviType = {
1: 'SOLU',
2: 'KACO',
3: 'E3DC_E',
};
const rscpPviSystemMode = {
0: 'IDLE',
1: 'NORMAL',
2: 'GRIDCHARGE',
3: 'BACKUPPOWER',
};
const rscpPviPowerMode = {
0: 'OFF',
1: 'ON',
100: 'OFF_FORCE',
101: 'ON_FORCE',
};
const rscpEmsCouplingMode = {
0: 'DC',
1: 'DC_MULTIWR',
2: 'AC',
3: 'HYBRID',
4: 'ISLAND',
};
const rscpEmsEmergencyPowerStatus = {
0: 'NOT_POSSIBLE',
1: 'ACTIVE',
2: 'NOT_ACTIVE',
3: 'NOT_AVAILABLE',
4: 'SWITCH_IN_ISLAND_STATE',
};
const rscpEmsSetEmergencyPower = {
0: 'NORMAL_GRID_MODE',
1: 'EMERGENCY_MODE',
2: 'ISLAND_NO_POWER_MODE',
};
const rscpEmsIdlePeriodType = {
0: 'IDLE_CHARGE',
1: 'IDLE_DISCHARGE',
};
const rscpEmsMode = {
0: 'IDLE',
1: 'DISCHARGE',
2: 'CHARGE',
};
const rscpEmsSetPowerMode = {
0: 'NORMAL',
1: 'IDLE',
2: 'DISCHARGE',
3: 'CHARGE',
4: 'GRID_CHARGE',
};
const rscpActivePhases = {
0: 'PHASE_000',
1: 'PHASE_001',
2: 'PHASE_010',
3: 'PHASE_011',
4: 'PHASE_100',
5: 'PHASE_101',
6: 'PHASE_110',
7: 'PHASE_111',
};
const rscpPmType = {
0: 'UNDEFINED',
1: 'ROOT',
2: 'ADDITIONAL',
3: 'ADDITIONAL_PRODUCTION',
4: 'ADDITIONAL_CONSUMPTION',
5: 'FARM',
6: 'UNUSED',
7: 'WALLBOX',
8: 'FARM_ADDITIONAL',
};
const rscpPmMode = {
0: 'ACTIVE',
1: 'PASSIVE',
2: 'DIAGNOSE',
3: 'ERROR_ACTIVE',
4: 'ERROR_PASSIVE',
};
const rscpSysSystemReboot = {
0: 'Reboot currently not possible, try later',
1: 'Reboot initiated',
2: 'Waiting for services to terminate, reboot will be initiated then',
};
const rscpWbMode = wb.rscpWbMode;
/* RSCP enumerations for later use:
const rscpReturnCodes = {
0: "OK",
-1: "ERR_INVALID_INPUT",
-2: "ERR_NO_MEMORY",
-3: "ERR_INVALID_MAGIC",
-4: "ERR_PROT_VERSION_MISMATCH",
-5: "ERR_INVALID_FRAME_LENGTH",
-6: "ERR_INVALID_CRC",
-7: "ERR_DATA_LIMIT_EXCEEDED",
};
const rscpEmsGeneratorState = {
0x00: "IDLE",
0x01: "HEATUP",
0x02: "HEATUPDONE",
0x03: "STARTING",
0x04: "STARTINGPAUSE",
0x05: "RUNNING",
0x06: "STOPPING",
0x07: "STOPPED",
0x10: "RELAISCONTROLMODE",
0xFF: "NO_GENERATOR",
};
const rscpUmUpdateStatus = {
0: "IDLE",
1: "UPDATE_CHECK_RUNNING",
2: "UPDATING_MODULES_AND_FILES",
3: "UPDATING_HARDWARE",
};
const rscpWbType = {
1: "E3DC",
2: "EASYCONNECT",
};
*/
// Assign enumerations to states:
const mapIdToCommonStates = {
'RSCP.GENERAL_ERROR': rscpGeneralError,
'RSCP.AUTHENTICATION': rscpAuthLevel,
'BAT.GENERAL_ERROR': rscpGeneralError,
'BAT.TRAINING_MODE': rscpBatTrainingMode,
'PVI.TYPE': rscpPviType,
'PVI.SYSTEM_MODE': rscpPviSystemMode,
'PVI.POWER_MODE': rscpPviPowerMode,
'EMS.GENERAL_ERROR': rscpGeneralError,
'EMS.RETURN_CODE': rscpReturnCode,
'EMS.COUPLING_MODE': rscpEmsCouplingMode,
'EMS.EMERGENCY_POWER_STATUS': rscpEmsEmergencyPowerStatus,
'EMS.IDLE_PERIOD_TYPE': rscpEmsIdlePeriodType,
'EMS.MODE': rscpEmsMode,
'EMS.BALANCED_PHASES': rscpActivePhases,
'PM.ACTIVE_PHASES': rscpActivePhases,
'PM.MODE': rscpPmMode,
'PM.TYPE': rscpPmType,
'WB.PM_ACTIVE_PHASES': rscpActivePhases,
'WB.MODE': rscpWbMode,
'SYS.SYTEM_REBOOT': rscpSysSystemReboot,
};
// List of writable states, with Mapping for response value handling.
// Key is returned_tag; value is (type_pattern: target_state)
// type "*" means: apply to all types
const mapReceivedIdToState = {
'EMS.RES_POWERSAVE_ENABLED': { '*': 'EMS.POWERSAVE_ENABLED' },
'EMS.RES_WEATHER_REGULATED_CHARGE_ENABLED': { '*': 'EMS.RETURN_CODE' },
'EMS.RES_MAX_CHARGE_POWER': { '*': 'EMS.RETURN_CODE' },
'EMS.RES_MAX_DISCHARGE_POWER': { '*': 'EMS.RETURN_CODE' },
'EMS.RES_POWER_LIMITS_USED': { '*': 'EMS.RETURN_CODE' },
'EMS.DISCHARGE_START_POWER': { Int32: 'EMS.DISCHARGE_START_POWER', Char8: 'EMS.RETURN_CODE' },
'EMS.USER_CHARGE_LIMIT': { '*': 'EMS.MAX_CHARGE_POWER' },
'EMS.USER_DISCHARGE_LIMIT': { '*': 'EMS.MAX_DISCHARGE_POWER' },
'EMS.DPP_SET_BATTERY_CHARGE_ENABLED': { '*': 'EMS.DPP_PRICE_BASED_BATTERY_CHARGE_ENABLED' },
'EMS.DPP_SET_PRICE_LIMIT_BATTERY': { '*': 'EMS.DPP_PRICE_LIMIT_BATTERY' },
};
// List of all writable states and define how to send a corresponding SET to E3/DC
// hash-key is the state id - '*' wildcards in path are allowed. This is the state the user will modify to trigger a change.
// hash-value is [optional_container_global_tag, setter_global_tag]. This is the tag the adapter will send to the E3/DC device.
// hash-value is [] (i.e. empty) for tags which are handled in a dedicated queue...() function
const mapChangedIdToSetTags = {
'EMS.MAX_CHARGE_POWER': ['TAG_EMS_REQ_SET_POWER_SETTINGS', 'TAG_EMS_MAX_CHARGE_POWER'],
'EMS.MAX_DISCHARGE_POWER': ['TAG_EMS_REQ_SET_POWER_SETTINGS', 'TAG_EMS_MAX_DISCHARGE_POWER'],
'EMS.DISCHARGE_START_POWER': ['TAG_EMS_REQ_SET_POWER_SETTINGS', 'TAG_EMS_DISCHARGE_START_POWER'],
'EMS.POWERSAVE_ENABLED': ['TAG_EMS_REQ_SET_POWER_SETTINGS', 'TAG_EMS_POWERSAVE_ENABLED'],
'EMS.POWER_LIMITS_USED': ['TAG_EMS_REQ_SET_POWER_SETTINGS', 'TAG_EMS_POWER_LIMITS_USED'],
'EMS.WEATHER_REGULATED_CHARGE_ENABLED': [
'TAG_EMS_REQ_SET_POWER_SETTINGS',
'TAG_EMS_WEATHER_REGULATED_CHARGE_ENABLED',
],
'EMS.MANUAL_CHARGE_ENERGY': ['', 'TAG_EMS_REQ_START_MANUAL_CHARGE'],
'EMS.SET_POWER_MODE': [],
'EMS.SET_POWER_VALUE': [],
'EMS.BATTERY_TO_CAR_MODE': ['', 'TAG_EMS_REQ_SET_BATTERY_TO_CAR_MODE'],
'EMS.BATTERY_BEFORE_CAR_MODE': ['', 'TAG_EMS_REQ_SET_BATTERY_BEFORE_CAR_MODE'],
'EMS.WB_DISCHARGE_BAT_UNTIL': ['', 'TAG_EMS_REQ_SET_WB_DISCHARGE_BAT_UNTIL'],
'EMS.WB_ENFORCE_POWER_ASSIGNMENT': ['', 'TAG_EMS_REQ_SET_WB_ENFORCE_POWER_ASSIGNMENT'],
'EMS.EMERGENCY_POWER': ['', 'TAG_EMS_REQ_SET_EMERGENCY_POWER'],
'EMS.START_EMERGENCY_POWER_TEST': ['', 'TAG_EMS_REQ_START_EMERGENCY_POWER_TEST'],
'EMS.OVERRIDE_AVAILABLE_POWER': ['', 'TAG_EMS_REQ_SET_OVERRIDE_AVAILABLE_POWER'],
'EMS.DPP_PRICE_LIMIT_BATTERY': ['', 'TAG_EMS_REQ_DPP_SET_PRICE_LIMIT_BATTERY'],
'EMS.DPP_PRICE_BASED_BATTERY_CHARGE_ENABLED': ['', 'TAG_EMS_REQ_DPP_SET_BATTERY_CHARGE_ENABLED'],
'EMS.DPP_SOC_BATTERY': ['', 'TAG_EMS_REQ_DPP_SET_SOC_BATTERY'],
'EMS.DPP_MONTHS_ACTIVE': ['', 'TAG_EMS_REQ_DPP_SET_MONTHS_ACTIVE'],
'EMS.*.*.IDLE_PERIOD_ACTIVE': [],
'EMS.*.*.START_HOUR': [],
'EMS.*.*.START_MINUTE': [],
'EMS.*.*.END_HOUR': [],
'EMS.*.*.END_MINUTE': [],
'EMS.*.*.PERIOD_ACTIVE': [],
'EMS.*.*.IDLE_PERIOD_TYPE': [],
'EMS.*.*.PERIOD_START': [],
'EMS.*.*.PERIOD_STOP': [],
'EMS.*.*.PERIOD_WEEKDAYS': [],
'EMS.*.*.PERIOD_DESCRIPTION': [],
'EP.*.PARAM_EP_RESERVE': [],
'EP.*.PARAM_EP_RESERVE_ENERGY': [],
'DB.HISTORY_DATA_DAY.*': [],
'DB.HISTORY_DATA_WEEK.*': [],
'DB.HISTORY_DATA_MONTH.*': [],
'DB.HISTORY_DATA_YEAR.*': [],
'SYS.SYSTEM_REBOOT': ['', 'TAG_SYS_REQ_SYSTEM_REBOOT'],
'SYS.RESTART_APPLICATION': ['', 'TAG_SYS_REQ_RESTART_APPLICATION'],
'WB.*.Control.*': [],
};
// RSCP is sloppy concerning Bool - some Char8 and UChar8 values must be converted:
const castToBooleanIds = [
'EMS.POWERSAVE_ENABLED',
'EMS.RES_POWERSAVE_ENABLED',
'EMS.WEATHER_REGULATED_CHARGE_ENABLED',
'EMS.hybridModeSupported',
'EMS.BATTERY_BEFORE_CAR_MODE',
'EMS.BATTERY_TO_CAR_MODE',
'EMS.WB_ENFORCE_POWER_ASSIGNMENT',
'EMS.EXT_SRC_AVAILABLE',
'SYS.IS_SYSTEM_REBOOTING',
'SYS.RESTART_APPLICATION',
];
// RSCP is sloppy concerning Timestamp - some UInt64 values must be converted:
const castToTimestampIds = [
'BAT.DCB_LAST_MESSAGE_TIMESTAMP',
'EMS.ERROR_TIMESTAMP',
'EP.PARAM_TIME_LAST_EMPTY',
'EP.PARAM_TIME_LAST_FULL',
];
// Adjust algebraic sign: e.g. discharge limit is sometimes positive, sometimes negative
const negateValueIds = ['EMS.USER_DISCHARGE_LIMIT'];
// Adjust to percent (divide by 100):
const percentValueIds = ['EMS.DERATE_AT_PERCENT_VALUE'];
// For multiple values within one frame, a subchannel will be generated
const multipleValueIds = ['BAT.DCB_CELL_TEMPERATURE', 'BAT.DCB_CELL_VOLTAGE', 'PVI.RELEASE'];
// Some indexed tags are grouped within a channel
const phaseIds = [
'PVI.AC_POWER',
'PVI.AC_VOLTAGE',
'PVI.AC_CURRENT',
'PVI.AC_APPARENTPOWER',
'PVI.AC_REACTIVEPOWER',
'PVI.AC_ENERGY_ALL',
'PVI.AC_ENERGY_GRID_CONSUMPTION',
];
const stringIds = ['PVI.DC_POWER', 'PVI.DC_VOLTAGE', 'PVI.DC_CURRENT', 'PVI.DC_STRING_ENERGY_ALL'];
// Some of the return values we do not want to see as (missing) states.
// "INDEX" and "..._INDEX" tags are automatically treated as subchannels, no need to list them here.
// Current implementation handles only containers having exactly 1 value attribute besides index.
const ignoreIds = [
'RSCP.UNDEFINED',
'EMS.UNDEFINED_POWER_VALUE',
'EMS.UNDEFINED_POWER_SETTING',
'EMS.MANUAL_CHARGE_START_COUNTER', // returns Int64, seems to be the same timestamp as in MANUAL_CHARGE_LAST_START
'EMS.PARAM_INDEX', // always 0, occurs in container EMERGENCY_POWER_OVERLOAD_STATUS
'EMS.SYS_SPEC_INDEX',
'EMS.SET_IDLE_PERIODS',
'EMS.SET_IDLE_PERIODS_2',
'EMS.SET_WB_DISCHARGE_BAT_UNTIL', // Response is always "true", not usable for state with unit "%"
'BAT.UNDEFINED',
'BAT.INTERNAL_CURRENT_AVG30',
'BAT.INTERNAL_MTV_AVG30',
'BAT.INTERNAL_MAX_CHARGE_CURRENT',
'BAT.INTERNAL_MAX_DISCHARGE_CURRENT',
'BAT.INTERNAL_MAX_CHARGE_CURR_PER_DCB',
'BAT.INTERNAL_MAX_DISCHARGE_CURR_PER_DCB',
'BAT.INTERNAL_MAX_CHARGE_CURR_DATA_LOG',
'BAT.INTERNAL_MAX_DISCHARGE_CURR_DATA_LOG',
'WB.EXTERN_DATA_LEN',
];
// Some of the INDEX values are redundant and can be safely ignored:
// Listed here are containers which contain redundant INDEX tags.
const ignoreIndexIds = [
'PVI.AC_MAX_APPARENTPOWER',
'PVI.MIN_TEMPERATURE',
'PVI.MAX_TEMPERATURE',
'WB.EXTERN_DATA_SUN',
'WB.EXTERN_DATA_NET',
'WB.EXTERN_DATA_ALL',
'WB.EXTERN_DATA_ALG',
'WB.EXTERN_RSP_PARAM_1',
'WB.EXTERN_RSP_PARAM_2',
];
// Some of the INDEX or COUNT tags must be treated as regular values, NOT as indexes
const notIndexIds = ['BAT.DCB_CYCLE_COUNT', 'BAT.SPECIFIED_MAX_DCB_COUNT'];
// Some of the tags are unavailable on some E3/DC devices.
// For those, automatically stop polling after the first RSCP_ERR_NOT_AVAILABLE response:
const stopPollingIds = ['PVI.REQ_FREQUENCY_UNDER_OVER', 'PVI.REQ_VOLTAGE_MONITORING'];
// For SYS_SPECs, names and values are transmitted over Interface, i.e. they are not in rscpTags[]
// Therefore we list the SYS_SPEC units here:
const sysSpecUnits = {
hybridModeSupported: '',
installedBatteryCapacity: 'Wh',
maxAcPower: 'W',
maxBatChargePower: 'W',
maxBatDischargPower: 'W',
maxChargePower: 'W',
maxDischargePower: 'W',
maxFbcChargePower: 'W',
maxFbcDischargePower: 'W',
maxPvPower: 'W',
maxStartChargePower: 'W',
maxStartDischargePower: 'W',
minStartChargePower: 'W',
minStartDischargePower: 'W',
recommendedMinChargeLimit: 'W',
recommendedMinDischargeLimit: 'W',
startChargeDefault: 'W',
startDischargeDefault: 'W',
};
// Encryption setup for E3/DC RSCP
// NOTE: E3/DC uses 256 bit block-size, which ist _not_ covered by AES standard.
// It seems that Rijndael CBC with 256 bit block-size fits.
const Net = require('net');
const CRC32 = require('crc-32');
const Rijndael = require('rijndael-js');
const BLOCK_SIZE = 32;
const KEY_SIZE = 32;
/*
* Created with @iobroker/create-adapter v1.31.0
*/
const utils = require('@iobroker/adapter-core');
//const { resourceLimits, threadId } = require('worker_threads');
//const { type } = require('os');
class E3dcRscp extends utils.Adapter {
/**
* @param {Partial<utils.AdapterOptions>} [options]
*/
constructor(options) {
super({
...options,
name: 'e3dc-rscp',
});
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));
// For preparing & buffering outbound frames:
this.frame = null;
this.queue = [];
// For keeping observed max. indexes, e.g. BAT.INDEX, DCB_COUNT etc.:
this.maxIndex = {}; // {path} - the biggest index with ok response, or 1 below the smallest index with error response
// For PM, there may be a non-sequential set of indexes like (0, 2, 6),
// so initialize with a best guess covering set, which will be pinched out dynamically.
this.indexSet = {}; // {path} - array of indexes with ok response
// For triggering the polling and setting requests:
this.dataPollingTimerS = null;
this.dataPollingTimerM = null;
this.dataPollingTimerL = null;
this.setPowerTimer = null;
this.checkAuthTimeout = null;
this.sendTupleTimeout = {}; // every tuple for grouped sending gets it's own timeout, e.g. "DB.HISTORY_DATA_DAY" or "EMS.IDLE_PERIODS_CHARGE.00-Monday"
this.probingTimeout = []; // for an initial series of probing requests
// For efficient access to polling intervals:
this.pollingInterval = []; // [tagCode]
// TCP connection:
this.tcpConnection = new Net.Socket();
this.inBuffer = null;
this.reconnectTimeout = null;
}
// Create channel to E3/DC: encapsulating TCP connection, encryption, message queuing
initChannel() {
if (!this.config.portal_user) {
this.config.portal_user = '';
}
if (!this.config.portal_password) {
this.config.portal_password = '';
}
this.aesKey = Buffer.alloc(KEY_SIZE, 0xff);
this.encryptionIV = Buffer.alloc(BLOCK_SIZE, 0xff);
this.decryptionIV = Buffer.alloc(BLOCK_SIZE, 0xff);
if (this.aesKey.write(this.config.rscp_password) > this.config.rscp_password.length) {
this.log.error('ERROR initializing AES-KEY!');
}
this.cipher = new Rijndael(this.aesKey, 'cbc');
this.queueRscpAuthentication();
this.checkAuthTimeout = this.setTimeout(() => {
this.getState('RSCP.AUTHENTICATION', (err, obj) => {
const auth = obj ? obj.val : 0;
if (typeof auth === 'number' && auth < 10) {
this.log.error(
'Authentication against E3/DC failed - check adapter settings, then restart instance.',
);
this.setState('info.connection', false, true);
}
});
}, 5000); // check authentication success after 5 seconds - no retry.
this.setState('info.connection', false, true);
if (this.config.e3dc_port && this.config.e3dc_ip) {
this.tcpConnection.connect(this.config.e3dc_port, this.config.e3dc_ip, () => {
this.setState('info.connection', true, true);
this.log.info('Connection to E3/DC is established');
this.sendFrameFIFO();
});
} else {
this.log.error('E3/DC IP address and/or port not set - check adapter configuration!');
// For exit codes see https://github.com/ioBroker/ioBroker.js-controller/blob/master/packages/common/src/lib/common/exitCodes.ts
this.terminate('Error due to missing ip/port', 2);
}
this.tcpConnection.on('data', data => {
this.setState('info.connection', true, true);
// Use inBuffer to handle TCP fragmentation:
if (this.inBuffer) {
this.inBuffer = Buffer.concat([this.inBuffer, data]);
} else {
this.inBuffer = Buffer.from(data);
}
if (this.inBuffer && this.inBuffer.length % 32 == 0) {
const receivedFrame = Buffer.from(this.cipher.decrypt(this.inBuffer, 256, this.decryptionIV));
this.log.silly('Received response');
if (rscpTag[receivedFrame.readUInt32LE(18)]) {
this.log.silly(rscpTag[receivedFrame.readUInt32LE(18)].TagNameGlobal);
}
if (this.decryptionIV) {
this.inBuffer.copy(this.decryptionIV, 0, this.inBuffer.length - BLOCK_SIZE);
} // last encrypted block will be used as IV for next frame
this.log.silly(`IN: ${printRscpFrame(receivedFrame)}`);
// this.log.silly( dumpRscpFrame(receivedFrame) );
this.processFrame(receivedFrame);
this.sendFrameFIFO();
this.inBuffer = null;
} else {
this.log.silly(
`inBuffer has length ${this.inBuffer.length} which is not a multiple of 256bit - waiting for next chunk...`,
);
}
});
this.tcpConnection.on('end', () => {
this.setState('info.connection', false, true);
this.log.warn('Disconnected from E3/DC');
this.reconnectChannel();
});
this.tcpConnection.on('close', () => {
this.setState('info.connection', false, true);
this.log.warn('E3/DC connection closed');
this.reconnectChannel();
});
this.tcpConnection.on('timeout', () => {
this.setState('info.connection', false, true);
this.log.info('E3/DC connection timeout');
this.reconnectChannel();
});
this.tcpConnection.on('error', () => {
this.setState('info.connection', false, true);
this.log.error('E3/DC connection error');
this.reconnectChannel();
});
// For BAT and PVI, there is no COUNT request, so initialize maxIndex to a best guess upper bound.
// Error responses due to out-of range index are handled by processTree(), and maxIndex is adjusted dynamically.
// Initialize index sets from adapter config:
this.maxIndex['BAT'] = this.config.maxindex_bat; // E3/DC tag list states that BAT INDEX is always 0, BUT there are counterexamples (see Issue#96)
// DCDC count should be the same as BAT BAT count, but don't know
this.maxIndex['DCDC'] = this.config.maxindex_dcdc;
this.maxIndex['PVI'] = this.config.maxindex_pvi;
this.maxIndex['WB'] = this.config.maxindex_wb;
this.indexSet['PM'] = [];
for (let i = 0; i <= this.config.maxindex_pm; i++) {
this.indexSet['PM'].push(i);
}
// Force some quick data requests for index probing and building up the object tree:
for (let i = 0; i < 5; i++) {
this.probingTimeout[i] = setTimeout(
() => {
this.requestAllData('');
},
i * 1000 * 7,
); // every 7 seconds
}
this.dataPollingTimerS = setInterval(() => {
this.requestAllData('S');
}, this.config.polling_interval_short * 1000); // seconds
this.dataPollingTimerM = setInterval(
() => {
this.requestAllData('M');
},
this.config.polling_interval_medium * 1000 * 60,
); // minutes
this.dataPollingTimerL = setInterval(
() => {
this.requestAllData('L');
},
this.config.polling_interval_long * 1000 * 3600,
); //hours
this.config.polling_intervals.forEach(element => {
this.pollingInterval[rscpTagCode[element.tag]] = element.interval;
});
// Cleanup v1/v2 idle period objects, according to config:
if (!this.config.periods_v1) {
this.deleteObjectRecursively('e3dc-rscp.0.EMS.IDLE_PERIODS_CHARGE');
this.deleteObjectRecursively('e3dc-rscp.0.EMS.IDLE_PERIODS_DISCHARGE');
}
if (!this.config.periods_v2) {
this.deleteObjectRecursively('e3dc-rscp.0.EMS.IDLE_PERIODS_2');
}
}
reconnectChannel() {
if (!this.reconnectTimeout) {
this.log.info('Stop communication with E3/DC and pause a minute before retry ...');
this.tcpConnection.removeAllListeners();
this.clearAllIntervals();
this.reconnectTimeout = setTimeout(() => {
this.reconnectTimeout = null;
this.log.info('Try reconnecting to E3/DC');
this.initChannel();
}, 60000);
}
}
clearFrame() {
// preset MAGIC and CTRL and reserve space for timestamp and length
this.frame = Buffer.from([
0xe3, 0xdc, 0x00, 0x11, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
]);
}
// Add one tag to the frame under preparation
// Not for Container tags, see startContainer
addTagtoFrame(tag, sml = '', value = Object(0)) {
if (!rscpTagCode[tag]) {
this.log.warn(`Unknown tag ${tag} with value ${value} - cannot add to frame.`);
return;
}
const tagCode = rscpTagCode[tag];
if (
sml != '' &&
!Object.keys(this.pollingInterval).includes(tagCode) &&
tag.includes('_REQ_') &&
!tag.endsWith('_COUNT')
) {
this.log.warn(`${tag} has no assigned polling interval - assuming 'M' - please check io-package.json`);
this.pollingInterval[tagCode] = 'M';
}
if (this.pollingInterval[tagCode] != 'N' && (sml == '' || sml == this.pollingInterval[tagCode])) {
const typeCode = parseInt(rscpTag[tagCode].DataTypeHex, 16);
const buf1 = Buffer.alloc(1);
const buf2 = Buffer.alloc(2);
const buf4 = Buffer.alloc(4);
const buf8 = Buffer.alloc(8);
buf4.writeInt32LE(tagCode);
this.frame = Buffer.concat([this.frame, buf4]);
this.frame = Buffer.concat([this.frame, Buffer.from([typeCode])]);
this.frame = Buffer.concat([this.frame, Buffer.from([0x00, 0x00])]); // reserve space for Length
switch (rscpType[typeCode]) {
case 'None':
break;
case 'Container':
this.log.warn(`Container-tag ${tag} passed to addTagToFrame - cannot add tag to frame.`);
return;
case 'CString':
if (typeof value != 'string') {
value = '';
}
this.frame.writeUInt16LE(value.length, this.frame.length - 2);
this.frame = Buffer.concat([this.frame, Buffer.from(value)]);
break;
case 'Bitfield':
case 'ByteArray':
if (typeof value != 'string') {
value = '';
}
this.frame.writeUInt16LE(helper.stringToBuffer(value).length, this.frame.length - 2);
this.frame = Buffer.concat([this.frame, helper.stringToBuffer(value)]);
break;
case 'Char8':
case 'UChar8':
case 'Error':
if (typeof value == 'boolean') {
value = value ? 1 : 0;
} else if (typeof value != 'number' || value < 0 || value > Math.pow(2, 8) - 1) {
value = 0;
}
this.frame.writeUInt16LE(1, this.frame.length - 2);
buf1.writeUInt8(value);
this.frame = Buffer.concat([this.frame, buf1]);
break;
case 'Bool': // bool is encoded as 0/1 byte
if (typeof value != 'boolean') {
value = false;
}
this.frame.writeUInt16LE(1, this.frame.length - 2);
buf1.writeUInt8(value ? 1 : 0);
this.frame = Buffer.concat([this.frame, buf1]);
break;
case 'Int16':
if (typeof value != 'number' || value < -Math.pow(2, 15) || value > Math.pow(2, 15) - 1) {
value = 0;
}
this.frame.writeUInt16LE(2, this.frame.length - 2);
buf2.writeInt16LE(value);
this.frame = Buffer.concat([this.frame, buf2]);
break;
case 'UInt16':
if (typeof value != 'number' || value < 0 || value > Math.pow(2, 16) - 1) {
value = 0;
}
this.frame.writeUInt16LE(2, this.frame.length - 2);
buf2.writeUInt16LE(value);
this.frame = Buffer.concat([this.frame, buf2]);
break;
case 'Int32':
if (typeof value != 'number' || value < -Math.pow(2, 31) || value > Math.pow(2, 31) - 1) {
value = 0;
}
this.frame.writeUInt16LE(4, this.frame.length - 2);
buf4.writeInt32LE(value);
this.frame = Buffer.concat([this.frame, buf4]);
break;
case 'UInt32':
if (typeof value != 'number' || value < 0 || value > Math.pow(2, 32) - 1) {
value = 0;
}
this.frame.writeUInt16LE(4, this.frame.length - 2);
buf4.writeUInt32LE(value);
this.frame = Buffer.concat([this.frame, buf4]);
break;
case 'Int64':
if (typeof value != 'number' || value < -Math.pow(2, 63) || value > Math.pow(2, 63) - 1) {
value = 0;
}
this.frame.writeUInt16LE(8, this.frame.length - 2);
buf8.writeBigInt64LE(value);
this.frame = Buffer.concat([this.frame, buf8]);
break;
case 'UInt64':
if (typeof value != 'number' || value < 0 || value > Math.pow(2, 64) - 1) {
value = 0;
}
this.frame.writeUInt16LE(8, this.frame.length - 2);
buf8.writeBigUInt64LE(value);
this.frame = Buffer.concat([this.frame, buf8]);
break;
case 'Float32':
if (typeof value != 'number') {
value = 0;
}
this.frame.writeUInt16LE(4, this.frame.length - 2);
buf4.writeFloatLE(value);
this.frame = Buffer.concat([this.frame, buf4]);
break;
case 'Double64':
if (typeof value != 'number') {
value = 0;
}
this.frame.writeUInt16LE(8, this.frame.length - 2);
buf8.writeDoubleLE(value);
this.frame = Buffer.concat([this.frame, buf8]);
break;
case 'Timestamp': // NOTE: treating value as seconds (float)
if (typeof value != 'number' || value < 0 || value > Math.pow(2, 64) - 1) {
value = 0;
}
this.frame.writeUInt16LE(12, this.frame.length - 2);
buf8.writeBigUInt64LE(BigInt(Math.floor(value)));
buf4.writeUInt32LE(Math.round((value - Math.floor(value)) * 1000000));
this.frame = Buffer.concat([this.frame, buf8, new Uint8Array([0x00, 0x00, 0x00, 0x00])]);
break;
default:
this.log.warn(`addTagtoFrame does not know how to handle data type ${rscpType[typeCode]}`);
}
return 0;
}
}
// Add a Container tag to frame under preparation
// Returns position of Container length within frame for use in endContainer
startContainer(tag, sml = '') {
if (!rscpTagCode[tag]) {
this.log.warn(`Unknown container tag ${tag} - cannot start container.`);
return 0;
}
const tagCode = rscpTagCode[tag];
if (sml == '' || !Object.keys(this.pollingInterval).includes(tagCode) || this.pollingInterval[tagCode] == sml) {
const typeCode = parseInt(rscpTag[tagCode].DataTypeHex, 16);
if (rscpType[typeCode] != 'Container') {
this.log.warn(`Non-container tag ${tag} passed to startContainer - cannot start container.`);
return 0;
}
const buf4 = Buffer.alloc(4);
buf4.writeInt32LE(tagCode);
this.frame = Buffer.concat([this.frame, buf4]);
this.frame = Buffer.concat([this.frame, Buffer.from([typeCode])]);
this.frame = Buffer.concat([this.frame, Buffer.from([0x00, 0x00])]); // reserve space for Length
return this.frame.length - 2;
}
return 0;
}
endContainer(pos) {
this.frame.writeUInt16LE(this.frame.length - pos - 2, pos);
}
// Finalize frame, then push it to the queue
// If pos > 0, then endContainer is inclusive
pushFrame(pos = 0) {
if (this.frame.length > 18) {
this.frame.writeUIntLE(Math.floor(new Date().getTime() / 1000), 4, 6); // set timestamp - bytes 7,8 remain zero (which will be wrong after 19.01.2038)
this.frame.writeUInt16LE(this.frame.length - 18, 16); // set total length
if (pos > 0) {
this.endContainer(pos);
}
const buf4 = Buffer.alloc(4);
buf4.writeInt32LE(CRC32.buf(this.frame));
this.frame = Buffer.concat([this.frame, buf4]); // concat returns a copy of this.frame, which therefore can be reused
this.queue.push(this.frame);
}
}
queueRscpAuthentication() {
this.clearFrame();
const pos = this.startContainer('TAG_RSCP_REQ_AUTHENTICATION');
this.addTagtoFrame('TAG_RSCP_AUTHENTICATION_USER', '', this.config.portal_user);
this.addTagtoFrame('TAG_RSCP_AUTHENTICATION_PASSWORD', '', this.config.portal_password);
this.pushFrame(pos);
}
queueBatRequestData(sml) {
for (let i = 0; i <= this.maxIndex['BAT']; i++) {
this.clearFrame();
const pos = this.startContainer('TAG_BAT_REQ_DATA');
this.addTagtoFrame('TAG_BAT_INDEX', '', i);
this.addTagtoFrame('TAG_BAT_REQ_MAX_BAT_VOLTAGE', sml);
this.addTagtoFrame('TAG_BAT_REQ_INFO', sml);
this.addTagtoFrame('TAG_BAT_REQ_ASOC', sml);
this.addTagtoFrame('TAG_BAT_REQ_RSOC_REAL', sml);
this.addTagtoFrame('TAG_BAT_REQ_TERMINAL_VOLTAGE', sml);
this.addTagtoFrame('TAG_BAT_REQ_MAX_DCB_CELL_TEMPERATURE', sml);
this.addTagtoFrame('TAG_BAT_REQ_MIN_DCB_CELL_TEMPERATURE', sml);
this.addTagtoFrame('TAG_BAT_REQ_READY_FOR_SHUTDOWN', sml);
this.addTagtoFrame('TAG_BAT_REQ_TRAINING_MODE', sml);
this.addTagtoFrame('TAG_BAT_REQ_DEVICE_STATE', sml);
this.addTagtoFrame('TAG_BAT_REQ_TOTAL_USE_TIME', sml);
this.addTagtoFrame('TAG_BAT_REQ_TOTAL_DISCHARGE_TIME', sml);
this.addTagtoFrame('TAG_BAT_REQ_USABLE_CAPACITY', sml);
this.addTagtoFrame('TAG_BAT_REQ_USABLE_REMAINING_CAPACITY', sml);
this.addTagtoFrame('TAG_BAT_REQ_MAX_CHARGE_CURRENT', sml);
this.addTagtoFrame('TAG_BAT_REQ_EOD_VOLTAGE', sml);
this.addTagtoFrame('TAG_BAT_REQ_MAX_DISCHARGE_CURRENT', sml);
this.addTagtoFrame('TAG_BAT_REQ_CHARGE_CYCLES', sml);
this.addTagtoFrame('TAG_BAT_REQ_FCC', sml);
this.addTagtoFrame('TAG_BAT_REQ_RC', sml);
this.addTagtoFrame('TAG_BAT_REQ_DCB_COUNT', sml);
this.addTagtoFrame('TAG_BAT_REQ_DEVICE_NAME', sml);
this.addTagtoFrame('TAG_BAT_REQ_SPECIFICATION', sml);
this.addTagtoFrame('TAG_BAT_REQ_INTERNALS', sml);
for (let j = 0; j <= this.maxIndex[`BAT_${i}.DCB`]; j++) {
this.addTagtoFrame('TAG_BAT_REQ_DCB_ALL_CELL_TEMPERATURES', sml, j);
this.addTagtoFrame('TAG_BAT_REQ_DCB_ALL_CELL_VOLTAGES', sml, j);
this.addTagtoFrame('TAG_BAT_REQ_DCB_INFO', sml, j);
}
this.pushFrame(pos);
}
}
queueDcdcRequestData(sml) {
for (let i = 0; i <= this.maxIndex['DCDC']; i++) {
this.clearFrame();
const pos = this.startContainer('TAG_DCDC_REQ_DATA');
this.addTagtoFrame('TAG_DCDC_INDEX', '', i);
this.addTagtoFrame('TAG_DCDC_REQ_I_BAT', sml);
this.addTagtoFrame('TAG_DCDC_REQ_U_BAT', sml);
this.addTagtoFrame('TAG_DCDC_REQ_P_BAT', sml);
this.addTagtoFrame('TAG_DCDC_REQ_I_DCL', sml);
this.addTagtoFrame('TAG_DCDC_REQ_U_DCL', sml);
this.addTagtoFrame('TAG_DCDC_REQ_P_DCL', sml);
this.addTagtoFrame('TAG_DCDC_REQ_FIRMWARE_VERSION', sml);
this.addTagtoFrame('TAG_DCDC_REQ_FPGA_FIRMWARE', sml);
this.addTagtoFrame('TAG_DCDC_REQ_SERIAL_NUMBER', sml);
this.addTagtoFrame('TAG_DCDC_REQ_BOARD_VERSION', sml);
this.addTagtoFrame('TAG_DCDC_REQ_STATUS', sml);
this.addTagtoFrame('TAG_DCDC_REQ_STATUS_AS_STRING', sml);
this.addTagtoFrame('TAG_DCDC_REQ_DEVICE_STATE', sml);
this.pushFrame(pos);
}
}
queuePviRequestData(sml) {
for (let i = 0; i <= this.maxIndex['PVI']; i++) {
this.clearFrame();
const pos = this.startContainer('TAG_PVI_REQ_DATA');
this.addTagtoFrame('TAG_PVI_INDEX', '', i);
this.addTagtoFrame('TAG_PVI_REQ_DC_MAX_STRING_COUNT', '');
this.addTagtoFrame('TAG_PVI_REQ_TEMPERATURE_COUNT', sml);
this.addTagtoFrame('TAG_PVI_REQ_TYPE', sml);
this.addTagtoFrame('TAG_PVI_REQ_SERIAL_NUMBER', sml);
this.addTagtoFrame('TAG_PVI_REQ_VERSION', sml);
this.addTagtoFrame('TAG_PVI_REQ_ON_GRID', sml);
this.addTagtoFrame('TAG_PVI_REQ_STATE', sml);
this.addTagtoFrame('TAG_PVI_REQ_LAST_ERROR', sml);
// this.addTagtoFrame( "TAG_PVI_REQ_COS_PHI", sml ); // always returns data type ERROR
this.addTagtoFrame('TAG_PVI_REQ_VOLTAGE_MONITORING', sml);
this.addTagtoFrame('TAG_PVI_REQ_POWER_MODE', sml);
this.addTagtoFrame('TAG_PVI_REQ_SYSTEM_MODE', sml);
this.addTagtoFrame('TAG_PVI_REQ_FREQUENCY_UNDER_OVER', sml);
this.addTagtoFrame('TAG_PVI_REQ_AC_MAX_PHASE_COUNT', sml);
this.addTagtoFrame('TAG_PVI_REQ_MAX_TEMPERATURE', sml);
this.addTagtoFrame('TAG_PVI_REQ_MIN_TEMPERATURE', sml);
this.addTagtoFrame('TAG_PVI_REQ_AC_MAX_APPARENTPOWER', sml);
this.addTagtoFrame('TAG_PVI_REQ_DEVICE_STATE', sml);
for (let j = 0; j <= this.maxIndex[`PVI_${i}.AC_MAX_PHASE`]; j++) {
for (const id of phaseIds) {
this.addTagtoFrame(`TAG_PVI_REQ_${id.split('.')[1]}`, sml, j);
}
}
for (let j = 0; j <= this.maxIndex[`PVI_${i}.DC_MAX_STRING`]; j++) {
for (const id of stringIds) {
this.addTagtoFrame(`TAG_PVI_REQ_${id.split('.')[1]}`, sml, j);
}
}
for (let j = 0; j <= this.maxIndex[`PVI_${i}.TEMPERATURE`]; j++) {
this.addTagtoFrame('TAG_PVI_REQ_TEMPERATURE', sml, j);
}
this.pushFrame(pos);
}
}
queueEmsRequestData(sml) {
this.clearFrame();
this.addTagtoFrame('TAG_EMS_REQ_GET_POWER_SETTINGS', sml);
this.addTagtoFrame('TAG_EMS_REQ_BATTERY_BEFORE_CAR_MODE', sml);
this.addTagtoFrame('TAG_EMS_REQ_BATTERY_TO_CAR_MODE', sml);
this.addTagtoFrame('TAG_EMS_REQ_POWER_PV', sml);
this.addTagtoFrame('TAG_EMS_REQ_POWER_BAT', sml);
this.addTagtoFrame('TAG_EMS_REQ_POWER_HOME', sml);
this.addTagtoFrame('TAG_EMS_REQ_POWER_GRID', sml);
this.addTagtoFrame('TAG_EMS_REQ_POWER_ADD', sml);
this.addTagtoFrame('TAG_EMS_REQ_BAT_SOC', sml);
this.addTagtoFrame('TAG_EMS_REQ_AUTARKY', sml);
this.addTagtoFrame('TAG_EMS_REQ_SELF_CONSUMPTION', sml);
this.addTagtoFrame('TAG_EMS_REQ_MODE', sml);
this.addTagtoFrame('TAG_EMS_REQ_POWER_WB_ALL', sml);
this.addTagtoFrame('TAG_EMS_REQ_POWER_WB_SOLAR', sml);
this.addTagtoFrame('TAG_EMS_REQ_ALIVE', sml);
this.addTagtoFrame('TAG_EMS_REQ_GET_MANUAL_CHARGE', sml);
this.addTagtoFrame('TAG_EMS_REQ_STATUS', sml);
this.addTagtoFrame('TAG_EMS_REQ_COUPLING_MODE', sml);
this.addTagtoFrame('TAG_EMS_REQ_BALANCED_PHASES', sml);
this.addTagtoFrame('TAG_EMS_REQ_USED_CHARGE_LIMIT', sml);
this.addTagtoFrame('TAG_EMS_REQ_USER_CHARGE_LIMIT', sml);
this.addTagtoFrame('TAG_EMS_REQ_BAT_CHARGE_LIMIT', sml);
this.addTagtoFrame('TAG_EMS_REQ_DCDC_CHARGE_LIMIT', sml);
this.addTagtoFrame('TAG_EMS_REQ_USED_DISCHARGE_LIMIT', sml);
this.addTagtoFrame('TAG_EMS_REQ_USER_DISCHARGE_LIMIT', sml);
this.addTagtoFrame('TAG_EMS_REQ_BAT_DISCHARGE_LIMIT', sml);
this.addTagtoFrame('TAG_EMS_REQ_DCDC_DISCHARGE_LIMIT', sml);
this.addTagtoFrame('TAG_EMS_REQ_WB_DISCHARGE_BAT_UNTIL', sml);
this.addTagtoFrame('TAG_EMS_REQ_WB_ENFORCE_POWER_ASSIGNMENT', sml);
this.addTagtoFrame('TAG_EMS_REQ_REMAINING_BAT_CHARGE_POWER', sml);
this.addTagtoFrame('TAG_EMS_REQ_REMAINING_BAT_DISCHARGE_POWER', sml);
this.addTagtoFrame('TAG_EMS_REQ_EMERGENCY_POWER_STATUS', sml);
this.addTagtoFrame('TAG_EMS_REQ_EMERGENCY_POWER_TEST_STATUS', sml);
// this.addTagtoFrame( "TAG_EMS_REQ_EMERGENCY_POWER_OVERLOAD_STATUS", sml ); // no response?
// this.addTagtoFrame( "TAG_EMS_REQ_EMERGENCY_POWER_RETRY", sml ); // CAUTION: stops inverter! Response is a bool & PARAM_0=(NO_REMAINING_ENTY,TIME_TO_RETRY)
this.addTagtoFrame('TAG_EMS_REQ_STORED_ERRORS', sml);
// this.addTagtoFrame( "TAG_EMS_REQ_GET_GENERATOR_STATE", sml ); // always returns ERROR data type
// this.addTagtoFrame( "TAG_EMS_REQ_ERROR_BUZZER_ENABLED", sml ); // always returns ERROR data type
this.addTagtoFrame('TAG_EMS_REQ_INSTALLED_PEAK_POWER', sml);
this.addTagtoFrame('TAG_EMS_REQ_DERATE_AT_PERCENT_VALUE', sml);
this.addTagtoFrame('TAG_EMS_REQ_DERATE_AT_POWER_VALUE', sml);
this.addTagtoFrame('TAG_EMS_REQ_EXT_SRC_AVAILABLE', sml);
if (this.config.periods_v1) {
this.addTagtoFrame('TAG_EMS_REQ_GET_IDLE_PERIODS', sml);
}
if (this.config.periods_v2) {
this.addTagtoFrame('TAG_EMS_REQ_GET_IDLE_PERIODS_2', sml);
}
this.addTagtoFrame('TAG_EMS_REQ_GET_SYS_SPECS', sml);
this.pushFrame();
}
queueDppRequestData(sml) {
this.clearFrame();
this.addTagtoFrame('TAG_EMS_REQ_DPP_PRICE_BASED_WB_CHARGE_ACTIVE', sml);
this.addTagtoFrame('TAG_EMS_REQ_DPP_PRICE_BASED_BATTERY_CHARGE_ACTIVE', sml);
this.addTagtoFrame('TAG_EMS_REQ_DPP_PRICE_LIMIT_BATTERY', sml);
this.addTagtoFrame('TAG_EMS_REQ_DPP_SOC_BATTERY', sml);
this.addTagtoFrame('TAG_EMS_REQ_DPP_MONTHS_ACTIVE', sml);
this.addTagtoFrame('TAG_EMS_REQ_DPP_PRICE_BASED_BATTERY_CHARGE_ENABLED', sml);
this.pushFrame();
}
sendEmsSetPower(mode, value) {
this.log.debug(`sendEmsSetPower( ${mode}, ${value} )`);
this.clearFrame();
const pos = this.startContainer('TAG_EMS_REQ_SET_POWER');
this.addTagtoFrame('TAG_EMS_REQ_SET_POWER_MODE', '', mode);
this.addTagtoFrame('TAG_EMS_REQ_SET_POWER_VALUE', '', value);
this.pushFrame(pos);
this.sendFrameLIFO();
this.clearFrame();
this.addTagtoFrame('TAG_EMS_REQ_MODE'); // separately update MODE because SET_POWER response contains VALUE, but not MODE
this.pushFrame();
this.sendFrameLIFO();
// Acknowledge SET_POWER_*
this.setState('EMS.SET_POWER_MODE', mode, true);
this.setState('EMS.SET_POWER_VALUE', value, true);
// E3/DC requires regular SET_POWER repetition, otherwise it will fall back to NORMAL mode:
if (mode > 0 && this.config.setpower_interval > 0 && !this.setPowerTimer) {
this.setPowerTimer = setInterval(() => {
this.getState('EMS.SET_POWER_VALUE', (err, vObj) => {
this.getState('EMS.SET_POWER_MODE', (err, mObj) => {
this.sendEmsSetPower(mObj ? mObj.val : 0, vObj ? vObj.val : 0);
});
});
}, this.config.setpower_interval * 1000);
} else if ((mode == 0 || this.config.setpower_interval == 0) && this.setPowerTimer) {
// clear timer when mode is set to NORMAL or interval is zero
clearInterval(this.setPowerTimer);
this.setPowerTimer = null; // nullify to enable "is timer running" check
}
}
queueSysRequestData(sml) {
this.clearFrame();
this.addTagtoFrame('TAG_SYS_REQ_IS_SYSTEM_REBOOTING', sml);
this.pushFrame();
}
queueInfoRequestData(sml) {
this.clearFrame();
this.addTagtoFrame('TAG_INFO_REQ_SERIAL_NUMBER', sml);
this.addTagtoFrame('TAG_INFO_REQ_PRODUCTION_DATE', sml);
this.addTagtoFrame('TAG_INFO_REQ_MODULES_SW_VERSIONS', sml);
this.addTagtoFrame('TAG_INFO_REQ_A35_SERIAL_NUMBER', sml);
this.addTagtoFrame('TAG_INFO_REQ_IP_ADDRESS', sml);
this.addTagtoFrame('TAG_INFO_REQ_SUBNET_MASK', sml);
this.addTagtoFrame('TAG_INFO_REQ_MAC_ADDRESS', sml);
this.addTagtoFrame('TAG_INFO_REQ_GATEWAY', sml);
this.addTagtoFrame('TAG_INFO_REQ_DNS', sml);
this.addTagtoFrame('TAG_INFO_REQ_DHCP_STATUS', sml);
this.addTagtoFrame('TAG_INFO_REQ_TIME', sml);
this.addTagtoFrame('TAG_INFO_REQ_UTC_TIME', sml);
this.addTagtoFrame('TAG_INFO_REQ_TIME_ZONE', sml);
this.addTagtoFrame('TAG_INFO_REQ_SW_RELEASE', sml);
this.addTagtoFrame('TAG_INFO_REQ_GUI_TARGET', sml);
this.addTagtoFrame('TAG_INFO_REQ_GET_FACILITY', sml);
this.addTagtoFrame('TAG_INFO_REQ_GET_FS_USAGE', sml);
// REQ_INFO is just a shortcut for REQ_SERIAL_NUMBER, REQ_MAC_ADDRESS, REQ_PRODUCTION_DATE; but it's buggy, delivers invalid PRODUCTION_DATE
// this.addTagtoFrame( "TAG_INFO_REQ_INFO", sml );
// The following requests all end up in "access denied":
// this.addTagtoFrame( "TAG_INFO_REQ_PLATFORM_TYPE", sml );
// this.addTagtoFrame( "TAG_INFO_REQ_IS_CALIBRATED", sml );
// this.addTagtoFrame( "TAG_INFO_REQ_CALIBRATION_CHECK", sml );
// this.addTagtoFrame( "TAG_INFO_REQ_HW_TIME", sml );
this.pushFrame();
}
queuePmRequestData(sml) {
this.clearFrame();
for (const i of this.indexSet['PM']) {
const pos = this.startContainer('TAG_PM_REQ_DATA');
this.addTagtoFrame('TAG_PM_INDEX', '', i);
this.addTagtoFrame('TAG_PM_REQ_DEVICE_STATE', sml);
this.addTagtoFrame('TAG_PM_REQ_POWER_L1', sml);
this.addTagtoFrame('TAG_PM_REQ_POWER_L2', sml);
this.addTagtoFrame('TAG_PM_REQ_POWER_L3', sml);
this.addTagtoFrame('TAG_PM_REQ_ACTIVE_PHASES', sml);
this.addTagtoFrame('TAG_PM_REQ_MODE', sml);
this.addTagtoFrame('TAG_PM_REQ_ENERGY_L1', sml);
this.addTagtoFrame('TAG_PM_REQ_ENERGY_L2', sml);
this.addTagtoFrame('TAG_PM_REQ_ENERGY_L3', sml);
this.addTagtoFrame('TAG_PM_REQ_DEVICE_ID', sml);
this.addTagtoFrame('TAG_PM_REQ_ERROR_CODE', sml);
this.addTagtoFrame('TAG_PM_REQ_FIRMWARE_VERSION', sml);
this.addTagtoFrame('TAG_PM_REQ_VOLTAGE_L1', sml);
this.addTagtoFrame('TAG_PM_REQ_VOLTAGE_L2', sml);
this.addTagtoFrame('TAG_PM_REQ_VOLTAGE_L3', sml);
this.addTagtoFrame('TAG_PM_REQ_TYPE', sml);
this.addTagtoFrame('TAG_PM_REQ_GET_PHASE_ELIMINATION', sml);
this.endContainer(pos);
}
this.pushFrame();
}
queueEpRequestData(sml) {
this.clearFrame();
this.addTagtoFrame('TAG_EP_REQ_IS_READY_FOR_SWITCH', sml);
this.addTagtoFrame('TAG_EP_REQ_IS_GRID_CONNECTED', sml);
this.addTagtoFrame('TAG_EP_REQ_IS_ISLAND_GRID', sml);
this.addTagtoFrame('TAG_EP_REQ_IS_POSSIBLE', sml);
this.addTagtoFrame('TAG_EP_REQ_IS_INVALID_STATE', sml);
this.addTagtoFrame('TAG_EP_REQ_EP_RESERVE', sml);
this.pushFrame();
}
// Only used for interface exploration:
queueDbRequestData() {
this.clearFrame();
const pos = this.startContainer('TAG_DB_REQ_HISTORY_DATA_DAY');
this.addTagtoFrame('TAG_DB_REQ_HISTORY_TIME_START', '', 1639609200);
this.addTagtoFrame('TAG_DB_REQ_HISTORY_TIME_INTERVAL', '', 1800);
this.addTagtoFrame('TAG_DB_REQ_HISTORY_TIME_SPAN', '', 86400);
this.endContainer(pos);
this.pushFrame();
}
queueSetValue(globalId, value) {
this.log.info(`queueSetValue( ${globalId}, ${value} )`);
const id = globalId.match('^[^.]+[.][^.]+[.](.*)')[1];
const setTags = getSetTags(id);
if (setTags && setTags.length == 2) {
if (setTags[0]) {
this.clearFrame();
const pos = this.startContainer(setTags[0]);
this.addTagtoFrame(setTags[1], '', value);
this.pushFrame(pos);
} else {
this.clearFrame();
this.addTagtoFrame(setTags[1], '', value);
this.pushFrame();
}
} else {
this.log.warn(`Don't know how to queue ${id}`);
}
}
queueSetEpReserve(globalId, value) {
this.log.info(`queueEpReserve( ${globalId} )`);
const tagname = globalId.match('^[^.]+[.][^.]+[.][^.]+[.][^.]+[.](.*)')[1];
this.clearFrame();
const pos = this.startContainer('TAG_EP_REQ_SET_EP_RESERVE');
const index = 0; // are there cases where index > 0 ?
this.addTagtoFrame('TAG_EP_PARAM_INDEX', '', index);
this.addTagtoFrame(`TAG_EP_${tagname}`, '', value);
this.pushFrame(pos);
}
queueSetIdlePeriod(globalId) {
this.log.info(`queueSetIdlePeriod( ${globalId} )`);
const el = globalId.split('.');
if (el.length == 6) {