Hoymiles HiBattery 1920 AC über MQTT an IPS

Moin , habe seit zwei Wochen einen AC-Speicher Hoymiles HiBattery 1920 AC (Datenblatt müsst ihr euch mal im Netz anschauen) hier im Einsatz, der in der letzten SW-Version auch MQTT -Service anbietet. Möchte hier kurz meine bisherigen Erfahrungen schildern. (Wobei ich bemerken möchte, dass ich noch alter WF-Fan und PHP-Scripte-Fan bin)
Die Einrichtung mit der S-Miles-Home-APP (v2.7.2) unter IPS 8.1/WIN10 lief ohne Probleme. Schaltet man anschließend die Kopplung auf Bluetooth um, ist auch eine Cloud-Verbindung nicht mehr erforderlich.
Die Einbindung über „MQTT Client Gerät“en an IPS , ermöglicht das Erfassen der Batterie-Parameter in drei Blöcken im „Quick“ (1sec-Takt), „Device“ u. „System“ (5min-Takt). Nutze dazu den IPS-MQTT-Broker und einen separaten Port 1887.
Eine „Watt-bezogene“ Ansteuerung des Speichers zum Laden/Entladen ist über „MQTT Client Gerät“ ebenfalls im Mode „mqtt_ctrl“ ohne Probleme möglich.
Da ich keinen Shelly Pro 3EM 3-Phasen Stromverbrauchsmessgerät, sondern mein go-eController für die Messung der Einspeisung/Verbrauchswerte besitze, musste ich eine rudimentäre PV-Überschußregelung selbst in PHP umsetzen.
Habe bisher in IPS verschiedene eigene Betriebsarten umgesetzt (Test laufen noch , doch leider spielt die Sonne z.Z. nicht mit) :
-Fest-Laden: Speicher wird mit einer festen Watt-Vorgabe geladen , bis zu einer max. SOC-Grenze
-Fest-Entladen: Speicher wird mit einer festen Watt-Vorgabe entladen , bis zu einer min. SOC-Grenze
-Auto-Mode : noch im Test , Speicher mit Grid-Überschuß laden , wenn Hausbedarf vorhanden wird teilweise aus dem geladenen Speicher Spitzen abgedeckt.
-Daily-Chg-ReChg: tagsüber wird der Grid-Überschuß in den Speicher geladen und nachts mit einer festen Watt-Vorgabe wieder entladen , bis min SOC erreicht ist. (wird wohl der Standartmode bei mir sein)
Versuch nur aus tech. Interesse: Wie gesagt läuft obiges ohne Cloud-Einbindung. Trotzdem versuche ich seit zwei Tagen , leider vergeblich, dem Speicher meinen go-eController als Shelly Pro 3EM vorzugaukeln (dazu aber Cloudzwang nötig).
Der Speicher selbst hat interne Betriebsarten wo er mit Hilfe von Drittanbietern wie Shelly Pro 3EM , ECO Tracker IR , Shelly Plus Plug S oder Hoymiles -Zählern selbst eine Überschuß-Regelung aufbaut.
Mein bisheriger Stand : Die Hoymiles-APP erkennt meinen PHP-Script mit Shelly Pro 3EM-Simulation (Leistungs-Inputwerte kommen vom go-eController) als Gerät an, aber die gelieferten Daten bzw Datenstruktur erkennt er nicht als echte 3EM-Werte und bricht dann ab. Vielleicht hat dazu ja jemand Erfahrung hier.
Fazit: Für mich interessanter, steuerbarer, kompakter AC-Speicher , der im Prinzip an jede Schuko-Steckdose angeschlossen werden kann (elktr. Randbedingungen beachten). Braucht nicht zwingend eine Verbindung zum PV-Wechselrichter. Kann im Notfall als größere 230VAC-Powerbank genutzt werden.
Interessante Infos auch unter :
https://www.photovoltaikforum.com/thread/252329-nachfolger-des-ac-speichers-hoymiles-ms-a2-hibattery-1920-ac-erfahrungsaustausch/
Anmerkung: Bin in keinster Art und Weise mit Hoymiles oder irgendwelchen Verkäufern verbandelt.

1 „Gefällt mir“

Was bekommst Du vom go-eController nach Symcon?
MQTT Daten?
Und was brauchst Su an Hilfe? Die Daten vom go-eController als MQTT zum Speicher oder was anderes?

vom goe-Controller bekomm ich sowohl meine Grid-Leistungswerte über MQTT wie auch über ModBus , pro Phase , wie auch als GesamtLeistung. Die sind plausibel und die verwende ich auch für andere Scripts. Das ist nicht das Problem. MQTT-mäßig kann ich den Speicher steuern und mit meinen Daten versorgen.
Mein Problem ist , dem Speicher ein Shelly Pro3em vorzumachen , dazu müssen
diese Werte als Key-Value Pairs innerhalb eines JSON-Objekts in korrekter Syntax/Struktur wie auch Werteformat an die Hoymiles-APP übergeben werden, damit die APP meint es ist ein echter Shelly Pro 3EM vorhanden. Das passiert in mehreren Schritten. Die APP fragt:

  1. nach Shelly.GetDeviceInfo das ist soweit i.O und die APP akzeptiert meine SIM-Antwort und zeigt mir einen 3EM an
  2. nach EM.GetConfig , da kämpfe ich mit , weil anscheinend die JSON-Daten nicht “3EM-normgerecht” sind.
    Aber wie ich schon schrieb ist das nur tech. Interesse , da die MQTT-Anbindung des Speichers ohne Probleme funktioniert.

Ich habe keinen Shelly 3EM , deshalb kann ich Dir den JSON String auch nicht bringen.
Aber hier gibt es bestimmt Shelly User. Toi Toi Toi :wink:

habs es jetzt mit dem Ecotracker gemacht , damit geht es einfacher und funktioniert auch ohne Probleme.

Gestern habe ich meine 1920AC erhalten.
Deine Hinweise zu den Topics haben mir weitergeholfen.
Gibt es irgendwo eine Beschreibung der MQTT-Topics und der einzelnen Werte, bzw. der Befehle?

