SolarWatt Battery Flex Akkustand Scapen

Hallo,
ich versuche mich auch an einer Webseite.
Verzweifel aber.

Kann mir jemand behilflich sein. Möchte den % Wert gerne als Sensor.

Hallo Malti,

ich mache das mit Node-RED,

http://IP-Adresse deines Speichers/stat

wenn du Unterstützung brauchst bitte melden…

:crayon:by HarryP: Zusammenführung Doppelpost (bei Änderungen oder hinzufügen von Inhalten bitte die „Bearbeitungsfunktion“ anstatt „Antworten“ zu nutzen)

Hallo Community,

wir haben für diese Geschichte einen kleinen Controller basierend auf ESPHome entwickelt, der nicht nur den SoC als Sensor ausliest und in Home Assistant bekannt macht sondern auch …

… Sensoren für:

  • Netz Bezug
  • Netz Einspeisung
  • Batterie Bezug
  • Batterie Einspeisung
  • SoH (State of Health) der Batterie
  • Kapazität
  • Batterielaufzeit

… zur Verfügung stellt. Die Gründe das Ganze in einen ESP32 auszulagern sind:

  • Decoupling vom Solarwatt‑REST-Interface
    Bisher erhielten wir den SoC über einen REST-Sensor, der mit dem Energy Manager von Solarwatt spricht. Änderungen an diesem Interface führten zu direkten Änderungen in Home Assistant. Nun sind mögliche Änderungen in einem eigenen Controller gekapselt.
  • Loadbalancing & Entlastung
    Der ESP32 übernimmt die Datenaufbereitung (SoH, Kapazität), Berechnungen und ggf. Caching. Home Assistant erhält „fertige“ Werte, an Stelle von Rohdaten.
  • Zusätzliche Daten
    Es werden Netz bzw. Batterie Verbrauch/Einspeisung direkt zur Verfügung gestellt ohne, dass sie vom Energy Manager von Solarwatt prozessiert und per REST verfügbar gemacht werden.
    Unsere Erwartung an den Controller ist, dass wir genügend Daten zur Verfügung haben, so dass wir das REST-Interface zum Energy Manager von Solarwatt abschalten können :grinning_face:
    Der Source Code des Controllers liegt unter Github.
    Sofern Interesse besteht, kann gerne noch etwas mehr an Erläuterung folgen :wink:

@Andreas59: hast du herausbekommen wie man eine Battery flex steuert? Würde dem Ding gerne sage “lade jetzt mit xxx Watt”. Vielen Dank für deine Hilfe

Guten Morgen Albrecht,

ich habe für meine PV-Anlage keinen Energy Manager von Solarwatt, daher weis ich nicht was jener für Daten zum BatteryFlex ausgiebt.

Ich lese die Daten des Speichers und der Packs mit NodeRED aus. Den Speicher erreiche ich über seine IP-Adresse/stat und die Packs IP-Adresse/pack?p=0 bzw. =1 für das Zweite Pack und so weiter. Die Daten liegen in JSON-Format vor.

Nun zu deiner eigentlichen Frage, ich glaube nicht das man in die direckte Steuerung / Regelung eingereifen kann.Nach meinen Erfahrungen mit Solarwatt sind sie mit Informationen sehr sparsam.

Wie man mir bei einem persönichen Gespräch bei Solarwatt erklärt hat ist die Steuerung sehr komplex so das der Endkunde da nicht eingreifen soll auch wegen der Garantie. Ob man was beim Energy Manager einstellen kann weis ich nicht, da ich keinen habe. Hast du einen?

Mann hat mir nicht mal sagen können was die Werte des SysStatus aussagen.

Das ist die Oberfläche vom Speicher über siene IP-Adresse.

Das andere kommt mit IP-Adresse/…

@Andreas59 : vielen Dank für die prompte Antwort. Dieser Punkt …

>>>>
… habe für meine PV-Anlage keinen Energy Manager von Solarwatt …
>>>>

… ist ja sehr interessant! Wie wird ohne einen Energy Manager, dann das Laden des Akkus gesteuert ODER(!) ist die Battery flex autonom und steuert den Ladevorgang über den AC-Sensor …

… selbst? Deine Aussage legt letzteres nahe … damit wäre der Energy Manager nicht das was er vorgibt zu sein, sondern vermutlich lediglich ein leider sehr, sehr teurer, Proxy :unamused_face:

>>>>
Wie man mir bei einem persönichen Gespräch bei Solarwatt erklärt hat ist die Steuerung sehr komplex so das der Endkunde da nicht eingreifen soll auch wegen der Garantie.
>>>>

Diese Story bekommen wir auch immer zu hören … um fair zu sein: den Teil mit der Garantie kann ich nachvollziehen. Der Eindruck, der bei uns nach einem solchen Gespräch aber immer hängen bleibt ist der, dass die Leute von Solarwatt schlicht sehr wenig Ahnung haben, leider auch nicht von ihrem eigenen System. Wie das Ganze zusammenspielt bzw. zusammenspielen soll, konnten sie bisher nicht wirklich erklären :sad_but_relieved_face:

