iobroker.sun2000
Version:
305 lines (286 loc) • 9.79 kB
JavaScript
'use strict';
const { deviceType, dataType } = require(`${__dirname}/../types.js`);
class ServiceQueueMap {
constructor(adapterInstance, emma) {
this.adapter = adapterInstance;
this.log = this.adapter.logger;
this.inverterInfo = emma;
this._modbusClient = null;
this._serviceMap = new Map();
this._eventMap = new Map();
this._initialized = false;
this._name = 'emma control';
this.serviceFields = [
{
state: { id: 'battery.ESSControlMode', name: 'ESS control mode', type: 'number', unit: '', role: 'level', desc: 'reg:40000, len:1' },
type: deviceType.battery,
fn: async event => {
let ret = false;
if (event.value > 6) {
event.value = 2;
}
if (event.value < 2 || event.value === 3) {
event.value = 2;
}
if (this.isTestMode()) {
this.log.info(`${this._name}: the test mode is active, so the ESS control mode is always written to register 47086`);
ret = await this._writeRegisters(47086, dataType.numToArray(event.value, dataType.uint16));
} else {
ret = await this._writeRegisters(40000, dataType.numToArray(event.value, dataType.uint16));
/*
if (ret) {
this.inverterInfo.instance.stateCache.set(`emma.${event.id}`, event.value, { type: 'number' });
}
*/
}
return ret;
},
},
{
state: {
id: 'battery.tou.preferredUseOfSurplusPvPower',
name: '[Time of Use mode] Preferred use of surplus PV power',
type: 'boolean',
unit: '',
role: 'switch.enable',
desc: 'reg: 40001, len: 1',
},
type: deviceType.battery,
fn: async event => {
let ret = false;
if (this.isTestMode()) {
this.log.info(`${this._name}: the test mode is active, so the maximum power for charging batteries from grid not transferred`);
ret = true;
} else {
ret = await this._writeRegisters(40001, event.value === true ? [1] : [0]);
}
/*
if (ret) {
this.inverterInfo.instance.stateCache.set(`emma.${event.id}`, event.value);
}
*/
return ret;
},
},
{
state: {
id: 'battery.tou.maximumPowerForChargingFromGrid',
name: '[Time of Use mode] Maximum power for charging batteries from grid',
type: 'number',
unit: 'kW',
role: 'level.power',
desc: 'reg: 40002, len: 2',
},
type: deviceType.battery,
fn: async event => {
let ret = false;
if (event.value > 50) {
event.value = 50;
}
if (event.value < 0) {
event.value = 0;
}
if (this.isTestMode()) {
this.log.info(`${this._name}: the test mode is active, so the maximum power for charging batteries from grid not transferred`);
ret = true;
} else {
ret = await this._writeRegisters(40002, dataType.numToArray(event.value * 1000, dataType.uint32));
}
/*
if (ret) {
this.inverterInfo.instance.stateCache.set(`emma.${event.id}`, event.value, { type: 'number' });
}
*/
return ret;
},
},
];
}
async _init() {
if (this.inverterInfo.instance) {
for (const item of this.serviceFields) {
if (item?.state) {
this._serviceMap.set(item.state.id, item);
}
}
for (const entry of this._serviceMap.values()) {
//await this._initState('emma.control.',entry.state);
const path = `emma.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);
}
}
//subscribe all control states
this.adapter.subscribeStates(`emma.control*`);
this._initialized = true;
if (this.adapter.settings?.cb.tou && !this.isTestMode()) {
const essControlMode = await this._readHoldingRegisters(40000, 1);
//const tou = await this._readHoldingRegisters(40004,43); //first periode
if (essControlMode && essControlMode[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(40004, tCDP)) {
this.inverterInfo.instance.addHoldingRegisters(40004, tCDP);
this.log.info(`${this._name}: The default TOU setting are transferred`);
}
}
}
}
if (this._initialized) {
this.log.info(`${this._name}: service queue initialized`);
}
}
/**
* Checks if the device is in test mode
* @returns {boolean} True if the device is in test mode, false otherwise
*/
isTestMode() {
return this.inverterInfo.instance?.isTestMode() || false;
}
/**
* @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(`${this._name}: Event - state: emma.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 });
}
}
}
}
/**
* 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) {
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.state.type === 'number') {
if (!this.isNumber(event)) {
this.log.warn(
`${this._name}: Event is discarded because the value ${event.value} is not a number. State: emma.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(`emma.control.${event.id}`, { val: event.value, ack: true });
this._eventMap.delete(event.id);
this.log.info(`${this._name}: write state emma.control.${event.id} : ${event.value} ack: true`);
} catch {
this.log.warn(`${this._name}: Can not write state emma.control.${event.id}`);
}
} else {
service.errorCount++;
if (service.errorCount > 1) {
this._eventMap.delete(event.id); //forget it
this.log.info(`${this._name}: Event is discarded because it could not be processed. State: emma.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); //write also to the modbus read cache
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;