Edit: Habe einen Link gefunden, der einiges zu bieten hat:
https://www.photovoltaikforum.com/core/file-download/556128/

Ich komme da nicht klar. Wie machst du das denn?
Ich habe für die Topics … quick, device und system je ein MQTT-Gerät eingerichtet und die ganzen Variablen eingerichtet. Die werden im 1-Sek/5Min. Takt gefüllt.
Aber bei z.B. PowerSet habe ich Probleme

9. Power Control (Device Subscription)

Note: Subscribed and responded by the device. This topic is only supported by the master unit and standalone units.

Topic:
homeassistant/number/<dev_id>/power_ctrl/set

Payload:
80.0

QoS:
1

Retain:
false

Note:
This function takes effect only after setting the mode to mqtt_ctrl via the homeassistant/select/<dev_id>/ems_mode/command topic.
The payload range is as defined in the min and max of the homeassistant/number/<dev_id>/power_ctrl/config topic.
This topic needs to be published periodically with an interval of no less than 1 minute; otherwise, the device will automatically switch to self-consumption mode.


Moin , habe fürs Control select (mqtt_ctrl , general) u. den Power Wert (number) je ein MQTT-Client vom Typ string eingerichtet:

das ist mein send-script dazu:
<?php

/**

* =============================================================================

* SCRIPT: AS2_SEND

* VERSION: 1.9.2 ENTERPRISE FINAL

* BUILD: 2026-03-20 Europe/Berlin

* AUTHOR: AS2 Automation Framework (Gerd)

* ROLE: MQTT-Transmitter (Postbote) – sendet Modus + Leistung an EMS

* =============================================================================

*

* ÄNDERUNGEN v1.9.2 gegenüber v1.9.1:

* [FIX-1] IPS_GetScriptTimer() gibt Float zurück → (int)-Cast verhindert

* strict_types-Mismatch und Timer-Loop bei jedem Durchlauf

* [FIX-2] GetValue() durch typisierte Getter ersetzt (GetValueFloat)

* [FIX-3] AS2_GetVarID() ohne doppelten IPS-Lookup (@ + direkter Aufruf)

* [FIX-4] Power-Reset bei Mode-Wechsel jetzt in try/catch –

* LastPower wird nur bei erfolgreichem RequestAction gesetzt

* [FIX-5] Duplizierter Mode-Sendeblock in Funktion AS2_DoSendMode()

* ausgelagert → eine Codebasis, kein Divergenzrisiko

* [FIX-6] Grenzfall modeStale=true + powerFresh: kein stiller Zustand mehr,

* explizites Logging

* [FIX-7] Fail-Safe-Kommentar erweitert: Verhalten bei Folgezyklen erklärt

*

* FUNKTION:

* - MODE (general / mqtt_ctrl):

* * Change-Only-Send + Force-Send alle X Sekunden

* * Fail-Safe: bei stale Power → Mode=general

*

* - POWER (AS2_Steuerwert_Manuell):

* * Change-Only-Send mit Float-Toleranz + Force-Send alle X Sekunden

*

* - TIMER:

* * Immer 2s aktiv (auch bei Stale/OFF) → Force/Recovery bleibt möglich

*

* - STALE:

* * Mode/Power-Quellen werden auf Stale geprüft

*

* - METADATEN:

* * LastSentMode, LastSentModeTs, LastSentPower, LastSentPowerTs

*

* PARAMETER:

* - ForceIntervalSec: 30 (alle x sec Force-Send)

* - FloatToleranceW : 0.5 (W)

* - StaleMaxAgeSec : 300 (5 Minuten)

* =============================================================================

*/

declare(strict_types=1);

// ============================================================================

// [0] KONFIGURATION

// ============================================================================

$ForceIntervalSec = 30; // Force-Send alle 30 sec

$FloatToleranceW = 0.5; // Toleranz für Power-Change

$StaleMaxAgeSec = 300; // Stale-Grenze für Mode/Power-Quellen

// ============================================================================

// [1] BASIS

// ============================================================================

$AS2_Root = IPS_GetParent($_IPS[‚SELF‘]);

$AS2_Conf = IPS_GetObjectIDByIdent(„AS2_Config“, $AS2_Root);

if ($AS2_Conf === 0) {

die("FEHLER: AS2_Config nicht gefunden unter Root-ID {$AS2_Root}!");

}

$now = time();

$nowText = date(„Y-m-d H:i:s“, $now);

// ============================================================================

// [2] HILFSFUNKTIONEN

// ============================================================================

/**

* Prüft, ob ein Ident unter einem Parent existiert.

*/

function AS2_IdentExists(int $parent, string $ident): bool {

foreach (IPS_GetChildrenIDs($parent) as $cid) {

    if (IPS_GetObject($cid)\['ObjectIdent'\] === $ident) return true;

}

return false;

}

/**

* [FIX-3] Liefert die ID einer Variable per Ident ohne doppelten IPS-Lookup.

* Statt erst AS2_IdentExists() → IPS_GetObjectIDByIdent() zu rufen,

* wird direkt mit @ gearbeitet – ein einziger IPS-Aufruf.

*/

function AS2_GetVarID(string $ident, int $parent): int {

$id = IPS_GetObjectIDByIdent($ident, $parent);

if ($id === false || $id === 0) return 0;

return IPS_VariableExists($id) ? $id : 0;

}

/**

* Liefert die ID einer Instanz per Ident.

*/

function AS2_GetInstID(string $ident, int $parent): int {

if (!AS2_IdentExists($parent, $ident)) return 0;

$id = IPS_GetObjectIDByIdent($ident, $parent);

return IPS_InstanceExists($id) ? $id : 0;

}

/**

* Findet die Action-Variable innerhalb einer MQTT-Instanz.

*/

function AS2_ResolveMqttChild(int $instID): int {

if ($instID > 0 && IPS_InstanceExists($instID)) {

    foreach (IPS_GetChildrenIDs($instID) as $cid) {

        if (IPS_VariableExists($cid)) {

            $v = IPS_GetVariable($cid);

            if ($v\['VariableAction'\] > 0) return $cid;

        }

    }

    echo date("Y-m-d H:i:s") . " WARNUNG: Instanz {$instID} hat keine Action-Variable.\\n";

}

return 0;

}

