UNPKG

iobroker.sun2000

Version:
768 lines (751 loc) 24.9 kB
'use strict'; const { deviceType, dataType } = require(`${__dirname}/../types.js`); class ServiceQueueMap { constructor(adapterInstance, inverter) { this.adapter = adapterInstance; this.log = this.adapter.logger; this.inverterInfo = inverter; this._modbusClient = null; this._serviceMap = new Map(); this._eventMap = new Map(); this._initialized = false; this.serviceFields = [ { state: { id: 'battery.chargeFromGridFunction', name: 'Charge from grid', type: 'boolean', role: 'switch.enable', desc: 'reg: 47087, len: 1' }, type: deviceType.battery, fn: async event => { const ret = await this._writeRegisters(47087, event.value === true ? [1] : [0]); if (ret) { this.inverterInfo.instance.stateCache.set(`${this.inverterInfo.path}.${event.id}`, event.value === true ? 1 : 0); } return ret; }, }, { state: { id: 'battery.maximumChargingPower', name: 'Maximum charging power', type: 'number', unit: 'W', role: 'level.power', desc: 'reg: 47075, len: 2', }, type: deviceType.battery, fn: async event => { const max = this.inverterInfo.instance.stateCache.get(`${this.inverterInfo.path}.battery.maximumChargePower`)?.value ?? 2500; if (event.value > max) { event.value = max; } if (event.value < 0) { event.value = 0; } const ret = await this._writeRegisters(47075, dataType.numToArray(event.value, dataType.uint32)); if (ret) { this.inverterInfo.instance.stateCache.set(`${this.inverterInfo.path}.${event.id}`, event.value, { type: 'number' }); } return ret; }, }, { state: { id: 'battery.maximumDischargingPower', name: 'Maximum discharge power', type: 'number', unit: 'W', role: 'level.power', desc: 'reg: 47077, len: 2', }, type: deviceType.battery, fn: async event => { const max = this.inverterInfo.instance.stateCache.get(`${this.inverterInfo.path}.battery.maximumDischargePower`)?.value ?? 2500; if (event.value > max) { event.value = max; } if (event.value < 0) { event.value = 0; } const ret = await this._writeRegisters(47077, dataType.numToArray(event.value, dataType.uint32)); if (ret) { this.inverterInfo.instance.stateCache.set(`${this.inverterInfo.path}.${event.id}`, event.value, { type: 'number' }); } return ret; }, }, { //@deprecated state: { id: 'battery.maximumChargePower', name: 'MaximumChargePower', type: 'number', unit: 'W', role: 'level.power', desc: 'deprecated use `maximumChargingPower` instead', }, type: deviceType.battery, fn: async event => { const max = this.inverterInfo.instance.stateCache.get(`${this.inverterInfo.path}.battery.maximumChargePower`)?.value ?? 2500; if (event.value > max) { event.value = max; } if (event.value < 0) { event.value = 0; } this.log.warn(`Control: maximumChargePower is deprecated use "maximumChargingPower" instead`); return await this._writeRegisters(47075, dataType.numToArray(event.value, dataType.uint32)); }, }, { //@deprecated state: { id: 'battery.maximumDischargePower', name: 'MaximumDischargePower', type: 'number', unit: 'W', role: 'level.power', desc: 'deprecated use `maximumDischargingPower` instead', }, type: deviceType.battery, fn: async event => { const max = this.inverterInfo.instance.stateCache.get(`${this.inverterInfo.path}.battery.maximumDischargePower`)?.value ?? 2500; if (event.value > max) { event.value = max; } if (event.value < 0) { event.value = 0; } this.log.warn(`Control: maximumDischargePower is deprecated use "maximumDischargingPower" instead`); return await this._writeRegisters(47077, dataType.numToArray(event.value, dataType.uint32)); }, }, { state: { id: 'battery.chargingCutoffCapacity', name: 'Charging cutoff capacity', type: 'number', unit: '%', role: 'level.max', desc: 'reg: 47081, len: 1', }, type: deviceType.battery, fn: async event => { if (event.value > 100) { event.value = 100; } if (event.value < 90) { event.value = 90; } const ret = await this._writeRegisters(47081, dataType.numToArray(event.value * 10, dataType.uint16)); if (ret) { this.inverterInfo.instance.stateCache.set(`${this.inverterInfo.path}.${event.id}`, event.value, { type: 'number' }); } return ret; }, }, { state: { id: 'battery.dischargeCutoffCapacity', name: 'Discharge cutoff capacity', type: 'number', unit: '%', role: 'level.min', desc: 'reg: 47082, len: 1', }, type: deviceType.battery, fn: async event => { if (event.value > 20) { event.value = 20; } if (event.value < 0) { event.value = 0; } const ret = await this._writeRegisters(47082, dataType.numToArray(event.value * 10, dataType.uint16)); if (ret) { this.inverterInfo.instance.stateCache.set(`${this.inverterInfo.path}.${event.id}`, event.value, { type: 'number' }); } return ret; }, }, { state: { id: 'battery.gridChargeCutoffSOC', name: 'Grid charge cutoff SOC', type: 'number', unit: '%', role: 'level', desc: 'reg:47088, len:1', }, type: deviceType.battery, fn: async event => { if (event.value > 100) { event.value = 100; } if (event.value < 20) { event.value = 20; } const ret = await this._writeRegisters(47088, dataType.numToArray(event.value * 10, dataType.uint16)); if (ret) { this.inverterInfo.instance.stateCache.set(`${this.inverterInfo.path}.${event.id}`, event.value, { type: 'number' }); } return ret; }, }, { state: { id: 'battery.workingModeSettings', name: 'Working mode settings', type: 'number', unit: '', role: 'level', desc: 'reg:47086, len:1' }, type: deviceType.battery, fn: async event => { if (event.value > 5) { event.value = 2; } if (event.value < 0) { event.value = 2; } const ret = await this._writeRegisters(47086, dataType.numToArray(event.value, dataType.uint16)); if (ret) { this.inverterInfo.instance.stateCache.set(`${this.inverterInfo.path}.${event.id}`, event.value, { type: 'number' }); } return ret; }, }, { state: { id: 'battery.powerOfChargeFromGrid', name: 'Power of charge from grid', type: 'number', unit: 'W', role: 'level.power', desc: 'reg: 47242, len: 2', }, type: deviceType.battery, fn: async event => { const max = this.inverterInfo.instance.stateCache.get(`${this.inverterInfo.path}.battery.maximumChargePower`)?.value ?? 2500; if (event.value > max) { event.value = max; } if (event.value < 0) { event.value = 0; } return await this._writeRegisters(47242, dataType.numToArray(event.value, dataType.uint32)); }, }, /* { state: { id: 'battery.forcedChargingAndDischargingPower', name: 'Forced charging and discharging power', type: 'number', unit: 'W', role: 'level.power', desc: 'reg: 47084, len: 2'}, type : deviceType.battery, fn: async event => { const max = this.inverterInfo.instance.stateCache.get(this.inverterInfo.path+'.battery.maximumChargePower')?.value ?? 2500; if (event.value > max) event.value = max; if (event.value < -max) event.value = -max; return await this._writeRegisters(47084,dataType.numToArray(event.value,dataType.int32)); } }, */ /* { state: { id: 'battery.maximumPowerOfChargeFromGrid', name: 'Maximum power of charge from grid', type: 'number', unit: 'W', role: 'level.power', desc: 'reg: 47244, len: 2'}, type : deviceType.battery, fn: async event => { const max = this.inverterInfo.instance.stateCache.get(this.inverterInfo.path+'.battery.maximumChargePower')?.value ?? 2500; if (event.value > max) event.value = max; if (event.value < 0) event.value = 0; return await this._writeRegisters(47244,dataType.numToArray(event.value,dataType.uint32)); } }, */ { state: { id: 'battery.forcibleChargePower', name: 'Forcible charge power', type: 'number', unit: 'W', role: 'level.power', desc: 'reg: 47247, len: 2', }, type: deviceType.battery, fn: async event => { const max = this.inverterInfo.instance.stateCache.get(`${this.inverterInfo.path}.battery.maximumChargePower`)?.value ?? 2500; if (event.value > max) { event.value = max; } if (event.value < 0) { event.value = 0; } return await this._writeRegisters(47247, dataType.numToArray(event.value, dataType.uint32)); }, }, { state: { id: 'battery.forcibleDischargePower', name: 'Forcible discharge power', type: 'number', unit: 'W', role: 'level.power', desc: 'reg: 47249, len: 2', }, type: deviceType.battery, fn: async event => { const max = this.inverterInfo.instance.stateCache.get(`${this.inverterInfo.path}.battery.maximumDischargePower`)?.value ?? 2500; if (event.value > max) { event.value = max; } if (event.value < 0) { event.value = 0; } return await this._writeRegisters(47249, dataType.numToArray(event.value, dataType.uint32)); }, }, { state: { id: 'battery.targetSOC', name: 'Target SOC', type: 'number', unit: '%', role: 'level', desc: 'reg: 47101 , len: 1' }, type: deviceType.battery, fn: async event => { if (event.value > 100) { event.value = 100; } if (event.value < 0) { event.value = 0; } const ret = await this._writeRegisters(47101, dataType.numToArray(event.value * 10, dataType.uint16)); if (ret) { this.inverterInfo.instance.stateCache.set(`${this.inverterInfo.path}.${event.id}`, event.value, { type: 'number' }); } return ret; }, }, { state: { id: 'battery.forcedChargingAndDischargingPeriod', name: 'Forced charging and discharging period', type: 'number', unit: '', role: 'level', desc: 'reg: 47083, len: 1', }, type: deviceType.battery, fn: async event => { if (event.value > 1440) { event.value = 1440; } if (event.value < 0) { event.value = 0; } return await this._writeRegisters(47083, dataType.numToArray(event.value, dataType.uint16)); }, }, { state: { id: 'battery.forcibleChargeOrDischargeSettingMode', name: 'Forcible charge/discharge setting mode (0: Duration,1: until SOC)', type: 'number', unit: '', role: 'level', desc: 'reg: 47246, len: 1', }, type: deviceType.battery, fn: async event => { if (event.value > 1) { event.value = 1; } if (event.value < 0) { event.value = 0; } return await this._writeRegisters(47246, dataType.numToArray(event.value, dataType.uint16)); }, }, { state: { id: 'battery.forcibleChargeOrDischarge', name: 'Forcible charge/discharge (0: Stop,1: Charge, 2: Discharge)', type: 'number', unit: '', role: 'level', desc: 'reg: 47100, len: 1', }, type: deviceType.battery, fn: async event => { if (event.value > 2) { event.value = 2; } if (event.value < 0) { event.value = 0; } return await this._writeRegisters(47100, dataType.numToArray(event.value, dataType.uint16)); }, }, { state: { id: 'battery.backupPowerSOC', name: 'Backup power SOC', type: 'number', unit: '%', role: 'level', desc: 'reg: 47102, len: 1' }, type: deviceType.battery, fn: async event => { const model = this.inverterInfo.instance.stateCache.get(`${this.inverterInfo.path}.battery.productModel`)?.value ?? 0; if (model === 1) { //LG if (event.value > 100) { event.value = 100; } if (event.value < 12) { event.value = 12; } } else { //LUNA if (event.value > 100) { event.value = 100; } if (event.value < 0) { event.value = 0; } } const ret = await this._writeRegisters(47102, dataType.numToArray(event.value * 10, dataType.uint16)); if (ret) { this.inverterInfo.instance.stateCache.set(`${this.inverterInfo.path}.${event.id}`, event.value, { type: 'number' }); } return ret; }, }, { state: { id: 'grid.maximumFeedGridPower', name: 'Maximum feed grid power', type: 'number', unit: 'kW', role: 'level.power', desc: 'reg: 47416, len: 2', }, type: deviceType.gridPowerControl, fn: async event => { const max = 100; //100 kW if (event.value > max) { event.value = max; } if (event.value < -1) { event.value = -1; } const ret = await this._writeRegisters(47416, dataType.numToArray(event.value * 1000, dataType.uint32)); if (ret) { this.inverterInfo.instance.stateCache.set(`${this.inverterInfo.path}.${event.id}`, event.value, { type: 'number' }); } return ret; }, }, { state: { id: 'grid.maximumFeedGridPower_percent', name: 'Maximum feed grid power %', type: 'number', unit: '%', role: 'level', desc: 'reg: 47418, len: 1', }, type: deviceType.gridPowerControl, fn: async event => { if (event.value > 100) { event.value = 100; } if (event.value < 0) { event.value = 0; } const ret = await this._writeRegisters(47418, dataType.numToArray(event.value * 10, dataType.int16)); if (ret) { this.inverterInfo.instance.stateCache.set(`${this.inverterInfo.path}.${event.id}`, event.value, { type: 'number' }); } return ret; }, }, { state: { id: 'grid.activePowerControlMode', name: '(0: Unlimited (default), 1: DIactive scheduling, 5: Zero power grid connection, 6: Power-limited grid connection (kW), 7: Power-limited grid connection (%))', type: 'number', unit: '', role: 'level', desc: 'reg:47415, len:1', }, type: deviceType.gridPowerControl, fn: async event => { if (event.value > 7) { event.value = 7; } if (event.value < 0) { event.value = 0; } const ret = await this._writeRegisters(47415, dataType.numToArray(event.value, dataType.uint16)); if (ret) { this.inverterInfo.instance.stateCache.set(`${this.inverterInfo.path}.${event.id}`, event.value, { type: 'number' }); } return ret; }, }, //power grid scheduling { state: { id: 'grid.scheduling.activePowerPercentageDerating', name: '[power grid scheduling] Active Power percentage derating', type: 'number', unit: '%', role: 'level', desc: 'reg:40125, len:1', }, type: deviceType.gridPowerControl, fn: async event => { if (event.value > 100) { event.value = 100; } if (event.value < 0) { event.value = 0; } const ret = await this._writeRegisters(40125, dataType.numToArray(event.value * 10, dataType.uint16)); if (ret) { this.inverterInfo.instance.stateCache.set(`${this.inverterInfo.path}.${event.id}`, event.value, { type: 'number' }); } return ret; }, }, { state: { id: 'grid.scheduling.FixedActivePowerDerated', name: '[power grid scheduling] Fixed active power derated', type: 'number', unit: 'W', role: 'level.power', desc: 'reg:40126, len:2', }, type: deviceType.gridPowerControl, fn: async event => { const max = this.inverterInfo.instance.stateCache.get(`${this.inverterInfo.path}.info.ratedPower`)?.value ?? 0; //+10% if (event.value > max * 1100) { event.value = max * 1100; } if (event.value < 0) { event.value = 0; } const ret = await this._writeRegisters(40126, dataType.numToArray(event.value, dataType.uint32)); if (ret) { this.inverterInfo.instance.stateCache.set(`${this.inverterInfo.path}.${event.id}`, event.value, { type: 'number' }); } return ret; }, }, ]; } async _init() { if (this.inverterInfo.instance) { for (const item of this.serviceFields) { //no battery - no controls //if (item.type == deviceType.battery && this.inverterInfo.instance.numberBatteryUnits() === 0) continue; if (item.type == deviceType.meter && !this.inverterInfo?.meter) { continue; } if (item.type == deviceType.gridPowerControl && !this.inverterInfo?.meter) { continue; } if (item?.state) { this._serviceMap.set(item.state.id, item); } } for (const entry of this._serviceMap.values()) { //await this._initState(this.inverterInfo.path+'.control.',entry.state); const path = `${this.inverterInfo.path}.control.`; await this._initState(path, entry.state); const state = await this.adapter.getState(path + entry.state.id); if (state && state.ack === false) { this.set(entry.state.id, state); } } //upgrade const tSOC = await this.adapter.getState(`${this.inverterInfo.path}.control.battery.targetSOC `); if (tSOC) { await this.adapter.delObject(`${this.inverterInfo.path}.control.battery.targetSOC `, { recursive: false }); if (tSOC.val !== null) { await this.adapter.setState(`${this.inverterInfo.path}.control.battery.targetSOC`, { val: tSOC.val, ack: tSOC.ack }); if (tSOC.ack === false) { this.set('battery.targetSOC', tSOC); } } } this.adapter.subscribeStates(`${this.inverterInfo.path}.control*`); this._initialized = true; if (this.adapter.settings?.cb.tou && this.inverterInfo.instance.numberBatteryUnits() > 0) { const workingMode = await this._readHoldingRegisters(47086, 1); //const tou = await this._readHoldingRegisters(47255,43); //first periode if (workingMode && workingMode[0] !== 5) { /* 127 - Working mode settings 2 : Maximise self consumptions (default) 5 : Time Of Use(Luna) - hilfreich bei dynamischem Stromtarif (z.B Tibber) Time of Using charging and discharging periodes (siehe Table 5-6) tCDP[3] = 127 - Working mode settings - load from grid (charge) tCDP[3] = 383 - Working mode settings - self-consumption (discharge) */ const tCDP = [ 1, 0, 1440, 127, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ]; if (await this._writeRegisters(47255, tCDP)) { this.log.info('Control: The default TOU setting are transferred'); } } } } if (this._initialized) { this.log.info('Control: Service queue initialized'); } } /** * @description Check if the value of the event is a number and optionally round it to the nearest integer. * @param {object} event The event object * @param {boolean} [round] If true, the value is rounded to the nearest integer * @returns {boolean} True if the value is a number, false otherwise */ isNumber(event, round = true) { if (isNaN(event.value)) { return false; } if (round) event.value = Math.round(event.value); return true; } get(id) { return this._eventMap.get(id); } set(id, state) { const service = this._serviceMap.get(id); if (state && service) { if (state.val !== null && !state.ack) { this.log.info(`Control: Event - state: ${this.inverterInfo.path}.control.${id} changed: ${state.val} ack: ${state.ack}`); const event = this._eventMap.get(id); if (event) { event.value = state.val; event.ack = false; } else { this._eventMap.set(id, { id: id, value: state.val, ack: false }); } } } } /* values() { return this._map.values(); } */ /** * Processes pending events in the service queue and attempts to execute their associated functions. * * @async * @param {object} modbusClient - The modbus client instance used for communication. * * @description This function iterates over the events in the service queue, checking whether each event * can be processed. It verifies conditions such as battery presence and running status, and checks if the * event value is a number when required. If the event is successfully processed, it acknowledges the event * by setting its state in the adapter and removes it from the event map. If the event cannot be processed * after multiple attempts, it is discarded. The function initializes the service queue if it is not already * initialized and the adapter is connected. */ async process(modbusClient) { //if (!this.inverterInfo.instance.modbusAllowed) return; this._modbusClient = modbusClient; if (this._initialized) { let count = 0; for (const event of this._eventMap.values()) { if (event.ack) { continue; } //allready done const service = this._serviceMap.get(event.id); if (!service.errorCount) { service.errorCount = 0; } if (event.value !== null && service.fn) { //check if battery is present and running if (service.type == deviceType.battery) { if (this.inverterInfo.instance.numberBatteryUnits() === 0) { this.log.warn(`Control: Event is discarded because no battery has been detected. `); if (!this.adapter.isReady) { this.log.warn( 'Control: The Adapter is not ready! Please check the value in the state sun2000.x.info.JSONhealth and the Log output.', ); } this._eventMap.delete(event.id); //forget the event continue; } const BatStatus = this.inverterInfo.instance.stateCache.get(`${this.inverterInfo.path}.battery.runningStatus`)?.value ?? -1; if (BatStatus !== 2 && BatStatus !== 1 && BatStatus !== -1) { this.log.warn( `Control: Event is discarded because battery is not running. State: ${this.inverterInfo.path}.battery.runningStatus = ${BatStatus}. `, ); this._eventMap.delete(event.id); //forget the event continue; } } if (service.state.type === 'number') { if (!this.isNumber(event)) { this.log.warn( `Control: Event is discarded because the value ${event.value} is not a number. State: ${this.inverterInfo.path}.control.${event.id}`, ); this._eventMap.delete(event.id); //forget the event continue; } } count++; if (await service.fn(event)) { service.errorCount = 0; try { event.ack = true; await this.adapter.setState(`${this.inverterInfo.path}.control.${event.id}`, { val: event.value, ack: true }); this._eventMap.delete(event.id); this.log.info(`Control: write state ${this.inverterInfo.path}.control.${event.id} : ${event.value} ack: true`); } catch { this.log.warn(`Control: Can not write state ${this.inverterInfo.path}.control.${event.id}`); } } else { service.errorCount++; if (service.errorCount > 1) { this._eventMap.delete(event.id); //forget it this.log.info(`Control: Event is discarded because it could not be processed. ${this.inverterInfo.path}.control.${event.id}`); } } } if (count > 1) { break; } //max 2 Events } } if (!this._initialized && this.adapter.isConnected) { await this._init(); } } async _writeRegisters(address, data) { try { this.log.debug(`Try to write data to id/address/length ${this._modbusClient.id}/${address}/${data.length}`); await this._modbusClient.writeRegisters(address, data); this.inverterInfo.instance.addHoldingRegisters(address, data); return true; } catch (err) { this.log.warn( `Error while writing to ${this._modbusClient.ipAddress} [Reg: ${address}, Len: ${data.length}, modbusID: ${this._modbusClient.id}] with: ${err.message}`, ); } } async _readHoldingRegisters(address, length) { try { this.log.debug(`Try to read data to id/address/length ${this._modbusClient.id}/${address}/${length}`); const data = await this._modbusClient.readHoldingRegisters(address, length); return data; } catch (err) { this.log.warn( `Error while reading from ${this._modbusClient.ipAddress} [Reg: ${address}, Len: ${length}, modbusID: ${this._modbusClient.id}] with: ${err.message}`, ); } } //state async _initState(path, state) { await this.adapter.extendObject(path + state.id, { type: 'state', common: { name: state.name, type: state.type, role: state.role, unit: state.unit, desc: state.desc, read: true, write: true, }, native: {}, }); } } module.exports = ServiceQueueMap;