2 Commits
Author SHA1 Message Date
dh 995b704e36 fix(manager): Prognoseberechtigungen verständlich diagnostizieren
Tests / test (push) Successful in 49s
2026-09-29 11:40:24 +00:00
dh 5ff0d405d4 fix(manager): feste Leistungsangebote immer zuteilen 2026-09-29 11:40:24 +00:00
7 changed files with 94 additions and 23 deletions
+5 -7
View File
@@ -2263,12 +2263,7 @@ class Manager extends IPSModule implements ManagerSchnittstelle
/** @param array<string, mixed> $lease */ /** @param array<string, mixed> $lease */
private function prognoseBerechtigungInLease(array $lease): bool private function prognoseBerechtigungInLease(array $lease): bool
{ {
return Lizenzpruefung::berechtigungsMenge($lease, Lizenzpruefung::NETZFAHRPLAN) > 0 return Lizenzpruefung::hatPrognoseBerechtigung($lease);
|| (Lizenzpruefung::berechtigungsMenge($lease, Lizenzpruefung::PROGNOSE_PV) > 0
&& Lizenzpruefung::berechtigungsMenge(
$lease,
Lizenzpruefung::PROGNOSE_VERBRAUCH
) > 0);
} }
private function berechtigungLizenziert(string $kennung, ?string $lizenzcode = null): bool private function berechtigungLizenziert(string $kennung, ?string $lizenzcode = null): bool
@@ -2418,7 +2413,10 @@ class Manager extends IPSModule implements ManagerSchnittstelle
if (!$lizenziert) { if (!$lizenziert) {
return 'Prognosen sind deaktiviert. Fuer die Manager-ID ' return 'Prognosen sind deaktiviert. Fuer die Manager-ID '
. $managerID . $managerID
. ' fehlen forecast_pv plus forecast_load oder grid_schedule.'; . ' ist keine Prognoseberechtigung vorhanden. Benoetigt wird entweder '
. 'PV-Ertragsprognose zusammen mit Verbrauchsprognose oder der '
. 'Intelligente Netzfahrplan. Nach der Erweiterung im Lizenzportal '
. 'die Lizenz erneut pruefen und binden.';
} }
return ($aktiv ? 'Prognosen sind aktiviert. ' : 'Prognosen sind deaktiviert. ') return ($aktiv ? 'Prognosen sind aktiviert. ' : 'Prognosen sind deaktiviert. ')
+6
View File
@@ -90,6 +90,12 @@ melden, ohne dass der Manager seine interne Geraetelogik kennen muss.
## Feste Regeln ## Feste Regeln
- Prioritaeten beginnen bei 0; eine kleinere Zahl bedeutet hoehere Prioritaet. - Prioritaeten beginnen bei 0; eine kleinere Zahl bedeutet hoehere Prioritaet.
- Jeder verfuegbare, synchronisierte Verbraucher mit einem nicht leeren
`Leistungswerte_W`-Angebot erhaelt einen Sollwert aus genau diesem Angebot.
- `AenderungMoeglich=false` kennzeichnet ein fixes Angebot und ist kein
Ausschlussgrund. Ein Angebot `[11000]` fuehrt deshalb zwingend zu `11000 W`.
- Prioritaeten verteilen nur die ueber den jeweiligen Mindestwert hinaus
verfuegbare Leistung; sie duerfen keinen angebotenen Mindestwert verdrängen.
- `Leistungsquelle`: 0 nicht vorhanden, 1 berechnet, 2 gemessen. - `Leistungsquelle`: 0 nicht vorhanden, 1 berechnet, 2 gemessen.
- Bei Leistungsquelle 0 ist `Istleistung_W` zwingend `null`. - Bei Leistungsquelle 0 ist `Istleistung_W` zwingend `null`.
- Leistungsbereiche enthalten jeden ganzen Wattwert von `Von_W` bis `Bis_W`. - Leistungsbereiche enthalten jeden ganzen Wattwert von `Von_W` bis `Bis_W`.
+14 -8
View File
@@ -279,6 +279,7 @@ stabile ID gebunden werden.
| HTTP `429` | Zu viele Aktivierungsversuche; vor dem naechsten Versuch warten. | | HTTP `429` | Zu viele Aktivierungsversuche; vor dem naechsten Versuch warten. |
| `Lizenzserver nicht erreichbar` | DNS, Internetzugang, HTTPS und Systemzeit des Symcon-Systems pruefen. Eine bestehende Lease gilt nur bis `offlineUntil`. | | `Lizenzserver nicht erreichbar` | DNS, Internetzugang, HTTPS und Systemzeit des Symcon-Systems pruefen. Eine bestehende Lease gilt nur bis `offlineUntil`. |
| `Offline-Freigabe ist abgelaufen.` | Verbindung zum Lizenzserver wiederherstellen und Lizenz erneut pruefen. | | `Offline-Freigabe ist abgelaufen.` | Verbindung zum Lizenzserver wiederherstellen und Lizenz erneut pruefen. |
| `Keine Prognoseberechtigung vorhanden` | Im Lizenzportal entweder PV-Ertragsprognose zusammen mit Verbrauchsprognose oder den Intelligenten Netzfahrplan ergaenzen. Anschliessend im Manager `Lizenz pruefen und binden` ausloesen, die Prognose aktivieren und die Konfiguration speichern. |
Fuer eine genauere Diagnose koennen die Diagnosevariablen eingeblendet werden. Fuer eine genauere Diagnose koennen die Diagnosevariablen eingeblendet werden.
`Lizenzstatus` zeigt dann den aktuellen Zustand. Mit aktiviertem Debug-Logging `Lizenzstatus` zeigt dann den aktuellen Zustand. Mit aktiviertem Debug-Logging
@@ -302,14 +303,19 @@ der aktiven Betriebsart und der aktuellen Leistung aller frisch gemeldeten
Verbraucher. Eine gültige Istleistung wird bevorzugt; fehlt sie, bleibt der im Verbraucher. Eine gültige Istleistung wird bevorzugt; fehlt sie, bleibt der im
Zustand bestätigte Sollwert konservativ reserviert. Zustand bestätigte Sollwert konservativ reserviert.
Nicht verfügbare, nicht änderbare oder aktuell angebotlose Verbraucher behalten Nicht verfügbare oder aktuell angebotlose Verbraucher behalten ihre Leistung
ihre Leistung und erhalten keine neue Vorgabe. Die übrigen Verbraucher werden und erhalten keine neue Vorgabe. Jeder verfügbare, synchronisierte Verbraucher
nach der gemeldeten PV- beziehungsweise Peak-Priorität und danach stabil nach mit einem nicht leeren Angebot erhält dagegen einen Sollwert aus diesem Angebot.
Instanz-ID sortiert. Von ihrem jeweils kleinsten erlaubten Leistungswert aus Das gilt auch bei `AenderungMoeglich=false`: Ein fixes Angebot wie `[11000]`
wird das Budget in dieser Reihenfolge aufgefüllt. Einzelwerte und ganzzahlige wird mit genau `11000 W` zugeteilt.
Bereiche werden direkt verarbeitet; Lücken werden nie durch unzulässige Werte
geschlossen. Eine verbleibende Abweichung wird im `Verbraucherstatus` Die Verbraucher werden nach der gemeldeten PV- beziehungsweise Peak-Priorität
dokumentiert. und danach stabil nach Instanz-ID sortiert. Zuerst reserviert der Manager für
jeden Verbraucher den kleinsten erlaubten Leistungswert. Nur das danach
verbleibende Budget wird in Prioritätsreihenfolge aufgefüllt. Einzelwerte und
ganzzahlige Bereiche werden direkt verarbeitet; Lücken werden nie durch
unzulässige Werte geschlossen. Eine verbleibende Abweichung wird im
`Verbraucherstatus` dokumentiert.
Die Betriebsart wechselt unterhalb des Solar-Sollwerts zu `PV` und oberhalb Die Betriebsart wechselt unterhalb des Solar-Sollwerts zu `PV` und oberhalb
der wirksamen Lastspitzengrenze zu `Peak`. Zwischen den beiden Zielwerten der wirksamen Lastspitzengrenze zu `Peak`. Zwischen den beiden Zielwerten
+13
View File
@@ -117,6 +117,19 @@ final class Lizenzpruefung
return $menge; return $menge;
} }
/**
* Eine Prognose ist entweder durch den Netzfahrplan oder durch beide
* Einzelberechtigungen fuer PV und Verbrauch freigeschaltet.
*
* @param array<string, mixed> $lease
*/
public static function hatPrognoseBerechtigung(array $lease): bool
{
return self::berechtigungsMenge($lease, self::NETZFAHRPLAN) > 0
|| (self::berechtigungsMenge($lease, self::PROGNOSE_PV) > 0
&& self::berechtigungsMenge($lease, self::PROGNOSE_VERBRAUCH) > 0);
}
/** /**
* Rechnet freie, bestehende Einzellizenzen auf nicht separat lizenzierte Waermepumpen an. * Rechnet freie, bestehende Einzellizenzen auf nicht separat lizenzierte Waermepumpen an.
* *
+1 -1
View File
@@ -120,7 +120,7 @@ final class ManagerRegler
$aktuelleLeistung += $leistung; $aktuelleLeistung += $leistung;
$daten = $eintrag['Daten']; $daten = $eintrag['Daten'];
if (!$daten['Verfuegbar'] || !$daten['AenderungMoeglich'] || $daten['Leistungswerte_W'] === []) { if (!$daten['Verfuegbar'] || $daten['Leistungswerte_W'] === []) {
$nichtSteuerbar += $leistung; $nichtSteuerbar += $leistung;
continue; continue;
} }
+41
View File
@@ -228,6 +228,47 @@ final class LizenzpruefungTest extends TestCase
Lizenzpruefung::pruefeVerbrauchermengen($lease, $pruefmengen); Lizenzpruefung::pruefeVerbrauchermengen($lease, $pruefmengen);
} }
public function testPvUndVerbrauchGebenPrognoseFrei(): void
{
$lease = $this->lease('manager_standard');
$lease['entitlements'][] = [
'catalog_key' => Lizenzpruefung::PROGNOSE_PV,
'quantity' => 1,
'status' => 'active',
];
$lease['entitlements'][] = [
'catalog_key' => Lizenzpruefung::PROGNOSE_VERBRAUCH,
'quantity' => 1,
'status' => 'active',
];
self::assertTrue(Lizenzpruefung::hatPrognoseBerechtigung($lease));
}
public function testEinzelnePrognoseBerechtigungReichtNichtAus(): void
{
$lease = $this->lease('manager_standard');
$lease['entitlements'][] = [
'catalog_key' => Lizenzpruefung::PROGNOSE_PV,
'quantity' => 1,
'status' => 'active',
];
self::assertFalse(Lizenzpruefung::hatPrognoseBerechtigung($lease));
}
public function testNetzfahrplanGibtPrognoseAlleineFrei(): void
{
$lease = $this->lease('manager_standard');
$lease['entitlements'][] = [
'catalog_key' => Lizenzpruefung::NETZFAHRPLAN,
'quantity' => 1,
'status' => 'active',
];
self::assertTrue(Lizenzpruefung::hatPrognoseBerechtigung($lease));
}
public function testLizenzcodeWirdWieAufDemServerNormalisiert(): void public function testLizenzcodeWirdWieAufDemServerNormalisiert(): void
{ {
self::assertSame( self::assertSame(
+14 -7
View File
@@ -113,19 +113,19 @@ final class ManagerReglerTest extends TestCase
self::assertSame(500, $ergebnis['Restabweichung_W']); self::assertSame(500, $ergebnis['Restabweichung_W']);
} }
public function testNichtAenderbarerVerbraucherReserviertSeineIstleistung(): void public function testFestwertWirdUnabhaengigVonAenderungMoeglichZugewiesen(): void
{ {
$fest = $this->verbraucher(10, 0, [0, 3000], 3000, false); $fest = $this->verbraucher(10, 0, [3000], 3000, false);
$steuerbar = $this->verbraucher(20, 1, [0, ['Von_W' => 1, 'Bis_W' => 4000]], 0); $steuerbar = $this->verbraucher(20, 1, [0, ['Von_W' => 1, 'Bis_W' => 4000]], 0);
$ergebnis = ManagerRegler::verteile(3000.0, 5000.0, [$fest, $steuerbar], 'PrioritaetPV'); $ergebnis = ManagerRegler::verteile(3000.0, 5000.0, [$fest, $steuerbar], 'PrioritaetPV');
self::assertSame(3000, $ergebnis['NichtSteuerbar_W']); self::assertSame(0, $ergebnis['NichtSteuerbar_W']);
self::assertSame([20 => 2000], $ergebnis['Sollwerte']); self::assertSame([10 => 3000, 20 => 2000], $ergebnis['Sollwerte']);
self::assertSame(0, $ergebnis['Restabweichung_W']); self::assertSame(0, $ergebnis['Restabweichung_W']);
} }
public function testVeralteterVerbraucherBlockiertUeberschussverteilungNicht(): void public function testFestwerteWerdenMitHoeherPriorisierterBatterieZugewiesen(): void
{ {
$ergebnis = ManagerRegler::verteile( $ergebnis = ManagerRegler::verteile(
-2411.0, -2411.0,
@@ -147,7 +147,13 @@ final class ManagerReglerTest extends TestCase
self::assertSame(12562, $ergebnis['Verteilbudget_W']); self::assertSame(12562, $ergebnis['Verteilbudget_W']);
self::assertSame( self::assertSame(
[44234 => 2411, 29743 => 0, 38676 => 0], [
44234 => -12288,
29743 => 0,
38676 => 0,
43878 => 13850,
46052 => 11000,
],
$ergebnis['Sollwerte'] $ergebnis['Sollwerte']
); );
self::assertSame(0, $ergebnis['Restabweichung_W']); self::assertSame(0, $ergebnis['Restabweichung_W']);
@@ -155,7 +161,7 @@ final class ManagerReglerTest extends TestCase
public function testFehlendeIstleistungFaelltAufAngenommenenSollwertZurueck(): void public function testFehlendeIstleistungFaelltAufAngenommenenSollwertZurueck(): void
{ {
$verbraucher = $this->verbraucher(10, 0, [0, 3000], null, false); $verbraucher = $this->verbraucher(10, 0, [3000], null, false);
$verbraucher['Daten']['Leistungsquelle'] = Nachrichtenvertrag::LEISTUNGSQUELLE_NICHT_VORHANDEN; $verbraucher['Daten']['Leistungsquelle'] = Nachrichtenvertrag::LEISTUNGSQUELLE_NICHT_VORHANDEN;
$verbraucher['Daten']['Istleistung_W'] = null; $verbraucher['Daten']['Istleistung_W'] = null;
$verbraucher['Daten']['Zustand'][0]['Wert'] = 3000; $verbraucher['Daten']['Zustand'][0]['Wert'] = 3000;
@@ -163,6 +169,7 @@ final class ManagerReglerTest extends TestCase
$ergebnis = ManagerRegler::verteile(3000.0, 0.0, [$verbraucher], 'PrioritaetPV'); $ergebnis = ManagerRegler::verteile(3000.0, 0.0, [$verbraucher], 'PrioritaetPV');
self::assertSame(3000, $ergebnis['AktuelleVerbraucherleistung_W']); self::assertSame(3000, $ergebnis['AktuelleVerbraucherleistung_W']);
self::assertSame([10 => 3000], $ergebnis['Sollwerte']);
self::assertSame(-3000, $ergebnis['Restabweichung_W']); self::assertSame(-3000, $ergebnis['Restabweichung_W']);
} }