>>>>
Hast du einen?
>>>>

Ja, bei uns gibt es einen Energy Manager flex …

… der …

  • im Untergeschoss hängt und Strom verbraucht :wink:
  • uns über 2200€ gekostet hat

… und keiner kann genau erklären, was das Ding eigentlich macht. Was er nicht macht/kann ist:

  • thermisches Energie Management … das wurde uns mit viel Aufwand verkauft, war aber vom Installateur nicht konfiguriert
  • PV-optimiertes Laden unseres E-Autos … es wurde entweder schnell, also nicht PV-optimiert, oder garnicht geladen. Ein Regeln des Ladestroms fand niemals statt. Das Ganze funktioniert erst, seit wir evcc am Laufen haben

… wenn bei dir kein Energy Manager am Laufen ist und du sagst, dass die Batterie trotzdem lädt, heißt das für uns: wir können den Energy Manager eigentlich abschalten, da er keine Funktion erfüllt.

>>>>
Mann hat mir nicht mal sagen können was die Werte des SysStatus aussagen.
>>>>

Passt voll ins Bild: uns wurde erklärt, dass sie keine Software machen würden sonder sich “nur” um die “Funktion” kümmern … was immer das auch heißen mag :wink:

Könntest du bitte bestättigen, dass bei dir kein Energy Manager von Solarwatt läuft und das Ganze trotzdem funktioniert. Dann ziehen wir unserem Energy Manager flex entgültig den Stecker …

um die ganze sache besser zu verstehen, ich bin gelernter Mess- Steuer- und Regelungstechniker kenne mich alse ein wenig mit Strom und Spannung aus.

2023 die erste PV-Anlage mit 10,9 kWp mit Süd West Ausrichtung errichten lassen, ohne Energy Manager, da mir die im Handbuch aufgeführten Komponenten die er managen kann zu übersichtlich (mager) war. Geschirrspühler und Waschmaschine kann er sowie so nicht. Zu diesem Zeitpunkt habe ich mich dann mit HA beschäftigt um den Kaco-Wechselrichter im lokalen LAN zu loggen. Geht mit HA und der SunSpec Integration über Mod-Bus. Im Dez. 2023 noch ein BatterieFlex AC1 (2 Packs 4800 Wh) installieren lassen, weil die Hausgeräte (die elek. Heizleistung lässt sich nicht steuern) es nicht interessiert wie viel PV vom Dach kommt. Mit Speicher kann mann das gut ausgleichen.

Der Batteriespeicher ist mit dem Solarwatt AC-Sensor über CAN-Bus verbunden und hat intern sein Batteriemamagement.

Die Netzleistung die vom AC-Sensor ermittelt wird ist entscheidend ob die Batterie geladen wird, bei PV-Überschuss oder ob entladen wird bei Netzbezug. Der Wert pendelt zwischen -10 und +10W solange was im Speicher ist bzw. genügend vom Dach kommt.

Im Okt. 2042 ein zweite PV-Anlage mit 4,5 kWp mit Nor Ost Ausrichtung angeschafft weil im Sommerhalbjahr Vormittags nur wenig PV-Leistung gegeben war.

Wenn ich jetzt bezüglich meiner Fragen bei Solarwatt ober bei meinem Installateur angerufen habe war die erste Frage immer haben sie keinen Energy Manager ich kann ihre Anlage nicht sehen. Mein Installateur meinte den braucht mann nur damit mann über die App mobil die Anlage sehen kann. Ich mache alles mit HA.

entweder mit Grafana oder

HA

das sind Werte aus dem Speicher mittels NodeRed,

das sind die Sensoren der Packs, aber keine Ahnung was sie bedeuten,

Es gibt z.b. ca. 10verschieden Batterie SysStatus Meldungen, als Dezimalwert oder in der Web-Oberfläche als Hex-Wert aber es kann oder will mir keiner sagen was sie bedeuten.

Zeigt dein Energy Manager Status Meldungen der Batterie an?

Hallo @Andreas59,

danke für deine Antwort. Das …

>>>>
Die Netzleistung die vom AC-Sensor ermittelt wird ist entscheidend ob die Batterie geladen wird, bei PV-Überschuss oder ob entladen wird bei Netzbezug.
>>>>

… und …

>>>>
Mein Installateur meinte den braucht mann nur damit mann über die App mobil die Anlage sehen kann.
>>>>

… war die “befürchtete”, wenn auch erwartete Antwort :wink:. Damit ist dieses sehr teure Ding nur ein überflüssiger Verbraucher. Fühlen uns nun noch mehr von Solarwatt “verschauckelt” :sob:
Auch wir machen, nach nur drei Monaten mit der Solarwatt App, alles über Home Assistant hier z.B. Teile unseres Solar Dashboards …

… und haben die App de-installiert.
Kurze Erläuterung: das Dashboard sieht im Moment nicht so beeindruckend aus, da es Nacht ist und unsere PV-Anlage nichts produziert :wink:

>>>>
Zeigt dein Energy Manager Status Meldungen der Batterie an?
>>>>

Nein, das UI des Energy Managers …

