iobroker.kecontact
Version:
Control your charging station and use automatic regulation e.g. to charge your vehicle by photovoltaic surplus
1,035 lines (967 loc) • 131 kB
JavaScript
'use strict';
/*
* Created with @iobroker/create-adapter v2.6.5
*/
// The adapter-core module gives you access to the core ioBroker functions
// you need to create an adapter
const utils = require('@iobroker/adapter-core');
// Load your modules here, e.g.:
const dgram = require('dgram');
const axios = require('axios');
const I18n = require('@iobroker/adapter-core').I18n;
class Kecontact extends utils.Adapter {
DEFAULT_UDP_PORT = 7090;
BROADCAST_UDP_PORT = 7092;
// eslint-disable-next-line jsdoc/check-tag-names
/** @type {dgram.Socket | null} */
txSocket = null;
// eslint-disable-next-line jsdoc/check-tag-names
/** @type {dgram.Socket | null} */
rxSocketReports = null;
// eslint-disable-next-line jsdoc/check-tag-names
/** @type {dgram.Socket | null} */
rxSocketBroadcast = null;
// eslint-disable-next-line jsdoc/check-tag-names
/** @type {NodeJS.Timeout | null} */
sendDelayTimer = null;
states = {}; // contains all actual state values
stateChangeListeners = {};
currentStateValues = {}; // contains all actual state values
sendQueue = [];
MODEL_P20 = 1; // product ID is like KC-P20-ES240030-000-ST
MODEL_P30 = 2;
MODEL_BMW = 3; // product ID is like BMW-10-EC2405B2-E1R
TYPE_A_SERIES = 1;
TYPE_B_SERIES = 2;
TYPE_C_SERIES = 3; // product ID for P30 is like KC-P30-EC240422-E00
TYPE_E_SERIES = 4; // product ID for P30 is like KC-P30-EC240422-E00
TYPE_X_SERIES = 5;
TYPE_D_EDITION = 6; // product id (only P30) is KC-P30-EC220112-000-DE, there's no other
chargeTextAutomatic = 'pvAutomaticActive';
chargeTextMax = 'pvAutomaticInactive';
wallboxWarningSent = false; // Warning for inacurate regulation with Deutshcland Edition
wallboxUnknownSent = false; // Warning wallbox not recognized
isPassive = true; // no automatic power regulation?
// eslint-disable-next-line jsdoc/check-tag-names
/** @type {Date | null} */
lastDeviceData = null; // time of last check for device information
intervalDeviceDataUpdate = 24 * 60 * 60 * 1000; // check device data (e.g. firmware) every 24 hours => 'report 1'
intervalPassiveUpdate = 10 * 60 * 1000; // check charging information every 10 minutes
// eslint-disable-next-line jsdoc/check-tag-names
/** @type {NodeJS.Timeout | null} */
timerDataUpdate = null; // interval object for calculating timer
intervalActiceUpdate = 15 * 1000; // check current power (and calculate PV-automatics/power limitation every 15 seconds (report 2+3))
// eslint-disable-next-line jsdoc/check-tag-names
/** @type {Date | null} */
lastCalculating = null; // time of last check for charging information
intervalCalculating = 25 * 1000; // calculate charging poser every 25(-30) seconds
chargingToBeStarted = false; // tried to start charging session last time?
loadChargingSessions = false;
photovoltaicsActive = false; // is photovoltaics automatic active?
useX1switchForAutomatic = true;
maxPowerActive = false; // is limiter for maximum power active?
lastPower = 0; // max power value when checking maxPower
maxAmperageActive = false; // is limiter for maximum amperage active?
lastAmperagePhase1; // last amperage value of phase 1 when checking maxAmperage
lastAmperagePhase2; // last amperage value of phase 2 when checking maxAmperage
lastAmperagePhase3; // last amperage value of phase 3 when checking maxAmperage
maxAmperageDeltaLimit = 1000; // raising limit (in mA) when an immediate max power calculation is enforced
wallboxIncluded = true; // amperage of wallbox include in energy meters 1, 2 or 3?
amperageDelta = 500; // default for step of amperage
underusage = 0; // maximum grid consumption use to reach minimal charge power for vehicle
minAmperageDefault = 6000; // default minimum amperage to start charging session
maxCurrentEnWG = 6000; // maximum current allowed when limitation of §14a EnWg is active
minAmperage = 5000; // minimum amperage to start charging session
minChargeSeconds = 0; // minimum of charge time even when surplus is not sufficient
minConsumptionSeconds = 0; // maximum time to accept grid consumption when charging
min1p3pSwSec = 0; // minimum time between phase switching
isMaxPowerCalculation = false; // switch to show if max power calculation is active
// eslint-disable-next-line jsdoc/check-tag-names
/** @type {boolean | number} */
valueFor1p3pOff = 0; // value that will be assigned to 1p/3p state when vehicle is unplugged (unpower switch)
// eslint-disable-next-line jsdoc/check-tag-names
/** @type {boolean | number} */
valueFor1pCharging = 0; // value that will be assigned to 1p/3p state to switch to 1 phase charging
// eslint-disable-next-line jsdoc/check-tag-names
/** @type {boolean | number} */
valueFor3pCharging = 1; // value that will be assigned to 1p/3p state to switch to 3 phase charging
// eslint-disable-next-line jsdoc/check-tag-names
/** @type {string | null} */
stateFor1p3pCharging = null; // state for switching installation contactor
stateFor1p3pAck = false; // Is state acknowledged?
stepFor1p3pSwitching = 0; // 0 = nothing to switch, 1 = stop charging, 2 = switch phases, 3 = acknowledge switching, -1 = temporarily disabled
retries1p3pSwitching = 0;
valueFor1p3pSwitching = null; // value for switch
batteryStrategy = 0; // default = don't care for a battery storage
startWithState5Attempted = false; // switch, whether a start command was tried once even with state of 5
voltage = 230; // calculate with european standard voltage of 230V
firmwareUrl = 'https://www.keba.com/en/emobility/service-support/downloads/downloads';
regexP30cSeries =
/<h3 .*class="headline *tw-h3 ">(?:(?:\s|\n|\r)*?)Updates KeContact P30 a-\/b-\/c-\/e-series((?:.|\n|\r)*?)<h3/gi;
//regexP30xSeries = /<h3 .*class="headline *tw-h3 ">(?:(?:\s|\n|\r)*?)Updates KeContact P30 x-series((?:.|\n|\r)*?)<h3/gi;
regexFirmware = /<div class="mt-3">Firmware Update\s+((?:.)*?)<\/div>/gi;
regexCurrFirmware = /P30 v\s+((?:.)*?)\s+\(/gi;
stateWallboxEnabled = 'enableUser'; /*Enable User*/
stateWallboxCurrent = 'currentUser'; /*Current User*/
stateWallboxMaxCurrent = 'currentHardware'; /*Maximum Current Hardware*/
stateWallboxCurrentWithTimer = 'currentTimer'; /*Current value for currTime */
stateTimeForCurrentChange = 'timeoutCurrentTimer'; /*Timer value for currTime */
stateWallboxPhase1 = 'i1'; /*Current 1*/
stateWallboxPhase2 = 'i2'; /*Current 2*/
stateWallboxPhase3 = 'i3'; /*Current 3*/
stateWallboxPlug = 'plug'; /*Plug status */
stateWallboxState = 'state'; /*State of charging session */
stateWallboxPower = 'p'; /*Power*/
stateAuthActivated = 'authON';
stateAuthPending = 'autoreq';
stateWallboxChargeAmount = 'ePres'; /*ePres - amount of charged energy in Wh */
stateWallboxDisplay = 'display';
stateWallboxOutput = 'output';
stateSetEnergy = 'setenergy';
stateReport = 'report';
stateStart = 'start';
stateStop = 'stop';
stateSetDateTime = 'setdatetime';
stateUnlock = 'unlock';
stateProduct = 'product';
stateX1input = 'input';
stateFirmware = 'firmware'; /*current running version of firmware*/
stateFirmwareAvailable = 'statistics.availableFirmware'; /*current version of firmware available at keba.com*/
stateSurplus = 'statistics.surplus'; /*current surplus for PV automatics*/
stateMaxPower = 'statistics.maxPower'; /*maximum power for wallbox*/
stateMaxAmperage = 'statistics.maxAmperage'; /*maximum amperage for wallbox*/
stateChargingPhases = 'statistics.chargingPhases'; /*number of phases with which vehicle is currently charging*/
statePlugTimestamp = 'statistics.plugTimestamp'; /*Timestamp when vehicled was plugged to wallbox*/
stateAuthPlugTimestamp =
'statistics.authPlugTimestamp'; /* Timestamp when vehicle was plugged and charging was authorized */
stateChargeTimestamp = 'statistics.chargeTimestamp'; /*Timestamp when charging (re)started */
stateConsumptionTimestamp =
'statistics.consumptionTimestamp'; /*Timestamp when charging session was continued with grid consumption */
state1p3pSwTimestamp = 'statistics.1p3pSwTimestamp'; /*Timestamp when 1p3pSw was changed */
stateSessionId = 'statistics.sessionId'; /*id of current charging session */
stateRfidTag = 'statistics.rfid_tag'; /*rfid tag of current charging session */
stateRfidClass = 'statistics.rfid_class'; /*rfid class of current charging session */
stateWallboxDisabled =
'automatic.pauseWallbox'; /*switch to generally disable charging of wallbox, e.g. because of night storage heater */
statePvAutomatic =
'automatic.photovoltaics'; /*switch to charge vehicle in grid consumption to surplus of photovoltaics (false= charge with max available power) */
stateAddPower = 'automatic.addPower'; /*additional grid consumption to run charging session*/
stateLimitCurrent = 'automatic.limitCurrent'; /*maximum amperage for charging*/
stateLimitCurrent1p = 'automatic.limitCurrent1p'; /*maximum amperage for charging when 1p 3p switch set to 1p */
stateManualPhases = 'automatic.calcPhases'; /*count of phases to calculate with for KeContact Deutschland-Edition*/
stateManual1p3p = 'automatic.1p3pCharging'; /*switch to permanently charge with 1p or 3p*/
stateBatteryStrategy = 'automatic.batteryStorageStrategy'; /*strategy to use for battery storage dynamically*/
stateMinimumSoCOfBatteryStorage =
'automatic.batterySoCForCharging'; /*SoC above which battery storage may be used for charging vehicle*/
stateLastChargeStart = 'statistics.lastChargeStart'; /*Timestamp when *last* charging session was started*/
stateLastChargeFinish = 'statistics.lastChargeFinish'; /*Timestamp when *last* charging session was finished*/
stateLastChargeAmount = 'statistics.lastChargeAmount'; /*Energy charging in *last* session in kWh*/
stateMsgFromOtherwallbox = 'internal.message'; /*Message passed on from other instance*/
stateX2Source = 'x2phaseSource'; /*X2 switch source */
stateX2Switch = 'x2phaseSwitch'; /*X2 switch */
/**
* @param [options] options for adapter start
*/
constructor(options) {
super({
...options,
name: 'kecontact',
});
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 (I18n === undefined) {
this.log.error(
'start of adapter not possible due to missing translation - please ensure js-controller >= 7',
);
return;
}
await I18n.init(__dirname, this);
if (!this.checkConfig()) {
this.log.error('start of adapter not possible due to config errors');
return;
}
if (this.loadChargingSessions) {
//History Datenpunkte anlegen
this.createHistory();
}
// Reset the connection indicator during startup
this.setState('info.connection', false, true);
// The adapters config (in the instance object everything under the attribute "native") is accessible via
// this.config:
this.log.debug(`config host: ${this.config.host}`);
this.log.debug(`config passiveMode: ${this.config.passiveMode}`);
this.log.debug(`config pollInterval: ${this.config.pollInterval}`);
this.log.debug(`config loadChargingSessions: ${this.config.loadChargingSessions}`);
this.log.debug(`config lessInfoLogs: ${this.config.lessInfoLogs}`);
this.log.debug(`config useX1forAutomatic: ${this.config.useX1forAutomatic}`);
this.log.debug(`config authChargingTime: ${this.config.authChargingTime}`);
this.log.debug(`config stateRegard: ${this.config.stateRegard}`);
this.log.debug(`config stateSurplus: ${this.config.stateSurplus}`);
this.log.debug(`config stateBatteryCharging: ${this.config.stateBatteryCharging}`);
this.log.debug(`config stateBatteryDischarging: ${this.config.stateBatteryDischarging}`);
this.log.debug(`config stateBatterySoC: ${this.config.stateBatterySoC}`);
this.log.debug(`config batteryPower: ${this.config.batteryPower}`);
this.log.debug(`config batteryChargePower: ${this.config.batteryChargePower}`);
this.log.debug(`config batteryMinSoC: ${this.config.batteryMinSoC}`);
this.log.debug(`config batteryLimitSoC: ${this.config.batteryLimitSoC}`);
this.log.debug(`config batteryStorageStrategy: ${this.config.batteryStorageStrategy}`);
this.log.debug(`config statesIncludeWallbox: ${this.config.statesIncludeWallbox}`);
this.log.debug(`config.state1p3pSwitch: ${this.config.state1p3pSwitch}`);
this.log.debug(`config.1p3pViax2: ${this.config['1p3pViaX2']}`);
this.log.debug(
`config.1p3pSwitchIsNO: ${this.config['1p3pSwitchIsNO']}, 1p = ${this.valueFor1pCharging}, 3p = ${
this.valueFor3pCharging
}, off = ${this.valueFor1p3pOff}`,
);
this.log.debug(`config minAmperage: ${this.config.minAmperage}`);
this.log.debug(`config addPower: ${this.config.addPower}`);
this.log.debug(`config delta: ${this.config.delta}`);
this.log.debug(`config underusage: ${this.config.underusage}`);
this.log.debug(`config minTime: ${this.config.minTime}`);
this.log.debug(`config regardTime: ${this.config.regardTime}`);
this.log.debug(`config stateEnWG: ${this.config.stateEnWG}`);
this.log.debug(`config dynamicEnWG: ${this.config.dynamicEnWG}`);
this.log.debug(`config maxPower: ${this.config.maxPower}`);
this.log.debug(`config stateEnergyMeter1: ${this.config.stateEnergyMeter1}`);
this.log.debug(`config stateEnergyMeter2: ${this.config.stateEnergyMeter2}`);
this.log.debug(`config stateEnergyMeter3: ${this.config.stateEnergyMeter3}`);
this.log.debug(`config wallboxNotIncluded: ${this.config.wallboxNotIncluded}`);
this.log.debug(`config maxAmperage: ${this.config.maxAmperage}`);
this.log.debug(`config stateAmperagePhase1: ${this.config.stateAmperagePhase1}`);
this.log.debug(`config stateAmperagePhase2: ${this.config.stateAmperagePhase2}`);
this.log.debug(`config stateAmperagePhase3: ${this.config.stateAmperagePhase3}`);
this.log.debug(`config amperageUnit: ${this.config.amperageUnit} => factor is ${this.getAmperageFactor()}`);
/*
For every state in the system there has to be also an object of type state
Here a simple template for a boolean variable named "testVariable"
Because every adapter instance uses its own unique namespace variable names can't collide with other adapters variables
*/
/* await this.setObjectNotExistsAsync("testVariable", {
type: "state",
common: {
name: "testVariable",
type: "boolean",
role: "indicator",
read: true,
write: true,
},
native: {},
}); */
// In order to get state updates, you need to subscribe to them. The following line adds a subscription for our variable we have created above.
//this.subscribeStates("testVariable");
// You can also add a subscription for multiple states. The following line watches all states starting with "lights."
// this.subscribeStates('lights.*');
// Or, if you really must, you can also watch all states. Don't do this if you don't need to. Otherwise this will cause a lot of unnecessary load on the system:
// this.subscribeStates('*');
/*
setState examples
you will notice that each setState will cause the stateChange event to fire (because of above subscribeStates cmd)
*/
// the variable testVariable is set to true as command (ack=false)
//await this.setStateAsync("testVariable", true);
// same thing, but the value is flagged "ack"
// ack should be always set to true if the value is received from or acknowledged from the target system
//await this.setStateAsync("testVariable", { val: true, ack: true });
// same thing, but the state is deleted after 30s (getState will return null afterwards)
//await this.setStateAsync("testVariable", { val: true, ack: true, expire: 30 });
// examples for the checkPassword/checkGroup functions
/*let result = await this.checkPasswordAsync("admin", "iobroker");
this.log.info("check user admin pw iobroker: " + result);*/
/*result = await this.checkGroupAsync("admin", "admin");
this.log.info("check group user admin group admin: " + result);*/
this.setupUdpCommunication();
//await adapter.setStateAsync('info.connection', true, true); // too early to acknowledge ...
this.initializeInternalStateValues();
}
/**
* Is called when adapter shuts down - callback has to be called under any circumstances!
*
* @param callback has to be called under any circumstances!
*/
onUnload(callback) {
try {
// Here you must clear all timeouts or intervals that may still be active
try {
if (this.sendDelayTimer) {
clearInterval(this.sendDelayTimer);
}
this.disableChargingTimer();
if (this.txSocket !== null) {
this.txSocket.close();
this.txSocket = null;
}
if (this.rxSocketReports !== null) {
this.rxSocketReports.close();
this.rxSocketReports = null;
}
if (this.rxSocketBroadcast !== null) {
this.rxSocketBroadcast.close();
this.rxSocketBroadcast = null;
}
if (this.isForeignStateSpecified(this.config.stateRegard)) {
this.unsubscribeForeignStates(this.config.stateRegard);
}
if (this.isForeignStateSpecified(this.config.stateSurplus)) {
this.unsubscribeForeignStates(this.config.stateSurplus);
}
if (this.isForeignStateSpecified(this.config.stateBatteryCharging)) {
this.unsubscribeForeignStates(this.config.stateBatteryCharging);
}
if (this.isForeignStateSpecified(this.config.stateBatteryDischarging)) {
this.unsubscribeForeignStates(this.config.stateBatteryDischarging);
}
if (this.isForeignStateSpecified(this.config.stateBatterySoC)) {
this.unsubscribeForeignStates(this.config.stateBatterySoC);
}
if (this.isForeignStateSpecified(this.config.stateEnergyMeter1)) {
this.unsubscribeForeignStates(this.config.stateEnergyMeter1);
}
if (this.isForeignStateSpecified(this.config.stateEnergyMeter2)) {
this.unsubscribeForeignStates(this.config.stateEnergyMeter2);
}
if (this.isForeignStateSpecified(this.config.stateEnergyMeter3)) {
this.unsubscribeForeignStates(this.config.stateEnergyMeter3);
}
if (this.isForeignStateSpecified(this.config.stateEnWG)) {
this.unsubscribeForeignStates(this.config.stateEnWG);
}
} catch (e) {
if (this.log) {
// got an exception 'TypeError: Cannot read property 'warn' of undefined'
this.log.warn(`Error while closing: ${e}`);
}
}
callback();
} catch (e) {
this.log.warn(`Error while disabling timers: ${e}`);
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 id name of the state that changed
* @param state object with all data of state
*/
onStateChange(id, state) {
if (state) {
// The state was changed
this.log.silly(`state ${id} changed: ${state.val} (ack = ${state.ack})`);
if (!id) {
return;
}
//this.log.silly('stateChange ' + id + ' ' + JSON.stringify(state));
// save state changes of foreign adapters - this is done even if value has not changed but acknowledged
const oldValue = this.getStateInternal(id);
let newValue = state.val;
this.setStateInternal(id, newValue);
// if vehicle is (un)plugged check if schedule has to be disabled/enabled
if (id == `${this.namespace}.${this.stateWallboxPlug}`) {
const wasVehiclePlugged = this.isVehiclePlugged(oldValue);
const isNowVehiclePlugged = this.isVehiclePlugged(newValue);
if (isNowVehiclePlugged && !wasVehiclePlugged) {
this.log.info('vehicle plugged to wallbox');
if (this.stepFor1p3pSwitching < 0) {
this.reset1p3pSwitching();
}
if (!this.isPvAutomaticsActive()) {
this.set1p3pSwitching(this.valueFor3pCharging);
}
this.initChargingSession();
this.forceUpdateOfCalculation();
} else if (!isNowVehiclePlugged && wasVehiclePlugged) {
this.log.info('vehicle unplugged from wallbox');
this.finishChargingSession();
if (this.isContinueDueToMin1p3pSwTime(new Date())) {
this.log.debug('wait for minimum time for phase switch to "off"');
} else {
this.set1p3pSwitching(this.valueFor1p3pOff);
if (this.stepFor1p3pSwitching < 0) {
this.reset1p3pSwitching();
}
}
}
}
// if the Wallbox have been disabled or enabled.
if (id == `${this.namespace}.${this.stateWallboxDisabled}`) {
if (oldValue != newValue) {
this.log.info(`change pause status of wallbox from ${oldValue} to ${newValue}`);
newValue = this.getBoolean(newValue);
this.forceUpdateOfCalculation();
}
}
// if PV Automatic has been disable or enabled.
if (id == `${this.namespace}.${this.statePvAutomatic}`) {
if (oldValue != newValue) {
this.log.info(`change of photovoltaics automatic from ${oldValue} to ${newValue}`);
newValue = this.getBoolean(newValue);
this.displayChargeMode();
this.forceUpdateOfCalculation();
}
}
// if the state of the X1 Input has chaned.
if (id == `${this.namespace}.${this.stateX1input}`) {
if (this.useX1switchForAutomatic) {
if (oldValue != newValue) {
this.log.info(`change of photovoltaics automatic via X1 from ${oldValue} to ${newValue}`);
this.displayChargeMode();
this.forceUpdateOfCalculation();
}
}
}
// if the value for AddPower was changes.
if (id == `${this.namespace}.${this.stateAddPower}`) {
if (oldValue != newValue) {
this.log.info(`change additional power from grid consumption from ${oldValue} to ${newValue}`);
}
}
if (id == `${this.namespace}.${this.stateFirmware}`) {
this.checkFirmware();
}
if (id == this.stateFor1p3pCharging) {
this.stateFor1p3pAck = state.ack;
}
if (this.maxPowerActive === true && typeof newValue == 'number') {
if (
id == this.config.stateEnergyMeter1 ||
id == this.config.stateEnergyMeter2 ||
id == this.config.stateEnergyMeter3
) {
if (this.getTotalPower() - this.lastPower > (this.maxAmperageDeltaLimit / 1000) * this.voltage) {
this.requestCurrentChargingValuesDataReport();
}
}
}
if (this.maxAmperageActive === true && typeof newValue == 'number') {
if (
id == this.config.stateAmperagePhase1 ||
id == this.config.stateAmperagePhase2 ||
id == this.config.stateAmperagePhase3
) {
if (
this.getStateDefault0(this.config.stateAmperagePhase1) * this.getAmperageFactor() -
this.lastAmperagePhase1 >
this.maxAmperageDeltaLimit ||
this.getStateDefault0(this.config.stateAmperagePhase2) * this.getAmperageFactor() -
this.lastAmperagePhase2 >
this.maxAmperageDeltaLimit ||
this.getStateDefault0(this.config.stateAmperagePhase3) * this.getAmperageFactor() -
this.lastAmperagePhase3 >
this.maxAmperageDeltaLimit
) {
this.requestCurrentChargingValuesDataReport();
}
}
}
if (state.ack) {
return;
}
if (!id.startsWith(this.namespace)) {
// do not care for foreign states
return;
}
if (!Object.prototype.hasOwnProperty.call(this.stateChangeListeners, id)) {
this.log.error(`Unsupported state change: ${id}`);
return;
}
this.stateChangeListeners[id](oldValue, newValue);
this.setStateAck(id, newValue);
} else {
// The state was deleted
this.log.debug(`state ${id} deleted`);
}
}
// 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
// */
// onMessage(obj) {
// if (typeof obj === 'object' && obj.message) {
// if (obj.command === 'send') {
// // e.g. send email or pushover or whatever
// this.log.info('send command');
// // Send response in callback if required
// if (obj.callback) this.sendTo(obj.from, obj.command, 'Message received', obj.callback);
// }
// }
// }
setupUdpCommunication() {
this.txSocket = dgram.createSocket('udp4');
this.rxSocketReports = dgram.createSocket({ type: 'udp4', reuseAddr: true });
this.rxSocketReports.on('error', err => {
this.log.error(`RxSocketReports error: ${err.message}\n${err.stack}`);
if (this.rxSocketReports !== null) {
this.rxSocketReports.close();
}
});
this.rxSocketReports.on('listening', () => {
if (this.rxSocketReports !== null) {
this.rxSocketReports.setBroadcast(true);
const address = this.rxSocketReports.address();
this.log.debug(`UDP server listening on ${address.address}:${address.port}`);
}
});
this.rxSocketReports.on('message', this.handleWallboxMessage.bind(this));
this.rxSocketReports.bind(this.DEFAULT_UDP_PORT, '0.0.0.0');
this.rxSocketBroadcast = dgram.createSocket({ type: 'udp4', reuseAddr: true });
this.rxSocketBroadcast.on('error', err => {
if (this.rxSocketBroadcast !== null) {
this.log.error(`RxSocketBroadcast error: ${err.message}\n${err.stack}`);
this.rxSocketBroadcast.close();
}
});
this.rxSocketBroadcast.on('listening', () => {
if (this.rxSocketBroadcast !== null) {
this.rxSocketBroadcast.setBroadcast(true);
this.rxSocketBroadcast.setMulticastLoopback(true);
const address = this.rxSocketBroadcast.address();
this.log.debug(`UDP broadcast server listening on ${address.address}:${address.port}`);
}
});
this.rxSocketBroadcast.on('message', this.handleWallboxBroadcast.bind(this));
this.rxSocketBroadcast.bind(this.BROADCAST_UDP_PORT);
}
initializeInternalStateValues() {
this.getStatesOf((err, data) => {
if (data) {
for (let i = 0; i < data.length; i++) {
if (data[i].native && data[i].native.udpKey) {
this.states[data[i].native.udpKey] = data[i];
}
}
}
// save all state value into internal store
this.getStates('*', (err, obj) => {
if (err) {
this.log.error(`error reading states: ${err}`);
} else {
if (obj) {
for (const i in obj) {
if (!Object.prototype.hasOwnProperty.call(obj, i)) {
continue;
}
if (obj[i] !== null) {
if (typeof obj[i] == 'object') {
this.setStateInternal(i, obj[i].val);
} else {
this.log.error(`unexpected state value: ${obj[i]}`);
}
}
}
} else {
this.log.error('not states found');
}
}
});
this.subscribeStatesAndStartWorking();
});
}
/**
* Function is called at the end of main function and will add the subscribed functions
* of all the states of the dapter.
*/
subscribeStatesAndStartWorking() {
this.subscribeStates('*');
this.stateChangeListeners[`${this.namespace}.${this.stateWallboxEnabled}`] = (_oldValue, newValue) => {
this.sendUdpDatagram(`ena ${newValue ? 1 : 0}`, true);
};
this.stateChangeListeners[`${this.namespace}.${this.stateWallboxCurrent}`] = (_oldValue, newValue) => {
this.sendUdpDatagram(`curr ${parseInt(newValue)}`, true);
};
this.stateChangeListeners[`${this.namespace}.${this.stateWallboxCurrentWithTimer}`] = (_oldValue, newValue) => {
this.sendUdpDatagram(
`currtime ${parseInt(newValue)} ${this.getStateDefault0(this.stateTimeForCurrentChange)}`,
true,
);
};
this.stateChangeListeners[`${this.namespace}.${this.stateTimeForCurrentChange}`] = () => {
// parameters (oldValue, newValue) can be ommited if not needed
// no real action to do
};
this.stateChangeListeners[`${this.namespace}.${this.stateWallboxOutput}`] = (_oldValue, newValue) => {
this.sendUdpDatagram(`output ${newValue ? 1 : 0}`, true);
};
this.stateChangeListeners[`${this.namespace}.${this.stateWallboxDisplay}`] = (_oldValue, newValue) => {
if (newValue !== null) {
if (typeof newValue == 'string') {
this.sendUdpDatagram(`display 0 0 0 0 ${newValue.replace(/ /g, '$')}`, true);
} else {
this.log.error(`invalid data to send to display: ${newValue}`);
}
}
};
this.stateChangeListeners[`${this.namespace}.${this.stateWallboxDisabled}`] = (_oldValue, newValue) => {
this.log.debug(`set ${this.stateWallboxDisabled} to ${newValue}`);
// no real action to do
};
this.stateChangeListeners[`${this.namespace}.${this.statePvAutomatic}`] = (_oldValue, newValue) => {
this.log.debug(`set ${this.statePvAutomatic} to ${newValue}`);
// no real action to do
};
this.stateChangeListeners[`${this.namespace}.${this.stateSetEnergy}`] = (_oldValue, newValue) => {
this.sendUdpDatagram(`setenergy ${parseInt(newValue) * 10}`, true);
};
this.stateChangeListeners[`${this.namespace}.${this.stateReport}`] = (_oldValue, newValue) => {
this.sendUdpDatagram(`report ${newValue}`, true);
};
this.stateChangeListeners[`${this.namespace}.${this.stateStart}`] = (_oldValue, newValue) => {
this.sendUdpDatagram(`start ${newValue}`, true);
};
this.stateChangeListeners[`${this.namespace}.${this.stateStop}`] = (_oldValue, newValue) => {
this.sendUdpDatagram(`stop ${newValue}`, true);
};
this.stateChangeListeners[`${this.namespace}.${this.stateSetDateTime}`] = (_oldValue, newValue) => {
this.sendUdpDatagram(`setdatetime ${newValue}`, true);
};
this.stateChangeListeners[`${this.namespace}.${this.stateUnlock}`] = () => {
this.sendUdpDatagram('unlock', true);
};
this.stateChangeListeners[`${this.namespace}.${this.stateX2Source}`] = (_oldValue, newValue) => {
this.sendUdpDatagram(`x2src ${newValue}`, true);
};
this.stateChangeListeners[`${this.namespace}.${this.stateX2Switch}`] = (_oldValue, newValue) => {
this.sendUdpDatagram(`x2 ${newValue}`, true);
this.setStateAck(this.state1p3pSwTimestamp, new Date().toString());
};
this.stateChangeListeners[`${this.namespace}.${this.stateAddPower}`] = (_oldValue, newValue) => {
this.log.debug(`set ${this.stateAddPower} to ${newValue}`);
// no real action to do
};
this.stateChangeListeners[`${this.namespace}.${this.stateManualPhases}`] = (_oldValue, newValue) => {
this.log.debug(`set ${this.stateManualPhases} to ${newValue}`);
// no real action to do
};
this.stateChangeListeners[`${this.namespace}.${this.stateLimitCurrent}`] = (_oldValue, newValue) => {
this.log.debug(`set ${this.stateLimitCurrent} to ${newValue}`);
// no real action to do
};
this.stateChangeListeners[`${this.namespace}.${this.stateManual1p3p}`] = (_oldValue, newValue) => {
this.log.debug(`set ${this.stateManual1p3p} to ${newValue}`);
// no real action to do
};
this.stateChangeListeners[`${this.namespace}.${this.stateLimitCurrent1p}`] = (_oldValue, newValue) => {
this.log.debug(`set ${this.stateLimitCurrent1p} to ${newValue}`);
// no real action to do
};
this.stateChangeListeners[`${this.namespace}.${this.stateBatteryStrategy}`] = (_oldValue, newValue) => {
this.log.debug(`set ${this.stateBatteryStrategy} to ${newValue}`);
// no real action to do
};
this.stateChangeListeners[`${this.namespace}.${this.stateMsgFromOtherwallbox}`] = (_oldValue, newValue) => {
this.handleWallboxExchange(newValue);
};
this.stateChangeListeners[`${this.namespace}.${this.stateMinimumSoCOfBatteryStorage}`] = (
_oldValue,
newValue,
) => {
this.log.debug(`set ${this.stateMinimumSoCOfBatteryStorage} to ${newValue}`);
// no real action to do
};
//sendUdpDatagram('i'); only needed for discovery
this.requestReports();
this.enableChargingTimer(this.isPassive ? this.intervalPassiveUpdate : this.intervalActiceUpdate);
}
/**
* Function which checks weahter the state given by the parameter is defined in the adapter.config page.
*
* @param stateValue is a string with the value of the state.
* @returns true if the state is specified.
*/
isForeignStateSpecified(stateValue) {
return (
stateValue &&
stateValue !== null &&
typeof stateValue == 'string' &&
stateValue !== '' &&
stateValue !== '[object Object]'
);
}
/**
* Function calls addForeignState which subscribes a foreign state to write values
* in 'currentStateValues'
*
* @param stateName is a string with the name of the state.
* @returns returns true if the function addForeingnState was executed successful
*/
addForeignStateFromConfig(stateName) {
if (this.isForeignStateSpecified(stateName)) {
if (this.addForeignState(stateName)) {
return true;
}
this.log.error(`Error when adding foreign state "${stateName}"`);
return false;
}
return true;
}
/**
* Function is called by onAdapterReady. Check if config data is fine for adapter start
*
* @returns returns true if everything is fine
*/
checkConfig() {
let everythingFine = true;
if (this.config.host == '0.0.0.0' || this.config.host == '127.0.0.1') {
this.log.warn(`Can't start adapter for invalid IP address: ${this.config.host}`);
everythingFine = false;
}
if (this.config.loadChargingSessions === true) {
this.loadChargingSessions = true;
}
this.isPassive = false;
if (this.config.passiveMode) {
this.isPassive = true;
if (everythingFine) {
this.log.info('starting charging station in passive mode');
}
if (this.config.pollInterval > 0) {
this.intervalPassiveUpdate = this.getNumber(this.config.pollInterval) * 1000;
}
} else {
if (everythingFine) {
this.log.info('starting charging station in active mode');
}
}
if (this.isForeignStateSpecified(this.config.stateRegard)) {
this.photovoltaicsActive = true;
everythingFine = this.addForeignStateFromConfig(this.config.stateRegard) && everythingFine;
}
if (this.isForeignStateSpecified(this.config.stateSurplus)) {
this.photovoltaicsActive = true;
everythingFine = this.addForeignStateFromConfig(this.config.stateSurplus) && everythingFine;
}
if (this.photovoltaicsActive) {
everythingFine = this.init1p3pSwitching(this.config.state1p3pSwitch) && everythingFine;
everythingFine = this.addForeignStateFromConfig(this.config.stateBatteryCharging) && everythingFine;
everythingFine = this.addForeignStateFromConfig(this.config.stateBatteryDischarging) && everythingFine;
everythingFine = this.addForeignStateFromConfig(this.config.stateBatterySoC) && everythingFine;
if (
this.isForeignStateSpecified(this.config.stateBatteryCharging) ||
this.isForeignStateSpecified(this.config.stateBatteryDischarging) ||
this.config.batteryPower > 0
) {
this.batteryStrategy = this.config.batteryStorageStrategy;
}
if (this.config.useX1forAutomatic) {
this.useX1switchForAutomatic = true;
} else {
this.useX1switchForAutomatic = false;
}
if (!this.config.delta || this.config.delta <= 50) {
this.log.info(`amperage delta not speficied or too low, using default value of ${this.amperageDelta}`);
} else {
this.amperageDelta = this.getNumber(this.config.delta);
}
if (!this.config.minAmperage || this.config.minAmperage == 0) {
this.log.info(`using default minimum amperage of ${this.minAmperageDefault}`);
this.minAmperage = this.minAmperageDefault;
} else if (this.config.minAmperage < this.minAmperage) {
this.log.info(`minimum amperage not speficied or too low, using default value of ${this.minAmperage}`);
} else {
this.minAmperage = this.getNumber(this.config.minAmperage);
}
if (this.config.addPower !== 0) {
this.setStateAck(this.stateAddPower, this.getNumber(this.config.addPower));
}
if (this.config.underusage !== 0) {
this.underusage = this.getNumber(this.config.underusage);
}
if (!this.config.minTime || this.config.minTime < 0) {
this.log.info(
`minimum charge time not speficied or too low, using default value of ${this.minChargeSeconds}`,
);
} else {
this.minChargeSeconds = this.getNumber(this.config.minTime);
}
if (!this.config.regardTime || this.config.regardTime < 0) {
this.log.info(
`minimum grid consumption time not speficied or too low, using default value of ${this.minConsumptionSeconds}`,
);
} else {
this.minConsumptionSeconds = this.getNumber(this.config.regardTime);
}
}
if (this.isX2PhaseSwitch()) {
if (this.isForeignStateSpecified(this.config.state1p3pSwitch)) {
everythingFine = false;
this.log.error('both, state for 1p/3p switch and switching via X2, must not be specified together');
}
const valueOn = 1;
const valueOff = 0;
this.valueFor1p3pOff = valueOff;
if (this.config['1p3pSwitchIsNO'] === true) {
this.valueFor1pCharging = valueOff;
this.valueFor3pCharging = valueOn;
} else {
this.valueFor1pCharging = valueOn;
this.valueFor3pCharging = valueOff;
}
this.min1p3pSwSec = 305;
this.log.info(`Using min time between phase switching of: ${this.min1p3pSwSec} sec`);
}
if (this.isEnWGDefined()) {
everythingFine = this.addForeignStateFromConfig(this.config.stateEnWG) && everythingFine;
}
if (this.config.maxPower && this.config.maxPower > 0) {
this.maxPowerActive = true;
if (this.config.maxPower <= 0) {
this.log.warn('max. power negative or zero - power limitation deactivated');
this.maxPowerActive = false;
}
}
if (this.maxPowerActive) {
everythingFine = this.addForeignStateFromConfig(this.config.stateEnergyMeter1) && everythingFine;
everythingFine = this.addForeignStateFromConfig(this.config.stateEnergyMeter2) && everythingFine;
everythingFine = this.addForeignStateFromConfig(this.config.stateEnergyMeter3) && everythingFine;
if (this.config.wallboxNotIncluded) {
this.wallboxIncluded = false;
} else {
this.wallboxIncluded = true;
}
if (everythingFine) {
if (
!(
this.isForeignStateSpecified(this.config.stateEnergyMeter1) ||
this.isForeignStateSpecified(this.config.stateEnergyMeter2) ||
this.isForeignStateSpecified(this.config.stateEnergyMeter3)
)
) {
this.log.error('no energy meters defined - power limitation deactivated');
this.maxPowerActive = false;
}
}
}
if (this.config.maxAmperage && this.config.maxAmperage > 0) {
this.maxAmperageActive = true;
if (this.config.maxAmperage <= 0) {
this.log.warn('max. current negative or zero - current limitation deactivated');
this.maxAmperageActive = false;
}
}
if (this.maxAmperageActive) {
everythingFine = this.addForeignStateFromConfig(this.config.stateAmperagePhase1) && everythingFine;
everythingFine = this.addForeignStateFromConfig(this.config.stateAmperagePhase2) && everythingFine;
everythingFine = this.addForeignStateFromConfig(this.config.stateAmperagePhase3) && everythingFine;
if (everythingFine) {
if (
!(
this.isForeignStateSpecified(this.config.stateAmperagePhase1) &&
this.isForeignStateSpecified(this.config.stateAmperagePhase2) &&
this.isForeignStateSpecified(this.config.stateAmperagePhase3)
)
) {
this.log.error('not all energy meters defined - amperage limitation deactivated');
this.maxAmperageActive = false;
}
}
}
return everythingFine;
}
/**
* Writes text to log with info or debug level depending on config setting "lessInfoLogs"
*
* @param text text to be logged
*/
logInfoOrDebug(text) {
if (this.config.lessInfoLogs === true) {
this.log.debug(text);
} else {
this.log.info(text);
}
}
init1p3pSwitching(stateNameFor1p3p) {
if (!this.isForeignStateSpecified(stateNameFor1p3p)) {
return true;
}
if (!this.addForeignStateFromConfig(stateNameFor1p3p)) {
return false;
}
this.getForeignState(stateNameFor1p3p, (err, obj) => {
if (err) {
this.log.error(`error reading state ${stateNameFor1p3p}: ${err}`);
return;
}
if (obj) {
this.stateFor1p3pCharging = stateNameFor1p3p;
let valueOn;
let valueOff;
if (typeof obj.val == 'boolean') {
valueOn = true;
valueOff = false;
} else if (typeof obj.val == 'number') {
valueOn = 1;
valueOff = 0;
} else {
this.log.error(`unhandled type ${typeof obj.val} for state ${stateNameFor1p3p}`);
return;
}
this.stateFor1p3pAck = obj.ack;
this.valueFor1p3pOff = valueOff;
if (this.config['1p3pSwitchIsNO'] === true) {
this.valueFor1pCharging = valueOff;
this.valueFor3pCharging = valueOn;
} else {
this.valueFor1pCharging = valueOn;
this.valueFor3pCharging = valueOff;
}
} else {
this.log.error(`state ${stateNameFor1p3p} not found!`);
}
});
return true;
}
// subscribe a foreign state to save values in 'currentStateValues'
addForeignState(id) {
if (typeof id !== 'string') {
return false;
}
if (id == '' || id == ' ') {
return false;
}
this.getForeignState(id, (err, obj) => {
if (err) {
this.log.error(`error subscribing ${id}: ${err}`);
} else {
if (obj) {
this.log.debug(`subscribe state ${id} - current value: ${obj.val}`);
this.setStateInternal(id, obj.val);
this.subscribeForeignStates(id); // there's no return value (success, ...)
//adapter.subscribeForeignStates({id: id, change: 'ne'}); // condition is not working
} else {
this.log.error(`state ${id} not found!`);
}
}
});
return true;
}
isMessageFromWallboxOfThisInstance(remote) {
return remote.address == this.config.host;
}
sendMessageToOtherInstance(message, remote) {
// save message for other instances by