4-Stufen (Phasen)-Schaltung mit Rotation nach Verbrauch

Um die Abnutzung bzw. Verwendung der 3 Heizstäbe einer Einbauheizung möglichst auszugleichen, habe ich mir ein kleines Skript erstellt, welches das Zu- bzw. Wegschalten von Stufen/Phasen anhand des jeweiligen (Strom-)Verbrauchs regelt.

Beim Zuschalten sollte die jeweilige(n) Phase(n) mit dem niedrigsten Verbrauch zuerst zugeschaltet werden, und umgekehrt.

Das Skript scheint soweit sogar zu funktionieren. :sweat_smile:
Was denkt Ihr dazu? Irgendwelche Ideen oder Vorschläge?

<?php
/**
 * Stufenschaltung für 3-Phasen Heizeinheit mit Rotation nach Verbrauch
 * 
 * Stufe 0 = Aus (0 Phasen)
 * Stufe 1 = Niedrig (1 Phase)
 * Stufe 2 = Mittel (2 Phasen)
 * Stufe 3 = Hoch (3 Phasen)
 */

// ===== KONFIGURATION: Object-IDs anpassen! =====

// Logging aktivieren/deaktivieren
$logging = false;  // true = Logging aktiv, false = kein Logging

// Schalter für die drei Phasen (Boolean)
$idSchalterL1 = 37173;
$idSchalterL2 = 27213;
$idSchalterL3 = 35071;

// Verbrauchszähler für die drei Phasen (Float, Einheit irrelevant)
$idVerbrauchL1 = 13871;
$idVerbrauchL2 = 31998;
$idVerbrauchL3 = 49869;

// ===== HAUPTLOGIK =====

// Gewünschte Stufe aus Aktionsskript-Ereignis auslesen
$stufe = $_IPS['VALUE'];

// Wert in Variable übernehmen
SetValue($_IPS['VARIABLE'], $stufe);

// Validierung
if ($stufe < 0 || $stufe > 3) {
    echo "Fehler: Stufe muss zwischen 0 und 3 liegen!\n";
    exit;
}

// Aktuellen Schaltzustand auslesen
$aktuellerZustand = [
    'L1' => GetValue($idSchalterL1),
    'L2' => GetValue($idSchalterL2),
    'L3' => GetValue($idSchalterL3)
];

// Aktuell eingeschaltete Phasen ermitteln
$phasenEin = [];
foreach ($aktuellerZustand as $phase => $zustand) {
    if ($zustand) {
        $phasenEin[] = $phase;
    }
}

$aktuelleAnzahl = count($phasenEin);

// Debug-Logging (erscheint im System-Log, nicht als Fehlermeldung)
if ($logging) {
    IPS_LogMessage("Stufenschaltung", "Gewünschte Stufe: $stufe (= $stufe Phasen)");
    IPS_LogMessage("Stufenschaltung", "Aktuell eingeschaltet: " . ($aktuelleAnzahl > 0 ? implode(', ', $phasenEin) : "keine") . " ($aktuelleAnzahl Phasen)");
}

// Wenn bereits die richtige Anzahl Phasen eingeschaltet ist, nichts tun
if ($aktuelleAnzahl == $stufe) {
    if ($logging) IPS_LogMessage("Stufenschaltung", "Bereits korrekt geschaltet - keine Änderung notwendig");
    exit;
}

// Verbrauchswerte auslesen
$verbrauch = [
    'L1' => GetValue($idVerbrauchL1),
    'L2' => GetValue($idVerbrauchL2),
    'L3' => GetValue($idVerbrauchL3)
];

if ($logging) IPS_LogMessage("Stufenschaltung", "Verbrauch: L1={$verbrauch['L1']} , L2={$verbrauch['L2']} , L3={$verbrauch['L3']} ");

// FALL 1: Stufe erhöhen (Phasen dazuschalten)
if ($stufe > $aktuelleAnzahl) {
    $anzahlDazu = $stufe - $aktuelleAnzahl;
    if ($logging) IPS_LogMessage("Stufenschaltung", "Erhöhe Stufe: $anzahlDazu Phase(n) dazuschalten");
    
    // Noch nicht eingeschaltete Phasen finden
    $phasenAus = [];
    foreach (['L1', 'L2', 'L3'] as $phase) {
        if (!in_array($phase, $phasenEin)) {
            $phasenAus[$phase] = $verbrauch[$phase];
        }
    }
    
    // Nach Verbrauch sortieren (niedrigster zuerst)
    asort($phasenAus);
    
    // Die ersten $anzahlDazu Phasen mit niedrigstem Verbrauch dazuschalten
    $i = 0;
    foreach ($phasenAus as $phase => $wert) {
        if ($i < $anzahlDazu) {
            $phasenEin[] = $phase;
            if ($logging) IPS_LogMessage("Stufenschaltung", "→ Schalte $phase dazu (Verbrauch: $wert)");
            $i++;
        }
    }
}