… dient ausschließlich der Konfiguration, was direkt nach Installation des Managers die Alarmglocken läuten lies, da wir erwartet hatten, dass da …

  • irgendwelche Grafiken
  • aktuellen Werte

… angezeigt werden, bzw. “Regeln” wie “wenn PV Überschuss, dann E-Bike laden” eingegeben werden können (machen wir in der Zwischenzeit über evcc :grinning_face:). Hatten extra mehrfach während des Verkaufsgesprächs nachgefragt: funktioniert das Ganze, wenn das Internet weg ist? Können wir unsere Anlage steuern? Klare Aussage: “aber natürlich” … tja … Satz mit X :wink:
Damit wird dem Ding der Stecker gezogen, am Internet hängt es sowieso schon länger nicht mehr, da Solarwatt, ohne vorherige Ankündigung, irgendwelche Software updates eingespielt hat, die unser Gesamtsystem disfunktional machten.
Nochmals Danke für deine Antworten. Dank des weiter oben erwähnten ESPHome basierten Controllers erhalten wir nun Werte wie SoC usw. direkt von der Batterie und können damit …

  1. auf REST Zugriffe auf den Energy Manager verzichten UND(!) …
  2. ihn, nach deiner Aussagen, abschalten

… vielleicht nimmt Solarwatt das Ding ja zurück … es ja noch nicht sonderlich alt :wink:

Gehe ich Richtig in der Annahme das du außer dem AC-Sensor der den Speicher mit Daten versorgt noch einen weiteren Sensor für Strom/Leistung (Shelly o.ä.) nach bem Zähler des Energieversorges hast, der den Energy Manager mit Daten füttert?

Hallo Andreas59,

>>>>
Gehe ich Richtig in der Annahme das du außer dem AC-Sensor der den Speicher mit Daten versorgt noch einen weiteren Sensor für Strom/Leistung (Shelly o.ä.) nach bem Zähler des Energieversorges hast, der den Energy Manager mit Daten füttert?
>>>>

Bingo :wink: … es gibt …

  • einen Shelly hinter dem AC-Sensor, wobei ich mir den nun, dank Smartmeter bzw. dem oben erwähnten Battery flex Controller sparen könnte
  • den Battery flex Controller, der die Batterie Stromaufnahme bzw. -abgabe sowie SoC, Soh liefert

… und es wird noch einen Shelly …

  • für den Hausverbrauch geben

… damit hätte wir alle Sensoren zusammen um das, was ursprünglich der Energy Manager von Solarwatt geliefert hat selbst zu ersetzen :grinning_face: und da ja, nach deiner Aussage, die Battery flex selbständig über Laden bzw. Entladen entscheiden, kann der Energy Manager abgeschaltet werden. Es ist zwar sehr, sehr ärgerlich wenn ich an die über 2200€ denke, die wir für diese Box bezahlt haben … “C’est la vie” :wink:. Angesichts dieser neuen und der vielen anderer negativen Erfahrungen, die wir mit Solarwatt gemacht haben, möchten wir niemals mehr etwas mit ihnen zu tun haben :enraged_face:
Möchte aber nicht weiter auf unsere Probleme mit Solarwatt eingehen sondern lieber unseren ESPHome basierten Battery flex Controller vorstellen, der eine eigene Battery flex Komponente sowie asynchrone Kommunikation besitzt :grinning_face:

Hallo Albrecht, ich habe mir heute mal deinen Code auf Github an geschaut, das sieht ja so ähnlich aus wie ich es mit NodeRed mache, nur ohne ESP23. Das Intervall für den HTTP-request auf die Daten /stat habe ich auf 1s und bei den Daten /pack?… habe ich auf 5s, habe bis jetzt keine Probleme damit. Bei 3 Paks sind das in Summe 164 Entitäten.

Hallo Community,

war längere Zeit mit etwas anderem beschäftigt … aber zurück zum ESPHome basierten Battery Flex Controller :wink:. Wie schon im ersten Beitrag geschrieben ist das Ziel dieses Controllers die Möglichkeit zu schaffen Dinge wie:

  • SoC
  • SoH
  • Batterie Bezug/Einspeisung
  • Netz Bezug/Einspeisung

in einem standardisierten Format direkt, also ohne zu Hilfenahme von REST Calls an den Energy Managers von Solarwatt, auszulesen und zwar so, dass Home Assistant selbst minimal belastet wird. Deshalb ein eigener Controller basierend auf ESPHome.
Um das Ganze ans Laufen zu bringen wurde eine ESPHome Custom Component definiert, die sich im YAML File wie folgt manifestiert:

external_components:
  - source: ./components

Durch diesen Abschnitt weist man ESPHome an, lokale Komponenten aus dem Verzeichnis ./components einzubinden. Der große Vorteil:

  • Volle Kontrolle über den Code, ohne auf offizielle Releases warten zu müssen.
  • Schnelle Iterationen beim Testen neuer Funktionen oder API‑Änderungen.
  • Reproduzierbare Builds, da die Komponente direkt im Projekt liegt.

