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.
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:
- nach Shelly.GetDeviceInfo das ist soweit i.O und die APP akzeptiert meine SIM-Antwort und zeigt mir einen 3EM an
- 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 ![]()
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 Probleme9. 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
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“;