/**

* Prüft ob eine Variable stale (veraltet) ist.

*/

function AS2_IsStaleVar(int $varID, int $maxAgeSec): bool {

if ($varID <= 0 || !IPS_VariableExists($varID)) return true;

$v = IPS_GetVariable($varID);

return (time() - $v\['VariableUpdated'\]) > $maxAgeSec;

}

/**

* Legt eine Float-Variable an, falls nicht vorhanden, und gibt die ID zurück.

*/

function AS2_GetOrCreateFloatVar(int $parent, string $ident, string $name): int {

if (!AS2_IdentExists($parent, $ident)) {

    $id = IPS_CreateVariable(2);

    IPS_SetParent($id, $parent);

    IPS_SetIdent($id, $ident);

    IPS_SetName($id, $name);

    SetValueFloat($id, 0.0);

    echo date("Y-m-d H:i:s") . " INFO: Float-Variable '{$name}' (Ident={$ident}) angelegt (ID={$id}).\\n";

    return $id;

}

$id = IPS_GetObjectIDByIdent($ident, $parent);

return IPS_VariableExists($id) ? $id : 0;

}

/**

* Legt eine Integer-Variable an, falls nicht vorhanden, und gibt die ID zurück.

*/

function AS2_GetOrCreateIntVar(int $parent, string $ident, string $name): int {

if (!AS2_IdentExists($parent, $ident)) {

    $id = IPS_CreateVariable(1);

    IPS_SetParent($id, $parent);

    IPS_SetIdent($id, $ident);

    IPS_SetName($id, $name);

    SetValueInteger($id, 0);

    echo date("Y-m-d H:i:s") . " INFO: Integer-Variable '{$name}' (Ident={$ident}) angelegt (ID={$id}).\\n";

    return $id;

}

$id = IPS_GetObjectIDByIdent($ident, $parent);

return IPS_VariableExists($id) ? $id : 0;

}

/**

* Legt eine String-Variable an, falls nicht vorhanden, und gibt die ID zurück.

*/

function AS2_GetOrCreateStringVar(int $parent, string $ident, string $name): int {

if (!AS2_IdentExists($parent, $ident)) {

    $id = IPS_CreateVariable(3);

    IPS_SetParent($id, $parent);

    IPS_SetIdent($id, $ident);

    IPS_SetName($id, $name);

    SetValueString($id, "");

    echo date("Y-m-d H:i:s") . " INFO: String-Variable '{$name}' (Ident={$ident}) angelegt (ID={$id}).\\n";

    return $id;

}

$id = IPS_GetObjectIDByIdent($ident, $parent);

return IPS_VariableExists($id) ? $id : 0;

}

/**

* [FIX-5] Ausgelagerter Mode-Sendeblock – ersetzt den duplizierten Code

* aus dem aktiven UND dem OFF/Stale-Zweig. Beide Pfade rufen diese

* Funktion auf, damit Änderungen nur an einer Stelle nötig sind.

*

* @param int $mqttModeID ID der MQTT-Mode-Action-Variable

* @param int $mqttPwrID ID der MQTT-Power-Action-Variable (0 = nicht vorhanden)

* @param string $targetMode Ziel-Mode („general“ oder „mqtt_ctrl“)

* @param int $lastModeID ID der LastSent-Mode-Variable

* @param int $lastModeTsID ID der LastSent-Mode-Timestamp-Variable

* @param int $lastPwrID ID der LastSent-Power-Variable

* @param int $lastPwrTsID ID der LastSent-Power-Timestamp-Variable

* @param int $now Aktueller Unix-Timestamp

* @param int $forceInterval Force-Intervall in Sekunden

* @param string $context Log-Kontext („aktiv“ oder „OFF/Stale“)

*/

function AS2_DoSendMode(

int    $mqttModeID,

int    $mqttPwrID,

string $targetMode,

int    $lastModeID,

int    $lastModeTsID,

int    $lastPwrID,

int    $lastPwrTsID,

int    $now,

int    $forceInterval,

string $context

): void {

$nowText = date("Y-m-d H:i:s", $now);



$lastMode   = GetValueString($lastModeID);

$lastModeTs = GetValueInteger($lastModeTsID);



$needModeChange = ($targetMode !== $lastMode);

$needModeForce  = (($now - $lastModeTs) >= $forceInterval);



if (!$needModeChange && !$needModeForce) {

    return; // Nichts zu tun

}



$reason = $needModeForce && !$needModeChange ? " (FORCE)" : "";

echo "{$nowText} MODE → {$targetMode}{$reason} \[{$context}\]\\n";



// \[FIX-4\] Power-Reset beim Wechsel mqtt_ctrl → general jetzt in try/catch.

// LastPower/LastPowerTs werden NUR bei erfolgreichem RequestAction gesetzt,

// damit kein inkonsistenter Zustand entsteht.

if ($lastMode === "mqtt_ctrl" && $targetMode === "general" && $mqttPwrID > 0) {

    try {

        RequestAction($mqttPwrID, 0);

        SetValueFloat($lastPwrID, 0.0);

        SetValueInteger($lastPwrTsID, $now);

        echo "{$nowText} POWER → 0 (Mode-Wechsel mqtt_ctrl → general)\\n";

    } catch (Exception $e) {

        echo "{$nowText} FEHLER: Power-Reset bei Mode-Wechsel: " . $e->getMessage() . "\\n";

        // LastPower wird NICHT gesetzt → nächster Zyklus versucht es erneut

    }

}



try {

    RequestAction($mqttModeID, $targetMode);

    SetValueString($lastModeID,   $targetMode);

    SetValueInteger($lastModeTsID, $now);

} catch (Exception $e) {

    echo "{$nowText} FEHLER: RequestAction MODE: " . $e->getMessage() . "\\n";

}

}

// ============================================================================

// [3] IDS LADEN

// ============================================================================

$id_mqtt_pwr_inst = AS2_GetInstID(„AS2_MQTT_Send_power“, $AS2_Root);