Um die Verarbeitung von Nachrichten sauber vom internen Scheduling von ESPHome zu entkoppeln, setzt der Controller auf AsyncTCP. Der Einstieg erfolgt über:

platformio_options:
  lib_deps:
    - me-no-dev/AsyncTCP

Damit läuft die Kommunikation asynchron:

  • Das Prozessieren einer Message wird unabhängig vom ESPHome‑Loop abgewickelt.
  • Timing‑Probleme oder Blockaden im Scheduler werden vermieden.
  • Die Steuerung bleibt responsiv, auch wenn komplexe Datenpakete verarbeitet werden.

Bottom line: AsyncTCP sorgt dafür, dass der Controller Nachrichten „nebenbei“ abarbeitet, ohne den Takt von ESPHome durcheinanderzubringen. :wink:

Damit die Sensoren der Custom Component überhaupt in Home Assistant auftauchen, reicht der reine C++‑Code leider nicht. Es braucht zusätzlich ein __init__.py (macht das Ganze zu einem gültigen ESPHome‑Modul), das in diesem Fall wie folgt aussieht:

# __init__.py
# BatteryFlex custom component package

from . import sensor

CONFIG_SCHEMA = sensor.CONFIG_SCHEMA
to_code = sensor.to_code

… sowie ein sensor.py. Letzteres definiert das YAML‑Schema und verknüpft die Einträge direkt mit der C++‑Welt.

import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_NAME
from esphome.components import sensor

battery_flex_ns = cg.esphome_ns.namespace('battery_flex')

BatteryFlexSensor = battery_flex_ns.class_('BatteryFlexSensor', cg.Component)

CONFIG_SCHEMA = cv.Schema({
    cv.GenerateID(): cv.declare_id(BatteryFlexSensor),
    cv.Required("grid_power_raw"): cv.use_id(sensor.Sensor),
    cv.Required("battery_power_raw"): cv.use_id(sensor.Sensor),
    cv.Required("battery_soc"): cv.use_id(sensor.Sensor),
    cv.Required("battery_soh"): cv.use_id(sensor.Sensor),
    cv.Required("battery_capacity"): cv.use_id(sensor.Sensor),
    cv.Required("battery_operation"): cv.use_id(sensor.Sensor),
})

async def to_code(config):
    var = cg.new_Pvariable(config[CONF_ID])
    await cg.register_component(var, config)
    
    grid = await cg.get_variable(config["grid_power_raw"])
    cg.add(var.set_grid_power_raw(grid))

    bat = await cg.get_variable(config["battery_power_raw"])
    cg.add(var.set_battery_power_raw(bat))

    soc = await cg.get_variable(config["battery_soc"])
    cg.add(var.set_battery_soc(soc))
    
    soh = await cg.get_variable(config["battery_soh"])
    cg.add(var.set_battery_soh(soh))
        
    cap = await cg.get_variable(config["battery_capacity"])
    cg.add(var.set_battery_capacity(cap))
            
    operation = await cg.get_variable(config["battery_operation"])
    cg.add(var.set_battery_operation(operation))

Der eigentlich C++ Code ist dann straight forward :wink:. Es wird eine BatteryFlexSensor Klasse definiert …

#pragma once

#include <array>
#include <string>

#include <AsyncTCP.h>

#include "esphome/components/sensor/sensor.h"
#include "esphome/core/component.h"

#include "Buffer.hpp"


namespace esphome
{
  namespace battery_flex
  {
	class BatteryFlexSensor : public PollingComponent //, public sensor::Sensor
	{
	public:

		inline static const	std::string_view		IP_BATTERY_FLEX			= "solar-batterie.fritz.box";
		
		inline static const	std::string_view		HTTP_COMMAND_GET		= "GET";
		inline static const std::string_view	    HTTP_STATUS_TARGET		= "/stat";
		inline static const std::string_view	    HTTP_PACK_TARGET		= "/pack?p=";
		inline static const std::string_view	    HTTP_PROTOCOL			= "HTTP/1.1";
		inline static const std::string_view	    HTTP_EOL				= "\r\n";
		inline static const	std::string_view		HTTP_HEADER_TERMINATOR	= Buffer::HTTP_HEADER_TERMINATOR;
		inline static const	std::string_view		HTTP_HOST				= "Host:";
		inline static const	std::string_view		HTTP_CONNECTION			= "Connection: close";
		inline static const	std::string_view		HTTP_ACCEPT_ANY			= "Accept: */*";
		
		inline static constexpr int 				HTTP_PORT				= 80;
		inline static constexpr int					HTTP_OK       			= 200;
		
		inline static constexpr int					PERIOD_MS				= 1000;   	// polling interval
		inline static constexpr int					PERIOD_PACK				= 5;
		
		inline static constexpr	size_t				NUM_PACKS				= 6;
		inline static constexpr float				PACK_CAPACITY			= 2400;		// capacity per pack in Wh
		
		inline static constexpr float				HOURS_IN_SEC			= 3600;

	
		BatteryFlexSensor();

		void setup() override;
		void update() override;
		