// FALL 2: Stufe reduzieren (Phasen ausschalten)
elseif ($stufe < $aktuelleAnzahl) {
    $anzahlWeg = $aktuelleAnzahl - $stufe;
    if ($logging) IPS_LogMessage("Stufenschaltung", "Reduziere Stufe: $anzahlWeg Phase(n) ausschalten");
    
    // Von eingeschalteten Phasen die mit höchstem Verbrauch ermitteln
    $phasenEinMitVerbrauch = [];
    foreach ($phasenEin as $phase) {
        $phasenEinMitVerbrauch[$phase] = $verbrauch[$phase];
    }
    
    // Nach Verbrauch sortieren (höchster zuerst)
    arsort($phasenEinMitVerbrauch);
    
    // Die ersten $anzahlWeg Phasen mit höchstem Verbrauch ausschalten
    $i = 0;
    foreach ($phasenEinMitVerbrauch as $phase => $wert) {
        if ($i < $anzahlWeg) {
            $phasenEin = array_diff($phasenEin, [$phase]);
            if ($logging) IPS_LogMessage("Stufenschaltung", "→ Schalte $phase aus (Verbrauch: $wert)");
            $i++;
        }
    }
}

// Schalter setzen
RequestAction($idSchalterL1, in_array('L1', $phasenEin));
RequestAction($idSchalterL2, in_array('L2', $phasenEin));
RequestAction($idSchalterL3, in_array('L3', $phasenEin));

// Debug-Ausgabe
if ($logging) IPS_LogMessage("Stufenschaltung", "Neue Schaltung: L1=" . (in_array('L1', $phasenEin) ? "EIN" : "AUS") . ", L2=" . (in_array('L2', $phasenEin) ? "EIN" : "AUS") . ", L3=" . (in_array('L3', $phasenEin) ? "EIN" : "AUS"));
?>

Hi phoibos,

erst einmal ein herzliches Willkommen im Forum :slight_smile:

Klasse, dass du deine Ideen mit uns teilst.

In welcher Richtung möchtest du Vorschläge erhalten? Geht es dir eher um die Programmiertechnik in Symcon oder mehr um den Lösungsansatz?

Zur Programmiertechnik:

hier würde ich erst einmal

declare(strict_types=1); 

setzen, damit es nicht zu unerwarteten Effekten kommt.

Dann würde ich die Konfiguration zu einem Array ändern:

// Phasen: Schalter (Bool) + Verbrauch (Float)
$phasen = [
    'L1' => ['switch' => 37173, 'consumption' => 13871],
    'L2' => ['switch' => 27213, 'consumption' => 31998],
    'L3' => ['switch' => 35071, 'consumption' => 49869],
];

Deine Lösung mit dem hoch und runterschalten finde ich super. Cool wäre noch, wenn eine Mindesthaltezeit eingestellt werden könnte, damit ein Flattern verhindert wird. Ich vermute, das Skript wird durch eine Automatik aufgerufen und da kann es bestimmt schnell zu einem „Flattern“ (1↔2↔1↔2 …) kommen, besonders wenn die Stufe aus einem schwankenden Messwert/Regler kommt.

Da ich die Aufgabenstellung spannend finde, habe ich mal ein bisschen mit deinem Skript und „meiner KI“ :slight_smile: gespielt und diskutiert. Ich stelle das Ergebnis mal hier ab. Vielleicht interessiert es dich. Ob es wirklich funktioniert, kann ich nicht sagen, aber es sieht sehr gut aus :joy:

<?php
declare(strict_types=1);

/**
 * Stufenschaltung für 3-Phasen-Heizeinheit mit Rotation nach Verbrauch
 *
 * Stufe 0 = Aus (0 Phasen)
 * Stufe 1 = Niedrig (1 Phase)
 * Stufe 2 = Mittel (2 Phasen)
 * Stufe 3 = Hoch (3 Phasen)
 *
 * Features:
 * - Semaphore verhindert parallele Läufe
 * - Mindesthaltezeit schützt Schütze/Kontaktoren
 * - Verzögertes Nachziehen: Sollwert wird gemerkt und nach Ablauf ausgeführt (keine Aufträge verlieren)
 */

// ===== KONFIGURATION =====
const LOGGING = true;
const LOG_CONTEXT = 'Stufenschaltung';

const MIN_HOLD_SEC = 60;
const SEMAPHORE_NAME = 'Heizung_3Phasen_Stufenschaltung';

