UNPKG

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
'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