		void set_grid_power_raw(sensor::Sensor* p_sensor);
		void set_battery_power_raw(sensor::Sensor* p_sensor);
		void set_battery_soc(sensor::Sensor* p_sensor);
		void set_battery_soh(sensor::Sensor* p_sensor);
		void set_battery_capacity(sensor::Sensor* p_sensor);
		void set_battery_operation(sensor::Sensor* p_sensor);
	
  
	private:
	
		struct PackData
		{
		  std::string								m_serialNumber;
		  float										m_soh;
		};
	
		void setupClientStatus();
		void setupClientBatteryPacks();
		
		void scheduleClientStatus();
		void scheduleClientBatteryPacks();
	
	    // members to deal with status information
		std::string									m_httpCommandStatus;
		
	    Buffer										m_bufferStatus;
	
	    AsyncClient									m_clientStatus;
		
		// members to deal with detailed information
		int											m_timerPacks;
		size_t										m_indexPack;
		std::array<std::string, NUM_PACKS>			m_httpCommandPacks;
		std::array<PackData, NUM_PACKS>				m_dataPacks;

		Buffer										m_bufferPack;
			
	    AsyncClient									m_clientPacks;
		
		// pointers to YAML-sensors
		sensor::Sensor*								m_grid_power_raw_sensor;
		sensor::Sensor* 							m_battery_power_raw_sensor;
		sensor::Sensor*								m_battery_soc_sensor;
		sensor::Sensor*								m_battery_soh_sensor;
		sensor::Sensor*								m_battery_capacity_sensor;
		sensor::Sensor*								m_battery_operation_sensor;
		
		// calculated values
		float										m_soh;
	};	
  }  // namespace battery_flex
}  // namespace esphome

… die dann die eigentliche Arbeit bzw. die Kommunikation zur Battery Flex übernimmt:

#include <string>

#include "esphome/core/hal.h"

#include "battery_flex.hpp"


namespace esphome
{
  namespace battery_flex
  {
	template<typename TypeValue>
	static void
	publishSensor
	 (const TypeValue		p_value,
	  const char*			p_name,
	  sensor::Sensor*		p_sensor)
	
	{
	  if (nullptr != p_name)
	  {
		if (nullptr != p_sensor)
		{
		  p_sensor->publish_state(p_value);
		}
		else
		{
		  ESP_LOGW("battery_flex", "readValue: sensor '%s' not bound", p_name);
		}
	  }
	  else
	  {
		ESP_LOGW("battery_flex", "readValue: got invalid name");
	  }
	}
	

	template<typename TypeValue>
	static void
	publishSensor
	 (const JsonDocument&	p_doc,
	  const char*			p_name,
	  sensor::Sensor*		p_sensor)
	
	{
	  TypeValue actValue	= p_doc[p_name].as<TypeValue>();
	  
	  publishSensor<TypeValue>(actValue, p_name, p_sensor);
	}
	
	
	BatteryFlexSensor::BatteryFlexSensor
	 () :
	 
	 PollingComponent(PERIOD_MS),
	 
	 m_indexPack(0),
	 
	 m_grid_power_raw_sensor(nullptr),
     m_battery_power_raw_sensor(nullptr),
     m_battery_soc_sensor(nullptr),
	 m_battery_soh_sensor(nullptr),
	 m_battery_capacity_sensor(nullptr),
	 m_battery_operation_sensor(nullptr),
	 
	 m_soh(100.0)
	 
	{
	  // nothing to be done here
	}	 


	void
	BatteryFlexSensor::setup
	 ()
	 
	{
	  setupClientStatus();
	  setupClientBatteryPacks();
	}


	void
	BatteryFlexSensor::update
	 ()
	
	{
	  scheduleClientStatus();
	  scheduleClientBatteryPacks(); 
    }
	
	
	void
	BatteryFlexSensor::set_grid_power_raw
	 (sensor::Sensor* p_sensor)
	 
	{
	  m_grid_power_raw_sensor	= p_sensor;
	}
	
	
	void
	BatteryFlexSensor::set_battery_power_raw
	 (sensor::Sensor* p_sensor)

    {
	  m_battery_power_raw_sensor = p_sensor;
	}
	
	
	void
	BatteryFlexSensor::set_battery_soc
	 (sensor::Sensor* p_sensor)
	 
	{
	  m_battery_soc_sensor = p_sensor;
	}
		
	
	void
	BatteryFlexSensor::set_battery_soh
	 (sensor::Sensor* p_sensor)
	 
	{
	  m_battery_soh_sensor = p_sensor;
	}
		
	void
	BatteryFlexSensor::set_battery_capacity
	 (sensor::Sensor* p_sensor)
	 
	{
	  m_battery_capacity_sensor = p_sensor;
	}
	
	
	void
	BatteryFlexSensor::set_battery_operation
	 (sensor::Sensor* p_sensor)
	 
	{
	  m_battery_operation_sensor = p_sensor;
	}
	
	
	void
	BatteryFlexSensor::setupClientStatus
	 ()
	 
