iobroker.nexowatt-ui 0.7.13 → 0.7.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ems/modules/charging-management.js +39 -8
- package/ems/modules/core-limits.js +89 -0
- package/ems/modules/grid-constraints.js +22 -5
- package/ems/modules/heating-rod-control.js +19 -5
- package/ems/modules/storage-control.js +5 -2
- package/ems/modules/tarif-vis.js +121 -10
- package/ems/modules/thermal-control.js +12 -5
- package/io-package.json +10 -2
- package/main.js +15 -0
- package/package.json +1 -1
- package/www/ems-apps.js +31 -0
- package/www/sw.js +1 -1
|
@@ -2193,6 +2193,27 @@ class ChargingManagementModule extends BaseModule {
|
|
|
2193
2193
|
// ignore
|
|
2194
2194
|
}
|
|
2195
2195
|
|
|
2196
|
+
// Gate E – Negativpreis / Netzbezug bevorzugt:
|
|
2197
|
+
// Wenn der dynamische effektive Tarif negativ ist, darf das Lade-/Lastmanagement
|
|
2198
|
+
// Netzladen freigeben und PV-only nur für Auto/Global-Modi aufheben. Harte Limits
|
|
2199
|
+
// (Netzanschluss, Phasen, §14a, Peak-Shaving) bleiben weiter aktiv.
|
|
2200
|
+
let tariffNegativeActive = false;
|
|
2201
|
+
let tariffGridImportPreferred = false;
|
|
2202
|
+
try {
|
|
2203
|
+
const stNeg = await this._getStateCached('tarif.negativpreisAktiv');
|
|
2204
|
+
const stPref = await this._getStateCached('tarif.netzbezugBevorzugt');
|
|
2205
|
+
tariffNegativeActive = stNeg ? !!stNeg.val : false;
|
|
2206
|
+
tariffGridImportPreferred = stPref ? !!stPref.val : tariffNegativeActive;
|
|
2207
|
+
} catch {
|
|
2208
|
+
tariffNegativeActive = false;
|
|
2209
|
+
tariffGridImportPreferred = false;
|
|
2210
|
+
}
|
|
2211
|
+
|
|
2212
|
+
if (tariffGridImportPreferred) {
|
|
2213
|
+
gridChargeAllowedRaw = true;
|
|
2214
|
+
dischargeAllowedRaw = false;
|
|
2215
|
+
}
|
|
2216
|
+
|
|
2196
2217
|
// Debounce gegen Flattern:
|
|
2197
2218
|
// - Sperren (false) wirken sofort (Safety-first)
|
|
2198
2219
|
// - Freigaben (true) erst nach stabiler True-Phase (hold)
|
|
@@ -2243,7 +2264,11 @@ class ChargingManagementModule extends BaseModule {
|
|
|
2243
2264
|
}
|
|
2244
2265
|
|
|
2245
2266
|
// Global default PV-only behaviour (same as before)
|
|
2246
|
-
const
|
|
2267
|
+
const pvSurplusOnlyCfgBase = cfg.pvSurplusOnly === true || mode === 'pvSurplus';
|
|
2268
|
+
// Bei Negativpreis wird Netzbezug wirtschaftlich bevorzugt. Deshalb darf die
|
|
2269
|
+
// globale PV-only-Vorgabe im Automatikpfad temporär aufgehoben werden. Explizite
|
|
2270
|
+
// Wallbox-Modi wie "PV" bleiben User-Wunsch und werden weiter respektiert.
|
|
2271
|
+
const pvSurplusOnlyCfg = tariffGridImportPreferred ? false : pvSurplusOnlyCfgBase;
|
|
2247
2272
|
const forcePvSurplusOnly = !gridChargeAllowed;
|
|
2248
2273
|
|
|
2249
2274
|
// Determine effective per-wallbox mode (runtime override via VIS)
|
|
@@ -2339,7 +2364,8 @@ class ChargingManagementModule extends BaseModule {
|
|
|
2339
2364
|
|
|
2340
2365
|
let allowGrid = true;
|
|
2341
2366
|
if (active) {
|
|
2342
|
-
if (
|
|
2367
|
+
if (p < -1e-9) allowGrid = true;
|
|
2368
|
+
else if (state === 'teuer') allowGrid = false;
|
|
2343
2369
|
else if (state === 'guenstig') allowGrid = !!allowEvcsCheap;
|
|
2344
2370
|
else allowGrid = true;
|
|
2345
2371
|
}
|
|
@@ -2559,13 +2585,15 @@ class ChargingManagementModule extends BaseModule {
|
|
|
2559
2585
|
// (Hard limits like §14a / Grid caps / Phase caps still apply later in the pipeline.)
|
|
2560
2586
|
eff = 'boost';
|
|
2561
2587
|
} else if (override === 'pv') {
|
|
2562
|
-
|
|
2588
|
+
// Gate E: Bei negativem dynamischem Tarif wird Netzbezug bewusst bevorzugt.
|
|
2589
|
+
// Dann heben wir auch den PV-Modus temporär auf, damit die Ladepunkte
|
|
2590
|
+
// wirtschaftlich Netzstrom abnehmen können. Harte Netz-/Phasen-/§14a-
|
|
2591
|
+
// Grenzen bleiben weiter aktiv.
|
|
2592
|
+
eff = tariffGridImportPreferred ? 'normal' : 'pv';
|
|
2563
2593
|
} else if (override === 'minpv') {
|
|
2564
|
-
// "Min+PV"
|
|
2565
|
-
//
|
|
2566
|
-
|
|
2567
|
-
// Therefore we do NOT force it to pure PV mode, even when tariff policy blocks grid-charging.
|
|
2568
|
-
eff = 'minpv';
|
|
2594
|
+
// "Min+PV" hält normalerweise die Mindestladung und begrenzt Mehrleistung auf PV.
|
|
2595
|
+
// Bei Negativpreis wird daraus temporär ein normaler netzfreigegebener Modus.
|
|
2596
|
+
eff = tariffGridImportPreferred ? 'normal' : 'minpv';
|
|
2569
2597
|
} else {
|
|
2570
2598
|
// auto: follow global defaults
|
|
2571
2599
|
eff = (forcePvForW || pvSurplusOnlyCfg) ? 'pv' : 'normal';
|
|
@@ -3008,6 +3036,9 @@ if (components.length) {
|
|
|
3008
3036
|
engine: true,
|
|
3009
3037
|
mode,
|
|
3010
3038
|
pvSurplusOnlyCfg,
|
|
3039
|
+
pvSurplusOnlyCfgBase,
|
|
3040
|
+
tariffNegativeActive,
|
|
3041
|
+
tariffGridImportPreferred,
|
|
3011
3042
|
forcePvSurplusOnly,
|
|
3012
3043
|
gridChargeAllowed,
|
|
3013
3044
|
dischargeAllowed,
|
|
@@ -220,6 +220,12 @@ class CoreLimitsModule extends BaseModule {
|
|
|
220
220
|
native: {},
|
|
221
221
|
});
|
|
222
222
|
|
|
223
|
+
await this.adapter.setObjectNotExistsAsync('ems.budget.tariff', {
|
|
224
|
+
type: 'channel',
|
|
225
|
+
common: { name: 'Budget Gate E - Tarif / Negativpreis' },
|
|
226
|
+
native: {},
|
|
227
|
+
});
|
|
228
|
+
|
|
223
229
|
const mk = async (id, name, type, role, unit = undefined, write = false) => {
|
|
224
230
|
await this.adapter.setObjectNotExistsAsync(id, {
|
|
225
231
|
type: 'state',
|
|
@@ -307,6 +313,24 @@ class CoreLimitsModule extends BaseModule {
|
|
|
307
313
|
await mk('ems.budget.forecast.source', 'PV forecast source', 'string', 'text');
|
|
308
314
|
await mk('ems.budget.forecast.snapshotJson', 'PV forecast gate snapshot (JSON)', 'string', 'json');
|
|
309
315
|
|
|
316
|
+
// Gate E - Tarif / Negativpreis. Advisory + permission gate for price-aware control.
|
|
317
|
+
// It does not bypass hard grid/phase/§14a/peak limits; it only tells apps that
|
|
318
|
+
// grid import is economically preferred during negative effective prices.
|
|
319
|
+
await mk('ems.budget.tariff.active', 'Tariff gate active', 'boolean', 'indicator');
|
|
320
|
+
await mk('ems.budget.tariff.state', 'Tariff state', 'string', 'text');
|
|
321
|
+
await mk('ems.budget.tariff.currentPriceEurKwh', 'Current tariff price (€/kWh)', 'number', 'value');
|
|
322
|
+
await mk('ems.budget.tariff.negativeActive', 'Negative price active', 'boolean', 'indicator');
|
|
323
|
+
await mk('ems.budget.tariff.gridImportPreferred', 'Grid import preferred', 'boolean', 'indicator');
|
|
324
|
+
await mk('ems.budget.tariff.storageGridChargeAllowed', 'Storage grid charge allowed by tariff', 'boolean', 'indicator');
|
|
325
|
+
await mk('ems.budget.tariff.evcsGridChargeAllowed', 'EVCS grid charge allowed by tariff', 'boolean', 'indicator');
|
|
326
|
+
await mk('ems.budget.tariff.dischargeAllowed', 'Discharge allowed by tariff', 'boolean', 'indicator');
|
|
327
|
+
await mk('ems.budget.tariff.pvCurtailRecommended', 'PV curtailment recommended by tariff', 'boolean', 'indicator');
|
|
328
|
+
await mk('ems.budget.tariff.negativeMinPriceEurKwh', 'Minimum negative price in horizon (€/kWh)', 'number', 'value');
|
|
329
|
+
await mk('ems.budget.tariff.nextNegativeFrom', 'Next negative price window from (ISO)', 'string', 'text');
|
|
330
|
+
await mk('ems.budget.tariff.nextNegativeTo', 'Next negative price window to (ISO)', 'string', 'text');
|
|
331
|
+
await mk('ems.budget.tariff.status', 'Tariff gate status', 'string', 'text');
|
|
332
|
+
await mk('ems.budget.tariff.snapshotJson', 'Tariff gate snapshot (JSON)', 'string', 'json');
|
|
333
|
+
|
|
310
334
|
// Per-consumer diagnostics for currently supported app families.
|
|
311
335
|
for (const key of ['evcs', 'thermal', 'heatingRod', 'generic']) {
|
|
312
336
|
await this.adapter.setObjectNotExistsAsync(`ems.budget.consumers.${key}`, {
|
|
@@ -577,6 +601,25 @@ class CoreLimitsModule extends BaseModule {
|
|
|
577
601
|
// provider JSON separately. It does not alter instantaneous PV budget here.
|
|
578
602
|
const forecastGate = this._makeForecastGate(now);
|
|
579
603
|
|
|
604
|
+
// Gate E: tariff/negative-price gate. This is advisory for all apps and
|
|
605
|
+
// permission-like for modules that already consume tariff flags.
|
|
606
|
+
const tSrc = (coreSnapshot && coreSnapshot.tariff && typeof coreSnapshot.tariff === 'object') ? coreSnapshot.tariff : {};
|
|
607
|
+
const tariffGate = {
|
|
608
|
+
active: !!tSrc.active,
|
|
609
|
+
state: String(tSrc.state || ''),
|
|
610
|
+
currentPriceEurKwh: isFiniteNumber(tSrc.currentPriceEurKwh) ? Number(tSrc.currentPriceEurKwh) : null,
|
|
611
|
+
negativeActive: !!tSrc.negativeActive,
|
|
612
|
+
gridImportPreferred: !!tSrc.gridImportPreferred,
|
|
613
|
+
storageGridChargeAllowed: !!tSrc.storageGridChargeAllowed,
|
|
614
|
+
evcsGridChargeAllowed: !!tSrc.evcsGridChargeAllowed,
|
|
615
|
+
dischargeAllowed: tSrc.dischargeAllowed !== false,
|
|
616
|
+
pvCurtailRecommended: !!tSrc.pvCurtailRecommended,
|
|
617
|
+
negativeMinPriceEurKwh: isFiniteNumber(tSrc.negativeMinPriceEurKwh) ? Number(tSrc.negativeMinPriceEurKwh) : null,
|
|
618
|
+
nextNegativeFrom: String(tSrc.nextNegativeFrom || ''),
|
|
619
|
+
nextNegativeTo: String(tSrc.nextNegativeTo || ''),
|
|
620
|
+
status: String(tSrc.status || (tSrc.gridImportPreferred ? 'grid_import_preferred' : (tSrc.active ? 'active' : 'inactive'))),
|
|
621
|
+
};
|
|
622
|
+
|
|
580
623
|
return {
|
|
581
624
|
ts: now,
|
|
582
625
|
active: true,
|
|
@@ -614,6 +657,7 @@ class CoreLimitsModule extends BaseModule {
|
|
|
614
657
|
dischargeW: roundW(storageDischargeW),
|
|
615
658
|
},
|
|
616
659
|
forecast: forecastGate,
|
|
660
|
+
tariff: tariffGate,
|
|
617
661
|
total: {
|
|
618
662
|
effectiveW: Number.isFinite(totalBudgetW) ? roundW(totalBudgetW) : null,
|
|
619
663
|
binding: bindings.join('+'),
|
|
@@ -708,6 +752,16 @@ class CoreLimitsModule extends BaseModule {
|
|
|
708
752
|
const gridChargeAllowed = await readStateBool(this.adapter, 'tarif.netzLadenErlaubt', true);
|
|
709
753
|
const dischargeAllowed = await readStateBool(this.adapter, 'tarif.entladenErlaubt', true);
|
|
710
754
|
|
|
755
|
+
const tariffActive = await readStateBool(this.adapter, 'tarif.aktiv', false);
|
|
756
|
+
const tariffState = await readStateString(this.adapter, 'tarif.state', '');
|
|
757
|
+
const tariffCurrentPrice = await readStateNumber(this.adapter, 'tarif.preisAktuellEurProKwh', null);
|
|
758
|
+
const tariffNegativeActive = await readStateBool(this.adapter, 'tarif.negativpreisAktiv', false);
|
|
759
|
+
const tariffGridImportPreferred = await readStateBool(this.adapter, 'tarif.netzbezugBevorzugt', tariffNegativeActive);
|
|
760
|
+
const tariffNegativeMinPrice = await readStateNumber(this.adapter, 'tarif.negativPreisMinEurProKwh', null);
|
|
761
|
+
const tariffNextNegativeFrom = await readStateString(this.adapter, 'tarif.naechstesNegativVon', '');
|
|
762
|
+
const tariffNextNegativeTo = await readStateString(this.adapter, 'tarif.naechstesNegativBis', '');
|
|
763
|
+
const tariffStatus = await readStateString(this.adapter, 'tarif.negativpreisStatus', '');
|
|
764
|
+
|
|
711
765
|
const p14a = (this.adapter && this.adapter._para14a && typeof this.adapter._para14a === 'object') ? this.adapter._para14a : null;
|
|
712
766
|
|
|
713
767
|
let para14aActive = false;
|
|
@@ -771,6 +825,18 @@ class CoreLimitsModule extends BaseModule {
|
|
|
771
825
|
budgetW: (typeof tariffBudgetW === 'number') ? tariffBudgetW : null,
|
|
772
826
|
gridChargeAllowed: !!gridChargeAllowed,
|
|
773
827
|
dischargeAllowed: !!dischargeAllowed,
|
|
828
|
+
active: !!tariffActive,
|
|
829
|
+
state: tariffState || '',
|
|
830
|
+
currentPriceEurKwh: isFiniteNumber(tariffCurrentPrice) ? Number(tariffCurrentPrice) : null,
|
|
831
|
+
negativeActive: !!tariffNegativeActive,
|
|
832
|
+
gridImportPreferred: !!tariffGridImportPreferred,
|
|
833
|
+
storageGridChargeAllowed: !!(tariffGridImportPreferred && gridChargeAllowed),
|
|
834
|
+
evcsGridChargeAllowed: !!(tariffGridImportPreferred && gridChargeAllowed),
|
|
835
|
+
pvCurtailRecommended: !!tariffGridImportPreferred,
|
|
836
|
+
negativeMinPriceEurKwh: isFiniteNumber(tariffNegativeMinPrice) ? Number(tariffNegativeMinPrice) : null,
|
|
837
|
+
nextNegativeFrom: tariffNextNegativeFrom || '',
|
|
838
|
+
nextNegativeTo: tariffNextNegativeTo || '',
|
|
839
|
+
status: tariffStatus || (tariffGridImportPreferred ? 'active_grid_import_preferred' : (tariffNegativeActive ? 'negative_detected' : 'inactive')),
|
|
774
840
|
},
|
|
775
841
|
para14a: {
|
|
776
842
|
active: !!para14aActive,
|
|
@@ -790,6 +856,7 @@ class CoreLimitsModule extends BaseModule {
|
|
|
790
856
|
this.adapter._emsCaps = snapshot;
|
|
791
857
|
this.adapter._emsBudget = budgetRuntime;
|
|
792
858
|
this.adapter._emsForecastGate = budgetSnapshot && budgetSnapshot.gates ? budgetSnapshot.gates.forecast : null;
|
|
859
|
+
this.adapter._emsTariffGate = budgetSnapshot && budgetSnapshot.gates ? budgetSnapshot.gates.tariff : null;
|
|
793
860
|
} catch {
|
|
794
861
|
// ignore
|
|
795
862
|
}
|
|
@@ -862,6 +929,22 @@ class CoreLimitsModule extends BaseModule {
|
|
|
862
929
|
await this.adapter.setStateAsync('ems.budget.forecast.source', String(fg.source || ''), true);
|
|
863
930
|
await this.adapter.setStateAsync('ems.budget.forecast.snapshotJson', JSON.stringify(fg), true);
|
|
864
931
|
|
|
932
|
+
const tg = (b.gates && b.gates.tariff) ? b.gates.tariff : {};
|
|
933
|
+
await this.adapter.setStateAsync('ems.budget.tariff.active', !!tg.active, true);
|
|
934
|
+
await this.adapter.setStateAsync('ems.budget.tariff.state', String(tg.state || ''), true);
|
|
935
|
+
await this.adapter.setStateAsync('ems.budget.tariff.currentPriceEurKwh', tg.currentPriceEurKwh === null || tg.currentPriceEurKwh === undefined ? null : Number(tg.currentPriceEurKwh), true);
|
|
936
|
+
await this.adapter.setStateAsync('ems.budget.tariff.negativeActive', !!tg.negativeActive, true);
|
|
937
|
+
await this.adapter.setStateAsync('ems.budget.tariff.gridImportPreferred', !!tg.gridImportPreferred, true);
|
|
938
|
+
await this.adapter.setStateAsync('ems.budget.tariff.storageGridChargeAllowed', !!tg.storageGridChargeAllowed, true);
|
|
939
|
+
await this.adapter.setStateAsync('ems.budget.tariff.evcsGridChargeAllowed', !!tg.evcsGridChargeAllowed, true);
|
|
940
|
+
await this.adapter.setStateAsync('ems.budget.tariff.dischargeAllowed', tg.dischargeAllowed !== false, true);
|
|
941
|
+
await this.adapter.setStateAsync('ems.budget.tariff.pvCurtailRecommended', !!tg.pvCurtailRecommended, true);
|
|
942
|
+
await this.adapter.setStateAsync('ems.budget.tariff.negativeMinPriceEurKwh', tg.negativeMinPriceEurKwh === null || tg.negativeMinPriceEurKwh === undefined ? null : Number(tg.negativeMinPriceEurKwh), true);
|
|
943
|
+
await this.adapter.setStateAsync('ems.budget.tariff.nextNegativeFrom', String(tg.nextNegativeFrom || ''), true);
|
|
944
|
+
await this.adapter.setStateAsync('ems.budget.tariff.nextNegativeTo', String(tg.nextNegativeTo || ''), true);
|
|
945
|
+
await this.adapter.setStateAsync('ems.budget.tariff.status', String(tg.status || ''), true);
|
|
946
|
+
await this.adapter.setStateAsync('ems.budget.tariff.snapshotJson', JSON.stringify(tg), true);
|
|
947
|
+
|
|
865
948
|
for (const key of ['evcs', 'thermal', 'heatingRod']) {
|
|
866
949
|
const c = b.consumers[key] || {};
|
|
867
950
|
await this.adapter.setStateAsync(`ems.budget.consumers.${key}.usedW`, roundW(c.usedW), true);
|
|
@@ -883,6 +966,12 @@ class CoreLimitsModule extends BaseModule {
|
|
|
883
966
|
this.adapter.updateValue('ems.budget.forecast.kwhNext6h', Number.isFinite(Number(b.gates.forecast.kwhNext6h)) ? Number(b.gates.forecast.kwhNext6h) : 0, now);
|
|
884
967
|
this.adapter.updateValue('ems.budget.forecast.usable', !!b.gates.forecast.usable, now);
|
|
885
968
|
}
|
|
969
|
+
if (b.gates && b.gates.tariff) {
|
|
970
|
+
this.adapter.updateValue('ems.budget.tariff.negativeActive', !!b.gates.tariff.negativeActive, now);
|
|
971
|
+
this.adapter.updateValue('ems.budget.tariff.gridImportPreferred', !!b.gates.tariff.gridImportPreferred, now);
|
|
972
|
+
this.adapter.updateValue('ems.budget.tariff.currentPriceEurKwh', b.gates.tariff.currentPriceEurKwh, now);
|
|
973
|
+
this.adapter.updateValue('ems.budget.tariff.status', String(b.gates.tariff.status || ''), now);
|
|
974
|
+
}
|
|
886
975
|
}
|
|
887
976
|
} catch {
|
|
888
977
|
// ignore
|
|
@@ -58,6 +58,17 @@ class GridConstraintsModule extends BaseModule {
|
|
|
58
58
|
return Math.min(Math.max(n, minV), maxV);
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
async _isTariffGridImportPreferred() {
|
|
62
|
+
try {
|
|
63
|
+
const tv = (this.adapter && this.adapter._tarifVis) ? this.adapter._tarifVis : null;
|
|
64
|
+
if (tv && (tv.gridImportPreferred || tv.netzbezugBevorzugt || tv.negativeActive)) return true;
|
|
65
|
+
const st = await this.adapter.getStateAsync('tarif.netzbezugBevorzugt');
|
|
66
|
+
return !!(st && st.val);
|
|
67
|
+
} catch {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
61
72
|
async init() {
|
|
62
73
|
if (!this._isEnabled()) return;
|
|
63
74
|
|
|
@@ -263,7 +274,10 @@ class GridConstraintsModule extends BaseModule {
|
|
|
263
274
|
const enabled = !!cfg.zeroExportEnabled;
|
|
264
275
|
const inv = this._normalizeInvList(cfg.pvCurtailInvertersZero);
|
|
265
276
|
|
|
266
|
-
const
|
|
277
|
+
const tariffGridImportPreferred = await this._isTariffGridImportPreferred();
|
|
278
|
+
const baseBiasW = Math.max(0, this._num(cfg.zeroExportBiasW, 80));
|
|
279
|
+
const negativeBiasW = tariffGridImportPreferred ? Math.max(0, this._num(cfg.zeroExportNegativePriceImportBiasW, 1000)) : 0;
|
|
280
|
+
const biasW = tariffGridImportPreferred ? Math.max(baseBiasW, negativeBiasW) : baseBiasW;
|
|
267
281
|
const deadbandW = Math.max(0, this._num(cfg.zeroExportDeadbandW, 50));
|
|
268
282
|
|
|
269
283
|
await this.adapter.setStateAsync('gridConstraints.zeroExport.enabled', enabled, true);
|
|
@@ -356,7 +370,7 @@ class GridConstraintsModule extends BaseModule {
|
|
|
356
370
|
applied = applied || (ok1 === true || ok1 === null) || (ok2 === true || ok2 === null) || (ok3 === true || ok3 === null);
|
|
357
371
|
}
|
|
358
372
|
|
|
359
|
-
await this.adapter.setStateAsync('gridConstraints.zeroExport.action', fastTrip ? 'group_fast' : 'group', true);
|
|
373
|
+
await this.adapter.setStateAsync('gridConstraints.zeroExport.action', tariffGridImportPreferred ? (fastTrip ? 'tariff_negative_group_fast' : 'tariff_negative_group') : (fastTrip ? 'group_fast' : 'group'), true);
|
|
360
374
|
await this.adapter.setStateAsync('gridConstraints.pvCurtail.applied', applied, true);
|
|
361
375
|
await this.adapter.setStateAsync('gridConstraints.pvCurtail.setpointW', Math.round(next), true);
|
|
362
376
|
await this.adapter.setStateAsync('gridConstraints.pvCurtail.setpointPct', Math.round(pct * 10) / 10, true);
|
|
@@ -461,7 +475,10 @@ class GridConstraintsModule extends BaseModule {
|
|
|
461
475
|
async _tickZeroExport(nowMs, gridW, cfg, gridStale) {
|
|
462
476
|
const enabled = !!cfg.zeroExportEnabled;
|
|
463
477
|
|
|
464
|
-
const
|
|
478
|
+
const tariffGridImportPreferred = await this._isTariffGridImportPreferred();
|
|
479
|
+
const baseBiasW = Math.max(0, this._num(cfg.zeroExportBiasW, 80));
|
|
480
|
+
const negativeBiasW = tariffGridImportPreferred ? Math.max(0, this._num(cfg.zeroExportNegativePriceImportBiasW, 1000)) : 0;
|
|
481
|
+
const biasW = tariffGridImportPreferred ? Math.max(baseBiasW, negativeBiasW) : baseBiasW;
|
|
465
482
|
const deadbandW = Math.max(0, this._num(cfg.zeroExportDeadbandW, 50));
|
|
466
483
|
|
|
467
484
|
await this.adapter.setStateAsync('gridConstraints.zeroExport.enabled', enabled, true);
|
|
@@ -542,7 +559,7 @@ class GridConstraintsModule extends BaseModule {
|
|
|
542
559
|
|
|
543
560
|
const ok = await this.dp.writeNumber('pv.limitW', next, false);
|
|
544
561
|
|
|
545
|
-
await this.adapter.setStateAsync('gridConstraints.zeroExport.action', fastTrip ? 'pvLimitW_fast' : 'pvLimitW', true);
|
|
562
|
+
await this.adapter.setStateAsync('gridConstraints.zeroExport.action', tariffGridImportPreferred ? (fastTrip ? 'tariff_negative_pvLimitW_fast' : 'tariff_negative_pvLimitW') : (fastTrip ? 'pvLimitW_fast' : 'pvLimitW'), true);
|
|
546
563
|
await this.adapter.setStateAsync('gridConstraints.pvCurtail.applied', ok === true || ok === null, true);
|
|
547
564
|
await this.adapter.setStateAsync('gridConstraints.pvCurtail.setpointW', Math.round(next), true);
|
|
548
565
|
await this.adapter.setStateAsync('gridConstraints.pvCurtail.setpointPct', 0, true);
|
|
@@ -579,7 +596,7 @@ class GridConstraintsModule extends BaseModule {
|
|
|
579
596
|
|
|
580
597
|
const ok = await this.dp.writeNumber('pv.limitPct', next, false);
|
|
581
598
|
|
|
582
|
-
await this.adapter.setStateAsync('gridConstraints.zeroExport.action', fastTrip ? 'pvLimitPct_fast' : 'pvLimitPct', true);
|
|
599
|
+
await this.adapter.setStateAsync('gridConstraints.zeroExport.action', tariffGridImportPreferred ? (fastTrip ? 'tariff_negative_pvLimitPct_fast' : 'tariff_negative_pvLimitPct') : (fastTrip ? 'pvLimitPct_fast' : 'pvLimitPct'), true);
|
|
583
600
|
await this.adapter.setStateAsync('gridConstraints.pvCurtail.applied', ok === true || ok === null, true);
|
|
584
601
|
await this.adapter.setStateAsync('gridConstraints.pvCurtail.setpointW', 0, true);
|
|
585
602
|
await this.adapter.setStateAsync('gridConstraints.pvCurtail.setpointPct', Math.round(next * 10) / 10, true);
|
|
@@ -775,6 +775,16 @@ class HeatingRodControlModule extends BaseModule {
|
|
|
775
775
|
pvBudgetSource = 'ems.budget.remainingPvW+ownAutoLoad';
|
|
776
776
|
pvBudgetFromCentral = true;
|
|
777
777
|
}
|
|
778
|
+
|
|
779
|
+
const tariffGate = snap.gates && snap.gates.tariff ? snap.gates.tariff : null;
|
|
780
|
+
const tariffImportPreferred = !!(tariffGate && tariffGate.gridImportPreferred);
|
|
781
|
+
if (tariffImportPreferred && Number.isFinite(remTotal) && remTotal >= 0) {
|
|
782
|
+
// Gate E: Bei Negativpreis darf der Heizstab als aktivierter flexibler
|
|
783
|
+
// Verbraucher Netzbudget nutzen. Gate A/A2/Peak bleiben über remTotal aktiv.
|
|
784
|
+
pvBudgetGateW = Math.max(0, remTotal + currentW - gateCfg.budgetSafetyReserveW);
|
|
785
|
+
pvBudgetSource = 'ems.budget.tariffNegative.remainingTotalW+ownAutoLoad';
|
|
786
|
+
pvBudgetFromCentral = true;
|
|
787
|
+
}
|
|
778
788
|
}
|
|
779
789
|
} catch (_e) {
|
|
780
790
|
// legacy NVP/CM fallback remains active
|
|
@@ -804,6 +814,7 @@ class HeatingRodControlModule extends BaseModule {
|
|
|
804
814
|
budgetGateEffectiveW: Math.max(0, effectiveGateW),
|
|
805
815
|
budgetGateSource: source,
|
|
806
816
|
pvBudgetFromCentral: !!pvBudgetFromCentral,
|
|
817
|
+
tariffGridImportPreferred: String(pvBudgetSource || '').includes('tariffNegative'),
|
|
807
818
|
cmActive,
|
|
808
819
|
cmStaleMeter: !!cmStaleMeter,
|
|
809
820
|
cmStaleBudget: !!cmStaleBudget,
|
|
@@ -840,7 +851,8 @@ class HeatingRodControlModule extends BaseModule {
|
|
|
840
851
|
_updateBudgetGateProtection(pvBase, now) {
|
|
841
852
|
const cfg = (pvBase && pvBase.gateCfg) ? pvBase.gateCfg : this._getBudgetGateCfg();
|
|
842
853
|
const st = this._budgetProtect || { importSinceMs: 0, dischargeSinceMs: 0 };
|
|
843
|
-
const
|
|
854
|
+
const tariffImportPreferred = !!(pvBase && pvBase.tariffGridImportPreferred);
|
|
855
|
+
const importActive = !!(!tariffImportPreferred && pvBase && pvBase.gridKnown && num(pvBase.importW, 0) > cfg.maxGridImportW);
|
|
844
856
|
const dischargeActive = !!(pvBase && num(pvBase.storageDischargeW, 0) > cfg.storageDischargeToleranceW);
|
|
845
857
|
const hardImport = !!(pvBase && pvBase.gridKnown && num(pvBase.importW, 0) > cfg.hardGridImportW);
|
|
846
858
|
const hardDischarge = !!(pvBase && num(pvBase.storageDischargeW, 0) > cfg.hardStorageDischargeW);
|
|
@@ -2072,7 +2084,7 @@ class HeatingRodControlModule extends BaseModule {
|
|
|
2072
2084
|
// Global PV-Auto minimum: this is now only a start/step-up gate.
|
|
2073
2085
|
// It must not be a hard OFF, because small cloud/PV transients would otherwise
|
|
2074
2086
|
// kill a stable stage and external KNX/manual switching would feel broken.
|
|
2075
|
-
const pvMinBlocksStepUp = !!(pvAutomationActive && !pvAutomationAllowedByMin);
|
|
2087
|
+
const pvMinBlocksStepUp = !!(pvAutomationActive && !pvBase.tariffGridImportPreferred && !pvAutomationAllowedByMin);
|
|
2076
2088
|
|
|
2077
2089
|
if (d.wiredStages < 1) {
|
|
2078
2090
|
const usedW = (typeof measuredW === 'number' && Number.isFinite(measuredW) && measuredW > 0)
|
|
@@ -2179,6 +2191,7 @@ class HeatingRodControlModule extends BaseModule {
|
|
|
2179
2191
|
const rt = this.adapter && this.adapter._emsBudget;
|
|
2180
2192
|
if (rt && typeof rt.reserve === 'function') {
|
|
2181
2193
|
const used = Math.max(0, Math.round(budgetUsedW || 0));
|
|
2194
|
+
const tariffImportPreferred = !!(pvBase && pvBase.tariffGridImportPreferred);
|
|
2182
2195
|
rt.reserve({
|
|
2183
2196
|
key: 'heatingRod',
|
|
2184
2197
|
app: 'heatingRodControl',
|
|
@@ -2186,10 +2199,10 @@ class HeatingRodControlModule extends BaseModule {
|
|
|
2186
2199
|
priority: 300,
|
|
2187
2200
|
requestedW: used,
|
|
2188
2201
|
reserveW: used,
|
|
2189
|
-
pvReserveW: used,
|
|
2202
|
+
pvReserveW: tariffImportPreferred ? 0 : used,
|
|
2190
2203
|
actualW: Math.max(0, Math.round(currentHeatingRodW || appliedTotalW || used || 0)),
|
|
2191
|
-
pvOnly:
|
|
2192
|
-
mode: 'pvAuto',
|
|
2204
|
+
pvOnly: !tariffImportPreferred,
|
|
2205
|
+
mode: tariffImportPreferred ? 'tariffNegative' : 'pvAuto',
|
|
2193
2206
|
});
|
|
2194
2207
|
}
|
|
2195
2208
|
} catch (_e) {
|
|
@@ -2266,6 +2279,7 @@ class HeatingRodControlModule extends BaseModule {
|
|
|
2266
2279
|
effectiveW: Math.round(num(pvBase.budgetGateEffectiveW, 0)),
|
|
2267
2280
|
source: pvBase.budgetGateSource,
|
|
2268
2281
|
pvBudgetFromCentral: !!pvBase.pvBudgetFromCentral,
|
|
2282
|
+
tariffGridImportPreferred: !!pvBase.tariffGridImportPreferred,
|
|
2269
2283
|
cmActive: pvBase.cmActive,
|
|
2270
2284
|
cmStaleMeter: !!pvBase.cmStaleMeter,
|
|
2271
2285
|
cmStaleBudget: !!pvBase.cmStaleBudget,
|
|
@@ -1015,6 +1015,7 @@ if (typeof soc === 'number') {
|
|
|
1015
1015
|
if (targetW === 0) {
|
|
1016
1016
|
const tv = (this.adapter && this.adapter._tarifVis) ? this.adapter._tarifVis : null;
|
|
1017
1017
|
const tvAktiv = !!(tv && tv.aktiv);
|
|
1018
|
+
const tariffNegativeImportPreferred = !!(tv && (tv.negativeActive || tv.gridImportPreferred || tv.netzbezugBevorzugt));
|
|
1018
1019
|
tarifState = (tvAktiv && typeof tv.state === 'string') ? tv.state : null;
|
|
1019
1020
|
|
|
1020
1021
|
if (tvAktiv) {
|
|
@@ -1065,7 +1066,7 @@ if (typeof soc === 'number') {
|
|
|
1065
1066
|
// Nicht gegen Einspeisung "anladen" – PV-Überschuss wird unten behandelt.
|
|
1066
1067
|
// Hinweis: Dadurch wird bei Einspeisung kein zusätzliches Netzladen erzwungen.
|
|
1067
1068
|
// (Bewusstes Design: PV-Überschuss-Laden übernimmt dann die Regelung.)
|
|
1068
|
-
if (nvpNowW < 0) {
|
|
1069
|
+
if (nvpNowW < 0 && !tariffNegativeImportPreferred) {
|
|
1069
1070
|
chargeW = 0;
|
|
1070
1071
|
}
|
|
1071
1072
|
|
|
@@ -1089,7 +1090,7 @@ if (typeof soc === 'number') {
|
|
|
1089
1090
|
let pvDebug = null;
|
|
1090
1091
|
try {
|
|
1091
1092
|
const pf = (this.adapter && this.adapter._pvForecast) ? this.adapter._pvForecast : null;
|
|
1092
|
-
const pvReserveEnabled = (cfg.tariffPvReserveEnabled !== false); // default: ON
|
|
1093
|
+
const pvReserveEnabled = (cfg.tariffPvReserveEnabled !== false) && !tariffNegativeImportPreferred; // default: ON, bei Negativpreis bewusst aus
|
|
1093
1094
|
if (pvReserveEnabled && pf && pf.valid && Array.isArray(pf.curve) && pf.curve.length) {
|
|
1094
1095
|
// Bei sehr alten Forecasts lieber keine PV‑Reserve erzwingen.
|
|
1095
1096
|
const maxAgeMs = 24 * 3600000;
|
|
@@ -1347,6 +1348,8 @@ if (typeof soc === 'number') {
|
|
|
1347
1348
|
targetW = -Math.max(0, chargeW);
|
|
1348
1349
|
if (pvBlockGridCharge) {
|
|
1349
1350
|
reason = pvBlockReason || 'Tarif: günstig – PV Forecast -> Netzladen gesperrt';
|
|
1351
|
+
} else if (tariffNegativeImportPreferred) {
|
|
1352
|
+
reason = (targetW === 0) ? 'Tarif: Negativpreis – Netzladen nicht möglich' : 'Tarif: Negativpreis – Netzladen bevorzugt';
|
|
1350
1353
|
} else {
|
|
1351
1354
|
reason = (targetW === 0) ? 'Tarif: günstig – Netzladen nicht möglich' : 'Tarif: günstig – Netzladen';
|
|
1352
1355
|
}
|
package/ems/modules/tarif-vis.js
CHANGED
|
@@ -78,6 +78,15 @@ class TarifVisModule extends BaseModule {
|
|
|
78
78
|
await mk('tarif.statusText', 'Tarif Status (VIS)', 'string', 'text');
|
|
79
79
|
await mk('tarif.netFeeEnabled', 'Zeitvariables Netzentgelt aktiv (VIS)', 'boolean', 'indicator');
|
|
80
80
|
await mk('tarif.netFeeMode', 'Netzentgelt Modus (NT/Standard/HT)', 'string', 'text');
|
|
81
|
+
|
|
82
|
+
// Gate E – Negativpreis / Tarif-Gewinnoptimierung
|
|
83
|
+
await mk('tarif.negativpreisAktiv', 'Negativpreis aktiv', 'boolean', 'indicator');
|
|
84
|
+
await mk('tarif.netzbezugBevorzugt', 'Netzbezug bevorzugt bei Negativpreis', 'boolean', 'indicator');
|
|
85
|
+
await mk('tarif.negativPreisAktuellEurProKwh', 'Negativpreis aktuell (€/kWh)', 'number', 'value');
|
|
86
|
+
await mk('tarif.negativPreisMinEurProKwh', 'Negativpreis Minimum (€/kWh, Horizon)', 'number', 'value');
|
|
87
|
+
await mk('tarif.naechstesNegativVon', 'Nächstes Negativpreis-Fenster ab (ISO)', 'string', 'text');
|
|
88
|
+
await mk('tarif.naechstesNegativBis', 'Nächstes Negativpreis-Fenster bis (ISO)', 'string', 'text');
|
|
89
|
+
await mk('tarif.negativpreisStatus', 'Negativpreis Status', 'string', 'text');
|
|
81
90
|
// VIS-Settings als Datenpunkte registrieren (nur wenn dp-Registry vorhanden ist)
|
|
82
91
|
if (this.dp && typeof this.dp.upsert === 'function') {
|
|
83
92
|
const visInst = this._getVisInstance();
|
|
@@ -710,6 +719,17 @@ class TarifVisModule extends BaseModule {
|
|
|
710
719
|
let nextCheapToIso = null;
|
|
711
720
|
let horizonCurve = null;
|
|
712
721
|
|
|
722
|
+
// Gate E – Negativpreis: Wenn der effektive dynamische Preis < 0 ist,
|
|
723
|
+
// soll Netzbezug bewusst bevorzugt werden. Today+Tomorrow werden als
|
|
724
|
+
// Planungs-/Diagnosehorizont ausgewertet, der aktuelle Preis bleibt
|
|
725
|
+
// aber die harte Live-Freigabe.
|
|
726
|
+
let negativeActive = false;
|
|
727
|
+
let negativeWindowNow = false;
|
|
728
|
+
let negativeCurrentPrice = null;
|
|
729
|
+
let negativeMinPrice = null;
|
|
730
|
+
let nextNegativeFromIso = null;
|
|
731
|
+
let nextNegativeToIso = null;
|
|
732
|
+
|
|
713
733
|
if (aktivEff && modusInt === 2) {
|
|
714
734
|
const rawToday = (this.dp && typeof this.dp.getEntry === 'function' && this.dp.getEntry('tarif.pricesTodayJson'))
|
|
715
735
|
? this.dp.getRaw('tarif.pricesTodayJson')
|
|
@@ -736,6 +756,50 @@ class TarifVisModule extends BaseModule {
|
|
|
736
756
|
}
|
|
737
757
|
}
|
|
738
758
|
|
|
759
|
+
// Negativpreis-Fenster aus Today+Tomorrow-Forecast ermitteln.
|
|
760
|
+
if (aktivEff) {
|
|
761
|
+
const negEps = 1e-9;
|
|
762
|
+
|
|
763
|
+
if (preisAktuellOk && preisAktuell < -negEps) {
|
|
764
|
+
negativeActive = true;
|
|
765
|
+
negativeCurrentPrice = preisAktuell;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
if (Array.isArray(horizonCurve) && horizonCurve.length > 0) {
|
|
769
|
+
const negAll = horizonCurve
|
|
770
|
+
.filter(x => x && Number.isFinite(x.startMs) && Number.isFinite(x.endMs) && Number.isFinite(x.priceEurKwh))
|
|
771
|
+
.filter(x => x.endMs > nowMs && x.priceEurKwh < -negEps)
|
|
772
|
+
.sort((a, b) => a.startMs - b.startMs);
|
|
773
|
+
|
|
774
|
+
if (negAll.length > 0) {
|
|
775
|
+
negativeMinPrice = Math.min(...negAll.map(x => x.priceEurKwh));
|
|
776
|
+
|
|
777
|
+
const activeSeg = negAll.find(x => x.startMs <= nowMs && x.endMs > nowMs);
|
|
778
|
+
if (activeSeg) {
|
|
779
|
+
negativeActive = true;
|
|
780
|
+
negativeWindowNow = true;
|
|
781
|
+
negativeCurrentPrice = activeSeg.priceEurKwh;
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
const first = negAll[0];
|
|
785
|
+
let winStart = first.startMs;
|
|
786
|
+
let winEnd = first.endMs;
|
|
787
|
+
for (let i = 1; i < negAll.length; i++) {
|
|
788
|
+
const it = negAll[i];
|
|
789
|
+
if (it.startMs <= (winEnd + 1000)) {
|
|
790
|
+
winEnd = Math.max(winEnd, it.endMs);
|
|
791
|
+
} else {
|
|
792
|
+
break;
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
nextNegativeFromIso = new Date(winStart).toISOString();
|
|
796
|
+
nextNegativeToIso = new Date(winEnd).toISOString();
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
const gridImportPreferred = !!(aktivEff && negativeActive);
|
|
802
|
+
|
|
739
803
|
const preisDurchschnittEff = preisDurchschnittOk ? preisDurchschnitt
|
|
740
804
|
: (Number.isFinite(preisDurchschnittCalc) ? preisDurchschnittCalc : null);
|
|
741
805
|
const preisDurchschnittEffOk = (typeof preisDurchschnittEff === 'number' && Number.isFinite(preisDurchschnittEff));
|
|
@@ -867,10 +931,20 @@ class TarifVisModule extends BaseModule {
|
|
|
867
931
|
tarifState = next;
|
|
868
932
|
}
|
|
869
933
|
|
|
934
|
+
// Negativpreis ist ein Sonderfall von "günstig", aber mit stärkerer
|
|
935
|
+
// Wirkung: Netzbezug wird bevorzugt, Speicherentladung wird gesperrt und
|
|
936
|
+
// die Speicher-Netzlade-Zeitfenster/PV-Reserve dürfen nicht blockieren.
|
|
937
|
+
if (gridImportPreferred) {
|
|
938
|
+
tarifState = 'guenstig';
|
|
939
|
+
this._tarifLastState = 'guenstig';
|
|
940
|
+
}
|
|
941
|
+
|
|
870
942
|
// --- Priorität ---
|
|
871
943
|
// 1 = Speicher | 2 = Auto | 3 = Ladestation
|
|
872
944
|
const allowStorageCheap = (prioritaet === 1 || prioritaet === 2);
|
|
873
945
|
const allowEvcsCheap = (prioritaet === 2 || prioritaet === 3);
|
|
946
|
+
const allowStorageCheapEff = gridImportPreferred ? true : allowStorageCheap;
|
|
947
|
+
const allowEvcsCheapEff = gridImportPreferred ? true : allowEvcsCheap;
|
|
874
948
|
|
|
875
949
|
// Speicher-SoC (optional)
|
|
876
950
|
//
|
|
@@ -956,15 +1030,16 @@ class TarifVisModule extends BaseModule {
|
|
|
956
1030
|
// Q1/Q4: 18:00–06:00 | Q2/Q3: 21:00–06:00
|
|
957
1031
|
// Ausnahme: Wenn zeitvariables Netzentgelt im NT ist, darf geladen werden.
|
|
958
1032
|
const storageTimeOk = !!storageChargeWindowOk;
|
|
959
|
-
const cheapWanted = (tarifState === 'guenstig' &&
|
|
1033
|
+
const cheapWanted = ((tarifState === 'guenstig' && allowStorageCheapEff) || gridImportPreferred);
|
|
960
1034
|
// Im Netzentgelt‑NT gilt das Zeitfenster als "kosten-günstig" genug,
|
|
961
1035
|
// damit Speicher‑Netzladung überhaupt erlaubt ist (abhängig von Priorität + SoC‑Latch).
|
|
962
1036
|
// Dadurch funktioniert NT auch dann, wenn der Stromtarif selbst nur neutral/teuer ist.
|
|
963
|
-
const cheapOrNtWanted = (cheapWanted || (netFeeIsNt &&
|
|
1037
|
+
const cheapOrNtWanted = (cheapWanted || (netFeeIsNt && allowStorageCheapEff));
|
|
964
1038
|
// Speicher darf laden, wenn (Tarif günstig ODER Netzentgelt‑NT) UND Zeitfenster ok.
|
|
965
|
-
//
|
|
966
|
-
//
|
|
967
|
-
|
|
1039
|
+
// Ausnahmen zur Zeitfenster‑Policy:
|
|
1040
|
+
// - Netzentgelt im NT
|
|
1041
|
+
// - Negativpreis: Netzbezug ist wirtschaftlich erwünscht und darf tagsüber laden.
|
|
1042
|
+
const chargeAllowed = (cheapOrNtWanted && (storageTimeOk || netFeeIsNt || gridImportPreferred));
|
|
968
1043
|
|
|
969
1044
|
if (netFeeIsHt) {
|
|
970
1045
|
// In HT: Speicher soll NICHT durch Tarif entladen/geladen werden → Eigenverbrauch
|
|
@@ -1032,10 +1107,12 @@ class TarifVisModule extends BaseModule {
|
|
|
1032
1107
|
|
|
1033
1108
|
// 1) Basis: dynamischer Tarif (falls aktiv)
|
|
1034
1109
|
if (aktivEff) {
|
|
1035
|
-
if (
|
|
1110
|
+
if (gridImportPreferred) {
|
|
1111
|
+
gridChargeAllowed = true;
|
|
1112
|
+
} else if (tarifState === 'teuer') {
|
|
1036
1113
|
gridChargeAllowed = false;
|
|
1037
1114
|
} else if (tarifState === 'guenstig') {
|
|
1038
|
-
gridChargeAllowed =
|
|
1115
|
+
gridChargeAllowed = allowEvcsCheapEff ? true : false;
|
|
1039
1116
|
} else {
|
|
1040
1117
|
gridChargeAllowed = true;
|
|
1041
1118
|
}
|
|
@@ -1054,6 +1131,13 @@ class TarifVisModule extends BaseModule {
|
|
|
1054
1131
|
// Standard (ST): kein Override
|
|
1055
1132
|
}
|
|
1056
1133
|
|
|
1134
|
+
// Negativpreis hat als wirtschaftliches Signal Vorrang vor PV-only-/HT-Sperren:
|
|
1135
|
+
// Netzbezug erlauben, Speicherentladung vermeiden. Harte Netz-/§14a-/Peak-Grenzen
|
|
1136
|
+
// werden später weiterhin durch die Gates begrenzt.
|
|
1137
|
+
if (gridImportPreferred) {
|
|
1138
|
+
gridChargeAllowed = true;
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1057
1141
|
// Entladen-Freigabe (für Speicher-/Assist-Logik):
|
|
1058
1142
|
//
|
|
1059
1143
|
// Ziel (Bugfix + bessere Praxis):
|
|
@@ -1075,7 +1159,10 @@ class TarifVisModule extends BaseModule {
|
|
|
1075
1159
|
const storageChargingPlanned = Number.isFinite(speicherSollW) && speicherSollW < 0;
|
|
1076
1160
|
const forceSelfConsumption = !!storageChargeBlockedByTime || netFeeIsHt;
|
|
1077
1161
|
|
|
1078
|
-
if (
|
|
1162
|
+
if (gridImportPreferred) {
|
|
1163
|
+
// Bei Negativpreis soll der Speicher nicht gegen den gewünschten Netzbezug entladen.
|
|
1164
|
+
dischargeAllowed = false;
|
|
1165
|
+
} else if (forceSelfConsumption) {
|
|
1079
1166
|
// Eigenverbrauch-Modus (tagsüber Policy oder HT): Entladen erlaubt.
|
|
1080
1167
|
dischargeAllowed = true;
|
|
1081
1168
|
} else if (netFeeIsNt) {
|
|
@@ -1097,7 +1184,7 @@ class TarifVisModule extends BaseModule {
|
|
|
1097
1184
|
|
|
1098
1185
|
// Ladepark-Limit: Standard = baseW; Reservierung wenn Speicher im Tarif-Fenster lädt
|
|
1099
1186
|
let limitW = baseW;
|
|
1100
|
-
if (((aktivEff && (tarifState === 'guenstig')) || (netFeeEff && netFeeMode === 'NT')) && speicherSollW < 0 && baseW > 0) {
|
|
1187
|
+
if (!gridImportPreferred && ((aktivEff && (tarifState === 'guenstig')) || (netFeeEff && netFeeMode === 'NT')) && speicherSollW < 0 && baseW > 0) {
|
|
1101
1188
|
const reserveW = Math.max(0, -speicherSollW);
|
|
1102
1189
|
const storageShare = (prioritaet === 1) ? 1.0 : (prioritaet === 3) ? 0.0 : 0.5;
|
|
1103
1190
|
limitW = Math.max(0, Math.round(baseW - (reserveW * storageShare)));
|
|
@@ -1131,6 +1218,14 @@ class TarifVisModule extends BaseModule {
|
|
|
1131
1218
|
await this._setIfChanged('tarif.netFeeEnabled', netFeeEff);
|
|
1132
1219
|
await this._setIfChanged('tarif.netFeeMode', netFeeMode);
|
|
1133
1220
|
|
|
1221
|
+
await this._setIfChanged('tarif.negativpreisAktiv', !!negativeActive);
|
|
1222
|
+
await this._setIfChanged('tarif.netzbezugBevorzugt', !!gridImportPreferred);
|
|
1223
|
+
await this._setIfChanged('tarif.negativPreisAktuellEurProKwh', (negativeActive && typeof negativeCurrentPrice === 'number' && Number.isFinite(negativeCurrentPrice)) ? negativeCurrentPrice : null);
|
|
1224
|
+
await this._setIfChanged('tarif.negativPreisMinEurProKwh', (typeof negativeMinPrice === 'number' && Number.isFinite(negativeMinPrice)) ? negativeMinPrice : null);
|
|
1225
|
+
await this._setIfChanged('tarif.naechstesNegativVon', nextNegativeFromIso || null);
|
|
1226
|
+
await this._setIfChanged('tarif.naechstesNegativBis', nextNegativeToIso || null);
|
|
1227
|
+
await this._setIfChanged('tarif.negativpreisStatus', gridImportPreferred ? 'active_grid_import_preferred' : (nextNegativeFromIso ? 'scheduled' : 'inactive'));
|
|
1228
|
+
|
|
1134
1229
|
|
|
1135
1230
|
// Kurz-Status für die VIS (Live-Ansicht)
|
|
1136
1231
|
// Ziel: Kunde sieht sofort, ob Tarif gerade Laden/Entladen triggert.
|
|
@@ -1170,7 +1265,15 @@ if (aktivEff || netFeeActive) {
|
|
|
1170
1265
|
// - Standard (ST): keine Sperre/Erzwingung → dynamischer Tarif wie bisher
|
|
1171
1266
|
const base = netFeeActive ? `Netzentgelt ${netFeeMode} | ${baseTarif}` : baseTarif;
|
|
1172
1267
|
|
|
1173
|
-
if (
|
|
1268
|
+
if (gridImportPreferred) {
|
|
1269
|
+
const parts = [];
|
|
1270
|
+
parts.push('Negativpreis aktiv');
|
|
1271
|
+
if (storageCharging) parts.push('Speicher Netzladen');
|
|
1272
|
+
else if (storageFullHold) parts.push('Speicher voll (ruht)');
|
|
1273
|
+
parts.push('EVCS/Verbraucher Netzbezug freigegeben');
|
|
1274
|
+
parts.push('Speicherentladung gesperrt');
|
|
1275
|
+
statusText = `${base}: ${parts.join(' + ')}`;
|
|
1276
|
+
} else if (netFeeOverlayUi) {
|
|
1174
1277
|
if (netFeeMode === 'NT') {
|
|
1175
1278
|
const parts = [];
|
|
1176
1279
|
if (storageCharging) parts.push('Speicher lädt');
|
|
@@ -1247,6 +1350,14 @@ await this._setIfChanged('tarif.statusText', statusText);
|
|
|
1247
1350
|
preisSchwelleGuensig: (typeof preisSchwelleGuensig === 'number' && Number.isFinite(preisSchwelleGuensig)) ? preisSchwelleGuensig : null,
|
|
1248
1351
|
nextCheapFromIso: nextCheapFromIso || null,
|
|
1249
1352
|
nextCheapToIso: nextCheapToIso || null,
|
|
1353
|
+
negativeActive: !!negativeActive,
|
|
1354
|
+
negativeWindowNow: !!negativeWindowNow,
|
|
1355
|
+
gridImportPreferred: !!gridImportPreferred,
|
|
1356
|
+
netzbezugBevorzugt: !!gridImportPreferred,
|
|
1357
|
+
negativeCurrentPrice: (negativeActive && typeof negativeCurrentPrice === 'number' && Number.isFinite(negativeCurrentPrice)) ? negativeCurrentPrice : null,
|
|
1358
|
+
negativeMinPrice: (typeof negativeMinPrice === 'number' && Number.isFinite(negativeMinPrice)) ? negativeMinPrice : null,
|
|
1359
|
+
nextNegativeFromIso: nextNegativeFromIso || null,
|
|
1360
|
+
nextNegativeToIso: nextNegativeToIso || null,
|
|
1250
1361
|
autoBandEur: (typeof autoBandEur === 'number' && Number.isFinite(autoBandEur)) ? autoBandEur : null,
|
|
1251
1362
|
horizonHours: (typeof horizonHours === 'number' && Number.isFinite(horizonHours)) ? horizonHours : null,
|
|
1252
1363
|
preisRef: (preisRef !== null && preisRef !== undefined && Number.isFinite(preisRef)) ? preisRef : null,
|
|
@@ -476,14 +476,20 @@ const mk = async (id, name, type, role, unit = undefined) => {
|
|
|
476
476
|
if (snap && age <= staleMs) {
|
|
477
477
|
const pvTotalW = snap.gates && snap.gates.pv ? Number(snap.gates.pv.effectiveW) : 0;
|
|
478
478
|
const remainingPvW = Number(snap.remainingPvW);
|
|
479
|
+
const tariffGate = snap.gates && snap.gates.tariff ? snap.gates.tariff : null;
|
|
480
|
+
const tariffImportPreferred = !!(tariffGate && tariffGate.gridImportPreferred);
|
|
481
|
+
const remainingTotalW = Number(snap.remainingTotalW);
|
|
479
482
|
if (Number.isFinite(remainingPvW) && remainingPvW >= 0) {
|
|
480
483
|
const evcs = snap.consumers && snap.consumers.evcs ? snap.consumers.evcs : null;
|
|
481
484
|
const evcsUsedW = evcs && Number.isFinite(Number(evcs.reserveW)) ? Math.max(0, Number(evcs.reserveW)) : 0;
|
|
485
|
+
const availableByTariffW = (tariffImportPreferred && Number.isFinite(remainingTotalW) && remainingTotalW >= 0)
|
|
486
|
+
? Math.max(0, remainingTotalW)
|
|
487
|
+
: null;
|
|
482
488
|
return {
|
|
483
489
|
pvCapW: Math.max(0, Number.isFinite(pvTotalW) ? pvTotalW : remainingPvW),
|
|
484
490
|
evcsUsedW,
|
|
485
|
-
availableW: Math.max(0, remainingPvW),
|
|
486
|
-
source: 'ems.budget',
|
|
491
|
+
availableW: availableByTariffW !== null ? availableByTariffW : Math.max(0, remainingPvW),
|
|
492
|
+
source: availableByTariffW !== null ? 'ems.budget.tariffNegative' : 'ems.budget',
|
|
487
493
|
};
|
|
488
494
|
}
|
|
489
495
|
}
|
|
@@ -902,6 +908,7 @@ const mk = async (id, name, type, role, unit = undefined) => {
|
|
|
902
908
|
const rt = this.adapter && this.adapter._emsBudget;
|
|
903
909
|
if (rt && typeof rt.reserve === 'function') {
|
|
904
910
|
const used = Math.max(0, Math.round(budgetUsedW || 0));
|
|
911
|
+
const tariffImportPreferred = String(pv.source || '').includes('tariffNegative');
|
|
905
912
|
rt.reserve({
|
|
906
913
|
key: 'thermal',
|
|
907
914
|
app: 'thermalControl',
|
|
@@ -909,9 +916,9 @@ const mk = async (id, name, type, role, unit = undefined) => {
|
|
|
909
916
|
priority: 200,
|
|
910
917
|
requestedW: used,
|
|
911
918
|
reserveW: used,
|
|
912
|
-
pvReserveW: used,
|
|
913
|
-
pvOnly:
|
|
914
|
-
mode: 'pvAuto',
|
|
919
|
+
pvReserveW: tariffImportPreferred ? 0 : used,
|
|
920
|
+
pvOnly: !tariffImportPreferred,
|
|
921
|
+
mode: tariffImportPreferred ? 'tariffNegative' : 'pvAuto',
|
|
915
922
|
});
|
|
916
923
|
}
|
|
917
924
|
} catch (_e) {
|
package/io-package.json
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"common": {
|
|
3
3
|
"name": "nexowatt-ui",
|
|
4
|
-
"version": "0.7.
|
|
4
|
+
"version": "0.7.14",
|
|
5
5
|
"news": {
|
|
6
|
+
"0.7.14": {
|
|
7
|
+
"en": "Added Gate E for negative dynamic electricity prices. When the effective tariff/forecast price is below zero, grid import is preferred: tariff flags allow storage grid charging and EVCS grid charging, block tariff-based storage discharge, publish ems.budget.tariff.*, and recommend PV curtailment. Hard grid, phase, §14a and peak-shaving limits remain active. Heating rod and thermal budget followers can use total budget in negative-price windows without consuming PV budget; MultiUse and peak-shaving behavior remains unchanged.",
|
|
8
|
+
"de": "Gate E für negative dynamische Strompreise ergänzt. Wenn der effektive Tarif-/Forecastpreis unter 0 liegt, wird Netzbezug bevorzugt: Tarif-Flags geben Speicher-Netzladen und EVCS-Netzladen frei, sperren tarifbasierte Speicherentladung, veröffentlichen ems.budget.tariff.* und empfehlen PV-Abregelung. Harte Netz-, Phasen-, §14a- und Peakshaving-Grenzen bleiben aktiv. Heizstab- und Thermik-Budgetfolger können in Negativpreisfenstern Gesamtbudget nutzen, ohne PV-Budget zu verbrauchen; MultiUse und Peakshaving bleiben unverändert."
|
|
9
|
+
},
|
|
6
10
|
"0.7.13": {
|
|
7
11
|
"en": "Heating rod PV-auto now treats fresh central EMS remaining PV budget as already priority-adjusted, so thermal budget is not subtracted twice. This fixes blocked auto-start and missing stage-up when the central Gates already deducted higher-priority consumers.",
|
|
8
12
|
"de": "Heizstab-PV-Auto nutzt frisches zentrales EMS-Rest-PV-Budget jetzt als bereits prioritätsbereinigt. Thermik wird dabei nicht mehr doppelt abgezogen. Das behebt blockierten Auto-Start und fehlendes Hochstufen, wenn die zentralen Gates Verbraucher höherer Priorität bereits abgezogen haben."
|
|
@@ -1871,8 +1875,12 @@
|
|
|
1871
1875
|
}
|
|
1872
1876
|
},
|
|
1873
1877
|
"instanceObjects": [],
|
|
1874
|
-
"version": "0.7.
|
|
1878
|
+
"version": "0.7.14",
|
|
1875
1879
|
"news": {
|
|
1880
|
+
"0.7.14": {
|
|
1881
|
+
"en": "Gate E - Tariff/negative price added. Negative dynamic tariff prices now prefer grid import, allow storage and EVCS grid charging, block storage discharge from tariff logic, expose ems.budget.tariff.* diagnostics and let PV curtailment/zero-export logic bias toward import while hard safety limits stay active. Thermal and heating-rod budget followers can use total budget in negative-price windows. MultiUse and peak-shaving unchanged. Web cache bumped.",
|
|
1882
|
+
"de": "Gate E - Tarif/Negativpreis ergänzt. Negative dynamische Tarifpreise bevorzugen jetzt Netzbezug, erlauben Speicher- und EVCS-Netzladen, sperren tarifbasierte Speicherentladung, stellen ems.budget.tariff.* als Diagnose bereit und lassen PV-Abregelung/0-Einspeisung in Richtung Import-Bias arbeiten, während harte Schutzgrenzen aktiv bleiben. Thermik- und Heizstab-Budgetfolger können in Negativpreisfenstern Gesamtbudget nutzen. MultiUse und Peakshaving unverändert. Webcache erhöht."
|
|
1883
|
+
},
|
|
1876
1884
|
"0.7.13": {
|
|
1877
1885
|
"en": "Heating rod PV-Auto now treats fresh central ems.budget.remainingPvW as already prioritized, so higher-priority thermal reservations are no longer subtracted a second time. This fixes PV-Auto start/step-up blocking where the rod only started after manual stage 1 and then stayed there. Legacy fallback budgets still keep the local thermal deduction. Web cache version bumped.",
|
|
1878
1886
|
"de": "Heizstab PV-Auto nutzt frisches zentrales ems.budget.remainingPvW jetzt als bereits priorisiertes Restbudget, sodass Thermik-Reservierungen mit höherer Priorität nicht noch einmal lokal abgezogen werden. Das behebt Start-/Hochschaltblockaden, bei denen der Heizstab erst nach manueller Stufe 1 lief und dann dort hängen blieb. Legacy-Fallback-Budgets behalten den lokalen Thermik-Abzug. Webcache erhöht."
|
package/main.js
CHANGED
|
@@ -10873,6 +10873,21 @@ app.get('/api/smarthome/type-detect', requireInstaller, async (req, res) => {
|
|
|
10873
10873
|
emsForecastKwhNext24h: await getOwn('ems.budget.forecast.kwhNext24h'),
|
|
10874
10874
|
emsForecastStatus: await getOwn('ems.budget.forecast.status'),
|
|
10875
10875
|
emsForecastSource: await getOwn('ems.budget.forecast.source'),
|
|
10876
|
+
|
|
10877
|
+
// Gate E - Tarif / Negativpreis
|
|
10878
|
+
emsTariffActive: await getOwn('ems.budget.tariff.active'),
|
|
10879
|
+
emsTariffState: await getOwn('ems.budget.tariff.state'),
|
|
10880
|
+
emsTariffCurrentPriceEurKwh: await getOwn('ems.budget.tariff.currentPriceEurKwh'),
|
|
10881
|
+
emsTariffNegativeActive: await getOwn('ems.budget.tariff.negativeActive'),
|
|
10882
|
+
emsTariffGridImportPreferred: await getOwn('ems.budget.tariff.gridImportPreferred'),
|
|
10883
|
+
emsTariffStorageGridChargeAllowed: await getOwn('ems.budget.tariff.storageGridChargeAllowed'),
|
|
10884
|
+
emsTariffEvcsGridChargeAllowed: await getOwn('ems.budget.tariff.evcsGridChargeAllowed'),
|
|
10885
|
+
emsTariffDischargeAllowed: await getOwn('ems.budget.tariff.dischargeAllowed'),
|
|
10886
|
+
emsTariffPvCurtailRecommended: await getOwn('ems.budget.tariff.pvCurtailRecommended'),
|
|
10887
|
+
emsTariffNegativeMinPriceEurKwh: await getOwn('ems.budget.tariff.negativeMinPriceEurKwh'),
|
|
10888
|
+
emsTariffNextNegativeFrom: await getOwn('ems.budget.tariff.nextNegativeFrom'),
|
|
10889
|
+
emsTariffNextNegativeTo: await getOwn('ems.budget.tariff.nextNegativeTo'),
|
|
10890
|
+
emsTariffStatus: await getOwn('ems.budget.tariff.status'),
|
|
10876
10891
|
};
|
|
10877
10892
|
|
|
10878
10893
|
const summary = {
|
package/package.json
CHANGED
package/www/ems-apps.js
CHANGED
|
@@ -7536,6 +7536,20 @@ function _collectFlowPowerDpIsWFromUI() {
|
|
|
7536
7536
|
return n.toFixed(n >= 10 ? 1 : 2) + ' kWh';
|
|
7537
7537
|
}
|
|
7538
7538
|
|
|
7539
|
+
function _fmtEurKwh(v) {
|
|
7540
|
+
const n = Number(v);
|
|
7541
|
+
if (!Number.isFinite(n)) return '—';
|
|
7542
|
+
return n.toFixed(4) + ' €/kWh';
|
|
7543
|
+
}
|
|
7544
|
+
|
|
7545
|
+
function _fmtIsoShort(v) {
|
|
7546
|
+
const s = String(v || '').trim();
|
|
7547
|
+
if (!s) return '—';
|
|
7548
|
+
const d = new Date(s);
|
|
7549
|
+
if (!Number.isFinite(d.getTime())) return s;
|
|
7550
|
+
return d.toLocaleString();
|
|
7551
|
+
}
|
|
7552
|
+
|
|
7539
7553
|
function _fmtPct(v) {
|
|
7540
7554
|
const n = Number(v);
|
|
7541
7555
|
if (!Number.isFinite(n)) return '—';
|
|
@@ -7759,6 +7773,23 @@ function _collectFlowPowerDpIsWFromUI() {
|
|
|
7759
7773
|
{ label: 'Status', value: String(ctrl.emsForecastStatus || '') },
|
|
7760
7774
|
], fcKind));
|
|
7761
7775
|
|
|
7776
|
+
const tariffNeg = b(ctrl.emsTariffNegativeActive);
|
|
7777
|
+
const tariffPref = b(ctrl.emsTariffGridImportPreferred);
|
|
7778
|
+
const tariffKind = tariffPref ? 'ok' : (b(ctrl.emsTariffActive) ? '' : 'warn');
|
|
7779
|
+
els.chargingBudget.appendChild(mkCard('Gate E – Tarif / Negativpreis', [
|
|
7780
|
+
{ label: 'Tarif aktiv', value: _fmtBool(b(ctrl.emsTariffActive), 'JA', 'NEIN') },
|
|
7781
|
+
{ label: 'Status', value: String(ctrl.emsTariffStatus || ctrl.emsTariffState || '') },
|
|
7782
|
+
{ label: 'Aktueller Preis', value: _fmtEurKwh(n(ctrl.emsTariffCurrentPriceEurKwh)) },
|
|
7783
|
+
{ label: 'Negativpreis aktiv', value: _fmtBool(tariffNeg, 'JA', 'NEIN') },
|
|
7784
|
+
{ label: 'Netzbezug bevorzugt', value: _fmtBool(tariffPref, 'JA', 'NEIN') },
|
|
7785
|
+
{ label: 'Speicher Netzladen', value: _fmtBool(b(ctrl.emsTariffStorageGridChargeAllowed), 'JA', 'NEIN') },
|
|
7786
|
+
{ label: 'EVCS Netzladen', value: _fmtBool(b(ctrl.emsTariffEvcsGridChargeAllowed), 'JA', 'NEIN') },
|
|
7787
|
+
{ label: 'Speicher Entladen', value: _fmtBool(b(ctrl.emsTariffDischargeAllowed), 'JA', 'NEIN') },
|
|
7788
|
+
{ label: 'PV-Abregelung empfohlen', value: _fmtBool(b(ctrl.emsTariffPvCurtailRecommended), 'JA', 'NEIN') },
|
|
7789
|
+
{ label: 'Min. negativ im Forecast', value: _fmtEurKwh(n(ctrl.emsTariffNegativeMinPriceEurKwh)) },
|
|
7790
|
+
{ label: 'Nächstes Negativfenster', value: `${_fmtIsoShort(ctrl.emsTariffNextNegativeFrom)} → ${_fmtIsoShort(ctrl.emsTariffNextNegativeTo)}` },
|
|
7791
|
+
], tariffKind));
|
|
7792
|
+
|
|
7762
7793
|
try {
|
|
7763
7794
|
const consumers = JSON.parse(String(ctrl.emsBudgetConsumersJson || '[]'));
|
|
7764
7795
|
if (Array.isArray(consumers) && consumers.length) {
|
package/www/sw.js
CHANGED