$id_mqtt_mode_inst = AS2_GetInstID(„AS2_MQTT_Send_ctrl“, $AS2_Root);

$id_mqtt_pwr = AS2_ResolveMqttChild($id_mqtt_pwr_inst);

$id_mqtt_mode = AS2_ResolveMqttChild($id_mqtt_mode_inst);

$id_val_pwr = AS2_GetVarID(„AS2_Steuerwert_Manuell“, $AS2_Conf);

$id_val_mode = AS2_GetVarID(„AS2_Soll_EMS_Mode“, $AS2_Conf);

$id_sys_akt = AS2_GetVarID(„AS2_System_Aktiv“, $AS2_Conf);

// LastSent-Variablen

$id_lastMode = AS2_GetOrCreateStringVar($AS2_Conf, „AS2_Last_Sent_Mode“, „AS2_Last_Sent_Mode“);

$id_lastModeTs = AS2_GetOrCreateIntVar ($AS2_Conf, „AS2_Last_Sent_Mode_Timestamp“, „AS2_Last_Sent_Mode_Timestamp“);

$id_lastPower = AS2_GetOrCreateFloatVar ($AS2_Conf, „AS2_Last_Sent_Power“, „AS2_Last_Sent_Power“);

$id_lastPowerTs = AS2_GetOrCreateIntVar ($AS2_Conf, „AS2_Last_Sent_Power_Timestamp“,„AS2_Last_Sent_Power_Timestamp“);

// HARTE VALIDIERUNG

if ($id_lastMode === 0 || $id_lastModeTs === 0 || $id_lastPower === 0 || $id_lastPowerTs === 0) {

echo "$nowText FATAL: LastSent-Variablen fehlen oder sind ungültig.\\n";

goto TIMER_ONLY;

}

// ============================================================================

// [4] SYSTEMSTATUS + STALE-CHECK

// ============================================================================

$isActive = ($id_sys_akt > 0 && IPS_VariableExists($id_sys_akt) && GetValueInteger($id_sys_akt) > 0);

$modeStale = ($id_val_mode <= 0) ? true : AS2_IsStaleVar($id_val_mode, $StaleMaxAgeSec);

$powerStale = ($id_val_pwr <= 0) ? true : AS2_IsStaleVar($id_val_pwr, $StaleMaxAgeSec);

if ($modeStale || $powerStale) {

echo $nowText

    . " WARNUNG: Stale Input"

    . " (ModeStale="  . ($modeStale  ? '1' : '0')

    . ", PowerStale=" . ($powerStale ? '1' : '0') . ")\\n";

}

// ============================================================================

// [5] TRANSMISSION

// ============================================================================

if ($isActive && !$modeStale && $id_mqtt_mode > 0 && $id_val_mode > 0) {

$targetMode = GetValueString($id_val_mode);



// FAIL-SAFE: mqtt_ctrl, aber Power stale → general senden.

// Im Folgezyklus ist LastMode="general", targetMode="general" (Fail-Safe

// greift erneut) → kein needModeChange, aber Force-Send hält den Rhythmus.

if ($targetMode === "mqtt_ctrl" && $powerStale) {

    echo "$nowText FAIL-SAFE: Power stale → Mode=general\\n";

    $targetMode = "general";

}



// \[FIX-5\] Mode-Senden über gemeinsame Funktion (kein duplizierter Block)

AS2_DoSendMode(

    $id_mqtt_mode,

    $id_mqtt_pwr,

    $targetMode,

    $id_lastMode,

    $id_lastModeTs,

    $id_lastPower,

    $id_lastPowerTs,

    $now,

    $ForceIntervalSec,

    "aktiv"

);



// POWER: nur bei mqtt_ctrl und Power nicht stale

if ($targetMode === "mqtt_ctrl" && !$powerStale && $id_mqtt_pwr > 0) {



    // \[FIX-2\] Typisierte Getter statt generischem GetValue()

    $finalPower  = round(GetValueFloat($id_val_pwr), 1);

    $lastPower   = GetValueFloat($id_lastPower);

    $lastPowerTs = GetValueInteger($id_lastPowerTs);



    $deltaPower      = abs($lastPower - $finalPower);

    $needPowerChange = ($deltaPower >= $FloatToleranceW);

    $needPowerForce  = (($now - $lastPowerTs) >= $ForceIntervalSec);



    if ($needPowerChange || $needPowerForce) {

        $reason = $needPowerForce && !$needPowerChange ? " (FORCE)" : "";

        echo "$nowText POWER → {$finalPower} W{$reason}\\n";

        try {

            RequestAction($id_mqtt_pwr, $finalPower);

            SetValueFloat($id_lastPower,    $finalPower);

            SetValueInteger($id_lastPowerTs, $now);

        } catch (Exception $e) {

            echo "$nowText FEHLER: RequestAction POWER: " . $e->getMessage() . "\\n";

        }

    }

}

} else {

// System OFF oder Stale → general erzwingen

if ($id_mqtt_mode > 0) {



    // \[FIX-5\] Auch hier über gemeinsame Funktion – kein duplizierter Code

    AS2_DoSendMode(

        $id_mqtt_mode,

        $id_mqtt_pwr,

        "general",

        $id_lastMode,

        $id_lastModeTs,

        $id_lastPower,

        $id_lastPowerTs,

        $now,

        $ForceIntervalSec,

        "OFF/Stale"

    );



} else {

    // \[FIX-6\] Explizites Log wenn mqtt_mode-ID fehlt – kein stiller Zustand

    echo "$nowText WARNUNG: id_mqtt_mode nicht verfügbar – kein Mode-Send möglich.\\n";

}

}

// ============================================================================

// [6] TIMER IMMER AKTIV HALTEN (2s)

// ============================================================================

TIMER_ONLY:

// [FIX-1] IPS_GetScriptTimer() gibt Float zurück.

// Mit strict_types wäre 2.0 !== 2 → true → Timer wird jedes Mal neu gesetzt.

// (int)-Cast verhindert diesen unnötigen IPS-Aufruf.