	{
	  // setup command to access status of battery
	  m_httpCommandStatus	= std::string(HTTP_COMMAND_GET) + " " + std::string(HTTP_STATUS_TARGET) + " " + std::string(HTTP_PROTOCOL) + std::string(HTTP_EOL)
							+ std::string(HTTP_HOST) + " " + std::string(IP_BATTERY_FLEX) + std::string(HTTP_EOL)
 							+ std::string(HTTP_CONNECTION) + std::string(HTTP_HEADER_TERMINATOR);
							
	  // setup callback for connect
	  m_clientStatus.onConnect
	   ([this]
	     (void* p_arg, AsyncClient* p_client)
		 
		{
		  p_client->write(m_httpCommandStatus.c_str());
		},
		nullptr);

      // setup callback for receiving data
	  m_clientStatus.onData
	   ([this]
	     (void* p_arg, AsyncClient* p_client, void* p_data, size_t p_length)
		 
		{
		  // add received data to buffer
		  Buffer::Status	actStatus	= m_bufferStatus.append(p_data, p_length);
		  
		  // check for overflow
		  if (Buffer::Status::SUCCESS != actStatus)
		  {
			ESP_LOGW("battery_flex", "m_clientStatus.onData: Buffer overflow, closing Client");
			
			// ensure, that client is available
			if (nullptr != p_client)
			{
			  p_client -> close();
			}
			else
			{
			  ESP_LOGW("battery_flex", "m_clientStatus.onData: unable to close Client");
			}
		  }
		},
		nullptr);

	  m_clientStatus.onDisconnect
	   ([this]
	     (void* p_arg, AsyncClient* p_client)
		 
		{
		  // get access to payload
		  const char*		actPayload					= m_bufferStatus.getPayload();
		  
		  // check if payload is available
		  if (nullptr != actPayload)
		  {
		    // read values from JSON Object
 		    JsonDocument	actDoc;
	
			DeserializationError actError				= deserializeJson(actDoc, actPayload);
		
			if (!actError)
			{
			  publishSensor<int>(actDoc, "SoC", m_battery_soc_sensor);
			  publishSensor<float>(actDoc, "PGrid", m_grid_power_raw_sensor);
			  publishSensor<float>(actDoc, "PBat", m_battery_power_raw_sensor);
			}
			else
			{
			  ESP_LOGW("battery_flex", "m_clientStatus.onDisconnect: unable to deserialize Json: %s", actError.c_str());
			}
		  }
		  else
		  {
			ESP_LOGW("battery_flex", "m_clientStatus.onDisconnect: unable to detect payload");
		  }
		  
		  // finally clear buffer
		  m_bufferStatus.clear();
	    },
		nullptr);	
	}
	
	
	void
	BatteryFlexSensor::setupClientBatteryPacks
	 ()
	 
	{
	  // setup commands to get information of individual battery pack
	  for (int actIndex = 0; actIndex < NUM_PACKS; actIndex++)
	  {
		// setup command to GET data related to pack
		char	actBuffer[16];
		
		itoa(actIndex, actBuffer, 10);
		
		m_httpCommandPacks[actIndex]	= std::string(HTTP_COMMAND_GET) + " " + std::string(HTTP_PACK_TARGET) + actBuffer + " " + std::string(HTTP_PROTOCOL) + std::string(HTTP_EOL)
										+ std::string(HTTP_HOST) + " " + std::string(IP_BATTERY_FLEX) + std::string(HTTP_EOL)
										+ std::string(HTTP_CONNECTION) + std::string(HTTP_EOL)
										+ std::string(HTTP_ACCEPT_ANY) + std::string(HTTP_HEADER_TERMINATOR);
										
		// setup State of Health related to pack to invalid
		m_dataPacks[actIndex].m_soh		= -1.0;
	  }
	  	  							
	  // setup callback for connect
	  m_clientPacks.onConnect
	   ([this]
	     (void* p_arg, AsyncClient* p_client)
		 
		{
		  p_client->write(m_httpCommandPacks[m_indexPack].c_str());
		},
		nullptr);

      // setup callback for receiving data
	  m_clientPacks.onData
	   ([this]
	     (void* p_arg, AsyncClient* p_client, void* p_data, size_t p_length)
		 
		{
		  // add received data to buffer
		  Buffer::Status	actStatus	= m_bufferPack.append(p_data, p_length);
		  
		  // check for overflow
		  if (Buffer::Status::SUCCESS != actStatus)
		  {
			ESP_LOGW("battery_flex", "m_clientPacks.onData: Buffer overflow, closing Client");
			
			// ensure, that client is available
			if (nullptr != p_client)
			{
			  p_client -> close();
			}
			else
			{
			  ESP_LOGW("battery_flex", "m_clientPacks.onData: unable to close Client");
			}
		  }
		},
		nullptr);

	  m_clientPacks.onDisconnect
	   ([this]
	     (void* p_arg, AsyncClient* p_client)
		 
		{
		  // get access to payload
		  const char*		actPayload					= m_bufferPack.getPayload();
		  
		  // check if payload is available
		  if (nullptr != actPayload)
		  {
		    // read values from JSON Object
 		    JsonDocument	actDoc;
	
			DeserializationError actError				= deserializeJson(actDoc, actPayload);
		
			if (!actError)
			{
			  m_dataPacks[m_indexPack].m_serialNumber	= actDoc["PK"]["SN"].as<std::string>();
			  m_dataPacks[m_indexPack].m_soh			= actDoc["PK"]["SOH"].as<float>();
			  
			  // calculate overall SoH of battery
			  m_soh										= std::min(m_soh, m_dataPacks[m_indexPack].m_soh);
			  
			  // publish SoH
			  publishSensor<float>(m_soh, "SoH", m_battery_soh_sensor);
			  
			  
			  // calculate remaining capacity of battery
			  float	actCapacity							= NUM_PACKS * PACK_CAPACITY * m_soh / 100;
			  
			  // publish remaining capacity
			  publishSensor<float>(actCapacity,"Capacity", m_battery_capacity_sensor);
			  
			  
			  // calculate opererating hours of battery
			  float actOperatingHours					= actDoc["PK"]["OP"].as<float>() / HOURS_IN_SEC;
			  
			  // publish operation hours
			  publishSensor<float>(actOperatingHours, "Operation", m_battery_operation_sensor);
			}
			else
			{
			  ESP_LOGW("battery_flex", "m_clientPacks.onDisconnect: unable to deserialize Json: %s", actError.c_str());
			}
		  }
		  else
		  {
			ESP_LOGW("battery_flex", "m_clientPacks.onDisconnect: unable to detect payload");
		  }
		  
		  // finally clear buffer
		  m_bufferPack.clear();
		  
		  // move to next battery pack
		  m_indexPack++;
		  
		  m_indexPack						%= NUM_PACKS;
	    },
		nullptr);	
	}
	
	
	void
	BatteryFlexSensor::scheduleClientStatus
	 ()
	 