// Merker-Variablen (Integer!) – bitte anlegen und IDs eintragen
const ID_LAST_STAGE    = 11111; // zuletzt erfolgreich geschaltete Stufe (0..3)
const ID_LAST_STAGE_TS = 22222; // Timestamp der letzten erfolgreichen Umschaltung (Unix time)
const ID_PENDING_STAGE = 33333; // gemerkter Sollwert, -1 = keiner

// ===== Hilfsfunktionen =====
function logMsg(string $msg): void
{
    if (LOGGING) {
        IPS_LogMessage(LOG_CONTEXT, $msg);
    }
}

function validateStage(int $stage): bool
{
    return $stage >= 0 && $stage <= 3;
}

function schedulePending(int $selfId, int $stage, int $seconds): void
{
    // Pending immer überschreiben: bei Sollwerten zählt der letzte
    SetValueInteger(ID_PENDING_STAGE, $stage);

    $seconds = max(1, $seconds);
    IPS_SetScriptTimer($selfId, $seconds);

    logMsg("PendingStage=$stage gesetzt, Timer=$seconds s");
}

/**
 * Wendet die Stufe an (Rotation nach Verbrauch).
 * @param int   $requestedStage 0..3
 * @param array $phasen Konfig: ['L1'=>['switch'=>id,'consumption'=>id], ...]
 *
 * @return bool true, wenn geschaltet wurde, false wenn bereits passend oder Fehler
 */
function applyStage(int $requestedStage, array $phasen): bool
{
    // Ist-Zustand + Verbrauch lesen
    $aktuellerZustand = [];
    $verbrauch = [];

    foreach ($phasen as $phase => $cfg) {
        $switchId = (int)$cfg['switch'];
        $consId   = (int)$cfg['consumption'];

        if (!@IPS_ObjectExists($switchId) || !@IPS_ObjectExists($consId)) {
            logMsg("Abbruch: Objekt fehlt für $phase (switch/consumption)");
            return false;
        }

        $aktuellerZustand[$phase] = (bool)GetValue($switchId);
        $verbrauch[$phase]        = (float)GetValue($consId);
    }

    $phasenEin = [];
    foreach ($aktuellerZustand as $phase => $isOn) {
        if ($isOn) {
            $phasenEin[] = $phase;
        }
    }
    $aktuelleAnzahl = count($phasenEin);

    logMsg("ApplyStage=$requestedStage, Aktuell=$aktuelleAnzahl, Ein=[" . implode(',', $phasenEin) . "]");

    if ($aktuelleAnzahl === $requestedStage) {
        logMsg('Bereits korrekt, keine Änderung');
        return false;
    }

    $einSet = array_fill_keys($phasenEin, true);

    // Rotation nach Verbrauch
    if ($requestedStage > $aktuelleAnzahl) {
        $anzahlDazu = $requestedStage - $aktuelleAnzahl;
        logMsg("Erhöhe: $anzahlDazu Phase(n) zuschalten (niedrigster Verbrauch zuerst)");

        $kandidaten = [];
        foreach (array_keys($phasen) as $phase) {
            if (!isset($einSet[$phase])) {
                $kandidaten[$phase] = $verbrauch[$phase];
            }
        }
        asort($kandidaten); // niedrigster zuerst

        foreach ($kandidaten as $phase => $wert) {
            if ($anzahlDazu <= 0) {
                break;
            }
            logMsg("→ Schalte $phase dazu (Verbrauch: $wert)");
            $einSet[$phase] = true;
            $anzahlDazu--;
        }
    } else {
        $anzahlWeg = $aktuelleAnzahl - $requestedStage;
        logMsg("Reduziere: $anzahlWeg Phase(n) abschalten (höchster Verbrauch zuerst)");

        $kandidaten = [];
        foreach (array_keys($einSet) as $phase) {
            $kandidaten[$phase] = $verbrauch[$phase];
        }
        arsort($kandidaten); // höchster zuerst

        foreach ($kandidaten as $phase => $wert) {
            if ($anzahlWeg <= 0) {
                break;
            }
            logMsg("→ Schalte $phase aus (Verbrauch: $wert)");
            unset($einSet[$phase]);
            $anzahlWeg--;
        }
    }

    // Schalten
    foreach ($phasen as $phase => $cfg) {
        $switchId = (int)$cfg['switch'];
        $targetOn = isset($einSet[$phase]);
        RequestAction($switchId, $targetOn);
    }

    logMsg('Neue Schaltung: ' .
           'L1=' . (isset($einSet['L1']) ? 'EIN' : 'AUS') . ', ' .
           'L2=' . (isset($einSet['L2']) ? 'EIN' : 'AUS') . ', ' .
           'L3=' . (isset($einSet['L3']) ? 'EIN' : 'AUS')
    );

    return true;
}