if ((int)IPS_GetScriptTimer($_IPS[‚SELF‘]) !== 2) {

IPS_SetScriptTimer($\_IPS\['SELF'\], 2);

echo "$nowText TIMER → 2s\\n";

}

/**

* =============================================================================

* ENDE DES SKRIPTS

* =============================================================================

*

* FIXES v1.9.2:

* [FIX-1] Timer-Cast: (int)IPS_GetScriptTimer() – kein strict_types-Loop

* [FIX-2] GetValueFloat() statt GetValue() für typsichere Power-Abfrage

* [FIX-3] AS2_GetVarID() ohne doppelten IPS-Lookup (@-Operator)

* [FIX-4] Power-Reset in try/catch – kein inkonsistenter LastPower-Zustand

* [FIX-5] Mode-Sendelogik in AS2_DoSendMode() – kein duplizierter Block

* [FIX-6] Explizites Log wenn mqtt_mode-ID fehlt

* [FIX-7] Fail-Safe-Folgezyklus-Verhalten kommentiert

*

* UNVERÄNDERTES VERHALTEN:

* - MODE: Change-Only + Force-Send alle X Sekunden

* - MODE: Fail-Safe mqtt_ctrl + stale Power → general

* - MODE: Power-Reset beim Wechsel mqtt_ctrl → general

* - POWER: Change-Only mit Float-Toleranz + Force-Send

* - TIMER: Immer 2s aktiv, auch bei Stale/OFF

* - STALE: Mode/Power-Quellen geprüft, Fail-Safe greift

* - METADATEN: LastSentMode/Ts, LastSentPower/Ts

* - LOGGING: Jede Aktion mit Datum+Uhrzeit, FORCE+Fail-Safe markiert

*

* =============================================================================

*/

der ist leider etwas kompliziert , da ich dort mit vielen Ident gearbeitet habe,in meinem Umfeld. Aber schau mal rein ,eigentlich brauchst du aber nur den Mode-select mit “general” ohne power setzen und den “mqtt-ctrl” mit anschließender Power -Übergabe in Watt . Wobei drauf achten: negative Werte (z.B. -500W) → Laden (Strom fließt in die Batterie) ; positive Werte (z.B. +333W) → Entladen (Strom fließt ins Hausnetz)
Gruß Gerd

1 „Gefällt mir“

Danke für das Script.
Ich werde mal versuchen, das durchzuarbeiten.

Hallo Gert,
Bin am Durcharbeiten, ist aber ziemlich kompliziert.
Könntest du mir mal komplette Bilder der MQTT-Clients posten, dass ich die Themen/abweichende übernehmen kann. Das würde mir helfen.
Danke

ich arbeite viel mit den Ident , die müssen unter dem richtigen parent liegen:
Die Watt-Vorgabe für die mqtt-ctrl liegt unter Ident AS2_Steuerwert_Manuell.
Haupt (Dummy-Modul) ist AS2 , dadrunter liegen AS2_Config u. die Scripte/MQTT-Clients. In AS2_Config liegen die ganzen Variablen.