	{
	  // check if connection is closed
	  if (false == m_clientStatus.connected())
	  {
		const std::string	actAddress(IP_BATTERY_FLEX);
		
		// re-open connection
		m_clientStatus.connect(actAddress.c_str(), HTTP_PORT);
	  }
	  else
	  {
		ESP_LOGW("battery_flex", "Client Status still connected – skipping new request");
	  }
	}	
		
	
	void
	BatteryFlexSensor::scheduleClientBatteryPacks
	 ()
	 
	{
	  // check if ready to access battery packs
	  if (0 == m_timerPacks)
	  {
	    // check if connection is closed
		if (false == m_clientPacks.connected())
	    {
		  const std::string	actAddress(IP_BATTERY_FLEX);
		
		  // re-open connection
		  m_clientPacks.connect(actAddress.c_str(), HTTP_PORT);
	    }
	    else
	    {
		  ESP_LOGW("battery_flex", "Client Packs still connected – skipping new request");
	    }
	  }
	  
	  m_timerPacks++;
	  
	  m_timerPacks	%= PERIOD_PACK;
	}
  }  // namespace battery_flex
}  // namespace esphome

… sehr schön ist hierbei das Aufsetzen der asynchronen Kommunikation zu sehen …

m_clientXYZ.onConnect ...
m_clientXYZ.onData ...
m_clientXYZ.onDisconnect ...

… in denen jeweils Callbacks für das entsprechende Verhalten der Kommunikation registriert werden.
Man kann sagen, dass dieses kleine Projekt richtig Spass gemacht hat :grinning_face:. Wenn es jemand nachbauen möchte: alle Sourcen liegen auf Github … viel Spaß :grinning_face:

Hallo Community,

vergaß gestern zu erwähnen, dass das Ganze noch einen Buffer …

#pragma once

#include <string>

namespace esphome
{
  namespace battery_flex
  {
	class Buffer
	{
	public:
	
		inline static const	std::string_view		HTTP_HEADER_TERMINATOR	= "\r\n\r\n";
	
		inline static constexpr size_t				CAPACITY_BUFFER		= 2048;
		
		enum class Status : int						{ SUCCESS = 0, OVERFLOW = -1 };

	
		Buffer();
		
		size_t		size() const;		
		
		void 		clear();
		
		const char*	getData() const;
		
		Status		append(void* p_data, size_t p_length); 
		
		const char*	getPayload() const;
	
  
	private:
	
		// disabled operations
		Buffer(const Buffer&)											= delete;
		Buffer(Buffer&&)												= delete;
		
		Buffer&		operator=(const Buffer&)							= delete;
		Buffer&		operator=(Buffer&&)									= delete;
		
		const std::string							m_http_header_terminator;
	
		size_t										m_lengthBuffer;
		
		char										m_buffer[CAPACITY_BUFFER];
	};	
  }  // namespace battery_flex
}  // namespace esphome

… besitzt, der es erlaubt:

  1. Daten Chunks zusammenzufügen
  2. die Payload innerhalb einer Nachricht herauszufiltern
  3. overload handling bereit hält

… und am Ende dafür sorgt, dass die Payload mit \0 beendet wird.

#include <algorithm>
#include <cstring>

#include "Buffer.hpp"


namespace esphome
{
  namespace battery_flex
  {
	Buffer::Buffer
	 () :
	 
	 m_http_header_terminator(HTTP_HEADER_TERMINATOR),
	 