// ===== PHASEN-KONFIGURATION =====
$phasen = [
    'L1' => ['switch' => 37173, 'consumption' => 13871],
    'L2' => ['switch' => 27213, 'consumption' => 31998],
    'L3' => ['switch' => 35071, 'consumption' => 49869],
];

// ===== Hauptlogik =====
$selfId = (int)($_IPS['SELF'] ?? 0);
$sender = (string)($_IPS['SENDER'] ?? '');

if ($selfId === 0) {
    logMsg('Abbruch: $_IPS[SELF] fehlt/0');
    return;
}

// Sollwert bestimmen:
// - TimerEvent: Pending anwenden
// - sonst: $_IPS['VALUE'] ist Sollstufe
if ($sender === 'TimerEvent') {
    $requestedStage = (int)GetValue(ID_PENDING_STAGE);

    if ($requestedStage < 0) {
        IPS_SetScriptTimer($selfId, 0);
        return;
    }

    logMsg("TimerEvent: PendingStage=$requestedStage wird angewendet");
} else {
    if (!isset($_IPS['VALUE'])) {
        logMsg('Abbruch: $_IPS[VALUE] fehlt');
        return;
    }
    $requestedStage = (int)$_IPS['VALUE'];
}

if (!validateStage($requestedStage)) {
    logMsg("Abbruch: ungültige Stufe: $requestedStage (erwartet 0..3)");
    return;
}

// Semaphore: wenn belegt, nichts verlieren => pending speichern und bald retry
if (!IPS_SemaphoreEnter(SEMAPHORE_NAME, 2000)) {
    schedulePending($selfId, $requestedStage, 2);
    logMsg('Semaphore belegt: verzögere Ausführung (Retry in 2s)');
    return;
}

try {
    $now = time();
    $lastStage = (int)GetValue(ID_LAST_STAGE);
    $lastTs    = (int)GetValue(ID_LAST_STAGE_TS);

    // Mindesthaltezeit: bei zu früher Änderung => pending + Timer auf Restzeit
    if (MIN_HOLD_SEC > 0 && $lastTs > 0 && $requestedStage !== $lastStage) {
        $age = $now - $lastTs;
        if ($age < MIN_HOLD_SEC) {
            $remaining = MIN_HOLD_SEC - $age;
            schedulePending($selfId, $requestedStage, $remaining);

            logMsg(sprintf(
                       'Änderung %d→%d verzögert (noch %ds)',
                       $lastStage,
                       $requestedStage,
                       $remaining
                   ));
            return;
        }
    }

    // Ausführen
    $didSwitch = applyStage($requestedStage, $phasen);

    if ($didSwitch) {
        SetValueInteger(ID_LAST_STAGE, $requestedStage);
        SetValueInteger(ID_LAST_STAGE_TS, $now);
    } elseif ($lastStage !== $requestedStage) {
        SetValueInteger(ID_LAST_STAGE, $requestedStage);
        SetValueInteger(ID_LAST_STAGE_TS, $now);
    }

    // Pending gelöscht, Timer aus
    SetValueInteger(ID_PENDING_STAGE, -1);
    IPS_SetScriptTimer($selfId, 0);
} finally {
    IPS_SemaphoreLeave(SEMAPHORE_NAME);
}

Du musst nur die drei Merker-IDs (Integer-Variablen) eintragen/anlegen: ID_LAST_STAGE, ID_LAST_STAGE_TS, ID_PENDING_STAGE.
ID_PENDING_STAGE solltest du initial auf -1 setzen.

Burkhard

Hallo Burkhard, danke für eine Antwort.

Ich hätte vermutlich dazu schreiben sollen, dass ich mir mit PHP ungalublich schwer tue und auch auf AI-Hilfe angewiesen bin. Aber AI und ich scheinen das eh recht gut hinbekommen zu haben. :sweat_smile:

Deine Idee mit der Warteschlange und der Mindestlaufzeit ist brilliant!

Ich habe mir unsere Warmwasserheizung umgebaut auf 3 einzelne Stufen, weil ich hoffe, den Energiemanager künftig nutzen zu können. Ein Flattern sollte weitestgehend dadurch verhindert werden, aber ich nutze auch gerne Fallbacks, nur zur Sicherheit.

Zu meinem Verständnis: Semaphore verhindert gleichzeitige Ausführung aber die Befehle werden gespeichert und mit Retry wiederholt, ist das korrekt?

Richtig, die Semaphore verhindert ein gleichzeitiges Ausführen. Wenn die Semaphore bereits belegt ist, wird zunächst gewartet, bis sie wieder frei ist, aber maximal 2 Sekunden lang.

Sollte sie wider Erwarten doch einmal nicht frei werden, dann wird ‚pending‘ gespeichert und ein ScriptTimer mit 2 Sekunden gestartet.
Der Vorschlag der KI ist hier perfekt.

1 „Gefällt mir“