die AS2_INIT legt das meiste davon an: (S.Nr des 1920AC ausge’x’t:
(wie packt man php in ein separates Fenster?)
<?php

/**

* =============================================================================

* SCRIPT: AS2_INIT

* VERSION: 3.7.9 ENTERPRISE (2026-03-20)

* ROLE: Initialer Struktur-Aufbau für AS2 (Dummy-Modul-Struktur)

*

* STRUKTUR (fix):

* - AS2 (Dummy Instanz) = $AS2_Root

* - Skripte + MQTT-Instanzen direkt unter AS2

* - Variablen unter AS2/AS2_Config

*

* UPDATE 3.7.9:

* [ADD-1] AS2_Hausverbrauch_ID (Integer, pos 320): Pointer auf externe Hausverbrauch-Variable

* Wird von AS2_CONTROL (Case 5 / Zero-Exp) und AS2_DASH benötigt.

* [ADD-2] AS2_Hysterese (Float, pos 220): Hysterese-Schwelle in W

* Wird von AS2_CONTROL in Case 2 (PV-Laden) verwendet.

* Fallback war bisher 20.0W — jetzt explizit konfigurierbar.

* [ADD-3] AS2_HTML (String, pos 610): Dashboard-HTML-Output von AS2_DASH

* Ohne diese Variable konnte DASH den HTML-Output nicht in IPS speichern.

*

* UPDATE 3.7.8 (unverändert übernommen):

* [FIX-4] Typ-Migration in AS2_SetConfigVar — verhindert IPS Warning bei

* bereits falsch angelegten Variablen

*

* UPDATE 3.7.7:

* [FIX-1] AS2_Aktueller_SOC: Typ 1 (Integer) → Typ 2 (Float)

* Integer schneidet Dezimalstellen ab: 19.7% → 19

* → SafetyLimit-Vergleich in AS2_Daily_Chg_ReChg greift falsch

*

* [FIX-2] AS2_Aktuelle_BattMinLeist: Typ 1 → Typ 2

* AS2_RECEIVE schreibt Float (min/max aus JSON als numeric)

* → Integer-Variable schneidet z.B. 150.5W auf 150 ab

*

* [FIX-3] AS2_Aktuelle_BattMaxLeist: Typ 1 → Typ 2

* Gleicher Grund wie FIX-2

*

* [FIX-4] AS2_SetConfigVar: Typ-Migration für bereits existierende Variablen

* v3.7.7 legte neue Variablen korrekt als Float an, migrierte aber

* bereits vorhandene Integer-Variablen nicht → IPS Warning:

* „Variablentyp stimmt nicht überein“ (Zeile 100)

* Fix: Wenn Variable existiert aber falscher Typ → alten Wert sichern,

* Variable löschen, neu anlegen mit korrektem Typ, Wert wiederherstellen.

*

* [DOC] AS2_Grid_ID: Kommentar ergänzt — enthält IPS-Objekt-ID

* als Integer-Pointer auf die externe Grid-Leistungs-Variable

*

* UPDATE 3.7.6 (unverändert übernommen):

* - Warning-Fix: IPS_ConnectInstance nur wenn ConnectionID != SocketID

* (verhindert „hat bereits ein übergeordnetes Objekt“)

* =============================================================================

*/

declare(strict_types=1);

// — 0) GRUND-KONFIGURATION —

$AS2_Root = IPS_GetParent($_IPS[‚SELF‘]); // Dummy-Modul „AS2“

$AS2_SocketID = 14624;

$AS2_DevID = „1234567890“;

$AS2_GuidMqttDev = „{91D174F2-AE0F-B8D8-5EF4-6232B9083CCF}“;

$nowText = date(„Y-m-d H:i:s“);

// -----------------------------------------------------------------------------

// [A] Helper: Child unter Parent per Ident finden (nur direkte Children)

// -----------------------------------------------------------------------------

function AS2_FindChildByIdent(int $parentID, string $ident): int

{

foreach (IPS_GetChildrenIDs($parentID) as $cid) {

    $obj = IPS_GetObject($cid);

    if ($obj\['ObjectIdent'\] === $ident) {

        return $cid;

    }

}

return 0;

}

// -----------------------------------------------------------------------------

// [B] Helper: gezieltes Suppress einer IPS/PHP-Warnung (ohne @)

// -----------------------------------------------------------------------------

function AS2_SuppressKnownWarning(callable $fn): void

{

$handler = function(int $errno, string $errstr) {

    if ($errno === E_WARNING && strpos($errstr, "hat bereits ein übergeordnetes Objekt") !== false) {

        return true; // handled

    }

    return false;

};



set_error_handler($handler, E_WARNING);

try {

    $fn();

} finally {

    restore_error_handler();

}

}

// -----------------------------------------------------------------------------

// [1] CONFIG-KATEGORIE SICHERSTELLEN (unter AS2)

// -----------------------------------------------------------------------------

$AS2_Conf = AS2_FindChildByIdent($AS2_Root, „AS2_Config“);

if ($AS2_Conf <= 0) {

$AS2_Conf = IPS_CreateCategory();

IPS_SetIdent($AS2_Conf, "AS2_Config");

IPS_SetName($AS2_Conf, "AS2_Config");



if (IPS_GetParent($AS2_Conf) !== $AS2_Root) {

    AS2_SuppressKnownWarning(function() use ($AS2_Conf, $AS2_Root) {

        IPS_SetParent($AS2_Conf, $AS2_Root);

    });

}



echo "$nowText INFO: AS2_Config neu angelegt (ID=$AS2_Conf)\\n";

} else {

echo "$nowText INFO: AS2_Config vorhanden (ID=$AS2_Conf)\\n";

}

// -----------------------------------------------------------------------------

// [2] MQTT-Instanz Setup (unter AS2)

// -----------------------------------------------------------------------------

$AS2_SetupMqttDirect = function(string $ident, string $topic) use ($AS2_Root, $AS2_GuidMqttDev, $AS2_SocketID, $nowText): int {

$inst_id = AS2_FindChildByIdent($AS2_Root, $ident);



if ($inst_id <= 0) {

    $inst_id = IPS_CreateInstance($AS2_GuidMqttDev);

    IPS_SetIdent($inst_id, $ident);

    IPS_SetName($inst_id, $ident);



    if (IPS_GetParent($inst_id) !== $AS2_Root) {

        AS2_SuppressKnownWarning(function() use ($inst_id, $AS2_Root) {

            IPS_SetParent($inst_id, $AS2_Root);

        });

    }



    echo "$nowText INFO: MQTT-Instanz neu angelegt: $ident (ID=$inst_id)\\n";

} else {

    if (!IPS_InstanceExists($inst_id)) {

        echo "$nowText WARNUNG: $ident gefunden (ID=$inst_id), aber ist keine Instanz. Überspringe.\\n";

        return 0;

    }

    echo "$nowText INFO: MQTT-Instanz vorhanden: $ident (ID=$inst_id)\\n";

}



// Topic setzen + apply

IPS_SetProperty($inst_id, "Topic", $topic);

IPS_ApplyChanges($inst_id);



// CONNECT-FIX: nur verbinden, wenn ConnectionID != SocketID

if (IPS_InstanceExists($AS2_SocketID)) {

    $instInfo = IPS_GetInstance($inst_id);

    $connId   = (int)$instInfo\['ConnectionID'\]; // 0 = keine Verbindung



    if ($connId !== $AS2_SocketID) {

        AS2_SuppressKnownWarning(function() use ($inst_id, $AS2_SocketID) {

            IPS_ConnectInstance($inst_id, $AS2_SocketID);

        });

        echo "$nowText INFO: CONNECT → Socket #$AS2_SocketID (vorher: #$connId)\\n";

    }

}



// Value-Child: erste Variable unter der Instanz, nur Name auf \*\_value

$var_id = 0;

foreach (IPS_GetChildrenIDs($inst_id) as $cid) {

    if (IPS_VariableExists($cid)) { $var_id = $cid; break; }

}



if ($var_id > 0) {

    $prettyName = $ident . "\_value";

    if (IPS_GetName($var_id) !== $prettyName) {

        IPS_SetName($var_id, $prettyName);

    }

} else {

    echo "$nowText WARNUNG: Instanz $ident (ID=$inst_id) hat kein Variable-Child gefunden.\\n";

}



return (int)$var_id;

};

// -----------------------------------------------------------------------------

// [3] Config-Variablen anlegen (unter AS2/AS2_Config)

//

// IPS Variablentypen:

// 0 = Boolean

// 1 = Integer

// 2 = Float

// 3 = String

// -----------------------------------------------------------------------------

$AS2_SetConfigVar = function(string $ident, int $type, int $pos) use ($AS2_Conf, $nowText): int {

$v_id = AS2_FindChildByIdent($AS2_Conf, $ident);



if ($v_id <= 0) {

    // Variable existiert nicht → neu anlegen

    $v_id = IPS_CreateVariable($type);

    IPS_SetIdent($v_id, $ident);

    IPS_SetName($v_id, $ident);



    if (IPS_GetParent($v_id) !== $AS2_Conf) {

        AS2_SuppressKnownWarning(function() use ($v_id, $AS2_Conf) {

            IPS_SetParent($v_id, $AS2_Conf);

        });

    }



    echo "$nowText INFO: Variable neu angelegt: $ident (Typ=$type, ID=$v_id)\\n";



} else {

    // Variable gefunden — Existenz prüfen

    if (!IPS_VariableExists($v_id)) {

        echo "$nowText WARNUNG: $ident gefunden (ID=$v_id), aber ist keine Variable. Überspringe.\\n";

        return 0;

    }



    // \[FIX-4\] Typ-Migration: falscher Typ → alten Wert sichern, löschen, neu anlegen

    // Ohne diesen Block: IPS wirft "Variablentyp stimmt nicht überein" wenn

    // z.B. AS2_Aktueller_SOC noch als Integer (Typ 1) existiert und

    // AS2_RECEIVE versucht einen Float-Wert zu schreiben.

    $varInfo    = IPS_GetVariable($v_id);

    $actualType = (int)$varInfo\['VariableType'\];



    if ($actualType !== $type) {

        // Alten Wert als String sichern für Logging

        $oldValStr = (string)GetValue($v_id);



        // Alte Variable löschen (Ident wird freigegeben)

        IPS_DeleteVariable($v_id);



        // Neu anlegen mit korrektem Typ

        $v_id = IPS_CreateVariable($type);

        IPS_SetIdent($v_id, $ident);

        IPS_SetName($v_id, $ident);



        if (IPS_GetParent($v_id) !== $AS2_Conf) {

            AS2_SuppressKnownWarning(function() use ($v_id, $AS2_Conf) {

                IPS_SetParent($v_id, $AS2_Conf);

            });

        }



        echo "$nowText MIGRATION: $ident Typ $actualType → $type (alter Wert: $oldValStr, ID neu=$v_id)\\n";

    }

}



IPS_SetPosition($v_id, $pos);

return (int)$v_id;

};

$AS2_SetConfigVarString = function(string $ident, int $pos) use ($AS2_SetConfigVar): int {

$v_id = $AS2_SetConfigVar($ident, 3, $pos);

if ($v_id > 0 && IPS_VariableExists($v_id)) {

    if (GetValueString($v_id) === "") {

        SetValueString($v_id, "n/a");

    }

}

return (int)$v_id;

};

// -----------------------------------------------------------------------------

// [4] MQTT-KANÄLE (Instanzen direkt unter AS2)

// -----------------------------------------------------------------------------

$AS2_SetupMqttDirect(„AS2_MQTT_Read_quick“, „homeassistant/sensor/MSA-2804253xxxxxx/quick/state“);

$AS2_SetupMqttDirect(„AS2_MQTT_Read_system“, „homeassistant/sensor/MSA-2804253xxxxxx/system/state“);

$AS2_SetupMqttDirect(„AS2_MQTT_Read_device“, „homeassistant/sensor/MSA-2804253xxxxx/device/state“);

$AS2_SetupMqttDirect(„AS2_MQTT_Read_config“, „homeassistant/sensor/MSA-2804253xxxxx/config/state“);

$AS2_SetupMqttDirect(„AS2_MQTT_Send_ctrl“, „homeassistant/select/$AS2_DevID/ems_mode/command“);

$AS2_SetupMqttDirect(„AS2_MQTT_Send_power“, „homeassistant/number/$AS2_DevID/power_ctrl/set“);

// -----------------------------------------------------------------------------

// [5] CONFIG-VARIABLEN (unter AS2/AS2_Config)

//

// TYPÜBERSICHT (nach Skript-Verwendung):

// Integer (1): Statuswerte, Zähler, Flags die IPS als Int erwartet

// Float (2): Alle Leistungs-, SOC-, Spannungs- und Stromwerte

// Boolean (0): Binäre Flags (LateLoad, NightStopFlag, Chg_ReChg)

// String (3): Textwerte (Log, Status, EMS-Mode)

// -----------------------------------------------------------------------------

// Steuerung & Betrieb

$AS2_SetConfigVar(„AS2_System_Aktiv“, 1, 5); // Integer: 0=aus, 1=aktiv, 2=debug

$AS2_SetConfigVar(„AS2_EMS_Betriebsmodus“, 1, 6); // Integer: Modus-Kennung

$AS2_SetConfigVar(„AS2_Steuerwert_Manuell“, 2, 7); // Float: Leistungsvorgabe in W

$AS2_SetConfigVar(„AS2_Chg_ReChg“, 0, 9); // Boolean: Lade-Flag

// Messwerte Batterie — alle Float wegen Dezimalwerten aus MQTT

// [FIX-1] SOC war Integer (Typ 1) → jetzt Float (Typ 2)

// Integer schnitt 19.7% auf 19 ab → SafetyLimit-Vergleich wich ab

$AS2_SetConfigVar(„AS2_Aktuelle_Leistung“, 2, 10); // Float: bat_p in W

// [FIX-2] BattMinLeist war Integer (Typ 1) → jetzt Float (Typ 2)

// RECEIVE schreibt numeric JSON-Wert als Float → sonst Abschneiden

$AS2_SetConfigVar(„AS2_Aktuelle_BattMinLeist“, 2, 12); // Float: min-Leistung in W

// [FIX-3] BattMaxLeist war Integer (Typ 1) → jetzt Float (Typ 2)

// Gleicher Grund wie FIX-2

$AS2_SetConfigVar(„AS2_Aktuelle_BattMaxLeist“, 2, 14); // Float: max-Leistung in W

// Lade-/Entlade-Limits

$AS2_SetConfigVar(„AS2_Limit_Laden“, 2, 16); // Float: negativ = Laden (W)

$AS2_SetConfigVar(„AS2_Limit_Entladen“, 2, 18); // Float: positiv = Entladen (W)

// SOC & Schwellen — Float wegen Dezimalgenauigkeit

// [FIX-1] hier bereits korrigiert (siehe oben bei pos=10)

$AS2_SetConfigVar(„AS2_Aktueller_SOC“, 2, 20); // Float: SOC in % ← [FIX-1]

$AS2_SetConfigVar(„AS2_Schwelle_MinSOC“, 2, 22); // Float: MinSOC in %

$AS2_SetConfigVar(„AS2_Schwelle_MaxSOC“, 2, 24); // Float: MaxSOC in %

// Weitere Messwerte

$AS2_SetConfigVar(„AS2_Aktuelle_Spannung“, 2, 30); // Float: bat_v in V

$AS2_SetConfigVar(„AS2_Aktuelle_Stromstaerke“, 2, 40); // Float: bat_i in A

$AS2_SetConfigVar(„AS2_Aktuelle_Temperatur“, 2, 50); // Float: bat_temp in °C

// PV & Energie

$AS2_SetConfigVar(„AS2_Aktuelle_Leistungsvorgabe“,2, 60); // Float: W

$AS2_SetConfigVar(„AS2_Aktuelle_PVpower“, 2, 65); // Float: pv_p in W

$AS2_SetConfigVar(„AS2_Aktuelle_DailyDischarge“, 2, 70); // Float: dchg_e in Wh

$AS2_SetConfigVar(„AS2_Aktuelle_DailyCharge“, 2, 75); // Float: chg_e in Wh

// Status-Strings

$AS2_SetConfigVar(„AS2_Aktuelle_Batt_status“, 3, 77); // String: bat_sts

$AS2_SetConfigVar(„AS2_Aktuelle_EMS_Modus“, 3, 79); // String: ems_mode (vom EMS empfangen)

$AS2_SetConfigVar(„AS2_Soll_EMS_Mode“, 3, 80); // String: Soll-Mode (von Strategie gesetzt)

// Festwerte

$AS2_SetConfigVar(„AS2_Fest_LadeWert“, 2, 82); // Float: W

$AS2_SetConfigVar(„AS2_Fest_EntLadeWert“, 2, 83); // Float: W

// Interne Metadaten

$AS2_SetConfigVar(„AS2_Last_Applied_Mode“, 1, 85); // Integer

$AS2_SetConfigVar(„AS2_Last_Applied_Status“, 1, 87); // Integer

$AS2_SetConfigVar(„AS2_Daily_Log“, 3, 89); // String: Dashboard-HTML

$AS2_SetConfigVar(„AS2_LastWriteTs“, 1, 91); // Integer: Unix-Timestamp

// [DOC] AS2_Grid_ID: enthält die IPS-Objekt-ID der externen Grid-Leistungs-Variable

// als Integer-Pointer. Strategie liest: $idGrid = GetValue($ids[‚GridPtr‘])

// dann: $AS2_GridVal = GetValue($idGrid). Muss nach Neuinstallation

// manuell auf die korrekte Grid-Variablen-ID gesetzt werden!

$AS2_SetConfigVar(„AS2_Grid_ID“, 1, 95); // Integer: Pointer auf Grid-Variable

$AS2_SetConfigVar(„AS2_Limit_Puffer“, 2, 97); // Float: W

// Boolean-Flags

$AS2_SetConfigVar(„AS2_Hoym_LateLoad“, 0, 98); // Boolean: von SolC_VorcastFuerHoymiles

                                                        //          true=warten, false=laden

$AS2_SetConfigVar(„AS2_NightStopFlag“, 0, 99); // Boolean: Nacht-Entladesperre

                                                        //          true=gesperrt bis Sonnenaufgang

                                                        //          Schutz vor Prognose-Sprung \~03:00 Uhr

// Enterprise-Variablen (String, Initialwert „n/a“)

$AS2_SetConfigVarString(„AS2_ParamHash“, 100); // String: MD5 der Strategie-Parameter

$AS2_SetConfigVarString(„AS2_LastState“, 110); // String: letzter Strategie-State

$AS2_SetConfigVarString(„AS2_Aktueller_Geraetestatus“, 120); // String: Gerätestatus

// [ADD-2] Regelungs-Parameter

// AS2_Hysterese: wird von AS2_CONTROL Case 2 (PV-Laden) verwendet.

// Fallback im Code war 20.0W — jetzt explizit in AS2_Config konfigurierbar.

// Muss nach Anlage manuell auf den gewünschten Wert gesetzt werden!

$AS2_SetConfigVar(„AS2_Hysterese“, 2, 220); // Float: Hysterese in W (z.B. 20.0)

// [ADD-1] Pointer auf externe Hausverbrauch-Variable

// AS2_Hausverbrauch_ID: enthält die IPS-Objekt-ID der Hausverbrauch-Messvariable.

// Wird von AS2_CONTROL (Case 5 / Zero-Exp) und AS2_DASH (Zeile 1 Anzeige) benötigt.

// Muss nach Neuinstallation manuell auf die korrekte Hausverbrauch-Variable zeigen!

$AS2_SetConfigVar(„AS2_Hausverbrauch_ID“, 1, 320); // Integer: Pointer auf Hausverbrauch-Variable

// [ADD-3] Dashboard-HTML-Output

// AS2_HTML: String-Variable in die AS2_DASH den generierten HTML-Code schreibt.

// Wird von IPS-Visualisierungen (WebFront, App) als HTML-Anzeige-Element genutzt.

$AS2_SetConfigVar(„AS2_HTML“, 3, 610); // String: Dashboard-HTML von AS2_DASH

// — 6) ABSCHLUSS —

echo „$nowText AS2_INIT v3.7.9 ENTERPRISE: Struktur geprüft, drei Variablen ergänzt (Hysterese/Hausverbrauch_ID/HTML).\n“;

echo „$nowText HINWEIS: AS2_Grid_ID + AS2_Hausverbrauch_ID nach Neuinstallation manuell setzen!\n“;

echo „$nowText HINWEIS: AS2_Hysterese Startwert prüfen (Default 0.0 → gewünschten Wert eintragen)!\n“;

echo „$nowText HINWEIS: AS2_Hoym_LateLoad (ID nach Anlage prüfen) und BattBalancing-ID in Strategie anpassen!\n“;

1 „Gefällt mir“

Hallo,

ich habe 2 x Hoymiles HiBattery 1920 AC und einen alten MS-A2 bekomme leider nur die Summe des ganzen Speichers. Messung mit einem Shelly Pro 3EM.

VG
Andreas