	 m_lengthBuffer(0)
	 
	{
	  // ensure, that buffer is cleared
	  memset(m_buffer, 0, sizeof(m_buffer));
	}	 


	size_t
	Buffer::size
	 () const
	 
	{
	  return m_lengthBuffer;
	}
	

	void
	Buffer::clear
	 ()
	 
	{
	  m_lengthBuffer	= 0;
	  
	  m_buffer[0]		= '\0';
	}


	const char*
	Buffer::getData
	 () const
	 
	{
	  return (m_buffer);
	}
	
	
	Buffer::Status
	Buffer::append
	 (void*		p_data,
	  size_t	p_length)
	
	{
	  Status	result				= Status::SUCCESS;
	  
	  size_t	actLength			= std::min(sizeof(m_buffer) - 1 - m_lengthBuffer, p_length);
	  
	  // check if there's still space available
	  if (0 < actLength)
	  {
		memcpy(m_buffer + m_lengthBuffer, p_data, actLength);
		
		m_lengthBuffer				+= actLength;
		
		// ensure, that buffer is terminated with '\0'
		m_buffer[m_lengthBuffer]	= '\0';
	  }
	  
	  // check for buffer overflow
	  if (p_length > actLength)
	  {
	    result						= Status::OVERFLOW;
	  }
	  
	  return result;
	}
	
	
	const char*
	Buffer::getPayload
	 () const
	 
	{
	  // setup default result
	  const char*	result	= nullptr;
	  
	  
	  // check if buffer holds any data
	  if (0 < m_lengthBuffer)
	  {
		result				= strstr(m_buffer, m_http_header_terminator.c_str());
		
		if (nullptr != result)
		{
		  // skip terminator
		  result			= &result[m_http_header_terminator.length()];
		}
	  }
	  
	  return result;
	}
  }  // namespace battery_flex
}  // namespace esphome

Damit wäre die Beschreibung des Controllers komplett. Viel Spass beim Nachbauen :grinning_face:

@Andreas59: hallo war krank, hatte einiges an Ärger mit einer KI … in der Zwischenzeit hat Solarwatt bestätigt, dass unser sehr teurer Energy Manager Flex wirklich “NICHTS” macht! Wie gesagt, wir nutzen die App von Solarwatt nicht … von daher müssen in unserem Fall keinen Daten in die Solarwatt Cloud und das wäre wohl das einzige was er in unserem Fall machen würde: “Datenschaufeln”. Werden also das Ding nun abschalten und hoffen, dass irgendwer bei dem bekannten Online Händler ihn kauft …
Ist schon ein sehr teurer Spaß für “nichts machen” :sad_but_relieved_face:

ja, ich war schon immer sehr interessiert seit der Smart Home Entwicklung wie die Geräte aller Hersteller mit einander kommunizieren und schon da ist mir aufgefallen das es oft nur Insellösungen von verschiedenen Herstellern gibt.

Hatte dann den Einstieg mit Homematic IP, war aber auch nur für ausgewählte Geräte geeignet. Als dann die PV-Anlage kam bin ich auf Home Assistant gewechselt weil es keine Herstellerbeschränkung gibt. So das ich die Möglichkeiten eines Energy Manager Flex laut Handbuch für mich als Alternative halt ausgeschlossen habe. Meine Erfahrung bis heute ist das das Teil nur da für da ist das di App geht und das die Installfirma / Solarwatt unkompliziert auf das System zugreifen kann.

Denn bei jeglicher Kommunikation mit der Installfirma / Solarwatt kam die Frage ich sehe Ihre Anlage nicht, ja ich habe keinen Energy Manager, und dann geht die eierei los….

Schönes Rest WE :slightly_smiling_face: :fu:

Hallo Community,

habe gerade eine 1.0 Version des weiter oben beschriebenen Battery Flex Controllers released. Die großen Änderungen sind:

  1. Einführung von Timeouts für die Kommunikation mit der Battery Flex … gebe zu, das hätte ich gleich mach sollen, durch diesen Ansatz verklemmt sich der Controller nicht mehr :wink:
  2. Reduktion des Logging Information … der Controller war schon etwas “chatty”, speziell wenn er ein Verbindung zur Battery Flex offen hatte kam es häufig zu Log Einträgen wie “Verbingung bereits offen …”. Da dies keine echten Mehrwert liefert wurden diese Log Einträge einfach entfernt.
  3. Es wird nur noch EIN GET ausgeführt und danach komplett abgearbeitet. Die ursprüngliche Implementierung mit zwei parallelen Request war zwar gut gemeint, hat das System jedoch schwer ins Schwitzen gebracht :wink:. Nachdem dies nun entfernt wurde läuft der Controller stabil 24-by-7 :grinning_face:

All diese Änderungen führten dazu, dass nun auch sein WEB Interface …

… sauber funktioniert inklusive eines, alle 5s blinkenden, Heartbeats :grinning_face:

Für uns ist damit der Solarwatt Energy Manager Flex …

… mit seinem nicht dokumentierten und instabilen REST-API, Geschichte :grinning_face: