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.
@@ -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
- if (this.dp && this.dp.getEntry && this.dp.getEntry(key)) {
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
- if (this.dp && this.dp.getEntry && this.dp.getEntry(key)) {
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
- if (batteryPowerW < 0 && chargeW <= 0) chargeW = Math.max(0, Math.abs(batteryPowerW));
579
- if (batteryPowerW > 0 && dischargeW <= 0) dischargeW = Math.max(0, Math.abs(batteryPowerW));
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
- cmPvGateW = Math.max(0, best.w - evcsUsedW + currentW - storageReserveW - gateCfg.budgetSafetyReserveW);
682
- cmPvGateSource = best.k;
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 - storageReserveW - gateCfg.budgetSafetyReserveW);
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 + storage.chargeW - storage.dischargeW - importExcessW)
731
+ ? Math.max(0, exportW + currentW + usableStorageChargeForNvpW - storage.dischargeW - importExcessW)
695
732
  : 0;
696
- const nvpAvailableW = Math.max(0, nvpSurplusBeforeFlexW - storageReserveW - gateCfg.budgetSafetyReserveW);
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
- _computeDesiredStage(d, remainingW, currentStage) {
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 cfg = d.stages[stage - 1];
1167
- if (!cfg) break;
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 cfg = d.stages[stage];
1174
- if (!cfg) break;
1175
- if (remainingW >= Math.max(0, num(cfg.onAboveW, 0))) stage++;
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._sumStagePower(d, baseStage);
1363
- const nextPowerW = this._sumStagePower(d, nextStage);
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._sumStagePower(d, effectiveTargetStage);
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.2",
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.1",
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.2",
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
@@ -1,6 +1,6 @@
1
1
  // Increment cache name on releases so browser updates JS/HTML reliably.
2
2
  // NOTE: Keep this monotonic to force SW updates on hotfixes.
3
- const CACHE_NAME = 'nexowatt-cache-v172';
3
+ const CACHE_NAME = 'nexowatt-cache-v174';
4
4
 
5
5
  const OFFLINE_URLS = [
6
6
  './',