iobroker.sun2000
Version:
434 lines (405 loc) • 17.6 kB
JavaScript
'use strict';
const { deviceType, driverClasses, dataRefreshRate } = require(`${__dirname}/types.js`);
const { StateMap } = require(`${__dirname}/tools.js`);
//const getDriverHandler = require(__dirname + '/drivers/drivers_old.js');
const getDriverHandler = require(`${__dirname}/drivers/index.js`);
class Registers {
constructor(adapterInstance) {
this.adapter = adapterInstance;
this.stateCache = new StateMap();
for (const device of this.adapter.devices) {
//DriverInfo Instance or Sdongle
const handler = getDriverHandler(device.driverClass);
if (handler) {
device.instance = new handler(this, device);
}
}
this.postProcessHooks = [];
this.inverterPostProcessHooks = [
{
refresh: dataRefreshRate.high,
states: [
{ id: 'collected.houseConsumption', name: 'House consumption', type: 'number', unit: 'kW', role: 'value.power', desc: 'Load power' },
{ id: 'collected.activePower', name: 'Active power', type: 'number', unit: 'kW', role: 'value.power.active', desc: 'Power currently used' },
{ id: 'collected.inputPower', name: 'Input power', type: 'number', unit: 'kW', role: 'value.power', desc: 'Power from solar' },
{
id: 'collected.inputPowerWithEfficiencyLoss',
name: 'input power with efficiency loss',
type: 'number',
unit: 'kW',
role: 'value.power',
desc: '',
},
{
id: 'collected.chargeDischargePower',
name: 'Charge/discharge power',
desc: '(>0 charging, <0 discharging)',
type: 'number',
unit: 'kW',
role: 'value.power',
},
{ id: 'collected.usableSurplusPower', name: 'usable surplus power', type: 'number', unit: 'kW', role: 'value.power' },
{ id: 'collected.externalPower', name: 'external power', type: 'number', unit: 'kW', role: 'value.power' },
//{ id: 'collected.ratedPower', name: 'rated power', type: 'number', unit: 'kW', role: 'value.power' },
],
fn: inverters => {
let actPower = 0;
let inPower = 0;
let inPowerEff = 0;
let chargeDischarge = 0;
let ratedPower = 0;
function calcUsableSurplus() {
let surplusPower = 0;
if (this.adapter.control) {
surplusPower += meterPower;
let minSoc = this.adapter.control.get('usableSurplus.minSoc')?.value ?? 0;
const bufferSoc = this.adapter.control.get('usableSurplus.bufferSoc')?.value ?? 0;
const residualPower = this.adapter.control.get('usableSurplus.residualPower')?.value ?? 0;
const soc = this.stateCache.get('collected.SOC')?.value ?? 0;
const allowNegativeValue = this.adapter.control.get('usableSurplus.allowNegativeValue')?.value ?? false;
const hysterese = this.adapter.control.get('usableSurplus.bufferHysteresis')?.value ?? 0;
// chargeDischarge
if (chargeDischarge < 0) {
surplusPower += chargeDischarge;
}
if (bufferSoc > 0) {
let threshold = hysterese / 2;
if (minSoc > bufferSoc - threshold) {
minSoc = bufferSoc - threshold;
}
if (this.bufferOn) {
threshold = -threshold;
}
if (soc >= bufferSoc + threshold) {
this.bufferOn = true;
const bufferPower = this.adapter.control.get('usableSurplus.bufferPower')?.value ?? 0;
surplusPower += bufferPower / 1000;
} else {
this.bufferOn = false;
}
} else {
this.bufferOn = false;
}
if (soc >= minSoc) {
if (chargeDischarge > 0) surplusPower += chargeDischarge;
if (!this.bufferOn) {
surplusPower -= residualPower / 1000;
}
}
if (surplusPower > ratedPower) {
surplusPower = ratedPower;
}
if (!allowNegativeValue) {
if (surplusPower < 0.1) surplusPower = 0;
}
this.adapter.log.debug(
`### Caculate usableSurplus power ${surplusPower} bufferOn ${this.bufferOn} soc ${soc} minSoc ${minSoc} bufferSoc ${bufferSoc} threshold ${hysterese / 2}`,
);
}
return surplusPower;
}
for (const inverter of inverters) {
if (inverter.driverClass != driverClasses.inverter) {
continue;
}
actPower += this.stateCache.get(`${inverter.path}.activePower`)?.value ?? 0;
inPower += this.stateCache.get(`${inverter.path}.inputPower`)?.value ?? 0;
inPowerEff += this.stateCache.get(`${inverter.path}.derived.inputPowerWithEfficiencyLoss`)?.value ?? 0;
chargeDischarge += this.stateCache.get(`${inverter.path}.battery.chargeDischargePower`)?.value ?? 0;
ratedPower += this.stateCache.get(`${inverter.path}.info.ratedPower`)?.value ?? 0;
//maxChargePower = this.stateCache.get(`${inverter.path}.battery.maximumChargingPower`)?.value ?? 0;
}
const meterPower = this.stateCache.get('meter.activePower')?.value ?? 0;
const extPower = this.adapter.control.get('externalPower')?.value ?? 0;
//extPower = extPower / 1000;
//Zu geringe Erzeugerenegie
let houseConsum = actPower - meterPower + extPower;
if (houseConsum < 0) {
houseConsum = 0;
}
//Überschuss (Differenz)
const surplusPower = calcUsableSurplus.bind(this)();
this.stateCache.set('collected.inputPower', inPower, { type: 'number', renew: true });
this.stateCache.set('collected.inputPowerWithEfficiencyLoss', inPowerEff, { type: 'number' });
this.stateCache.set('collected.activePower', actPower, { type: 'number', renew: true });
this.stateCache.set('collected.houseConsumption', houseConsum, { type: 'number' });
this.stateCache.set('collected.chargeDischargePower', chargeDischarge, { type: 'number' });
this.stateCache.set('collected.usableSurplusPower', surplusPower, {
type: 'number',
});
this.stateCache.set('collected.externalPower', extPower, { type: 'number' });
//this.stateCache.set('collected.ratedPower', ratedPower, { type: 'number' });
},
},
{
refresh: dataRefreshRate.low,
states: [
{
id: 'collected.dailyEnergyYield',
name: 'Daily energy yield',
type: 'number',
unit: 'kWh',
role: 'value.power.consumption',
desc: 'daily output yield of the inverters',
},
{
id: 'collected.dailyInputYield',
name: 'Daily portal yield',
type: 'number',
unit: 'kWh',
role: 'value.power.consumption',
desc: 'Try to recreate the yield from the portal',
},
{
id: 'collected.dailySolarYield',
name: 'Daily solar yield',
type: 'number',
unit: 'kWh',
role: 'value.power.consumption',
desc: 'Riemann sum of input power with efficiency loss',
},
{ id: 'collected.accumulatedEnergyYield', name: 'Accumulated energy yield', type: 'number', unit: 'kWh', role: 'value.power.consumption' },
{ id: 'collected.consumptionSum', name: 'Consumption sum', type: 'number', unit: 'kWh', role: 'value.power.consumption' },
{ id: 'collected.gridExportStart', name: 'Grid export start today', type: 'number', unit: 'kWh', role: 'value.power.consumption' },
{ id: 'collected.gridImportStart', name: 'Grid export start today', type: 'number', unit: 'kWh', role: 'value.power.consumption' },
{ id: 'collected.consumptionStart', name: 'Consumption start today', type: 'number', unit: 'kWh', role: 'value.power.consumption' },
{ id: 'collected.gridExportToday', name: 'Grid export today', type: 'number', unit: 'kWh', role: 'value.power.consumption' },
{ id: 'collected.gridImportToday', name: 'Grid import today', type: 'number', unit: 'kWh', role: 'value.power.consumption' },
{ id: 'collected.consumptionToday', name: 'Consumption today', type: 'number', unit: 'kWh', role: 'value.power.consumption' },
{ id: 'collected.totalCharge', name: 'Total charge of battery', type: 'number', unit: 'kWh', role: 'value.power.consumption' },
{ id: 'collected.totalDischarge', name: 'Total discharge of battery', type: 'number', unit: 'kWh', role: 'value.power.consumption' },
{
id: 'collected.currentDayChargeCapacity',
name: 'Current day charge capacity of battery',
type: 'number',
unit: 'kWh',
role: 'value.power.consumption',
},
{
id: 'collected.currentDayDischargeCapacity',
name: 'Current day discharge capacity of battery',
type: 'number',
unit: 'kWh',
role: 'value.power.consumption',
desc: '',
},
{ id: 'collected.SOC', name: 'State of battery capacity', type: 'number', unit: '%', role: 'value.battery', desc: 'SOC' },
{ id: 'collected.ratedCapacity', name: 'Rated of battery capacity', type: 'number', unit: 'Wh', role: 'value.capacity' },
],
fn: inverters => {
let inYield = 0;
let solarYield = 0;
let outYield = 0;
let enYield = 0;
let charge = 0;
let disCharge = 0;
let totalDisCharge = 0;
let totalCharge = 0;
let ratedCap = 0;
let load = 0;
for (const inverter of inverters) {
//v0.4.x
if (inverter.driverClass != driverClasses.inverter) {
continue;
}
outYield += this.stateCache.get(`${inverter.path}.dailyEnergyYield`)?.value;
inYield += this.stateCache.get(`${inverter.path}.derived.dailyInputYield`)?.value;
solarYield += this.stateCache.get(`${inverter.path}.derived.dailySolarYield`)?.value;
enYield += this.stateCache.get(`${inverter.path}.accumulatedEnergyYield`)?.value;
if (this.stateCache.get(`${inverter.path}.battery.ratedCapacity`)?.value > 0) {
charge += this.stateCache.get(`${inverter.path}.battery.currentDayChargeCapacity`)?.value ?? 0;
disCharge += this.stateCache.get(`${inverter.path}.battery.currentDayDischargeCapacity`)?.value ?? 0;
totalCharge += this.stateCache.get(`${inverter.path}.battery.totalCharge`)?.value ?? 0;
totalDisCharge += this.stateCache.get(`${inverter.path}.battery.totalDischarge`)?.value ?? 0;
load +=
this.stateCache.get(`${inverter.path}.battery.ratedCapacity`)?.value *
this.stateCache.get(`${inverter.path}.battery.SOC`)?.value;
ratedCap += this.stateCache.get(`${inverter.path}.battery.ratedCapacity`)?.value;
}
}
this.stateCache.set('collected.dailyEnergyYield', outYield, { type: 'number' });
this.stateCache.set('collected.dailyInputYield', inYield, { type: 'number' }); //deprecated
this.stateCache.set('collected.dailySolarYield', solarYield, { type: 'number' });
this.stateCache.set('collected.accumulatedEnergyYield', enYield, { type: 'number' });
const conSum = enYield + this.stateCache.get('meter.reverseActiveEnergy')?.value - this.stateCache.get('meter.positiveActiveEnergy')?.value;
this.stateCache.set('collected.consumptionSum', conSum, { type: 'number' });
// compute export and import today
this.stateCache.set(
'collected.gridExportToday',
this.stateCache.get('meter.positiveActiveEnergy')?.value - this.stateCache.get('collected.gridExportStart')?.value,
{ type: 'number' },
);
this.stateCache.set(
'collected.gridImportToday',
this.stateCache.get('meter.reverseActiveEnergy')?.value - this.stateCache.get('collected.gridImportStart')?.value,
{ type: 'number' },
);
// compute consumption today
this.stateCache.set(
'collected.consumptionSum',
this.stateCache.get('collected.accumulatedEnergyYield')?.value +
this.stateCache.get('meter.reverseActiveEnergy')?.value -
this.stateCache.get('meter.positiveActiveEnergy')?.value,
{ type: 'number' },
);
this.stateCache.set(
'collected.consumptionToday',
this.stateCache.get('collected.consumptionSum')?.value - this.stateCache.get('collected.consumptionStart')?.value,
{ type: 'number' },
);
//compute battery
this.stateCache.set('collected.totalCharge', totalCharge, { type: 'number' });
this.stateCache.set('collected.totalDischarge', totalDisCharge, { type: 'number' });
this.stateCache.set('collected.currentDayChargeCapacity', charge, { type: 'number' });
this.stateCache.set('collected.currentDayDischargeCapacity', disCharge, { type: 'number' });
this.stateCache.set('collected.ratedCapacity', ratedCap, { type: 'number' });
this.stateCache.set('collected.SOC', Math.round(load / ratedCap), { type: 'number' });
},
},
];
//only Inverter
this.postProcessHooks.push.apply(this.postProcessHooks, this.inverterPostProcessHooks);
this._loadStates();
}
//state
async initState(path, state) {
//this.adapter.log.debug('[_initStat] path+id '+path+state.id);
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: false,
},
native: {},
});
}
async storeStates() {
for (const stateEntry of this.stateCache.values()) {
//if (stateEntry?.storeType === storeType.never) continue;
if (stateEntry.stored) {
continue;
}
//if (stateEntry?.storeType !== storeType.always && stateEntry.stored) continue;
if (stateEntry.value !== null) {
try {
stateEntry.stored = true;
await this.adapter.setState(stateEntry.id, { val: stateEntry.value, ack: true });
this.adapter.logger.debug(`Fetched ${stateEntry.id}, val=${stateEntry.value}`);
} catch (err) {
stateEntry.stored = false;
this.adapter.logger.warn(`Error while fetching ${stateEntry.id}, val=${stateEntry.value} err=${err.message}`);
}
}
}
}
//wrapper
async updateStates(device, modbusClient, refreshRate, duration) {
//this.adapter.log.debug('### DeviceInfo: '+device.index+' '+JSON.stringify(device.instance.info));
if (device.instance) {
if (device.instance.newInstance) {
this.adapter.logger.debug(`DeviceInfo: ${device.index} ${JSON.stringify(device.instance.info)}`);
device.instance = device.instance.newInstance;
this.adapter.logger.debug(`Device: ${device.index} ${JSON.stringify(device.instance.info)}`);
}
return device.instance.updateStates(modbusClient, refreshRate, duration);
}
this.adapter.logger.error(
`No device instance for has been initialized! {index:${device?.index}, driverClass:${device?.driverClass}, modbusID:${device?.modbusId}}`,
);
return 0;
}
//state
async runPostProcessHooks(refreshRate) {
for (const hook of this.postProcessHooks) {
if (dataRefreshRate.compare(refreshRate, hook.refresh)) {
for (const state of hook.states) {
if (!hook.initState) {
await this.initState('', state);
}
}
hook.initState = true;
hook.fn(this.adapter.devices);
}
}
this.storeStates(); //fire and forget
}
//state
async _loadStates() {
let state = await this.adapter.getState('collected.gridExportStart');
this.stateCache.set('collected.gridExportStart', state?.val, { type: 'number', stored: true });
state = await this.adapter.getState('collected.gridImportStart');
this.stateCache.set('collected.gridImportStart', state?.val, { type: 'number', stored: true });
state = await this.adapter.getState('collected.consumptionStart');
this.stateCache.set('collected.consumptionStart', state?.val, { type: 'number', stored: true });
}
//state
CheckReadError(timeShift) {
const now = new Date();
for (const device of this.adapter.devices) {
if (device.instance) {
for (const [i, reg] of device.instance.registerFields.entries()) {
if (!device.instance.modbusAllowed) {
continue;
} //standby
if (reg.type == deviceType.meter && !device?.meter) {
continue;
} //not meter
if (reg.type == deviceType.gridPowerControl && !device?.meter) {
continue;
} //power control v0.8.x
if (reg.checkIfActive && !reg.checkIfActive()) {
continue;
} //NEW, PATH
if (reg.states && reg.refresh) {
const lastread = reg.lastread;
const ret = {
errno: 0,
address: reg.address,
info: reg.info,
inverter: device.index,
modbusID: device.modbusId,
tc: now.getTime(),
};
if (lastread) {
ret.lastread = lastread;
} else {
ret.lastread = 0;
}
if (now.getTime() - ret.lastread > timeShift) {
if (reg.lastread == 0 && i == 0) {
ret.errno = 101;
ret.message = "Can't read data from device! Please check the configuration.";
} else {
ret.errno = 102;
ret.message = 'Not all data can be read! Please inspect the sun2000 log.';
}
return ret;
}
}
}
}
}
return { message: 'No problems detected' };
}
// one minute before midnight - perform housekeeping actions
//state
async mitnightProcess() {
// copy current export/import kWh - used to compute daily import/export in kWh
this.stateCache.set('collected.gridExportStart', this.stateCache.get('meter.positiveActiveEnergy')?.value, { type: 'number' });
this.stateCache.set('collected.gridImportStart', this.stateCache.get('meter.reverseActiveEnergy')?.value, { type: 'number' });
// copy consumption Sum to Start for the next day
this.stateCache.set('collected.consumptionStart', this.stateCache.get('collected.consumptionSum')?.value, { type: 'number' });
for (const device of this.adapter.devices) {
if (device.instance.mitnightProcess) {
await device.instance.mitnightProcess();
}
}
this.storeStates(); //fire and forget
}
}
module.exports = Registers;