iobroker.nexowatt-ui 0.7.2 → 0.7.4
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 +7 -2
- package/ems/modules/heating-rod-control.js +129 -24
- package/io-package.json +23 -2
- package/package.json +2 -3
- package/www/ems-apps.js +3 -0
- package/www/sw.js +1 -1
|
@@ -2622,6 +2622,11 @@ class ChargingManagementModule extends BaseModule {
|
|
|
2622
2622
|
// (b) no active wallbox is in a grid-allowed mode (normal/minpv/boost).
|
|
2623
2623
|
const capTotalBudgetByPv = (pvSurplusOnlyCfg || forcePvSurplusOnly) && !anyGridAllowedActive;
|
|
2624
2624
|
const needPvBudget = anyPvLimitedActive || capTotalBudgetByPv;
|
|
2625
|
+
// Das PV-Gate ist inzwischen ein zentrales Diagnose-/Budget-Signal für nachgelagerte Apps
|
|
2626
|
+
// (z. B. Heizstab). Deshalb muss der PV-Überschuss auch dann berechnet und veröffentlicht
|
|
2627
|
+
// werden, wenn gerade keine Wallbox im PV-Modus aktiv ist. Wichtig: Das beeinflusst NICHT
|
|
2628
|
+
// die EVCS-Budgetbegrenzung; angewendet wird pvCapW weiterhin nur, wenn needPvBudget/capTotalBudgetByPv aktiv ist.
|
|
2629
|
+
const needPvDiagnostics = true;
|
|
2625
2630
|
|
|
2626
2631
|
// PV surplus / cap (used for PV-limited wallboxes; and optionally to cap total budget)
|
|
2627
2632
|
let pvCapW = null;
|
|
@@ -2638,7 +2643,7 @@ class ChargingManagementModule extends BaseModule {
|
|
|
2638
2643
|
let pvSurplusNoEvRawWState = 0;
|
|
2639
2644
|
let pvSurplusNoEvAvg5mWState = 0;
|
|
2640
2645
|
|
|
2641
|
-
if (needPvBudget) {
|
|
2646
|
+
if (needPvBudget || needPvDiagnostics) {
|
|
2642
2647
|
// PV-Überschuss sauber ermitteln:
|
|
2643
2648
|
// Problem (vorher): PV-Cap wurde aus dem NVP (grid export) direkt abgeleitet.
|
|
2644
2649
|
// Sobald die Wallbox startet, sinkt der Export (weil EVCS selbst verbraucht)
|
|
@@ -2894,7 +2899,7 @@ class ChargingManagementModule extends BaseModule {
|
|
|
2894
2899
|
|
|
2895
2900
|
}
|
|
2896
2901
|
|
|
2897
|
-
if (!needPvBudget) {
|
|
2902
|
+
if (!needPvBudget && !needPvDiagnostics) {
|
|
2898
2903
|
this._pvAvailable = false;
|
|
2899
2904
|
this._pvAboveSinceMs = 0;
|
|
2900
2905
|
this._pvBelowSinceMs = 0;
|
|
@@ -497,12 +497,19 @@ class HeatingRodControlModule extends BaseModule {
|
|
|
497
497
|
for (const key of list) {
|
|
498
498
|
if (!key) continue;
|
|
499
499
|
try {
|
|
500
|
-
|
|
500
|
+
const hasDpEntry = !!(this.dp && this.dp.getEntry && this.dp.getEntry(key));
|
|
501
|
+
if (hasDpEntry) {
|
|
502
|
+
// Registered datapoints carry freshness metadata. If such a datapoint is
|
|
503
|
+
// stale, never resurrect the old value from the raw adapter cache. This is
|
|
504
|
+
// especially important for Batterie-Entladen: an old discharge value would
|
|
505
|
+
// otherwise block Heizstab step-up although the live NVP/PV budget is clean.
|
|
506
|
+
if (typeof this.dp.isStale === 'function' && this.dp.isStale(key, staleMs)) continue;
|
|
501
507
|
const v = this.dp.getNumberFresh ? this.dp.getNumberFresh(key, staleMs, null) : this.dp.getNumber(key, null);
|
|
502
508
|
if (typeof v === 'number' && Number.isFinite(v)) return v;
|
|
509
|
+
continue;
|
|
503
510
|
}
|
|
504
511
|
} catch (_e) {
|
|
505
|
-
// ignore
|
|
512
|
+
// ignore and try the raw cache fallback for unregistered aliases below
|
|
506
513
|
}
|
|
507
514
|
const c = this._readCacheNumber(key, null);
|
|
508
515
|
if (typeof c === 'number' && Number.isFinite(c)) return c;
|
|
@@ -515,13 +522,15 @@ class HeatingRodControlModule extends BaseModule {
|
|
|
515
522
|
for (const key of list) {
|
|
516
523
|
if (!key) continue;
|
|
517
524
|
try {
|
|
518
|
-
|
|
525
|
+
const hasDpEntry = !!(this.dp && this.dp.getEntry && this.dp.getEntry(key));
|
|
526
|
+
if (hasDpEntry) {
|
|
519
527
|
if (typeof this.dp.isStale === 'function' && this.dp.isStale(key, staleMs)) continue;
|
|
520
528
|
const v = this.dp.getBoolean ? this.dp.getBoolean(key, null) : null;
|
|
521
529
|
if (v !== null && v !== undefined) return !!v;
|
|
530
|
+
continue;
|
|
522
531
|
}
|
|
523
532
|
} catch (_e) {
|
|
524
|
-
// ignore
|
|
533
|
+
// ignore and try the raw cache fallback for unregistered aliases below
|
|
525
534
|
}
|
|
526
535
|
const raw = this._readCacheRaw(key, null);
|
|
527
536
|
if (raw === null || raw === undefined) continue;
|
|
@@ -559,6 +568,7 @@ class HeatingRodControlModule extends BaseModule {
|
|
|
559
568
|
storageDischargeHoldSec: pickNum(['storageDischargeHoldSec', 'storageDischargeTripSec', 'pvStorageDischargeHoldSec'], 15, 0, 3600),
|
|
560
569
|
hardStorageDischargeW: pickNum(['hardStorageDischargeW', 'pvHardStorageDischargeW'], 1200, 0, 1000000),
|
|
561
570
|
budgetSafetyReserveW: pickNum(['budgetSafetyReserveW', 'pvSafetyReserveW'], 150, 0, 1000000),
|
|
571
|
+
stageUpDelaySec: pickNum(['stageUpDelaySec', 'budgetStageUpDelaySec', 'pvStageUpDelaySec'], 10, 0, 3600),
|
|
562
572
|
};
|
|
563
573
|
}
|
|
564
574
|
|
|
@@ -575,8 +585,22 @@ class HeatingRodControlModule extends BaseModule {
|
|
|
575
585
|
|
|
576
586
|
const batteryPowerW = this._readNumberAny(['batteryPower'], staleMs, null);
|
|
577
587
|
if (typeof batteryPowerW === 'number' && Number.isFinite(batteryPowerW)) {
|
|
578
|
-
|
|
579
|
-
|
|
588
|
+
const signedW = Math.round(batteryPowerW);
|
|
589
|
+
const noiseW = 25;
|
|
590
|
+
// batteryPower is the canonical direction signal in this adapter:
|
|
591
|
+
// +W = discharge, -W = charge. Prefer that direction over separate
|
|
592
|
+
// charge/discharge aliases, because those can be delayed or vendor-specific.
|
|
593
|
+
// Otherwise a charging battery can look like discharge and block stage-up.
|
|
594
|
+
if (signedW < -noiseW) {
|
|
595
|
+
chargeW = Math.max(chargeW, Math.abs(signedW));
|
|
596
|
+
dischargeW = 0;
|
|
597
|
+
} else if (signedW > noiseW) {
|
|
598
|
+
dischargeW = Math.max(dischargeW, signedW);
|
|
599
|
+
chargeW = 0;
|
|
600
|
+
} else {
|
|
601
|
+
chargeW = 0;
|
|
602
|
+
dischargeW = 0;
|
|
603
|
+
}
|
|
580
604
|
}
|
|
581
605
|
|
|
582
606
|
const socPct = this._readNumberAny([
|
|
@@ -637,6 +661,13 @@ class HeatingRodControlModule extends BaseModule {
|
|
|
637
661
|
const storageReserveW = (storageKnown && !(typeof storage.socPct === 'number' && storage.socPct >= storageTargetSocPct))
|
|
638
662
|
? storageReserveCfgW
|
|
639
663
|
: 0;
|
|
664
|
+
// Speicherreserve sauber bilanzieren: Was der Speicher bereits lädt, erfüllt zuerst
|
|
665
|
+
// die Reserve. Nur die noch fehlende Reserve wird vom Heizstab-Budget abgezogen;
|
|
666
|
+
// Speicherladung oberhalb der Reserve darf als nutzbarer PV-Überschuss gelten.
|
|
667
|
+
const storageReserveMissingW = Math.max(0, storageReserveW - Math.max(0, storage.chargeW));
|
|
668
|
+
const storageChargeUsableW = storageReserveW > 0
|
|
669
|
+
? Math.max(0, Math.max(0, storage.chargeW) - storageReserveW)
|
|
670
|
+
: Math.max(0, storage.chargeW);
|
|
640
671
|
|
|
641
672
|
// Gate A/T/§14a/Peak: consume the remaining central budget after EVCS.
|
|
642
673
|
// This is intentionally read-only: Heizstab does not change the load management budget engine.
|
|
@@ -678,10 +709,15 @@ class HeatingRodControlModule extends BaseModule {
|
|
|
678
709
|
if (finite(cmPvNoEvAvg) && cmPvNoEvAvg > 0) candidates.push({ k: 'cm.pvSurplusNoEvAvg5mW', w: cmPvNoEvAvg });
|
|
679
710
|
if (candidates.length) {
|
|
680
711
|
const best = candidates.reduce((a, b) => (b.w > a.w ? b : a), candidates[0]);
|
|
681
|
-
|
|
682
|
-
|
|
712
|
+
// The EVCS PV gate reports the currently visible PV surplus. For Heizstab
|
|
713
|
+
// targeting this is a total flexible-load budget: keep the already running
|
|
714
|
+
// Heizstab stage in the budget and only reserve actual battery charging power.
|
|
715
|
+
// A storage reserve must not blindly eat visible NVP export, otherwise the rod
|
|
716
|
+
// can get stuck on stage 1 although several kW are still exported.
|
|
717
|
+
cmPvGateW = Math.max(0, best.w - evcsUsedW + currentW + storageChargeUsableW - storage.dischargeW - storageReserveMissingW - gateCfg.budgetSafetyReserveW);
|
|
718
|
+
cmPvGateSource = `${best.k}+nvp-follow`;
|
|
683
719
|
} else if (cmPvAvailable === false && finite(cmPvCapEffectiveRaw)) {
|
|
684
|
-
cmPvGateW = Math.max(0, currentW -
|
|
720
|
+
cmPvGateW = Math.max(0, currentW - storageReserveMissingW - gateCfg.budgetSafetyReserveW);
|
|
685
721
|
cmPvGateSource = 'cm.pvAvailable.false_hold_only';
|
|
686
722
|
}
|
|
687
723
|
}
|
|
@@ -690,10 +726,11 @@ class HeatingRodControlModule extends BaseModule {
|
|
|
690
726
|
// import above the configured tolerance consumes the budget; small import remains allowed
|
|
691
727
|
// to keep the stages running calmly like PV-only EV charging.
|
|
692
728
|
const importExcessW = gridKnown ? Math.max(0, importW - gateCfg.maxGridImportW) : 0;
|
|
729
|
+
const usableStorageChargeForNvpW = storageChargeUsableW;
|
|
693
730
|
const nvpSurplusBeforeFlexW = gridKnown
|
|
694
|
-
? Math.max(0, exportW + currentW +
|
|
731
|
+
? Math.max(0, exportW + currentW + usableStorageChargeForNvpW - storage.dischargeW - importExcessW)
|
|
695
732
|
: 0;
|
|
696
|
-
const nvpAvailableW = Math.max(0, nvpSurplusBeforeFlexW -
|
|
733
|
+
const nvpAvailableW = Math.max(0, nvpSurplusBeforeFlexW - storageReserveMissingW - gateCfg.budgetSafetyReserveW);
|
|
697
734
|
|
|
698
735
|
let pvBudgetGateW = nvpAvailableW;
|
|
699
736
|
let pvBudgetSource = gridKnown ? 'nvp+ownLoad+storageReserve' : 'no-fresh-nvp';
|
|
@@ -746,7 +783,11 @@ class HeatingRodControlModule extends BaseModule {
|
|
|
746
783
|
nonPvEnergyActive,
|
|
747
784
|
storageSocPct: storage.socPct,
|
|
748
785
|
storageReserveW,
|
|
786
|
+
storageReserveMissingW,
|
|
787
|
+
storageChargeUsableW,
|
|
749
788
|
storageTargetSocPct,
|
|
789
|
+
usableStorageChargeForNvpW,
|
|
790
|
+
stageUpDelaySec: gateCfg.stageUpDelaySec,
|
|
750
791
|
nvpSurplusBeforeFlexW,
|
|
751
792
|
cmAvailableW: (cmPvGateW !== null && Number.isFinite(cmPvGateW)) ? Math.max(0, cmPvGateW) : 0,
|
|
752
793
|
nvpAvailableW,
|
|
@@ -1159,26 +1200,84 @@ class HeatingRodControlModule extends BaseModule {
|
|
|
1159
1200
|
return prev;
|
|
1160
1201
|
}
|
|
1161
1202
|
|
|
1162
|
-
|
|
1203
|
+
_stagePowerScale(d, observedStage = 0, measuredW = null) {
|
|
1204
|
+
const st = (d && d.id && this._stageCtl && this._stageCtl.get) ? (this._stageCtl.get(d.id) || null) : null;
|
|
1205
|
+
const learned = st && Number.isFinite(Number(st.stagePowerScale)) ? clamp(Number(st.stagePowerScale), 0.25, 4) : 1;
|
|
1206
|
+
const obs = Math.max(0, Math.min(Math.round(Number(observedStage) || 0), d && d.stages ? d.stages.length : 0));
|
|
1207
|
+
const measured = Number(measuredW);
|
|
1208
|
+
if (obs <= 0 || !Number.isFinite(measured) || measured <= 50) return learned;
|
|
1209
|
+
const configuredW = Math.max(0, this._sumStagePower(d, obs));
|
|
1210
|
+
if (configuredW <= 50) return learned;
|
|
1211
|
+
const ratio = measured / configuredW;
|
|
1212
|
+
if (!Number.isFinite(ratio) || ratio <= 0) return learned;
|
|
1213
|
+
// Clamp keeps a noisy meter from destroying the stage model, but still corrects
|
|
1214
|
+
// common setups where the configured default says 2 kW/stage and the real rod is 1 kW/stage.
|
|
1215
|
+
const scale = clamp(ratio, 0.25, 4);
|
|
1216
|
+
if (d && d.id && this._stageCtl && this._stageCtl.set) {
|
|
1217
|
+
const next = Object.assign({}, st || { targetStage: obs, lastIncreaseMs: 0, lastDecreaseMs: 0 }, { stagePowerScale: scale });
|
|
1218
|
+
this._stageCtl.set(d.id, next);
|
|
1219
|
+
}
|
|
1220
|
+
return scale;
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
_sumStagePowerModel(d, stageCount, observedStage = 0, measuredW = null) {
|
|
1224
|
+
const configuredW = this._sumStagePower(d, stageCount);
|
|
1225
|
+
const scale = this._stagePowerScale(d, observedStage, measuredW);
|
|
1226
|
+
return this._capDevicePower(d, Math.round(configuredW * scale));
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
_stageThresholdModel(d, stageIndexZeroBased, key, observedStage = 0, measuredW = null, fallbackStageCount = null) {
|
|
1230
|
+
const stage = d && d.stages ? d.stages[stageIndexZeroBased] : null;
|
|
1231
|
+
const scale = this._stagePowerScale(d, observedStage, measuredW);
|
|
1232
|
+
const raw = stage && Number.isFinite(Number(stage[key])) ? Math.max(0, Number(stage[key])) : null;
|
|
1233
|
+
if (raw !== null) return Math.round(raw * scale);
|
|
1234
|
+
const cnt = fallbackStageCount !== null ? fallbackStageCount : (stageIndexZeroBased + 1);
|
|
1235
|
+
return this._sumStagePowerModel(d, cnt, observedStage, measuredW);
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
_computeDesiredStage(d, remainingW, currentStage, measuredW = null) {
|
|
1163
1239
|
let stage = Math.max(0, Math.min(Math.round(Number(currentStage) || 0), d.stageCount));
|
|
1240
|
+
const budgetW = Math.max(0, Math.round(num(remainingW, 0)));
|
|
1164
1241
|
|
|
1165
1242
|
while (stage > 0) {
|
|
1166
|
-
const
|
|
1167
|
-
if (
|
|
1168
|
-
if (remainingW < Math.max(0, num(cfg.offBelowW, 0))) stage--;
|
|
1243
|
+
const offBelowW = this._stageThresholdModel(d, stage - 1, 'offBelowW', currentStage, measuredW, stage);
|
|
1244
|
+
if (budgetW < Math.max(0, offBelowW)) stage--;
|
|
1169
1245
|
else break;
|
|
1170
1246
|
}
|
|
1171
1247
|
|
|
1172
1248
|
while (stage < d.stageCount) {
|
|
1173
|
-
const
|
|
1174
|
-
|
|
1175
|
-
|
|
1249
|
+
const thresholdCfgW = this._stageThresholdModel(d, stage, 'onAboveW', currentStage, measuredW, stage + 1);
|
|
1250
|
+
const nextPowerW = this._sumStagePowerModel(d, stage + 1, currentStage, measuredW);
|
|
1251
|
+
// Use the lower of the explicit threshold and the learned real cumulative power.
|
|
1252
|
+
// This lets PV-Auto follow the real hardware when the default/configured stage
|
|
1253
|
+
// power is too high, without breaking installers that intentionally entered
|
|
1254
|
+
// higher thresholds for hysteresis.
|
|
1255
|
+
const onAboveW = Math.max(0, Math.min(thresholdCfgW, nextPowerW || thresholdCfgW));
|
|
1256
|
+
if (budgetW >= onAboveW) stage++;
|
|
1176
1257
|
else break;
|
|
1177
1258
|
}
|
|
1178
1259
|
|
|
1179
1260
|
return Math.max(0, Math.min(stage, d.stageCount));
|
|
1180
1261
|
}
|
|
1181
1262
|
|
|
1263
|
+
_limitBudgetStageStepUp(d, desiredStage, observedStage, now) {
|
|
1264
|
+
const cfg = this._getBudgetGateCfg();
|
|
1265
|
+
const st = this._ensureStageCtlState(d.id, observedStage);
|
|
1266
|
+
const base = Math.max(0, Math.min(Math.round(Number(st.targetStage ?? observedStage) || 0), d.stageCount));
|
|
1267
|
+
let target = Math.max(0, Math.min(Math.round(Number(desiredStage) || 0), d.stageCount));
|
|
1268
|
+
if (target <= base) return target;
|
|
1269
|
+
|
|
1270
|
+
const nextPhysical = this._nextPhysicalStageAbove(d, base);
|
|
1271
|
+
if (nextPhysical > base) target = Math.min(target, nextPhysical);
|
|
1272
|
+
const waitMs = Math.max(0, Math.round(num(cfg.stageUpDelaySec, 10) * 1000));
|
|
1273
|
+
const lastUp = Math.max(num(st.budgetLastStepUpMs, 0), num(st.lastIncreaseMs, 0));
|
|
1274
|
+
if (waitMs > 0 && lastUp > 0 && (now - lastUp) < waitMs) return base;
|
|
1275
|
+
|
|
1276
|
+
st.budgetLastStepUpMs = now;
|
|
1277
|
+
this._stageCtl.set(d.id, st);
|
|
1278
|
+
return target;
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1182
1281
|
_applyTiming(d, desiredStage, observedStage) {
|
|
1183
1282
|
const st = this._ensureStageCtlState(d.id, observedStage);
|
|
1184
1283
|
const now = nowMs();
|
|
@@ -1209,7 +1308,7 @@ class HeatingRodControlModule extends BaseModule {
|
|
|
1209
1308
|
return currentStage;
|
|
1210
1309
|
}
|
|
1211
1310
|
|
|
1212
|
-
_applyZeroExportStageStrategy(d, desiredStage, observedStage, pvBase, zeroInfo, now) {
|
|
1311
|
+
_applyZeroExportStageStrategy(d, desiredStage, observedStage, pvBase, zeroInfo, now, measuredW = null) {
|
|
1213
1312
|
const info = zeroInfo || this._computeZeroExportInfo(pvBase);
|
|
1214
1313
|
const cfg = (info && info.cfg) ? info.cfg : this._getZeroExportCfg();
|
|
1215
1314
|
const st = this._ensureStageCtlState(d.id, observedStage);
|
|
@@ -1359,8 +1458,8 @@ class HeatingRodControlModule extends BaseModule {
|
|
|
1359
1458
|
const stepWaitMs = Math.max(0, cfg.stepUpDelaySec * 1000);
|
|
1360
1459
|
const mayStep = nextStage > baseStage && (!lastUp || (now - lastUp) >= stepWaitMs);
|
|
1361
1460
|
if (mayStep) {
|
|
1362
|
-
const basePowerW = this.
|
|
1363
|
-
const nextPowerW = this.
|
|
1461
|
+
const basePowerW = this._sumStagePowerModel(d, baseStage, observedStage, measuredW);
|
|
1462
|
+
const nextPowerW = this._sumStagePowerModel(d, nextStage, observedStage, measuredW);
|
|
1364
1463
|
targetStage = Math.max(targetStage, nextStage);
|
|
1365
1464
|
st.zeroLastStepUpMs = now;
|
|
1366
1465
|
st.zeroProbe = {
|
|
@@ -1783,7 +1882,7 @@ class HeatingRodControlModule extends BaseModule {
|
|
|
1783
1882
|
continue;
|
|
1784
1883
|
}
|
|
1785
1884
|
|
|
1786
|
-
let desiredStage = this._computeDesiredStage(d, remainingW, observedStage);
|
|
1885
|
+
let desiredStage = this._computeDesiredStage(d, remainingW, observedStage, measuredW);
|
|
1787
1886
|
let zeroDecision = null;
|
|
1788
1887
|
|
|
1789
1888
|
// Wenn der Budget-Gate-Schutz gerade einen Netzbezug/Speicherbezug beobachtet,
|
|
@@ -1791,6 +1890,8 @@ class HeatingRodControlModule extends BaseModule {
|
|
|
1791
1890
|
// laufen, damit PV/FEMS sauber nachregeln kann.
|
|
1792
1891
|
if (budgetProtection && budgetProtection.watchActive && desiredStage > observedStage) {
|
|
1793
1892
|
desiredStage = observedStage;
|
|
1893
|
+
} else if (desiredStage > observedStage) {
|
|
1894
|
+
desiredStage = this._limitBudgetStageStepUp(d, desiredStage, observedStage, now);
|
|
1794
1895
|
}
|
|
1795
1896
|
|
|
1796
1897
|
// 0-/Minus-Einspeiseanlagen verstecken PV-Überschuss am Netzpunkt, weil der
|
|
@@ -1799,7 +1900,7 @@ class HeatingRodControlModule extends BaseModule {
|
|
|
1799
1900
|
// Speicher-SOC und Einspeiselimit zusammenpassen. Danach entscheidet der Netzpunkt:
|
|
1800
1901
|
// Netzbezug oder Speicherentladung -> schnell reduzieren; stabil PV -> halten/weiter prüfen.
|
|
1801
1902
|
if (zeroExportInfo.active) {
|
|
1802
|
-
zeroDecision = this._applyZeroExportStageStrategy(d, desiredStage, observedStage, pvBase, zeroExportInfo, now);
|
|
1903
|
+
zeroDecision = this._applyZeroExportStageStrategy(d, desiredStage, observedStage, pvBase, zeroExportInfo, now, measuredW);
|
|
1803
1904
|
desiredStage = Math.max(0, Math.min(num(zeroDecision.targetStage, desiredStage), d.stageCount));
|
|
1804
1905
|
await this._setStateIfChanged(`heatingRod.devices.${d.id}.zeroExportReason`, String(zeroDecision.reason || zeroExportInfo.reason || ''));
|
|
1805
1906
|
await this._setStateIfChanged(`heatingRod.devices.${d.id}.zeroExportNextAllowedAt`, Math.round(num(zeroDecision.nextAllowedAt, 0)));
|
|
@@ -1829,7 +1930,7 @@ class HeatingRodControlModule extends BaseModule {
|
|
|
1829
1930
|
const forcePvWrite = !!(forceStorageProtectOff || forceNonPvDown || (targetStage <= 0 && ((typeof measuredW === 'number' && Number.isFinite(measuredW) && measuredW > 50) || Math.max(0, feedback.appliedPowerW || 0) > 0)));
|
|
1830
1931
|
const res = await this._applyStageState(d, targetStage, feedback, { force: forcePvWrite });
|
|
1831
1932
|
const effectiveTargetStage = Math.max(0, Math.min(num(res.targetStage, targetStage), d.wiredStages));
|
|
1832
|
-
const targetW = this.
|
|
1933
|
+
const targetW = this._sumStagePowerModel(d, effectiveTargetStage, observedStage, measuredW);
|
|
1833
1934
|
const usedW = (typeof measuredW === 'number' && Number.isFinite(measuredW) && measuredW > 0)
|
|
1834
1935
|
? Math.max(0, measuredW)
|
|
1835
1936
|
: targetW;
|
|
@@ -1900,8 +2001,12 @@ class HeatingRodControlModule extends BaseModule {
|
|
|
1900
2001
|
nonPvEnergyActive: !!pvBase.nonPvEnergyActive,
|
|
1901
2002
|
storageSocPct: pvBase.storageSocPct,
|
|
1902
2003
|
storageReserveW: Math.round(num(pvBase.storageReserveW, 0)),
|
|
2004
|
+
storageReserveMissingW: Math.round(num(pvBase.storageReserveMissingW, 0)),
|
|
2005
|
+
storageChargeUsableW: Math.round(num(pvBase.storageChargeUsableW, 0)),
|
|
1903
2006
|
storageTargetSocPct: pvBase.storageTargetSocPct,
|
|
1904
2007
|
nvpSurplusBeforeFlexW: Math.round(num(pvBase.nvpSurplusBeforeFlexW, 0)),
|
|
2008
|
+
usableStorageChargeForNvpW: Math.round(num(pvBase.usableStorageChargeForNvpW, 0)),
|
|
2009
|
+
stageUpDelaySec: Math.round(num(pvBase.stageUpDelaySec, 0)),
|
|
1905
2010
|
nvpAvailableW: Math.round(num(pvBase.nvpAvailableW, 0)),
|
|
1906
2011
|
cmAvailableW: Math.round(num(pvBase.cmAvailableW, 0)),
|
|
1907
2012
|
availableW: Math.round(num(pvBase.availableW, 0)),
|
package/io-package.json
CHANGED
|
@@ -1,8 +1,25 @@
|
|
|
1
1
|
{
|
|
2
2
|
"common": {
|
|
3
3
|
"name": "nexowatt-ui",
|
|
4
|
-
"version": "0.7.
|
|
4
|
+
"version": "0.7.4",
|
|
5
5
|
"news": {
|
|
6
|
+
"0.7.4": {
|
|
7
|
+
"en": "Heating rod PV-Auto now follows the PV/NVP budget as a discrete budget-gate consumer: visible NVP export and usable storage charge above the reserve are used for stage targeting, running rod load is kept in the budget, and stage power can be learned from measured rod power so wrong default stage sizes no longer block higher stages. A configurable budget stage-up delay was added; charging management and storage control were not changed. The storage reserve is now evaluated as missing reserve: storage charge above the reserve is usable while missing reserve remains withheld.",
|
|
8
|
+
"de": "Heizstab-PV-Auto folgt jetzt dem PV-/NVP-Budget als diskreter Budget-Gate-Verbraucher: sichtbare NVP-Einspeisung und nutzbare Speicherladung oberhalb der Reserve fließen in die Stufenzielberechnung ein, laufende Heizstableistung bleibt im Budget, und die reale Stufenleistung kann aus der gemessenen Heizstableistung gelernt werden. Dadurch blockieren falsche Default-Stufengrößen kein Hochfahren mehr. Eine einstellbare Budget-Stufe-hoch-Wartezeit wurde ergänzt; Lade-/Lastmanagement und Speicherregelung wurden nicht geändert. Die Speicherreserve wird als fehlende Reserve bewertet: bereits vorhandene Speicherladung oberhalb der Reserve wird nutzbar, fehlende Reserve bleibt zurückgehalten."
|
|
9
|
+
},
|
|
10
|
+
"0.7.3": {
|
|
11
|
+
"en": "PV gate diagnostics are now calculated continuously for downstream apps such as heating rod control. Heating rod PV budget no longer falls back to stale raw cache values when registered datapoints are stale.",
|
|
12
|
+
"de": "PV-Gate-Diagnose wird jetzt dauerhaft für nachgelagerte Apps wie die Heizstabsteuerung berechnet. Die Heizstab-PV-Freigabe fällt bei stale registrierten Datenpunkten nicht mehr auf alte Rohcache-Werte zurück.",
|
|
13
|
+
"ru": "Диагностика PV Gate теперь рассчитывается постоянно для downstream-приложений, например управления ТЭНом. PV-бюджет ТЭНа больше не использует устаревшие значения raw cache для stale зарегистрированных datapoints.",
|
|
14
|
+
"pt": "O diagnóstico PV Gate agora é calculado continuamente para apps posteriores, como o controlo do aquecedor. O orçamento PV do aquecedor já não recorre a valores antigos do raw cache quando datapoints registados estão stale.",
|
|
15
|
+
"nl": "PV Gate-diagnose wordt nu continu berekend voor downstream apps zoals de verwarmingsstaafregeling. Het PV-budget van de verwarmingsstaaf valt bij stale geregistreerde datapunten niet meer terug op oude raw-cachewaarden.",
|
|
16
|
+
"fr": "Le diagnostic PV Gate est désormais calculé en continu pour les applications aval comme la commande de résistance chauffante. Le budget PV de la résistance ne retombe plus sur d’anciennes valeurs de cache brut quand des datapoints enregistrés sont périmés.",
|
|
17
|
+
"it": "La diagnostica PV Gate ora viene calcolata continuamente per app a valle come il controllo della resistenza. Il budget PV della resistenza non usa più vecchi valori raw cache quando datapoint registrati risultano stale.",
|
|
18
|
+
"es": "El diagnóstico PV Gate ahora se calcula continuamente para apps posteriores como el control del calentador. El presupuesto PV del calentador ya no vuelve a valores antiguos de caché bruto cuando los datapoints registrados están obsoletos.",
|
|
19
|
+
"pl": "Diagnostyka PV Gate jest teraz liczona stale dla aplikacji downstream, np. sterowania grzałką. Budżet PV grzałki nie wraca już do starych wartości raw cache, gdy zarejestrowane datapointy są stale.",
|
|
20
|
+
"uk": "Діагностика PV Gate тепер розраховується постійно для downstream-додатків, наприклад керування ТЕНом. PV-бюджет ТЕНа більше не використовує застарілі значення raw cache для stale зареєстрованих datapoints.",
|
|
21
|
+
"zh-cn": "PV Gate 诊断现在会为加热棒控制等下游应用持续计算。注册数据点过期时,加热棒 PV 预算不再回退到旧的原始缓存值。"
|
|
22
|
+
},
|
|
6
23
|
"0.7.2": {
|
|
7
24
|
"en": "Heating rod PV-Auto rebuilt as a read-only budget-gate consumer. It now uses the existing charging/load-management remaining budget and PV gate before switching stages, keeps running through short inverter/FEMS transients, and reduces only after configurable grid-import or battery-discharge hold times. Zero/minus feed-in test loads now verify that PV generation actually rises and retry after a failed PV-rise check. Charging management, storage control and storage-farm distribution were not changed.",
|
|
8
25
|
"de": "Heizstab-PV-Auto als lesender Budget-Gate-Verbraucher neu aufgebaut. Die App nutzt jetzt Restbudget und PV-Gate aus dem bestehenden Lade-/Lastmanagement, bevor Stufen geschaltet werden, bleibt bei kurzen WR-/FEMS-Transienten stabil und reduziert erst nach einstellbarer Netzbezug- oder Speicherentlade-Haltezeit. 0-/Minus-Einspeise-Testlasten prüfen jetzt, ob die PV-Erzeugung wirklich nachregelt, und warten nach fehlendem PV-Anstieg bis zum nächsten Versuch. Lade-/Lastmanagement, Speicherregelung und Speicherfarm-Verteilung wurden nicht geändert."
|
|
@@ -1822,8 +1839,12 @@
|
|
|
1822
1839
|
}
|
|
1823
1840
|
},
|
|
1824
1841
|
"instanceObjects": [],
|
|
1825
|
-
"version": "0.7.
|
|
1842
|
+
"version": "0.7.4",
|
|
1826
1843
|
"news": {
|
|
1844
|
+
"0.7.4": {
|
|
1845
|
+
"en": "Heating rod PV-Auto now follows the PV/NVP budget as a discrete budget-gate consumer. Visible NVP export, running rod load and usable storage charge above the reserve are used for stage targeting; measured rod power can correct wrong default stage sizes. Charging management and storage control were not changed. The storage reserve is evaluated as missing reserve.",
|
|
1846
|
+
"de": "Heizstab-PV-Auto folgt jetzt dem PV-/NVP-Budget als diskreter Budget-Gate-Verbraucher. Sichtbare NVP-Einspeisung, laufende Heizstableistung und nutzbare Speicherladung oberhalb der Reserve werden für die Stufenzielberechnung genutzt; gemessene Heizstableistung kann falsche Default-Stufengrößen korrigieren. Lade-/Lastmanagement und Speicherregelung wurden nicht geändert. Die Speicherreserve wird als fehlende Reserve bewertet."
|
|
1847
|
+
},
|
|
1827
1848
|
"0.7.1": {
|
|
1828
1849
|
"en": "Storage control stabilized: self-consumption and tariff discharge now hold the last active setpoint inside the NVP target band instead of following delayed/noisy battery or storage-farm actual-power values. The demand safety clamp also treats the last commanded discharge setpoint as plausible discharge power, preventing stale 0 W telemetry from pulling the farm down. No changes to heating rod logic, PV surplus charging, storage-farm distribution, or zero-feed-in control.",
|
|
1829
1850
|
"de": "Speicherregelung stabilisiert: Eigenverbrauchs- und Tarif-Entladung halten innerhalb des NVP-Zielbands den letzten aktiven Sollwert, statt verzögerten/rauschenden Batterie- oder Speicherfarm-Istleistungen zu folgen. Der Sicherheits-Lastdeckel wertet den letzten geschriebenen Entlade-Sollwert zusätzlich als plausible Entladeleistung, damit stale 0-W-Telemetrie die Farm nicht herunterzieht. Keine Änderungen an Heizstablogik, PV-Überschussladung, Speicherfarm-Verteilung oder 0-Einspeise-Regelung."
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "iobroker.nexowatt-ui",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.4",
|
|
4
4
|
"description": "Responsive NexoWatt EMS visualization adapter.",
|
|
5
5
|
"author": "NexoWatt",
|
|
6
6
|
"license": "Proprietary License (NPL) v1.0",
|
|
@@ -49,6 +49,5 @@
|
|
|
49
49
|
"README.md",
|
|
50
50
|
"LICENSE",
|
|
51
51
|
"ems/**/*"
|
|
52
|
-
]
|
|
53
|
-
"type": "commonjs"
|
|
52
|
+
]
|
|
54
53
|
}
|
package/www/ems-apps.js
CHANGED
|
@@ -1685,6 +1685,7 @@ function _collectFlowPowerDpIsWFromUI() {
|
|
|
1685
1685
|
storageDischargeHoldSec: ['storageDischargeHoldSec', 'storageDischargeTripSec'],
|
|
1686
1686
|
hardStorageDischargeW: ['hardStorageDischargeW'],
|
|
1687
1687
|
budgetSafetyReserveW: ['budgetSafetyReserveW', 'pvSafetyReserveW'],
|
|
1688
|
+
stageUpDelaySec: ['stageUpDelaySec', 'budgetStageUpDelaySec', 'pvStageUpDelaySec'],
|
|
1688
1689
|
};
|
|
1689
1690
|
const hn = (key, def, min, max) => {
|
|
1690
1691
|
let raw = h[key];
|
|
@@ -1706,6 +1707,7 @@ function _collectFlowPowerDpIsWFromUI() {
|
|
|
1706
1707
|
hn('storageDischargeHoldSec', 15, 0, 3600);
|
|
1707
1708
|
hn('hardStorageDischargeW', 1200, 0, 1000000);
|
|
1708
1709
|
hn('budgetSafetyReserveW', 150, 0, 1000000);
|
|
1710
|
+
hn('stageUpDelaySec', 10, 0, 3600);
|
|
1709
1711
|
h.zeroExport = (h.zeroExport && typeof h.zeroExport === 'object') ? h.zeroExport : {};
|
|
1710
1712
|
const z = h.zeroExport;
|
|
1711
1713
|
const zn = (key, def, min, max, integer = true) => {
|
|
@@ -2121,6 +2123,7 @@ function _collectFlowPowerDpIsWFromUI() {
|
|
|
2121
2123
|
grpGate.body.appendChild(_mkCfgField('Speicherentladung-Haltezeit (s)', _mkCfgInput('number', cfg.storageDischargeHoldSec, (v) => { cfg.storageDischargeHoldSec = Math.max(0, Math.round(Number(v) || 0)); setDirty(); }, { min: 0, step: 1, width: '150px' }), 'Nach dieser Zeit mit Speicherentladung über der Toleranz wird eine physische Stufe reduziert.'));
|
|
2122
2124
|
grpGate.body.appendChild(_mkCfgField('Harte Speicherentladung AUS (W)', _mkCfgInput('number', cfg.hardStorageDischargeW, (v) => { cfg.hardStorageDischargeW = Math.max(0, Math.round(Number(v) || 0)); setDirty(); }, { min: 0, step: 50, width: '150px' }), 'Ab dieser Speicherentladung wird sofort komplett zurückgenommen.'));
|
|
2123
2125
|
grpGate.body.appendChild(_mkCfgField('Budget-Sicherheitsreserve (W)', _mkCfgInput('number', cfg.budgetSafetyReserveW, (v) => { cfg.budgetSafetyReserveW = Math.max(0, Math.round(Number(v) || 0)); setDirty(); }, { min: 0, step: 10, width: '150px' }), 'Wird vom berechneten Heizstab-PV-Budget abgezogen, damit Speicher, Hauslast und Messrauschen nicht gegeneinander regeln.'));
|
|
2126
|
+
grpGate.body.appendChild(_mkCfgField('Stufe-hoch Wartezeit (s)', _mkCfgInput('number', cfg.stageUpDelaySec, (v) => { cfg.stageUpDelaySec = Math.max(0, Math.round(Number(v) || 0)); setDirty(); }, { min: 0, step: 1, width: '150px' }), 'PV-Budgetfolger: erhöht maximal eine physische Heizstab-Stufe je Wartezeit. Reduzieren bleibt schnell, wenn Netzbezug/Speicherentladung dauerhaft überschritten wird.'));
|
|
2124
2127
|
els.heatingRodDevices.appendChild(grpGate.wrap);
|
|
2125
2128
|
|
|
2126
2129
|
const zeroCfg = cfg.zeroExport || {};
|
package/www/sw.js
CHANGED