/** * ecoflow-connector.js * Version: 1.1.6.2 * Release date: 04.11.2023 * Autor: Waly_de * Forum: https://forum.iobroker.net/topic/66743/ecoflow-connector-script-zur-dynamischen-leistungsanpassung * * This JavaScript file establishes a simple connection between IOBroker and EcoFlow. * It automatically creates known states under 0_userdata. * * Please note that adjustments in the ConfigData section are required. Here, you need to enter your access credentials * used for the EcoFlow app, as well as the serial numbers of your devices. * * If you have a state that displays the current power consumption (SmartmeterID), please provide it as well. * This value will be used to dynamically adjust the Powerstream's feed-in power. * * Not all parameters of the Powerstream data are known yet. All known parameters will be automatically created as states. * By modifying the "protoSource" constant, newly discovered data will also be automatically created. * * The raw data of the interface is logged as a HEX string. * * Please exercise caution as this is the initial version of the script. Use it at your own risk! * * Requirements: * - Install protobuf. Simply add these libs to your javascript instance configuration (Zusätzliche NPM-Module) * - The "Paho MQTT Client" is also required. If not already installed, use the javascript instance configuration. * * * Note: It is encouraged to discover and publish missing data definitions to improve the script. * Suggestions, optimizations, and extensions are welcome at any time. * * Special thanks to all contributors for their valuable input and support. * * Unterstütze das Projekt 'ecoflow-connector' * Wenn dir das Script zur dynamischen Leistungsanpassung für den IObroker gefällt und du es nützlich findest, * ziehe bitte in Erwägung, eine kleine Spende via PayPal zu hinterlassen. * Jeder Beitrag hilft, das Projekt am Laufen zu halten und weitere Updates zu ermöglichen. * Danke für deine Unterstützung! * https://www.paypal.com/donate/?hosted_button_id=4J7JDDALF3N5L * * * Changelog: * ----------------------------------------------------------- * (0.4) 29.06.2023 * Da der MQTT von ecoflow regelmäßig aufhört zu senden, vor allem, wenn die App genutzt und komplett geschlossen wird, * habe ich eine Überwachung der letzten ankommenden Nachrichten eingebaut. Kommt 5 Minuten lang nichts Neues vom PowerStream, * wird die Verbindung zum MQTT komplett neu aufgebaut. * * Ein Fehler bei der Erstellung der States wurde beseitigt. * ----------------------------------------------------------- * (0.5.2) 06.07.2023 * State sumPV hinzugefügt (Summe aus PV1 und PV2) = Solar-Leistung gesamt. * Abweichung der PV Power von der App versucht zu kompensieren. * Neuen State RealPower zur besseren Ermittlung der Einspeiseleistung angelegt. Zeigt den Verbrauch im Haus ohne Einspeisung. * History für RealPower wird automatisch aktiviert. * Die Koordinaten werden aus den Systemeinstellungen ermittelt (sonst einfach selbst angeben). * Diverse kleine Anpassungen und Bugfixes. * Es werden jetzt mehrere PowerStreams berücksichtigt. In der Konfiguration muss das Flag "isPowerStream" gesetzt werden. * Gesteuert wird aber bisher nur der erste PowerStream. * Die protoSource so angepasst, dass in "Item" enthaltene unbekannte Daten auch als State angelegt werden. Bitte helft mit zu identifizieren, * was was ist. Dann können wir die Felder entsprechend benennen... * Parameter "subscribe" bei der Gerätekonfiguration hinzugefügt. Damit lässt sich der Empfang von MQTT Telegrammen für das Gerät abstellen. * Mein Delta Max hatte derart viel gesendet, dass der Raspi nicht mehr in der Lage war alles zu verarbeiten. Für die Steuerung braucht man die Daten der Batterie nicht. * Nach Sonnenuntergang wird jetzt weniger oft reconnectet, wenn keine Daten mehr kommen. * Reaktionszeiten für die Anpassung der Einspeisung wurden erhöht (30 Sekunden). * State "totalPV" für die derzeitige komplette PV Leistung hinzugefügt. * Funktion hinzugefügt, die die Einspeisung bei voller Batterie auf Maximum stellt. Ein- und Ausschaltprozent können mit battPozOn und battPozOff eingestellt werden. * ----------------------------------------------------------- * (0.6.1) 26.07.2023 * ACHTUNG: Die Felddefinitionen für den Powerstream sind jetzt vollständig und an die von der Community ermittelten Daten angepasst. * Das bedeutet aber leider auch, dass alle States mit neuen Namen neu angelegt werden. Die vom Script generierten States bleiben erhalten (SerAC, totalPV, sumPV). * * Für die Delta Max sind nun einige States auch zum Schreiben verfügbar. Die Delta muss dazu nicht unbedingt auf "subscribe: true" gestellt werden. * Damit die States angelegt werden, müssen sie bei laufendem Script einmal in der App verändert werden. Möglich sind bisher: * Beep, slowChgPower, ACPower, DCPower, USBPower, minDsgSoc, maxChgSoc, curr12VMax, standByModeMins, lcdTimeMins, ACstandByMins, openOilSoc, closeOilSoc. * * Ob diese States auch so bei anderen Deltas funktionieren, kann ich nicht sagen. Wenn nicht, solltet ihr im Log einen Eintrag finden: "Unbekannter Set Befehl:". * Wenn ich diesen Eintrag mit einer kurzen Beschreibung erhalte, was es ist, kann ich es auch einbauen. * * Die States werden hier angelegt: 0_userdata.0.ecoflow.app_XXXXXXXXXXXXXXXXXXX_XXXXXXXXXXXXXXX_thing_property_set.writeables * * (0.6.7) 31.07.2023 * * Writeables für Delta2 angelegt. * totalPV / 10 geändert, damit ein echter Wattwert angezeigt wird. * Anpassung für neues Datenformat nach diversen Updates von ecoflow. * Diverse Optimierungen und Bugfixes. * * (0.6.8) 03.08.2023 * * Neue Einstellung: "Regulation: false" zum Abstellen der Regulierung des PowerStreams (Read Only Modus). * Anpassung an neues Format. * Bugfixes. * * (1.0.0) 06.08.2023 * * - Neuer State "lowestValue" zeigt die Grundlage zur Berechnung der Einspeiseleistung an und repräsentiert den niedrigsten Wert des realen Verbrauchs in den * letzten mit "MinValueMin" eingestellten Minuten. * - Neue Einstellung MinValueAg: Art der Ermittlung des kleinsten Wertes 0 = Minimalwert, 1 = Durchschnittswert. * - Neues Feature: Wenn die volle Leistung (600w) in die Batterie geht, wird die Einspeiseleistung in Stufen erhöht, auch wenn dann * Leistung ins Netz geht, um möglichst das volle Potenzial der vorhandenen Solarenergie zu nutzen. * - Neue Einstellungen: lowBatLimitPozOn, lowBatLimitPozOff und lowBatLimit. Bei Unterschreiten der Batterieladung von "lowBatLimitPozOn" % ist die maximale Einspeiseleistung auf * "lowBatLimit" W limitiert, bis der Ladezustand wieder bei "lowBatLimitPozOff" ist. * - Neue Einstellungen: RegulationState. Frei wählbar. Wenn angegeben, kann mit diesem State die Regulation ein- und ausgeschaltet werden (Wird automatisch unterhalb 0_userdata.0.ecoflow angelegt). * - Neue Einstellungen: RegulationOffPower. Wird die Regulation per State abgestellt, wird die Einspeiseleistung des ersten Powerstreams auf diesen Wert gesetzt. (-1 = keine Änderung). * * (1.0.1) 07.08.2023 * * - Writeables auch für PowerStreams angelegt (SetDisplayBrightness, SetPrio (0=Stromversorgung, 1= Batterie ), SetBatLimitLow, SetBatLimitHigh, SetAC).Sie tauchen auf, wenn bei laufendem Script per App geändert wird. * - Verbesserung der Reguierung * * (1.0.2) 09.08.2023 * * - Unterstützung für SmartPlugs. Bisher ein Writeable: SwitchPlug mit den Werten 0= AUS und 1= AN * - Bugfix und Optimierungen * * (1.1.2) 17.08.2023 * * - Breaking Changes bei der Konfiguration. Viele Daten sind jetzt zu den einzelnen PowerStream gewandert. * - Unterstützung der Steuerung von mehreren PowerStream in 2 Modes (Balance und Serial) * Balance: die PS werden nacheinander angesprochen, dabei versucht jeder Einzelne für sich den Bedarf zu decken. * Serial: Der Bedarf wird in der Reihenfolge der Konfiguration verteilt. Erst wenn der erste es nicht mehr schafft den Bedarf zu decken, wird der Nächste hinzugezogen * - Automatisches Wechseln in den Batterieprioritätsmodus. battOnSwitchPrio: true/false wenn battPozOn erreicht ist * - Festlegen des Gerätetyps bei der Konfiguration. Typ: Powerstrem:"PS"; DeltaMax:"DM"; DeltaMax2: "DM2"; SmartPlug: "SM"; Andere: "NA" * * (1.1.3.2) 31.08.2023 * * - 0 bei MinValueMin greift auf Echtzeitdaten (Realpower) zu. (nicht empfohlen) * - Neue Writeables für den Delta 2 Max: quietCfg,ACenabled,maxChgSoc,minDsgSoc,bpPowerSoc,fastChgWatts,slowChgWatts,chgPauseFlag,dcChgCfg,dcChgCfg2,USB,12VDC,smartgenClose,smartgenOpen,standbyTime,lcdTime * - Eigene Bezeichnung für den Delta 2 Max (D2M) * - Achtung neue Bezeichnungen für Typen: Powerstrem:"PS"; DeltaMax:"DM"; Delta2: "D2" ; Delta2 Max: "D2M"; SmartPlug: "SM"; Andere: "NA" * - RegulationOffPower ist in die Powerstream-Einstellungen gewandert und kann für jeden PS einzeln eingestellt werden. * - RegulationState schaltet jetzt RegulationOffPower für alle PS einzeln * - RegulationOffPower: -2 schaltet den PS in den Batterie-Prioritätsmodus wenn über RegulationState abgestellt wird * - Neue Einstellung für PS: prioOffOnDemand: Wattwert des Bedarfs, bei dem dem zurück in den Strom-Priomodus geschaltet wird. 0 für kein Rückschalten. * - Verschiedene Optimierungen * * * (1.1.4.1) 23.09.2023 * - Neue Einstellung: "AdditionalPower": Wenn es weitere Wechselrichter gibt, die in ioBroker erfasst werden, können diese hier angelegt werden. * Die Leistung wird dann für die Berechnung von "Realpower" berücksichtigt. * - Delta 2 Max "Writeables" erneut überarbeitet (Danke an Ponti92 aus dem ioBroker-Forum). * - "Maxpower" ist jetzt in den Einstellungen für jeden einzelnen PS verfügbar. * - Komplett neues Regelwerk zum Einstellen und Verteilen der Einspeiseleistung auf mehrere PS: * Im "Balance Mode" wird zunächst nur die verfügbare PV-Leistung aller PS eingespeist; erst danach wird aus den Batterien entnommen. Dabei richtet sich die * Entladeleistung nach dem Ladestand der Batterien. Alle PS werden bei einem Messintervall gleichzeitig geregelt. * - "battOnSwitchPrio" überarbeitet und verbessert. * - SmartPlugs sollten nun bei Verwendung den Regelprozess nicht mehr komplett verwirren. * - Für SmartPlugs gibt es nun auch historische Leistungsdaten in den Objekten. * - Auch für PS wurden die historischen Leistungsdaten neu angelegt. Die Interpretation dieser Daten steht noch aus. Vielleicht möchte das einer von euch machen? * - Neue Einstellung: "SmartmeterTimeoutMin" und "SmartmeterFallbackPower". Wenn der letzte Wert vom Smartmeter älter als "SmartmeterTimeoutMin" ist, wird das Skript mit * "SmartmeterFallbackPower" als aktuellem Realpowerwert weiterarbeiten, bis wieder aktuelle Daten geliefert werden. * - Bekannte Writeables werden nun automatisch angelegt, auch ohne dass sie in der App geändert werden müssen (voraussetzung ist die richtige Einstellung des Typs bei den Geräteeinstellungen) * - Verschiedene Optimierungen und Bugfixes * * * (1.1.4.2) 26.09.2023 * - BugFix * - Neuer Parameter: Zusatzpower_Offset: Zusatzpower startet ab einer Batterieladeleistung von MaxPower - Zusatzpower_Offset * * * (1.1.5) 02.10.2023 * - BugFix * - externConfig Eingeführt um beliebige Einstellungspunkte über einen beliebigen States zu steuern * - Wechsel des automatischen Priomodus fühestens alle 60 Sekunden, um dem PS Zeit für die Umstellung zu geben. * * * (1.1.6) 30.10.2023 * - Versuchter Delta 2 Max Writeables Fix * * * (1.1.6.1) 1.11.2023 * - Delta 2 Max Writeables Fix für chgPauseFlag * - Log-Möglichkeit für Writeables von Deltas per Seriennummer eingebaut. Es wird ein Logfile erstellt, das unter Protokolle / Log Herunterladen erreichbar ist. * SERIAL_TO_LOG enthällt eine oder mehrere Seriennummern per "," getrennt * - Kleine Anpassungen der Writeables für D2M * * * (1.1.6.2) 4.11.2023 * - Neues PowerStream-Writeable "feed_priority" eingebaut 0: Wenn Batt voll, alle PV-Power einspeisen 1: Nur was eingestellt ist * - Konfiguration wird jetzt für Module in einem State unter Settings gespeichert * * */ // Systemkoordinaten werden versucht zu ermitteln und als Default den Variablen zugeordnet. var latitude, logM = false var longitude; // Ermitteln des Standortes aus den Einstellungen. getStandortKoordinaten() const logpath = '/opt/iobroker/log/'; const SERIAL_TO_LOG = "DAEBXXXXXXXXXXXXX" /*************************************** ********** YOUR DATA HERE ************ ****************************************/ var ConfigData = { email: "your@mail.com", // Die App-Zugangsdaten von ecoFlow passwort: "yourAppPasswort", SmartmeterID: "sonoff.0.Stromzaehler1.MT175_P", // State, der den aktuellen Gesamtverbrauch in Watt anzeigt seriennummern: [ //############# Diesen Abschnitt für jedes einzelne Gerät anlegen ################ { seriennummer: "XXXXXXXXXXXXX", // Die Seriennummer des Gerätes name: "PowerStream", // beliebiger Namen MaxPower: 600, // Der höchstmögliche Wert in Watt für die Einspeiseleistung subscribe: true, // "true": Alle Daten für dieses Gerät werden angefragt. "false": Es werden keine Statusdaten abgefragt typ: "PS", // Welches Gerät ist es: Powerstrem:"PS"; DeltaMax:"DM"; Delta2: "D2" ; Delta2 Max: "D2M"; SmartPlug: "SM"; Andere: "NA" // Parameter an hier nur für PowerStream. regulation: true, // "true": Dieser PowerStream soll vom Script reguliert werden RegulationOffPower: -1, // Wird die Regulation per State abgestellt, wird die Einspeiseleistung des ersten Powerstreams auf diesen Wert gesetzt (-1 = keine Änderung, -2 = Batterie Priomodus) hasBat: true, // "true": Eine Batterie ist angeschlossen. Nur für PowerStream relevant. battPozOn: 99, battPozOff: 94, // Wenn die Batterie bei battPozOn ist, Einspeisung auf MaxPower. Bei BattPozOff Normalbetrieb battOnSwitchPrio: true, // "true": Bei battPozOn wird in den Batterie-Prioritätsmodus gewechselt prioOffOnDemand: 30, // Wattwert des Bedarfs, bei dem zurück in den Strom-Priomodus gechaltet wird. 0 für kein Rückschalten. lowBatLimitPozOn: 5, lowBatLimitPozOff: 15, // Bei Unterschreiten der Batterieladung von "lowBatLimitPozOn" % ist die maximale Einspeiseleistung auf lowBatLimit: 150, // "lowBatLimit" limitiert, bis der Ladezustand wieder bei "lowBatLimitPozOff" ist }, //####################################################################### { seriennummer: "XXXXXXXXXXXXX", name: "DELTA Max", typ: "DM", subscribe: false, // "true": Alle Daten für dieses Gerät werden angefragt. "false": Es werden keine Statusdaten abgefragt }, //####################################################################### { seriennummer: "XXXXXXXXXXXXX", name: "SmartPlug 1", typ: "SM", subscribe: true, // "true": Alle Daten für dieses Gerät werden angefragt. "false": Es werden keine Statusdaten abgefragt }, //####################################################################### ], AdditionalPower: [ // Wenn es weitere Wechselrichter gibt, die in IOBroker erfasst werden, können diese hier Angelegt werden //############# Diesen Abschnitt für jedes einzelne Gerät anlegen ################ { name: "Hoymiles2000", // Beliebiger Name id: "mqtt.0.solar.1234567890.0.power" // Der Objektpfad zu dem Leistungswert in Watt (W) }, //####################################################################### ], //**************************************** // Erweiterte Einstellungen: //**************************************** SmartmeterTimeoutMin: 4, // Wenn der letzte Wert vom Smartmeter älter als "SmartmeterTimeoutMin" ist, wird das Script mit... SmartmeterFallbackPower: 150, // SmartmeterFallbackPower als aktuellem Realpowerwert weiter arbeiten, bis wieder aktuelle Daten geliefert werden. Regulation: true, // 'false' stellt das Setzen der Einspeiseleistung ab RegulationState: "Regulate", // Wenn angegeben, kann mit diesem State die Regulation ein- und ausgeschaltet werden (Wird automatisch unter 0_userdata.0.ecoflow angelegt) RegulationMultiPsMode: 0, // Wenn mehrere PS reguliert werden sollen. "balance" = 0 oder "serial" = 1 BasePowerOffset: 30, // Wird vom aktuellen Verbrauch abgezogen, um die Einspeiseleistung zu berechnen Zusatzpower_Offset: 10, // Zusatzpower startet ab einer Batterieladeleistung von MaxPower - Zusatzpower_Offset MinValueMin: 3, // Der Zeitraum in Minuten, aus dem der niedrigste Gesamtverbrauchswert geholt werden soll. 0 für Echtzeitwert MinValueAg: 0, // Art der Ermittlung des kleinsten Wertes: 0 = Minimalwert, 1 = Durchschnittswert ReconnectMin: 30, // Zeit in Minuten, nach der die Anwendung neu gestartet wird, wenn keine neuen Daten eintreffen statesPrefix: "0_userdata.0.ecoflow", // Hier werden die ecoFlow States angelegt latitude: latitude, // Breitengrad des Standortes (wird automatisch eingesetzt) longitude: longitude, // Längengrad des Standortes (wird automatisch eingesetzt) //**************************************** // Überschuss Ladung: //**************************************** ExcessCharge: false, // ExcessChargePowerID: "0_userdata.0.ecoflow.app_xxxxxxxxxxxxxxxxxxxx_DAxxxxxxxxxxxxxxxxxx_thing_property_set.writeables.slowChgPower", ExcessChargeSwitchID: "hm-rpc.3.xxxxxxxxxxxxxxxxxx.3.STATE", ExcessChargeSwitchOn: true, ExcessChargeSwitchOff: false, ExcessChargeSwitchMin: 10, ExcessChargeMaxPower: 1500, ExcessChargeStartPower: 50, ExcessChargeStartPowerDurationMin: 5, //**************************************** Debug: false, PlotCmdID: 99999, } //***************************************/ //***************************************/ // EXTERNE STATES zum Manipulieren von Konfigurationsvariablen var externConfig = [ //############# Diesen Abschnitt für jeden einzelne Wert anlegen ################ { VarName: "seriennummern[0].lowBatLimitPozOn", //Variabelname aus "ConfigData" bei Aufzählungen [0...X] in der Reihenfolge der Angaben id: "0_userdata.0.ecoflow.ExStateTest" //Das Objekt (State) das den Wert für diese Variable enthalten soll. Muss manuell angelegt werden. }, //####################################################################### ]; //####################################################################### // Funktion zum Setzen einer verschachtelten Eigenschaft in einem Objekt function setNestedProperty(obj, path, value) { let keys = path.split('.'); let lastKey = keys.pop(); let tempObj = obj; keys.forEach(key => { if (key.match(/\[\d+\]/)) { let index = parseInt(key.match(/\[(\d+)\]/)[1]); key = key.split('[')[0]; tempObj = tempObj[key][index]; } else { tempObj = tempObj[key]; } }); tempObj[lastKey] = value; } function setConfigValueFromId(VarName, id) { if (isState2(id)) { let value = getState(id).val; // Wert von ioBroker abrufen setNestedProperty(ConfigData, VarName, value); // Wert in ConfigData setzen } } // Initialisierung bei Programmstart externConfig.forEach(config => { setConfigValueFromId(config.VarName, config.id); }); // Änderungen während der Laufzeit überwachen externConfig.forEach(config => { if (isState2(config.id)) { on(config.id, function (obj) { setConfigValueFromId(config.VarName, obj.id); log("ExternesEvent:" + obj.id + " : " + config.VarName + ":" + obj.state.val) }); } }); //####################################################################### //setStateNE(ConfigData.statesPrefix + ".Settings.ConfigData", ConfigData) setStateNE(ConfigData.statesPrefix + ".Settings.ConfigData", JSON.stringify(ConfigData)) const LogAllHeader = false //"HW" const protoSource2 = ` syntax = "proto3"; message Message { repeated Header header = 1 ; bytes payload = 2; } message Header { bytes pdata = 1 [proto3_optional = false]; int32 src = 2 [proto3_optional = true]; int32 dest = 3 [proto3_optional = true]; int32 d_src = 4 [proto3_optional = true]; int32 d_dest = 5 [proto3_optional = true]; int32 enc_type = 6 [proto3_optional = true]; int32 check_type = 7 [proto3_optional = true]; int32 cmd_func = 8 [proto3_optional = true]; int32 cmd_id = 9 [proto3_optional = true]; int32 data_len = 10 [proto3_optional = true]; int32 need_ack = 11 [proto3_optional = true]; int32 is_ack = 12 [proto3_optional = true]; int32 seq = 14 [proto3_optional = true]; int32 product_id = 15 [proto3_optional = true]; int32 version = 16 [proto3_optional = true]; int32 payload_ver = 17 [proto3_optional = true]; int32 time_snap = 18 [proto3_optional = true]; int32 is_rw_cmd = 19 [proto3_optional = true]; int32 is_queue = 20 [proto3_optional = true]; int32 ack_type = 21 [proto3_optional = true]; string code = 22 [proto3_optional = true]; string from = 23 [proto3_optional = true]; string module_sn = 24 [proto3_optional = true]; string device_sn = 25 [proto3_optional = true]; } message InverterHeartbeat { optional uint32 inv_err_code = 1; optional uint32 inv_warn_code = 3; optional uint32 pv1_err_code = 2; optional uint32 pv1_warn_code = 4; optional uint32 pv2_err_code = 5; optional uint32 pv2_warning_code = 6; optional uint32 bat_err_code = 7; optional uint32 bat_warning_code = 8; optional uint32 llc_err_code = 9; optional uint32 llc_warning_code = 10; optional uint32 pv1_statue = 11; optional uint32 pv2_statue = 12; optional uint32 bat_statue = 13; optional uint32 llc_statue = 14; optional uint32 inv_statue = 15; optional int32 pv1_input_volt = 16; optional int32 pv1_op_volt = 17; optional int32 pv1_input_cur = 18; optional int32 pv1_input_watts = 19; optional int32 pv1_temp = 20; optional int32 pv2_input_volt = 21; optional int32 pv2_op_volt = 22; optional int32 pv2_input_cur = 23; optional int32 pv2_input_watts = 24; optional int32 pv2_temp = 25; optional int32 bat_input_volt = 26; optional int32 bat_op_volt = 27; optional int32 bat_input_cur = 28; optional int32 bat_input_watts = 29; optional int32 bat_temp = 30; optional uint32 bat_soc = 31; optional int32 llc_input_volt = 32; optional int32 llc_op_volt = 33; optional int32 llc_temp = 34; optional int32 inv_input_volt = 35; optional int32 inv_op_volt = 36; optional int32 inv_output_cur = 37; optional int32 inv_output_watts = 38; optional int32 inv_temp = 39; optional int32 inv_freq = 40; optional int32 inv_dc_cur = 41; optional int32 bp_type = 42; optional int32 inv_relay_status = 43; optional int32 pv1_relay_status = 44; optional int32 pv2_relay_status = 45; optional uint32 install_country = 46; optional uint32 install_town = 47; optional uint32 permanent_watts = 48; optional uint32 dynamic_watts = 49; optional uint32 supply_priority = 50; optional uint32 lower_limit = 51; optional uint32 upper_limit = 52; optional uint32 inv_on_off = 53; optional uint32 wireless_err_code = 54; optional uint32 wireless_warn_code = 55; optional uint32 inv_brightness = 56; optional uint32 heartbeat_frequency = 57; optional uint32 rated_power = 58; optional uint32 feed_priority = 61; } message InverterHeartbeat2 { int32 X_Unknown_1 = 1; int32 X_Unknown_2 = 2; int32 X_Unknown_3 = 3; int32 X_Unknown_4 = 4; int32 X_Unknown_5 = 5; int32 X_Unknown_6 = 6; int32 X_Unknown_7 = 7; int32 X_Unknown_8 = 8; int32 X_Unknown_9 = 9; int32 X_Unknown_10 = 10; int32 X_Unknown_11 = 11; int32 X_Unknown_12 = 12; int32 X_Unknown_13 = 13; int32 X_Unknown_14 = 14; int32 X_Unknown_15 = 15; int32 X_Unknown_16 = 16; int32 X_Unknown_17 = 17; int32 X_Unknown_18 = 18; int32 X_Unknown_19 = 19; int32 X_Unknown_20 = 20; int32 X_Unknown_21 = 21; int32 X_Unknown_22 = 22; int32 X_Unknown_23 = 23; int32 X_Unknown_24 = 24; int32 X_Unknown_25 = 25; int32 X_Unknown_26 = 26; int32 X_Unknown_27 = 27; int32 X_Unknown_28 = 28; int32 X_Unknown_29 = 29; int32 X_Unknown_30 = 30; int32 X_Unknown_31 = 31; int32 X_Unknown_32 = 32; int32 X_Unknown_33 = 33; int32 X_Unknown_34 = 34; int32 X_Unknown_35 = 35; int32 X_Unknown_36 = 36; int32 X_Unknown_37 = 37; int32 X_Unknown_38 = 38; int32 X_Unknown_39 = 39; int32 X_Unknown_40 = 40; int32 X_Unknown_41 = 41; int32 X_Unknown_42 = 42; int32 X_Unknown_43 = 43; int32 X_Unknown_44 = 44; int32 X_Unknown_45 = 45; int32 X_Unknown_46 = 46; int32 X_Unknown_47 = 47; int32 X_Unknown_48 = 48; int32 X_Unknown_49 = 49; int32 X_Unknown_50 = 50; int32 X_Unknown_51 = 51; int32 X_Unknown_52 = 52; } message setMessage { setHeader header = 1; } message setHeader { setValue pdata = 1 [proto3_optional = true]; int32 src = 2 [proto3_optional = true]; int32 dest = 3 [proto3_optional = true]; int32 d_src = 4 [proto3_optional = true]; int32 d_dest = 5 [proto3_optional = true]; int32 enc_type = 6 [proto3_optional = true]; int32 check_type = 7 [proto3_optional = true]; int32 cmd_func = 8 [proto3_optional = true]; int32 cmd_id = 9 [proto3_optional = true]; int32 data_len = 10 [proto3_optional = true]; int32 need_ack = 11 [proto3_optional = true]; int32 is_ack = 12 [proto3_optional = true]; int32 seq = 14 [proto3_optional = true]; int32 product_id = 15 [proto3_optional = true]; int32 version = 16 [proto3_optional = true]; int32 payload_ver = 17 [proto3_optional = true]; int32 time_snap = 18 [proto3_optional = true]; int32 is_rw_cmd = 19 [proto3_optional = true]; int32 is_queue = 20 [proto3_optional = true]; int32 ack_type = 21 [proto3_optional = true]; string code = 22 [proto3_optional = true]; string from = 23 [proto3_optional = true]; string module_sn = 24 [proto3_optional = true]; string device_sn = 25 [proto3_optional = true]; } message setValue { optional int32 value = 1; } message permanent_watts_pack { optional int32 permanent_watts = 1; } message supply_priority_pack { optional int32 supply_priority = 1; } message bat_lower_pack { optional int32 lower_limit = 1; } message bat_upper_pack { optional int32 upper_limit = 1; } message PowerItem { optional uint32 timestamp = 1; optional sint32 timezone = 2; optional uint32 inv_to_grid_power = 3; optional uint32 inv_to_plug_power = 4; optional int32 battery_power = 5; optional uint32 pv1_output_power = 6; optional uint32 pv2_output_power = 7; } message PowerPack2 { optional uint32 sys_seq = 1; repeated PowerItem EnergyItem = 2; } message PowerPack32 { optional uint32 sys_seq = 1; repeated EnergyItem EnergyItem = 2; } message PowerPack133 { optional uint32 sys_seq = 1; repeated EnergyItem EnergyItem = 2; } message PowerPack138 { optional uint32 sys_seq = 1; repeated PowerItem EnergyItem = 2; } message PowerPack135 { optional uint32 sys_seq = 1; repeated PowerItem EnergyItem = 2; } message PowerPack136 { optional uint32 sys_seq = 1; repeated PowerItem EnergyItem = 2; } message PowerPack { optional uint32 sys_seq = 1; repeated PowerItem sys_power_stream = 2; } message PowerAckPack { optional uint32 sys_seq = 1; } message node_massage { optional string sn = 1; optional bytes mac = 2; } message mesh_child_node_info { optional uint32 topology_type = 1; optional uint32 mesh_protocol = 2; optional uint32 max_sub_device_num = 3; optional bytes parent_mac_id = 4; optional bytes mesh_id = 5; repeated node_massage sub_device_list = 6; } message EnergyItem { optional uint32 timestamp = 1; optional uint32 watth_type = 2; repeated uint32 watth = 3; } message EnergyTotalReport { optional uint32 watth_seq = 1; optional EnergyItem watth_item = 2; } message BatchEnergyTotalReport { optional uint32 watth_seq = 1; repeated EnergyItem watth_item = 2; } message EnergyTotalReportAck { optional uint32 result = 1; optional uint32 watth_seq = 2; optional uint32 watth_type = 3; } message EventRecordItem { optional uint32 timestamp = 1; optional uint32 sys_ms = 2; optional uint32 event_no = 3; repeated float event_detail = 4; } message EventRecordReport { optional uint32 event_ver = 1; optional uint32 event_seq = 2; repeated EventRecordItem event_item = 3; } message EventInfoReportAck { optional uint32 result = 1; optional uint32 event_seq = 2; optional uint32 event_item_num = 3; } message ProductNameSet { optional string name = 1; } message ProductNameSetAck { optional uint32 result = 1; } message ProductNameGet {} message ProductNameGetAck { optional string name = 3; } message RTCTimeGet {} message RTCTimeGetAck { optional uint32 timestamp = 1; optional int32 timezone = 2; } message RTCTimeSet { optional uint32 timestamp = 1; optional int32 timezone = 2 [(nanopb).default = 0]; } message RTCTimeSetAck { optional uint32 result = 1; } message country_town_message { optional uint32 country = 1; optional uint32 town = 2; } message time_task_config { optional uint32 task_index = 1; optional time_range_strategy time_range = 2; optional uint32 type = 3; } message time_task_delet { optional uint32 task_index = 1; } message time_task_config_post { optional time_task_config task1 = 1; optional time_task_config task2 = 2; optional time_task_config task3 = 3; optional time_task_config task4 = 4; optional time_task_config task5 = 5; optional time_task_config task6 = 6; optional time_task_config task7 = 7; optional time_task_config task8 = 8; optional time_task_config task9 = 9; optional time_task_config task10 = 10; optional time_task_config task11 = 11; } message time_task_config_ack { optional uint32 task_info = 1; } message rtc_data { optional int32 week = 1 [(nanopb).default = 0]; optional int32 sec = 2 [(nanopb).default = 0]; optional int32 min = 3 [(nanopb).default = 0]; optional int32 hour = 4 [(nanopb).default = 0]; optional int32 day = 5 [(nanopb).default = 0]; optional int32 month = 6 [(nanopb).default = 0]; optional int32 year = 7 [(nanopb).default = 0]; } message time_range_strategy { optional bool is_config = 1; optional bool is_enable = 2; optional int32 time_mode = 3; optional int32 time_data = 4; optional rtc_data start_time = 5; optional rtc_data stop_time = 6; } message plug_ack_message { optional uint32 ack = 1; } message plug_heartbeat_pack { optional uint32 err_code = 1 [(nanopb).default = 0]; optional uint32 warn_code = 2 [(nanopb).default = 0]; optional uint32 country = 3 [(nanopb).default = 0]; optional uint32 town = 4 [(nanopb).default = 0]; optional int32 max_cur = 5 [(nanopb).default = 0]; optional int32 temp = 6 [(nanopb).default = 0]; optional int32 freq = 7 [(nanopb).default = 0]; optional int32 current = 8 [(nanopb).default = 0]; optional int32 volt = 9 [(nanopb).default = 0]; optional int32 watts = 10 [(nanopb).default = 0]; optional bool switch = 11 [(nanopb).default = false]; optional int32 brightness = 12 [(nanopb).default = 0]; optional int32 max_watts = 13 [(nanopb).default = 0]; optional int32 heartbeat_frequency = 14 [(nanopb).default = 0]; optional int32 mesh_enable = 15 [(nanopb).default = 0]; } message plug_switch_message { optional uint32 plug_switch = 1; } message brightness_pack { optional int32 brightness = 1 [(nanopb).default = 0]; } message max_cur_pack { optional int32 max_cur = 1 [(nanopb).default = 0]; } message max_watts_pack { optional int32 max_watts = 1 [(nanopb).default = 0]; } message mesh_ctrl_pack { optional uint32 mesh_enable = 1 [(nanopb).default = 0]; } message ret_pack { optional bool ret_sta = 1 [(nanopb).default = false]; } enum CmdFunction { Unknown = 0; PermanentWattsPack = 129; SupplyPriorityPack = 130; } `; const writeables = [ //PowerStream { id: 1, name: 'InverterHeartbeat', Typ: 'PS', Templet: 'InverterHeartbeat', writeable: false, cmdFunc: 20 }, { id: 4, name: 'InverterHeartbeat2', Typ: 'PS', Templet: 'InverterHeartbeat2', writeable: false, cmdFunc: 20 }, { id: 11, name: 'Ping', Typ: 'PS', Templet: 'setValue', writeable: false, cmdFunc: 32 }, { id: 32, name: 'PowerPack_32', Typ: 'PS', Templet: 'PowerPack32', writeable: false, Ignor: true, cmdFunc: 254 }, { id: 134, name: 'Ignor', Typ: 'PS', Templet: '', writeable: false, Ignor: true, cmdFunc: 20 }, { id: 135, name: 'SetDisplayBrightness', Typ: 'PS', Templet: 'setValue', writeable: true, ValueName: 'value', Ignor: false, cmdFunc: 20 }, { id: 136, name: 'PowerPack_136', Typ: 'PS', Templet: 'PowerPack136', writeable: false, cmdFunc: 20 }, { id: 138, name: 'PowerPack_138', Typ: 'PS', Templet: 'PowerPack138', writeable: false, cmdFunc: 20 }, { id: 130, name: 'SetPrio', Typ: 'PS', Templet: 'setValue', writeable: true, ValueName: 'value', cmdFunc: 20 }, { id: 132, name: 'SetBatLimitLow', Typ: 'PS', Templet: 'setValue', writeable: true, ValueName: 'value', cmdFunc: 20 }, { id: 133, name: 'SetBatLimitHigh', Typ: 'PS', Templet: 'setValue', writeable: true, ValueName: 'value', cmdFunc: 20 }, { id: 143, name: 'feed_priority', Typ: 'PS', Templet: 'setValue', writeable: true, ValueName: 'value', cmdFunc: 20 }, { id: 129, name: 'SetAC', Typ: 'PS', Templet: 'setValue', writeable: true, ValueName: 'value', cmdFunc: 20 }, //Delta Max { id: 38, name: 'Beep', ValueName: 'enabled', Typ: 'DM' }, { id: 69, name: 'slowChgPower', ValueName: 'slowChgPower', Typ: 'DM' }, { id: 69, name: 'chgPause', ValueName: 'chgPause', Typ: 'DM' }, { id: 66, name: 'ACPower', ValueName: 'enabled', Typ: 'DM' }, { id: 66, name: 'xboost', ValueName: 'xboost', Typ: 'DM' }, { id: 81, name: 'DCPower', ValueName: 'enabled', Typ: 'DM' }, { id: 34, name: 'USBPower', ValueName: 'enabled', Typ: 'DM' }, { id: 51, name: 'minDsgSoc', ValueName: 'minDsgSoc', Typ: 'DM' }, { id: 49, name: 'maxChgSoc', ValueName: 'maxChgSoc', Typ: 'DM' }, { id: 71, name: 'curr12VMax', ValueName: 'currMa', Typ: 'DM' }, { id: 33, name: 'standByModeMins', ValueName: 'standByMode', Typ: 'DM' }, { id: 49, name: 'lcdTimeMins', ValueName: 'lcdTime', Typ: 'DM' }, { id: 153, name: 'ACstandByMins', ValueName: 'standByMins', Typ: 'DM' }, { id: 52, name: 'openOilSoc', ValueName: 'openOilSoc', Typ: 'DM' }, { id: 53, name: 'closeOilSoc', ValueName: 'closeOilSoc', Typ: 'DM' }, //Delta 2 { id: 0, name: 'acChgCfg_D2', ValueName: 'chgWatts', Typ: 'D2', MT: 5, AddParam: '{"chgWatts":600,"chgPauseFlag":255}' }, { id: 0, name: 'acOutCfg_D2', ValueName: 'enabled', Typ: 'D2', MT: 3 }, { id: 0, name: 'dcOutCfg_D2', ValueName: 'enabled', Typ: 'D2', MT: 1 }, { id: 0, name: 'quietMode_D2', ValueName: 'enabled', Typ: 'D2', MT: 5 }, { id: 0, name: 'dcChgCfg_D2', ValueName: 'dcChgCfg', Typ: 'D2', MT: 5 }, //Delta 2 Max { id: 0, name: 'quietCfg', ValueName: 'enabled', Typ: 'D2M', MT: 1, OT: 'quietCfg' }, // 0 = Piepen ein, 1 = Piepen aus { id: 0, name: 'xboost', ValueName: 'xboost', Typ: 'D2M', MT: 3, OT: 'acOutCfg', AddParam: '{"enabled":255,"out_freq":255,"out_voltage":4294967295,"xboost":0}' }, // 0 ist aus und 1 ist ein (default = 255) { id: 0, name: 'ACenabled', ValueName: 'enabled', Typ: 'D2M', MT: 3, OT: 'acOutCfg', AddParam: '{"enabled":0,"out_freq":255,"out_voltage":4294967295,"xboost":255}' }, // 0 ist aus und 1 ist ein (default = 255) // xboost und ACenabled funktionieren zwar, aber beim Setzen in der App des einen Parameters wird der andere Parameter in iobroker auf 255 gesetzt, was aber keine Auswirkungen hat // { id: 0, name: 'maxChgSoc', ValueName: 'maxChgSoc', Typ: 'D2M', MT: 2, OT: 'upsConfig' }, // Ladegrenzwert -> dasselbe wie SetBatLimitHigh im PS { id: 0, name: 'minDsgSoc', ValueName: 'minDsgSoc', Typ: 'D2M', MT: 2, OT: 'dsgCfg' }, // Entladegrenzwert -> dasselbe wie SetBatLimitLow im PS // { id: 0, name: 'bpPowerSoc', ValueName: 'bpPowerSoc', Typ: 'D2M', MT: 1, OT: 'watthConfig', AddParam: '{"bpPowerSoc":12,"minChgSoc":0,"isConfig":0,"minDsgSoc":0}' }, // Backup-Reserve Sicherung in % { id: 0, name: 'bpPowerEnable', ValueName: 'isConfig', Typ: 'D2M', MT: 1, OT: 'watthConfig', AddParam: '{"bpPowerSoc":12,"minChgSoc":0,"isConfig":0,"minDsgSoc":0}' }, // Enable Backup-Reserve // { id: 0, name: 'slowChgWatts', ValueName: 'slowChgWatts', Typ: 'D2M', MT: 3, OT: 'acChgCfg', AddParam: '{"fastChgWatts":255, "slowChgWatts":0,"chgPauseFlag":255}' }, // Objekt angelegt, schreibbar { id: 0, name: 'chgPauseFlag', ValueName: 'chgPauseFlag', Typ: 'D2M', MT: 3, OT: 'acChgCfg', AddParam: '{"fastChgWatts":255, "slowChgWatts":255,"chgPauseFlag":0}' }, // TODO: chgPauseFlag testen, ob dann die Ladung pausiert // { id: 0, name: 'dcChgCfg', ValueName: 'dcChgCfg', Typ: 'D2M', MT: 5, OT: 'dcChgCfg', AddParam: '{"dcChgCfg":0, "dcChgCfg2":0}' }, // Ausgang1: Werte sind 8000 (8A), 6000 (6A) und 4000 (4A) { id: 0, name: 'dcChgCfg2', ValueName: 'dcChgCfg2', Typ: 'D2M', MT: 5, OT: 'dcChgCfg', AddParam: '{"dcChgCfg":0, "dcChgCfg2":0}' }, // Ausgang2: Werte sind 8000 (8A), 6000 (6A) und 4000 (4A) // { id: 0, name: 'USB', ValueName: 'enabled', Typ: 'D2M', MT: 1, OT: 'dcOutCfg' }, // USB ein/aus { id: 0, name: '12VDC', ValueName: 'enabled', Typ: 'D2M', MT: 5, OT: 'mpptCar' }, // 12V Auto ein/aus { id: 0, name: 'smartgenClose', ValueName: 'closeOilSoc', Typ: 'D2M', MT: 2, OT: 'closeOilSoc' }, // Automatisches Einschalten des Smart Generators in % { id: 0, name: 'smartgenOpen', ValueName: 'openOilSoc', Typ: 'D2M', MT: 2, OT: 'openOilSoc' }, // Automatisches Ausschalten des Smart Generators in % { id: 0, name: 'standbyTime', ValueName: 'standbyMin', Typ: 'D2M', MT: 1, OT: 'standbyTime' }, // Geräte-Zeitüberschreitung (in min) 0 => Nie { id: 0, name: 'lcdTime', ValueName: 'delayOff', Typ: 'D2M', MT: 1, OT: 'lcdCfg', AddParam: '{"brighLevel":255}' }, // Bildschirm-Zeitüberschreitung (in Sekunden) 0 => Nie { id: 0, name: 'setRtcTime', Ignor: true, ValueName: 'sec', Typ: 'D2M', MT: 2, OT: 'setRtcTime', AddParam: '{"sec":4,"min": 13, "week": 3, "month": 9,"hour": 11,"year": 2023, "day":6}' }, // Ignorieren { id: 0, name: 'AcAlwaysOn', ValueName: 'enabled', Typ: 'D2M', MT: 1, OT: 'newAcAutoOnCfg', AddParam: '{"enabled":0, "minAcSoc":5}' }, // Smart Plugs { id: 2, name: 'PowerPack_2', Typ: 'SM', Templet: 'PowerPack2', writeable: false, ValueName: '', Ignor: false, cmdFunc: 2 }, { id: 11, name: 'Ping', Typ: 'SM', Templet: 'setValue', writeable: false, cmdFunc: 32 }, { id: 32, name: 'PowerPack_32', Typ: 'SM', Templet: 'PowerPack32', writeable: false, ValueName: '', Ignor: false, cmdFunc: 254 }, { id: 133, name: 'PowerPack_133', Typ: 'SM', Templet: 'PowerPack133', writeable: false, ValueName: '', Ignor: true, cmdFunc: 2 }, { id: 135, name: 'PowerPack_135', Typ: 'SM', Templet: 'PowerPack135', writeable: false, ValueName: '', Ignor: true, cmdFunc: 2 }, { id: 1, name: 'plug_heartbeat_pack', Typ: 'SM', Templet: 'plug_heartbeat_pack', writeable: false, cmdFunc: 2 }, { id: 129, name: 'SwitchPlug', Typ: 'SM', Templet: 'setValue', writeable: true, ValueName: 'value', cmdFunc: 2 }, ]; const musterGetPS = { "header": { "src": 32, "dest": 32, "seq": 1651831507, "OS": "ios" } } const musterSetAC = { header: { pdata: { value: 1300, }, src: 32, dest: 53, dSrc: 1, dDest: 1, checkType: 3, cmdFunc: 20, cmdId: 129, dataLen: 3, needAck: 1, seq: 1651831507, version: 19, payloadVer: 1, from: 'ios', deviceSn: 'ABCxxxxxxx123' } }; const musterSetAC2 = { header: { pdata: { value: 17477, }, src: 32, dest: 53, dSrc: 1, dDest: 1, checkType: 3, cmdFunc: 32, cmdId: 11, dataLen: 4, needAck: 1, seq: 1651831507, version: 19, payloadVer: 1, from: 'ios', deviceSn: 'ABCxxxxxxx123' } }; const musterslowChgPower = { "from": "iOS", "operateType": "TCP", "id": "816376009", "lang": "de-de", "params": { "id": 69, }, "version": "1.0" }; const musterDelta2 = { "from": "Android", "id": "458115693", "moduleType": 5, "operateType": "acChgCfg", "params": { } , "version": "1.0" } const musterDELTA2MAX = { "params": { }, "from": "iOS", "lang": "de-de", "id": "873106536", "moduleSn": "XXXXXXXXXXXXXXXX", "moduleType": 1, "operateType": "quietCfg", "version": "1.0" } // @ts-ignore const mqtt = require('mqtt'); const https = require('https'); // @ts-ignore const protobuf = require("protobufjs"); // Verbindungsstatus speichern let isMqttConnected = false; const mqttDaten = { UserID: '', User: '', Passwort: '', URL: '', Port: '', protocol: '', clientID: '' } // Initialisiere die Globalen variablen const GlobalObj = {} //Die erste PowerStream ermitteln let firstPsSn = ConfigData.seriennummern[0].seriennummer; let firstPsSnIndex = -1 GetNextAsn() //log("firstPsSn: " + GetNextAsn()) function GetNextAsn() { if (ConfigData.RegulationMultiPsMode == 1) { //return firstPsSn firstPsSnIndex = -1 } var length = ConfigData.seriennummern.length; for (var j = 0; j < length; j++) { var i = (firstPsSnIndex + j + 1) % length; if (ConfigData.seriennummern[i].typ == "PS" && ConfigData.seriennummern[i].regulation && ConfigData.seriennummern[i].seriennummer != "XXXXXXXXXXXXX") { firstPsSn = ConfigData.seriennummern[i].seriennummer; firstPsSnIndex = i break; } } //log("GetNextAsn:" + firstPsSn + " Index:" + firstPsSnIndex) return firstPsSn } /*======================================================= ========= Timer ============ =======================================================*/ //jede x Sekunden var intervalID = setInterval(function () { if (true || istTag()) { CheckforReconnect(function () { //SetBasePower(GetNextAsn()); SetBasePower() }); } else { ////SetBasePower(firstPsSn); } }, 10 * 1000); // Hartbeat der App simmulieren var intervalID3 = setInterval(function () { if (isMqttConnected) { for (var i = 0; i < ConfigData.seriennummern.length; i++) { if (ConfigData.seriennummern[i].typ == "PS" && ConfigData.seriennummern[i].seriennummer != "XXXXXXXXXXXXX") { setmusterGetPS(ConfigData.seriennummern[i].seriennummer); } } } }, 32 * 1000); /* var intervalID2 = setInterval(function () { getLowestValue(ConfigData.statesPrefix + ".RealPower", 2) .then(lowestValue => { log( "lowestValue:" + lowestValue)// }) .catch((error) => { console.warn('Fehler beim Abrufen des niedrigsten Werts:', error); }); }, 2 * 1000); //*/ // @ts-ignore await getEcoFlowMqttData(ConfigData.email, ConfigData.passwort) async function getEcoFlowMqttData(email, password) { const options = { hostname: 'api.ecoflow.com', path: '/auth/login', method: 'POST', headers: { 'Host': 'api.ecoflow.com', 'lang': 'de-de', 'platform': 'android', 'sysversion': '11', 'version': '4.1.2.02', 'phonemodel': 'SM-X200', 'content-type': 'application/json', 'user-agent': 'okhttp/3.14.9' } }; const data = { appVersion: "4.1.2.02", email: email, os: "android", osVersion: "30", password: Buffer.from(password).toString('base64'), scene: "IOT_APP", userType: "ECOFLOW" }; function httpsRequest(options, data) { return new Promise((resolve, reject) => { const req = https.request(options, res => { let data = ''; res.on('data', chunk => { data += chunk; }); res.on('end', () => { resolve(data); }); }); req.on('error', error => { reject(error); }); if (data) { req.write(JSON.stringify(data)); } req.end(); }); } function uuidv4() { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { var r = Math.random() * 16 | 0, v = c === 'x' ? r : (r & 0x3 | 0x8); return v.toString(16); }); } let response = await httpsRequest(options, data); try { let token = JSON.parse(response).data.token; let userid = JSON.parse(response).data.user.userId; } catch (error) { log(response) // throw new Error("Ein Fehler bei der Ermittlung der Zugangsdaten ist aufgetreten. Bitte prüfe die Zugangsdaten."); } let token = JSON.parse(response).data.token; let userid = JSON.parse(response).data.user.userId; options.path = `/iot-auth/app/certification?userId=${userid}`; options.method = 'GET'; options.headers.authorization = `Bearer ${token}`; response = await httpsRequest(options); try { mqttDaten.Passwort = JSON.parse(response).data.certificatePassword mqttDaten.Port = JSON.parse(response).data.port mqttDaten.UserID = userid mqttDaten.User = JSON.parse(response).data.certificateAccount mqttDaten.URL = JSON.parse(response).data.url mqttDaten.protocol = JSON.parse(response).data.protocol mqttDaten.clientID = "ANDROID_" + uuidv4() + "_" + userid } catch (error) { log(response)// throw new Error("Ein Fehler bei der Ermittlung der Zugangsdaten ist aufgetreten. Bitte prüfe die Zugangsdaten."); } /* log("UserID: " + userid); // log("User: " + JSON.parse(response).data.certificateAccount); // log("Passwort: " + JSON.parse(response).data.certificatePassword); // log("URL: " + JSON.parse(response).data.url); // log("Port: " + JSON.parse(response).data.port); // log("protocol: " + JSON.parse(response).data.protocol); // log("clientID: ANDROID_" + uuidv4() + "_" + userid); // */ } // @ts-ignore await createMyState("LastTopic") //################ MQTT Verbindung ################## function setupMQTTConnection() { //log("Neue MQTT Verbindung startet") // Verbindung herstellen const options = { port: mqttDaten.Port, clientId: mqttDaten.clientID, username: mqttDaten.User, password: mqttDaten.Passwort, protocol: mqttDaten.protocol }; const client = mqtt.connect("mqtt://" + mqttDaten.URL, options); // Event-Handler für Verbindungsaufbau client.on('connect', function () { console.log('Verbunden mit dem Ecoflow MQTT-Broker')// SubscribeEco(); for (var i = 0; i < ConfigData.seriennummern.length; i++) { if (ConfigData.seriennummern[i].typ == "PS" && ConfigData.seriennummern[i].seriennummer != "XXXXXXXXXXXXX") { setmusterGetPS(ConfigData.seriennummern[i].seriennummer); } } isMqttConnected = true }); // Auf Nachricht empfangen Ereignis reagieren client.on('message', async function (topic, message) { //log("Incomming Massage: " + topic) //log("Incomming Massage: " + message.length) if (message.length == 0) return //return var jsonMessage = "" const mqState = topic.replace(/^\//, '').replace(/\//g, '_') //log(mqState) await createMyState(mqState + ".RAW") setState(ConfigData.statesPrefix + ".LastTopic", topic) try { jsonMessage = JSON.parse(message); if (false || ConfigData.Debug) log('JSON-Nachricht empfangen:' + topic + ':' + JSON.stringify(jsonMessage)) // if (!pruefeID(jsonMessage, mqState)) { return } setState(ConfigData.statesPrefix + '.' + mqState + ".RAW", JSON.stringify(jsonMessage)) generateAndSyncSub("data", jsonMessage, false, ConfigData.statesPrefix + '.' + mqState) } catch (error) { if (error.name != "SyntaxError") log(error.stack, "warn") // //if (topic.indexOf("/set") !== -1) log('Binäre Nachricht empfangen:' + topic + ':' + message.toString('hex')); if (ConfigData.Debug) log('Binäre Nachricht empfangen:' + topic + ':' + message.toString('hex'));// await createMyState(mqState + ".RAW_HEX") setState(ConfigData.statesPrefix + '.' + mqState + ".RAW_HEX", message.toString('hex')) const messagedecoded = decodeAndPrint(message.toString('hex'), mqState) //log(messagedecoded) if (ConfigData.Debug) log('Decodierte Nachricht:' + messagedecoded) // setState(ConfigData.statesPrefix + '.' + mqState + ".RAW", messagedecoded) generateAndSyncSub("", JSON.parse(messagedecoded), false, ConfigData.statesPrefix + '.' + mqState) } }); // Event-Handler für getrennte Verbindung client.on('close', () => { //console.log("MQTT-Client ist getrennt"); isMqttConnected = false; }); // Callback für Fehler client.on('error', function (error) { log('Fehler bei der Ecoflow MQTT-Verbindung:' + error, 'warn'); // }); client.on('reconnect', function () { console.log('Reconnecting to Ecoflow MQTT broker...'); // }); // Weitere Event-Handler hier... return client; } function SubscribeEco() { ConfigData.seriennummern.forEach(item => { if (item.seriennummer != "XXXXXXXXXXXXX") { client.subscribe('/app/' + mqttDaten.UserID + '/' + item.seriennummer + '/thing/property/set'); client.subscribe('/app/' + mqttDaten.UserID + '/' + item.seriennummer + '/thing/property/get'); if (item.subscribe) { client.subscribe('/app/device/property/' + item.seriennummer); } } }); } function findWriteableByID(id) { const foundItem = writeables.find((item) => item.id === id); return foundItem || null; } function findWriteableByValueName(id) { //log("suche nach writeable: " + id) const foundItem = writeables.find((item) => item.name === id); return foundItem || null; } function pruefeID(json, mqState) { //log(mqState + " : " + JSON.stringify(json)) if (mqState.includes("thing_property_set")) { const asn = mqState.match(/app_.*?_(.*?)_thing_property_set/)?.[1] || null; let TypOfDevice = null if (asn) TypOfDevice = ConfigData.seriennummern.find(item => item.seriennummer === asn).typ || null //if (SERIAL_TO_LOG.split(",").serialNumbers.some(serial => mqState.includes(serial))){ if (SERIAL_TO_LOG.split(",").includes(asn)) { writeLog(JSON.stringify(json), asn + "_SetLog.csv").catch(error => console.error(error)) log("Eintag in Logfile " + asn + "_SetLog.csv geschrieben. Download unter 'Protokolle / Log Herunterladen'") } //log("pruefeID/Seriennummer: " + asn + " aus mqState:" + mqState + " Typ:" + TypOfDevice) if (TypOfDevice == "DM" || ('params' in json && 'id' in json.params)) { // ('params' in json && 'id' in json.params) (Delta Max) const Ignores = [40, 72, 68]; //const writeables = [69]; if (Ignores.includes(json.params.id)) { //log("Ignore: "+ JSON.stringify(json)) return false; } else if (mqState.includes("thing_property_set")) { const suchwriteable = writeables.find((item) => item.id == json.params.id && json.params.hasOwnProperty(item.ValueName)) || null; if (suchwriteable) { if (suchwriteable.Ignor) { return false;; } //log("Schreibbar: " + JSON.stringify(json)) //log("Schreibbardaten: " + JSON.stringify(suchwriteable)) //log("wert: " + JSON.stringify(json.params[suchwriteable.ValueName])) if (!isState2(ConfigData.statesPrefix + "." + mqState + ".writeables." + suchwriteable.name)) { createMyState(mqState + ".writeables." + suchwriteable.name, Number(json.params[suchwriteable.ValueName])) } else { setState(ConfigData.statesPrefix + "." + mqState + ".writeables." + suchwriteable.name, Number(json.params[suchwriteable.ValueName]).toFixed(0), true) } } else { log("Unbekannter Set Befehl: " + JSON.stringify(json)) // log("Adresse: " + mqState) // } return true; } } else if (TypOfDevice == "D2M") { // Delta2Max let suchwriteables suchwriteables = writeables.filter((item) => item.OT === json.operateType && item.Typ == "D2M" && json.params.hasOwnProperty(item.ValueName)); if (suchwriteables.length > 0) { suchwriteables.forEach((suchwriteable) => { if (suchwriteable.Ignor) { return false;; } //log("Schreibbar: " + JSON.stringify(json)) //log("Schreibbardaten: " + JSON.stringify(suchwriteable)) //log("wert: " + JSON.stringify(json.params[suchwriteable.ValueName])) if (Number(json.params[suchwriteable.ValueName].toFixed(0)) != 255) { if (!isState2(ConfigData.statesPrefix + "." + mqState + ".writeables." + suchwriteable.name)) { createMyState(mqState + ".writeables." + suchwriteable.name, Number(json.params[suchwriteable.ValueName])) } else { setState(ConfigData.statesPrefix + "." + mqState + ".writeables." + suchwriteable.name, Number(json.params[suchwriteable.ValueName]).toFixed(0), true) } } }) } else { log("Unbekannter Delta2Max Set Befehl: " + JSON.stringify(json)) // log("Adresse: " + mqState) // } return true; } else if (TypOfDevice == "D2" || ('params' in json && 'moduleType' in json && 'operateType' in json)) { // Delta2 const suchwriteable = writeables.find((item) => item.name == json.operateType + "_D2") || null; /* for (const paramName in json.params) { suchwriteable = findWriteableByValueName(paramName) if(suchwriteable) break } */ if (suchwriteable) { if (suchwriteable.Ignor) { return false;; } //log("Schreibbar: " + JSON.stringify(json)) //log("Schreibbardaten: " + JSON.stringify(suchwriteable)) //log("wert: " + JSON.stringify(json.params[suchwriteable.ValueName])) if (!isState2(ConfigData.statesPrefix + "." + mqState + ".writeables." + suchwriteable.name)) { createMyState(mqState + ".writeables." + suchwriteable.name, Number(json.params[suchwriteable.ValueName])) } else { setState(ConfigData.statesPrefix + "." + mqState + ".writeables." + suchwriteable.name, Number(json.params[suchwriteable.ValueName]).toFixed(0), true) } } else { log("Unbekannter Delta2 Set Befehl: " + JSON.stringify(json)) // log("Adresse: " + mqState) // } return true; } else { // ('params' in json && 'id' in json.params) (Delta Max) log("pruefeID: nix gefunden") // return true; } } //"thing_property_set" return true; } // Verbindung herstellen let client = setupMQTTConnection(); // Funktion zum Trennen und Neuaufbau der Verbindung function reconnect() { client.end(); // Verbindung trennen setTimeout(function () { client = setupMQTTConnection(); // Neue Verbindung herstellen //log("Ecoflow neuverbindung"); }, 2000); // Wartezeit } // close connection if script stopped onStop(function (callback) { if (client) { // close connection client.end(); log("Ecoflow MQTT-Client beendet") // clearInterval(intervalID); //clearInterval(intervalID2); clearInterval(intervalID3); } callback(); }, 2000); function CheckforReconnect(callback) { //log("CheckforReconnect") //return let wartezeit = 15 //bis eine Stunde nach Sonneuntergang kurze Reconnects dann 15 min. if (istTag(60)) wartezeit = 1 if (getState(ConfigData.statesPrefix + ".LastTopic")?.ts < Date.now() - ConfigData.ReconnectMin * 60 * 1000) { console.log("Der letzte Eintrag ist älter als " + ConfigData.ReconnectMin + " Minuten. Versuche Neustart."); // setState(ConfigData.statesPrefix + ".LastTopic", "Last Action Restart:" + new Date().toLocaleString()) runScript(); return; // Wenn letzte Powerstream-Meldung älter als min ist, reconnecte } else if (getState(ConfigData.statesPrefix + '.app_device_property_' + firstPsSn + '.RAW')?.ts < Date.now() - (wartezeit * 60 * 1000)) { //log("Reconnect zu Ecoflow MQTT für PowerStream - Daten") //.ts Updaten const oldvalue = getState(ConfigData.statesPrefix + '.app_device_property_' + firstPsSn + '.RAW').val setState(ConfigData.statesPrefix + '.app_device_property_' + firstPsSn + '.RAW', oldvalue) reconnect(); return; //runScript(); } else { callback(); } } //const hextest = "0a0a1020182070f9f0b6830b" function decodeAndPrint(hexString, mqState = "") { //log(mqState) if (typeof hexString !== 'string' || !hexString) { log('Ungültiger hexString: "' + hexString + '"'); // return "{}"; } const root = protobuf.parse(protoSource2).root; const PowerMessage = root.lookupType("Message"); //const message = PowerMessage.decode(Buffer.from(hexString, "hex")); let message = {} try { message = PowerMessage.decode(Buffer.from(hexString, "hex")); } catch (error) { log('Fehler beim Decodieren:' + error.message); // //log('hexString: "' + hexString +'"'); //log('buffer: ' + Buffer.from(hexString, "hex")) //log('PowerMessage: ' + PowerMessage) return "{}"; } let logflag = false if (LogAllHeader) { // @ts-ignore for (let element of LogAllHeader.split(",")) { if (mqState.includes(element)) { //log("Ganze Nachricht :" + mqState + " : " + JSON.stringify(convconvertBuffersForLog(message))); logflag = true } } } let Rueckgabe = {} if (Array.isArray(message.header)) { //log("Nachricht Anzahl :" + message.header.length); let outputObject = {} for (let i = 0; i < message.header.length; i++) { const header = message.header[i]; const asn = (header.deviceSn || mqState.match(/app_.*?_(.*?)_thing_property_.et/)?.[1] || null) let foundItem = null if (asn) { foundItem = ConfigData.seriennummern.find(item => item.seriennummer === asn); //log("Gerätename:" + foundItem.name) } else { log("Keine SerienNummer gefunden:") logflag = true } if (!header.cmdId) header.cmdId = 0; if (!header.cmdFunc) header.cmdFunc = 0 let matchedEntry = writeables.find((entry) => entry.id == header.cmdId && entry.cmdFunc == header.cmdFunc && entry.Typ == foundItem.typ) || { id: header.cmdId, name: 'nichtDefiniert', Typ: 'PS', Templet: 'nichtDefiniert', writeable: false, Ignor: false }; if (matchedEntry.Templet == "nichtDefiniert") { //matchedEntry = writeables.find((entry) => entry.id == header.cmdId && entry.Typ == "PS") || { id: header.cmdId, name: 'nichtDefiniert', Typ: 'PS', Templet: 'nichtDefiniert', writeable: false, Ignor: false }; } //log(JSON.stringify(matchedEntry)) const MessageType = matchedEntry.Templet //const MessageType = messageIDTypes[header.cmdId] || "nichtDefiniert" //log("MessageType: " + MessageType) if (logflag && header.cmdId > -1) log("Serial:" + asn + " cmdId:" + header.cmdId + " cmdFunc:" + header.cmdFunc + " Pdata Hex: " + header.pdata.toString('hex')) // if (logflag && (header.cmdId) == (ConfigData.PlotCmdID)) { log("--------------------------------------------")// log("Gerät: " + foundItem.name)// log("ASN: " + asn)// log("topic: " + mqState)// log("Definition: " + JSON.stringify(matchedEntry))// log("cmdId: " + header.cmdId)// log("RAW: " + hexString)// log("Nachricht: " + JSON.stringify(convconvertBuffersForLog(message)))// log("Header: " + i + " von(" + message.header.length + ") " + JSON.stringify(convconvertBuffersForLog(header)))// //log("Pdata Hex: " + header.pdata.toString('hex')) log("--------------------------------------------")// } if (matchedEntry.Ignor) { //log("Ignor: " + JSON.stringify(matchedEntry)) //log(hexString) //log(JSON.stringify(convconvertBuffersForLog(message))) continue; } if (MessageType == "nichtDefiniert") { if (header.cmdId != 0 && header.cmdId) { console.warn('Nicht definierter cmd_func-Wert. [' + foundItem.name + '] cmdId:' + header.cmdId + " cmdFunc:" + header.cmdFunc); log("hexString: " + hexString)// log("Nachricht: " + JSON.stringify(convconvertBuffersForLog(message)))// } continue; } else { const PdataMessage = root.lookupType(MessageType); const pdata = PdataMessage.decode(header.pdata); const pdataObject = PdataMessage.toObject(pdata, { longs: Number, // Konvertiere Long-Werte in Zahlen (optional) enums: String, // Konvertiere Enum-Werte in Strings (optional) bytes: Buffer, // Konvertiere Bytes in Buffer (optional) }); outputObject[MessageType] = pdataObject if (matchedEntry.writeable && mqState.includes("thing_property_set")) { //log("Wert = " + JSON.stringify(outputObject) + " topic:"+ mqState) const setvalue = pdataObject[matchedEntry.ValueName] || 0 setStateNE(ConfigData.statesPrefix + "." + mqState + ".writeables." + matchedEntry.name, setvalue.toString(), true) } if (logflag && header.cmdId == ConfigData.PlotCmdID) { log("outputObject: " + JSON.stringify(convconvertBuffersForLog(outputObject))) // log("--------------------------------------------") // } Rueckgabe.data = outputObject; continue; } }; } else { // Das Ergebnis ist eine einzelne Message log("Es wurde eine einzelne Message dekodiert:" + JSON.stringify(convconvertBuffersForLog(message))); // return JSON.stringify(Rueckgabe); } //log("DURCHLAUF:" + JSON.stringify(Rueckgabe)) return JSON.stringify(Rueckgabe) } function convconvertBuffersForLog(obj, hash = new WeakMap()) { // if (obj == null || typeof obj !== 'object') return obj; //if (Buffer.isBuffer(obj)) return Buffer.from(obj); //Original Buffer if (Buffer.isBuffer(obj)) return obj.toString('hex'); //Hexstring if (hash.has(obj)) return hash.get(obj); let copy = Array.isArray(obj) ? [] : {}; hash.set(obj, copy); for (let key in obj) { if (obj.hasOwnProperty(key)) { copy[key] = convconvertBuffersForLog(obj[key], hash); // } } return copy; } function convertBuffersToHexStrings(obj) { for (let key in obj) { //if (key == "pdata") log("pdata gefunden. typ: " + typeof obj[key]) if (obj[key] instanceof Buffer) { obj[key] = obj[key].toString('hex'); } else if (typeof obj[key] === 'object') { convertBuffersToHexStrings(obj[key]); } } } function SendProto(protomsg, topic) { //return const root = protobuf.parse(protoSource2).root; const PowerMessage = root.lookupType("setMessage"); const message = PowerMessage.create(JSON.parse(protomsg)); const messageBuffer = PowerMessage.encode(message).finish(); //log("Modifizierter Hex-String:" + Buffer.from(messageBuffer).toString("hex")); //log("topic:" + topic); client.publish(topic, messageBuffer, { qos: 1 }, function (error) { if (error) { console.error('Fehler beim Veröffentlichen der MQTT-Nachricht:', error); } else { if (ConfigData.Debug) log('Die MQTT-Nachricht wurde erfolgreich veröffentlicht.'); // } }); } function SendJSON(protomsg, topic) { //log("topic:" + topic); client.publish(topic, protomsg, { qos: 1 }, function (error) { if (error) { console.error('Fehler beim Veröffentlichen der MQTT-Nachricht:', error); } else { if (ConfigData.Debug) log('Die MQTT-Nachricht wurde erfolgreich veröffentlicht.'); // } }); } function getStandortKoordinaten() { var obj = getObject('system.config'); if (obj) { latitude = obj.common.latitude; longitude = obj.common.longitude; } else { console.error('Fehler beim Abrufen der Einstellungen'); } } //Anmeldenachrichten der APP function setmusterGetPS(asn) { let updatedMusterSetAC = JSON.parse(JSON.stringify(musterGetPS)); updatedMusterSetAC.header.seq = Date.now() //log(JSON.stringify(updatedMusterSetAC)); SendProto(JSON.stringify(updatedMusterSetAC), '/app/' + mqttDaten.UserID + '/' + asn + '/thing/property/get'); SendProto(JSON.stringify(updatedMusterSetAC), '/app/' + mqttDaten.UserID + '/' + asn + '/thing/property/get'); SendProto(JSON.stringify(updatedMusterSetAC), '/app/' + mqttDaten.UserID + '/' + asn + '/thing/property/get'); // @ts-ignore updatedMusterSetAC = JSON.parse(JSON.stringify(musterSetAC2)); updatedMusterSetAC.header.seq = Date.now() updatedMusterSetAC.header.deviceSn = asn //log(JSON.stringify(updatedMusterSetAC)); SendProto(JSON.stringify(updatedMusterSetAC), '/app/' + mqttDaten.UserID + '/' + asn + '/thing/property/set'); } function generateAndSyncSub(id, JElements, sub = false, preset = "0_userdata.0") { if (!JElements || typeof JElements !== 'object') { log('Ungültige JElements übergeben!'); // return; } for (var JElement in JElements) { var AktVal; if (typeof JElements[JElement] === "object") { if (id === "") { generateAndSyncSub(JElement, JElements[JElement], true, preset); } else { generateAndSyncSub(id + "." + JElement, JElements[JElement], true, preset); } //generateAndSyncSub(id + "." + JElement, JElements[JElement], true, preset); } else { try { if (isState2(preset + "." + id + "." + JElement)) AktVal = getState(preset + "." + id + "." + JElement).val; else AktVal = null } catch (e) { log("Fehler: " + e); // } // Überprüfung für den Elementnamen "timestamp" if (JElement === "timestamp") { const TimeValue = isValidUnixTimestampAndConvert(JElements[JElement]) if (TimeValue) JElements[JElement] = TimeValue; } if (AktVal == null) { createState(preset + "." + id + "." + JElement, JElements[JElement], false); AktVal = JElements[JElement]; } if (AktVal != JElements[JElement]) { if (isState2(preset + "." + id + "." + JElement)) { setState(preset + "." + id + "." + JElement, JElements[JElement]); } } } } } //--------- Prüft übergebne zeiträume und Tage und gibt True zurück wenn innerhalb //log("return: " + CheckTime2("22:00","11:00",[0,1,2,3,4,5,6],getDateObject("06 Nov 2018 08:30:00 GMT+0100"))); function CheckTime2(Startzeit, Endzeit, Wochentage, d = new Date()) { var locStartDate = getDateObject(formatDate(d, "MM DD YYYY " + Startzeit)); var locEndDate = getDateObject(formatDate(d, "MM DD YYYY " + Endzeit)); var LocOriginal = getDateObject(formatDate(d, "MM DD YYYY hh:mm:ss")); Wochentage = Wochentage.map(function (x) { return parseInt(x, 10); }); if (locStartDate.getTime() > locEndDate.getTime()) { if (LocOriginal.getTime() >= locStartDate.getTime() && LocOriginal.getTime() <= getDateObject(formatDate(d, "MM DD YYYY 23:59:59")).getTime()) { d.setDate(d.getDate() + 1); locEndDate = getDateObject(formatDate(d, "MM DD YYYY " + Endzeit)); } else { d.setDate(d.getDate() - 1); locStartDate = getDateObject(formatDate(d, "MM DD YYYY " + Startzeit)); } } var n = getDateObject(locStartDate).getDay(); if (Wochentage.includes(n) && LocOriginal.getTime() >= getDateObject(locStartDate).getTime() && LocOriginal.getTime() <= getDateObject(locEndDate).getTime()) { return true } else { return false } } function SunTimes(time = 0) { // @ts-ignore const SunCalc = require('suncalc'); const date = new Date(); // Berechnung von Sonnenaufgang und Sonnenuntergang const sunTimes = SunCalc.getTimes(date, ConfigData.latitude, ConfigData.longitude); const sunrise = sunTimes.sunrise.getHours() + ':' + sunTimes.sunrise.getMinutes(); const sunset = sunTimes.sunset.getHours() + ':' + sunTimes.sunset.getMinutes(); if (time == 0) { return sunrise } else { return sunset } } function istTag(offsetMin = 0) { //log("Ist Tag?: " + CheckTime2(SunTimes(0).toString(), addMinutesToTime(SunTimes(1).toString(),offsetMin), [0, 1, 2, 3, 4, 5, 6], new Date())); return CheckTime2(SunTimes(0).toString(), addMinutesToTime(SunTimes(1).toString(), offsetMin), [0, 1, 2, 3, 4, 5, 6]) } function addMinutesToTime(time, minutesToAdd) { var parts = time.split(":"); var hours = parseInt(parts[0]); var minutes = parseInt(parts[1]); var totalMinutes = hours * 60 + minutes + minutesToAdd; var newHours = Math.floor(totalMinutes / 60) % 24; var newMinutes = totalMinutes % 60; var newTime = newHours.toString().padStart(2, "0") + ":" + newMinutes.toString().padStart(2, "0"); return newTime; } //############ Funktionen zum Setzen von Werten for (var i = 0; i < ConfigData.seriennummern.length; i++) { if (ConfigData.seriennummern[i].typ == "PS" && ConfigData.seriennummern[i].seriennummer != "XXXXXXXXXXXXX") { const asn = ConfigData.seriennummern[i].seriennummer //log(asn) // @ts-ignore await createMyState('app_' + mqttDaten.UserID + '_' + asn + '_thing_property_set.setAC') on({ id: ConfigData.statesPrefix + '.app_' + mqttDaten.UserID + '_' + asn + '_thing_property_set.setAC', change: "any", ack: false }, function (obj) { setAC(asn, Number(obj.state.val)) setState(obj.id, obj.state.val, true); }); //Powersumme bilden und schreiben data.InverterHeartbeat.pv1InputWatts on({ id: new RegExp(ConfigData.statesPrefix + '.app_device_property_' + asn + '.data.InverterHeartbeat.pv.InputWatts'), change: "any" }, function (obj) { let state1 = ConfigData.statesPrefix + '.app_device_property_' + asn + '.data.InverterHeartbeat.pv1InputWatts'; let state2 = ConfigData.statesPrefix + '.app_device_property_' + asn + '.data.InverterHeartbeat.pv2InputWatts'; //let korstate = ConfigData.statesPrefix + '.app_device_property_' + asn + '.data.InverterHeartbeat.X_Unknown_5'; let sumState = ConfigData.statesPrefix + '.app_device_property_' + asn + '.data.InverterHeartbeat.sumPV'; if (existsState(state1) || existsState(state2)) { let pv1InputWatts = 0, pv2InputWatts = 0 if (existsState(state1)) pv1InputWatts = GetValAkt(state1, 30).val if (existsState(state2)) pv2InputWatts = GetValAkt(state2, 30).val //let sum = GetValAkt(state1, 30).val + GetValAkt(state2, 30).val - (getState(korstate).val * 20); let sum = (pv1InputWatts + pv2InputWatts) * (0.97); if (!existsState(sumState)) { createState(sumState, sum); } else { setState(sumState, sum); //log("Summe gesetzt für "+asn+": "+ sum) } } }); } } // Writeables prüfen und alegen wenn nicht vorhanden ConfigData.seriennummern.forEach((geraet) => { if (geraet.seriennummer != "XXXXXXXXXXXXX") { let suchwriteables suchwriteables = writeables.filter((item) => item.Typ == geraet.typ && item.writeable != false); if (suchwriteables.length > 0) { suchwriteables.forEach((suchwriteable) => { let writeableName = suchwriteable.name //log("Schreibbardaten für: "+ geraet.name + " : " + JSON.stringify(suchwriteable)) if (!isState2(ConfigData.statesPrefix + '.app_' + mqttDaten.UserID + '_' + geraet.seriennummer + '_thing_property_set.writeables.' + writeableName)) { log("Neues Writeable angelegt. Gerät:" + geraet.name + " Typ:" + geraet.typ + " Wert: " + writeableName) createMyState('app_' + mqttDaten.UserID + '_' + geraet.seriennummer + '_thing_property_set.writeables.' + writeableName, 0) } }) } } }) const idRegex = new RegExp(ConfigData.statesPrefix + '\.app_[A-Za-z0-9_]+_thing_property_set\\.writeables\\..*'); on({ id: idRegex, change: "any", ack: false }, function (obj) { const idParts = obj.id.split('.'); const lastPart = idParts[idParts.length - 1]; const asn = obj.id.match(/.*?\.app_.*?_(.*?)_thing_property_set.*/)[1]; const TypOfDevice = ConfigData.seriennummern.find(item => item.seriennummer === asn).typ || null const matchedEntry = writeables.find((entry) => entry.name === lastPart && entry.Typ === TypOfDevice); //const matchedEntry = writeables.find((entry) => entry.name === lastPart && entry.typ === "PS"); if (matchedEntry) { //log("Write Event: " + obj.id + " val: " + obj.state.val + " | Matched Entry: " + JSON.stringify(matchedEntry)); let updatedMuster if (matchedEntry.Typ == "DM") { updatedMuster = JSON.parse(JSON.stringify(musterslowChgPower));; updatedMuster.id = Date.now().toString() updatedMuster.params.id = matchedEntry.id updatedMuster.params[matchedEntry.ValueName] = Number(obj.state.val) SendJSON(JSON.stringify(updatedMuster), '/app/' + mqttDaten.UserID + '/' + asn + '/thing/property/set'); } else if (matchedEntry.Typ == "D2") { updatedMuster = JSON.parse(JSON.stringify(musterDelta2));; updatedMuster.id = Date.now().toString() updatedMuster.moduleType = Number(matchedEntry.MT) updatedMuster.operateType = matchedEntry.name.replace("_D2", "") if (matchedEntry.AddParam) { updatedMuster.params = JSON.parse(matchedEntry.AddParam); let suchwriteables = writeables.filter((item) => item.OT === matchedEntry.OT && item.Typ == matchedEntry.Typ && updatedMuster.params.hasOwnProperty(item.ValueName) && item.name != matchedEntry.name); if (suchwriteables.length > 0) { suchwriteables.forEach((suchwriteable) => { const addval = getStateCr(obj.id.replace(lastPart, suchwriteable.name), updatedMuster.params[suchwriteable.ValueName]).val //log("Adparam: " + suchwriteable.name + " Wert aus State:" + addval + " initvalue:" + updatedMuster.params[suchwriteable.ValueName]) updatedMuster.params[suchwriteable.ValueName] = addval }) } } //updatedMuster.params.chgPauseFlag = 255 updatedMuster.params[matchedEntry.ValueName] = Number(obj.state.val) SendJSON(JSON.stringify(updatedMuster), '/app/' + mqttDaten.UserID + '/' + asn + '/thing/property/set'); } else if (matchedEntry.Typ == "D2M") { updatedMuster = JSON.parse(JSON.stringify(musterDELTA2MAX));; updatedMuster.id = Date.now().toString() updatedMuster.moduleSn = asn updatedMuster.moduleType = Number(matchedEntry.MT) updatedMuster.operateType = matchedEntry.OT if (matchedEntry.AddParam) { updatedMuster.params = JSON.parse(matchedEntry.AddParam); let suchwriteables = writeables.filter((item) => item.OT === matchedEntry.OT && item.Typ == matchedEntry.Typ && updatedMuster.params.hasOwnProperty(item.ValueName) && item.name != matchedEntry.name); if (suchwriteables.length > 0) { suchwriteables.forEach((suchwriteable) => { let addval = getStateCr(obj.id.replace(lastPart, suchwriteable.name), updatedMuster.params[suchwriteable.ValueName]).val if (Number(updatedMuster.params[suchwriteable.ValueName]) == 255) { addval = 255 } //log("Adparam: " + suchwriteable.name + " Wert aus State:" + addval + " initvalue:" + updatedMuster.params[suchwriteable.ValueName]) updatedMuster.params[suchwriteable.ValueName] = addval }) } } //updatedMuster.params.chgPauseFlag = 255 updatedMuster.params[matchedEntry.ValueName] = Number(obj.state.val) SendJSON(JSON.stringify(updatedMuster), '/app/' + mqttDaten.UserID + '/' + asn + '/thing/property/set'); } else if (matchedEntry.Typ == "PS" || matchedEntry.Typ == "SM") { updatedMuster = JSON.parse(JSON.stringify(musterSetAC)); if (Number(obj.state.val) <= -1) { delete updatedMuster.item.meta; delete updatedMuster.item.ValByte; } else { updatedMuster.header.pdata[matchedEntry.ValueName] = Number(obj.state.val) updatedMuster.header.dataLen = getVarintByteSize(Number(obj.state.val)) } updatedMuster.header.cmdId = matchedEntry.id updatedMuster.header.cmdFunc = matchedEntry.cmdFunc || 20 updatedMuster.header.seq = Date.now() updatedMuster.header.deviceSn = asn //log(JSON.stringify(updatedMuster)) SendProto(JSON.stringify(updatedMuster), '/app/' + mqttDaten.UserID + '/' + asn + '/thing/property/set'); } else { log("Write Event Unbekannter Typ: (" + matchedEntry.Typ + ") " + obj.id + " val: " + obj.state.val + ""); // } //log("Gefunden und gesendet:Topic: " + '/app/' + mqttDaten.UserID + '/' + asn + '/thing/property/set' + " Daten:" + JSON.stringify(updatedMuster)) //SendJSON(JSON.stringify(updatedMuster), '/app/' + mqttDaten.UserID + '/' + asn + '/thing/property/set'); //delete updatedMuster.params[matchedEntry.ValueName] } else { log("Write Event: " + obj.id + " val: " + obj.state.val + " | No matching entry found."); // } setState(obj.id, obj.state.val, true); }); //State für die gesamte PV-Leistung 'totalPV' erstellen und beschreiben on({ id: new RegExp(ConfigData.statesPrefix + '\.app_device_property_[A-Za-z0-9]{13,17}\.data\.InverterHeartbeat\.sumPV|' + ConfigData.statesPrefix + '\.AdditionalPowerSum'), change: "any" }, function (obj) { //log("sumpv Evemnt:" + obj.id + " val: " + obj.state.val) let totalPV = 0 for (var i = 0; i < ConfigData.seriennummern.length; i++) { if (ConfigData.seriennummern[i].typ == "PS" && ConfigData.seriennummern[i].seriennummer != "XXXXXXXXXXXXX") { const asn = ConfigData.seriennummern[i].seriennummer if (isState2(ConfigData.statesPrefix + '.app_device_property_' + asn + '.data.InverterHeartbeat.sumPV')) { totalPV = totalPV + GetValAkt(ConfigData.statesPrefix + '.app_device_property_' + asn + '.data.InverterHeartbeat.sumPV', 60).val } } } totalPV = totalPV / 10 if (isState2(ConfigData.statesPrefix + '.AdditionalPowerSum')) { totalPV = totalPV + Number(GetValAkt(ConfigData.statesPrefix + '.AdditionalPowerSum').val) } let totalPVState = ConfigData.statesPrefix + '.totalPV'; if (!existsState(totalPVState)) { createState(totalPVState, Number(totalPV.toFixed(0))); } else { setState(totalPVState, Number(totalPV.toFixed(0))); //log("Summe gesetzt für "+asn+": "+ sum) } }) //AdditionalPower Sumieren und in State schreiben let RegAP = "(" for (var i = 0; i < ConfigData.AdditionalPower.length; i++) { if (isState2(ConfigData.AdditionalPower[i].id)) { if (i > 0) RegAP = RegAP + "|" RegAP = RegAP + "(" + ConfigData.AdditionalPower[i].id + ")" } } RegAP = RegAP + ")" if (RegAP != "()") { on({ id: new RegExp(RegAP), change: "any" }, function (obj) { //log("AdditionalPower Event:" + obj.id) let aPV = 0 for (var i = 0; i < ConfigData.AdditionalPower.length; i++) { if (isState2(ConfigData.AdditionalPower[i].id)) { aPV = aPV + GetValAkt(ConfigData.AdditionalPower[i].id, 60).val } } //log(aPV) setStateNE(ConfigData.statesPrefix + '.AdditionalPowerSum', aPV) aPV = 0 }) } else { if (isState2(ConfigData.statesPrefix + '.AdditionalPowerSum')) setState(ConfigData.statesPrefix + '.AdditionalPowerSum', 0) } // Einstellen der Einspeiseleistung function setAC(asn, Value) { if (!ConfigData.Regulation) { //return } let updatedMusterSetAC = musterSetAC; if (Value <= -1) { delete updatedMusterSetAC.item.meta; delete updatedMusterSetAC.item.ValByte; } else { updatedMusterSetAC.header.pdata.value = Value updatedMusterSetAC.header.dataLen = getVarintByteSize(Value) } updatedMusterSetAC.header.seq = Date.now() updatedMusterSetAC.header.deviceSn = asn //log(JSON.stringify(updatedMusterSetAC)) setState(ConfigData.statesPrefix + '.app_' + mqttDaten.UserID + '_' + asn + '_thing_property_set.setAC', Value.toString(), true) SendProto(JSON.stringify(updatedMusterSetAC), '/app/' + mqttDaten.UserID + '/' + asn + '/thing/property/set'); } // ########### Grundbedarf/Einspeiseleistung steuern // Den niedrigsten oder durchschnittlichen Wert vom Gesamtverbrauch der letzten x Minuten ermitteln function getLowestValue(id, minuten = 120) { if (Number(minuten) == 0) { const value = Math.floor(Number(getState(id).val)); setStateNE(ConfigData.statesPrefix + ".lowestValue", value); return Promise.resolve(value); } const now = Date.now(); const range = minuten * 60 * 1000; return new Promise((resolve, reject) => { sendTo('history.0', 'getHistory', { id: id, options: { start: now - range, end: now, aggregate: 'none', ignoreNull: true } }, (result) => { if (result.error) { log("getLowestValue-error: " + result.error);// reject(result.error); } else if (result.result && result.result.length > 0) { let lowestValue = result.result[0].val; let totalValue = result.result[0].val; if (ConfigData.MinValueAg === 0) { for (let i = 1; i < result.result.length; i++) { if (result.result[i].val < lowestValue) { lowestValue = result.result[i].val; } } } else if (ConfigData.MinValueAg === 1) { for (let i = 1; i < result.result.length; i++) { totalValue += result.result[i].val; } lowestValue = totalValue / result.result.length; // Durchschnittswert } let Dauer = ((Date.now() - now) / 1000) if (Dauer > 1) log("getLowestValue-Duration: " + ((Date.now() - now) / 1000) + "s") // setStateNE(ConfigData.statesPrefix + ".lowestValue", Math.floor(Number(lowestValue))); resolve(Math.floor(Number(lowestValue))); } else { reject(new Error('No data')); } }); }); } //Einspeiseleistung berechnen und bei Änderung setzen //Regulation State if (ConfigData.RegulationState != "") { let eventid = ConfigData.statesPrefix + '.' + ConfigData.RegulationState ConfigData.Regulation = Boolean(getStateCr(eventid, ConfigData.Regulation, true).val) on({ id: eventid, change: "any", ack: false }, function (obj) { let name = obj.id.split('.').pop(); //log(name + ":" + obj.state.val) for (var i = 0; i < ConfigData.seriennummern.length; i++) { if (ConfigData.seriennummern[i].typ == "PS" && ConfigData.seriennummern[i].seriennummer != "XXXXXXXXXXXXX") { const asn = ConfigData.seriennummern[i].seriennummer var foundItem = ConfigData.seriennummern.find(item => item.seriennummer === asn); if (foundItem.RegulationOffPower >= 0 && !obj.state.val) { setAC(asn, foundItem.RegulationOffPower * 10) GlobalObj[asn].OldNewValue = 0 } else if (foundItem.RegulationOffPower == -2 && !obj.state.val) { log("Regulation Off, PrioMode On für:" + asn) setStateNE(ConfigData.statesPrefix + '.app_' + mqttDaten.UserID + '_' + asn + '_thing_property_set.writeables.SetPrio', "1", false) } else if (foundItem.RegulationOffPower == -2 && obj.state.val) { log("Regulation On, PrioMode Off für:" + asn) setStateNE(ConfigData.statesPrefix + '.app_' + mqttDaten.UserID + '_' + asn + '_thing_property_set.writeables.SetPrio', "0", false) } } } ConfigData.Regulation = Boolean(obj.state.val) }) } //**************************************************************** // Funktionen zur Steuerung der Einspeisung //**************************************************************** const ZUSATZPOWER_INCREMENT = 20 const BAT_MAX_OFFSET = ConfigData.Zusatzpower_Offset const GAP_MESSUNGEN = 3 let LastSetSumme = 0, BatBedarf = 0 let cutoff = 0 for (var i = 0; i < ConfigData.seriennummern.length; i++) { const sn = ConfigData.seriennummern[i].seriennummer; if (!GlobalObj[sn]) { GlobalObj[sn] = {}; } if (ConfigData.seriennummern[i].typ == "PS") { GlobalObj[sn].OldNewValue = -1 GlobalObj[sn].FullPower = false GlobalObj[sn].zusatzpower = 0 GlobalObj[sn].TempMaxPower = ConfigData.seriennummern[i].MaxPower GlobalObj[sn].regulieren = false GlobalObj[sn].GapArray = [] GlobalObj[sn].GAPdurchschnitt = 0 GlobalObj[sn].dynamicWatts = 0 GlobalObj[sn].sumPV = 0 GlobalObj[sn].gapWait = 0 GlobalObj[sn].PsName = "[" + ConfigData.seriennummern[i].name + "]" GlobalObj[sn].TempPrioOff = false GlobalObj[sn].LastSetNewValue = Date.now() } } function SetBasePower() { //logM = true //return // Reguliert wird nur wenn ein Smartmeter Existiert und die Regelung eingeschatet ist if (isState2(ConfigData.SmartmeterID) && ConfigData.Regulation) { mlog("SetBasePower !") // Durchlaufen allen PS, Sonderfaelle schalten und messen for (var i = 0; i < ConfigData.seriennummern.length; i++) { if (ConfigData.seriennummern[i].typ == "PS" && ConfigData.seriennummern[i].seriennummer != "XXXXXXXXXXXXX") { let skip = false const asn = ConfigData.seriennummern[i].seriennummer GlobalObj[asn].regulieren = true const invOutputWattsID = ConfigData.statesPrefix + ".app_device_property_" + asn + ".data.InverterHeartbeat.invOutputWatts" GlobalObj[asn].dynamicWatts = Math.floor(Number(GetValAkt(ConfigData.statesPrefix + ".app_device_property_" + asn + ".data.InverterHeartbeat.dynamicWatts", 600).val) / 10) GlobalObj[asn].batstate = Number(GetValAkt(ConfigData.statesPrefix + ".app_device_property_" + asn + ".data.InverterHeartbeat.batSoc", 600).val) GlobalObj[asn].sumPV = Math.floor(Number(GetValAkt(ConfigData.statesPrefix + ".app_device_property_" + asn + ".data.InverterHeartbeat.sumPV", 600).val) / 10) GlobalObj[asn].invOutputWatts = Math.floor(Number(GetValAkt(invOutputWattsID, 90, false).val) / 10) const PrioMode = getStateCr(ConfigData.statesPrefix + '.app_' + mqttDaten.UserID + '_' + asn + '_thing_property_set.writeables.SetPrio', "0", true).val == "1" ? true : false const PrioModeTS = getStateCr(ConfigData.statesPrefix + '.app_' + mqttDaten.UserID + '_' + asn + '_thing_property_set.writeables.SetPrio', "0", true).ts if (GlobalObj[asn].OldNewValue == -1) { GlobalObj[asn].LeiststungsGap = 0 } else { GlobalObj[asn].LeiststungsGap = Math.floor((GlobalObj[asn].OldNewValue + GlobalObj[asn].dynamicWatts) - GlobalObj[asn].invOutputWatts) } let toBatPower = Math.floor(Number(GetValAkt(ConfigData.statesPrefix + ".app_device_property_" + asn + ".data.InverterHeartbeat.batInputWatts", 60).val / 10)) var foundItem = ConfigData.seriennummern.find(item => item.seriennummer === asn); if (!ConfigData.seriennummern[i].regulation) break //Hat keine Batterie if (!foundItem.hasBat) { //Volle leistung schon on? Wenn nicht einschalten if (!GlobalObj[asn].FullPower) { GlobalObj[asn].FullPower = true if (true || ConfigData.Debug) log("PowerStream " + GlobalObj[asn].PsName + " hat keine Batterie konfiguriert. Einspeisung auf Maximum. (" + foundItem.MaxPower + ")") setAC(asn, (Math.floor(foundItem.MaxPower) * 10)) GlobalObj[asn].OldNewValue = (Math.floor(foundItem.MaxPower)) } GlobalObj[asn].regulieren = false skip = true //GlobalObj[asn].oldInvOutputWatts = GlobalObj[asn].invOutputWatts } if ((GlobalObj[asn].batstate >= foundItem.battPozOn || (GlobalObj[asn].batstate >= foundItem.battPozOff && GlobalObj[asn].TempPrioOff)) && !skip) { // Ladezustand der Batterie liegt über battPozOn if (foundItem.battOnSwitchPrio) { mlog("PrioMode: " + PrioMode) // Fall: battOnSwitchPrio statt leistungsabgabe if (!PrioMode && BatBedarf <= foundItem.prioOffOnDemand && PrioModeTS < (Date.now() + (1000 * 60))) { // Wenn noch nicht aktiv, Bat-Priomode einschalten und Einspeiseleistung auf MAX setAC(asn, (Math.floor(foundItem.MaxPower) * 10)) setStateNE(ConfigData.statesPrefix + '.app_' + mqttDaten.UserID + '_' + asn + '_thing_property_set.writeables.SetPrio', "1", false) //log("Math.floor(foundItem.MaxPower * 10) :" +Math.floor(foundItem.MaxPower * 10) ) GlobalObj[asn].OldNewValue = (Math.floor(foundItem.MaxPower)) //GlobalObj[asn].OldNewValue = -1 if (true || ConfigData.Debug) log("PowerStream " + GlobalObj[asn].PsName + " Batterie ist bei " + foundItem.battPozOn + "%: Schalte auf Batterie Prioritätsmodus.") GlobalObj[asn].regulieren = false GlobalObj[asn].TempPrioOff = true skip = true } else if (PrioMode) { if (BatBedarf > foundItem.prioOffOnDemand && foundItem.prioOffOnDemand != 0) { if (PrioModeTS < (Date.now() + (1000 * 60))) { log("PowerStream " + GlobalObj[asn].PsName + " Batterie-Prioritätsmodus wird temporär abgestellt.") setStateNE(ConfigData.statesPrefix + '.app_' + mqttDaten.UserID + '_' + asn + '_thing_property_set.writeables.SetPrio', "0", false) GlobalObj[asn].regulieren = true } else { GlobalObj[asn].regulieren = false } } else { GlobalObj[asn].regulieren = false } // Hier noch Regeln einbinden zum frühzeitigen beeneden des Priomode /* if (foundItem.prioOffOnDemand != 0 && LeiststungsGap > foundItem.prioOffOnDemand) { log("Priomode ist an. LeiststungsGap " + LeiststungsGap.toFixed(1) + " OldNewValue:" + GlobalObj[asn].OldNewValue + " invOutputWatts:" + GlobalObj[asn].invOutputWatts) log("Schalte den jetzt Priomode aus") setStateNE(ConfigData.statesPrefix + '.app_' + mqttDaten.UserID + '_' + asn + '_thing_property_set.writeables.SetPrio', "0", false) GlobalObj[asn].regulieren = true } //*/ } } else { GlobalObj[asn].TempPrioOff = false // Kein Priofall also Leistung auf Fullpower if (!GlobalObj[asn].FullPower) { GlobalObj[asn].FullPower = true setAC(asn, (Math.floor(foundItem.MaxPower) * 10)) GlobalObj[asn].OldNewValue = (Math.floor(foundItem.MaxPower)) if (true || ConfigData.Debug) log("PowerStream " + GlobalObj[asn].PsName + " Batterie ist bei " + foundItem.battPozOn + "%: Einspeisung auf Maximum.") } GlobalObj[asn].regulieren = false skip = true } } else if (GlobalObj[asn].batstate <= foundItem.battPozOff && !skip) { GlobalObj[asn].TempPrioOff = false // Wenn die Ladung der Batterie kleiner der battPozOff ist if (foundItem.battOnSwitchPrio) { // Wenn Bat-Priomode an dann ausschalten if (PrioMode) { if (true || ConfigData.Debug) log("PowerStream " + GlobalObj[asn].PsName + " Batterie runter auf " + foundItem.battPozOff + "%: Priomodus wird ausgeschaltet.") setStateNE(ConfigData.statesPrefix + '.app_' + mqttDaten.UserID + '_' + asn + '_thing_property_set.writeables.SetPrio', "0", false) } GlobalObj[asn].regulieren = true skip = false } else if (GlobalObj[asn].FullPower) { // Wenn Fullpower an ist, dann ausschalten if (GlobalObj[asn].FullPower) { GlobalObj[asn].FullPower = false if (true || ConfigData.Debug) log("PowerStream " + GlobalObj[asn].PsName + " Batterie runter auf " + foundItem.battPozOff + "%: Normalbetrieb.") } GlobalObj[asn].regulieren = true skip = false } } if (GlobalObj[asn].batstate < foundItem.lowBatLimitPozOn && !skip) { // Maximale Einspeiseleistung bei niedrieger batterie begrenzen (lowBatLimitPozOn regelung) if (GlobalObj[asn].TempMaxPower == foundItem.MaxPower) log("PowerStream " + GlobalObj[asn].PsName + " Batteriestand unter Limit:" + foundItem.lowBatLimitPozOn + "% (" + GlobalObj[asn].batstate + "%). Limitiere Einspeiseleistung auf: " + foundItem.lowBatLimit + "W") GlobalObj[asn].TempMaxPower = foundItem.lowBatLimit } else if (GlobalObj[asn].batstate >= foundItem.lowBatLimitPozOff && !skip) { if (GlobalObj[asn].TempMaxPower == foundItem.lowBatLimit) { log("PowerStream " + GlobalObj[asn].PsName + " Batteriestand ist jetzt über Limit:" + foundItem.lowBatLimitPozOff + "% (" + GlobalObj[asn].batstate + "%). Maximale Einspeisung wieder bei: " + foundItem.MaxPower + "W") GlobalObj[asn].TempMaxPower = foundItem.MaxPower } } // Zusatzpower regeln if (GlobalObj[asn].regulieren) { if (toBatPower <= ((foundItem.MaxPower - BAT_MAX_OFFSET) * -1)) { // Wenn in die Batterie die Maximale Einpeiseleistung - BAT_MAX_OFFSET geht, zusatzpower um ZUSATZPOWER_INCREMENT erhöhen if (GlobalObj[asn].zusatzpower < 300) { if (GlobalObj[asn].zusatzpower == 0) { GlobalObj[asn].zusatzpower = GlobalObj[asn].invOutputWatts } GlobalObj[asn].zusatzpower += ZUSATZPOWER_INCREMENT log("PowerStream " + GlobalObj[asn].PsName + " Maximalleistung geht in die Batterie. Stelle zusätzlich Einspeisung auf " + GlobalObj[asn].zusatzpower + " W") setAC(asn, ((GlobalObj[asn].zusatzpower) * 10)); GlobalObj[asn].OldNewValue = (GlobalObj[asn].zusatzpower) } GlobalObj[asn].regulieren = false } else { if (toBatPower >= ((foundItem.MaxPower - 200) * -1) && GlobalObj[asn].zusatzpower > 0) { // Wenn der weniger Leistung in die Batterie geht als MaxPower - 200W dann Zusatzpower sofort ausschalten GlobalObj[asn].zusatzpower = 0 log("PowerStream " + GlobalObj[asn].PsName + " Zusatzpower sofort aus !") GlobalObj[asn].regulieren = true } else { // Wenn in die Batterie weniger als die Maximale Einpeiseleistung - BAT_MAX_OFFSET geht, zusatzpower um ZUSATZPOWER_INCREMENT verringern GlobalObj[asn].zusatzpower = GlobalObj[asn].zusatzpower - ZUSATZPOWER_INCREMENT if (GlobalObj[asn].zusatzpower > 0) { log("PowerStream " + GlobalObj[asn].PsName + " Maximalleistung geht in die Batterie. Stelle zusätzlich Einspeisung auf " + GlobalObj[asn].zusatzpower + " W") setAC(asn, ((GlobalObj[asn].zusatzpower) * 10)); GlobalObj[asn].OldNewValue = (GlobalObj[asn].zusatzpower) GlobalObj[asn].regulieren = false } else { if (GlobalObj[asn].zusatzpower > -ZUSATZPOWER_INCREMENT && GlobalObj[asn].zusatzpower <= 0) log("PowerStream " + GlobalObj[asn].PsName + " Zusatzpower aus !") GlobalObj[asn].zusatzpower = 0 GlobalObj[asn].regulieren = true } } } } //log(GlobalObj[asn].regulieren) } } //Fallbackpower setzten, wenn Das smartmeter zu alte daten hat zu alt ist if (Date.now() - getState(ConfigData.SmartmeterID).ts > (1000 * 60 * ConfigData.SmartmeterTimeoutMin)) { if (getState(ConfigData.statesPrefix + ".RealPower").val = !ConfigData.SmartmeterFallbackPower) { mlog("SmartmeterTimeoutMin hat gegriffen. Letzter Wert ist " + ((Date.now() - getState(ConfigData.SmartmeterID).ts) / 1000 / 60).toFixed(1) + "min. alt. Realpower auf Fallback (" + ConfigData.SmartmeterFallbackPower + ") gesetzt", 1) } setState(ConfigData.statesPrefix + ".RealPower", ConfigData.SmartmeterFallbackPower) } //log("Alter des Smartmeterwertes:" + (Date.now() - getState(ConfigData.SmartmeterID).ts) / 1000 + "s") // Hier geht es nun um die Messung des Bedarfs und der Regelung der zu regelnden PS // zunächst den aktuellen echten Bedarf ermitteln getLowestValue(ConfigData.statesPrefix + ".RealPower", ConfigData.MinValueMin) .then(lowestValue => { //mlog("SetBasePower lowestValue " + lowestValue) if (lowestValue != 0) { //Gesamtbedarf ist: let NewValue = lowestValue - ConfigData.BasePowerOffset // Die Einspeiseleistung aller NICHT regulierten Quellen ermitteln um die zu regelnde Leistung zu erhalten let otherPS = 0 for (var i = 0; i < ConfigData.seriennummern.length; i++) { const asn = ConfigData.seriennummern[i].seriennummer if (ConfigData.seriennummern[i].typ == "PS" && ConfigData.seriennummern[i].seriennummer != "XXXXXXXXXXXXX" && !GlobalObj[asn].regulieren) { otherPS = otherPS + GlobalObj[asn].invOutputWatts //mlog("otherPS: " + otherPS / 10) } } //mlog(otherPS) // Die Einspeiseleistung aller weiteren Quellen ermitteln und hinzurechnen if (isState2(ConfigData.statesPrefix + '.AdditionalPowerSum')) { otherPS += Number(GetValAkt(ConfigData.statesPrefix + '.AdditionalPowerSum', 15).val) } //mlog(otherPS) //Diese vom Bedarf abziehen: NewValue = NewValue - otherPS //mlog(NewValue) // Gap Durchschnitt bestimmen let gapSumme = 0, calcgapSumme = 0, totalPV = 0 for (var i = 0; i < ConfigData.seriennummern.length; i++) { const asn = ConfigData.seriennummern[i].seriennummer if (ConfigData.seriennummern[i].typ === "PS" && ConfigData.seriennummern[i].seriennummer != "XXXXXXXXXXXXX" && GlobalObj[asn].regulieren) { //let dynamicWatts = let neueMessung = GlobalObj[asn].LeiststungsGap; // Die aktuelle Messung // Hinzufügen der neuen Messung zur Liste der letzten GAP_MESSUNGEN Messungen GlobalObj[asn].GapArray.push(neueMessung); // Entfernen der ältesten Messung, wenn mehr als GAP_MESSUNGEN Messungen gespeichert sind if (GlobalObj[asn].GapArray.length > GAP_MESSUNGEN) { GlobalObj[asn].GapArray.shift(); } // Berechnen der gapSumme, wenn GAP_MESSUNGEN Messungen vorhanden sind if (GlobalObj[asn].GapArray.length === GAP_MESSUNGEN) { calcgapSumme = GlobalObj[asn].GapArray.reduce((a, b) => a + b, 0); // Summe der letzten 5 Messungen // Berechnen des Durchschnitts GlobalObj[asn].GAPdurchschnitt = Math.floor(calcgapSumme / GAP_MESSUNGEN); } mlog("Durchschnitt: PS:" + GlobalObj[asn].PsName + " : " + GlobalObj[asn].GAPdurchschnitt); gapSumme = gapSumme + GlobalObj[asn].GAPdurchschnitt totalPV = totalPV + GlobalObj[asn].sumPV //Die PV Leistung sollte immer etwas gößer sein, damit das PV-Potential grundsätzlich ausgeschöpft wird. totalPV = totalPV + 10 } } if (gapSumme > NewValue) gapSumme = NewValue // Setzen der Einspeisung let psCounter = 0, psBatSumme = 0 for (var i = 0; i < ConfigData.seriennummern.length; i++) { const asn = ConfigData.seriennummern[i].seriennummer if (ConfigData.seriennummern[i].typ == "PS" && ConfigData.seriennummern[i].seriennummer != "XXXXXXXXXXXXX" && GlobalObj[asn].regulieren) { psCounter = psCounter + 1 psBatSumme = psBatSumme + GlobalObj[asn].batstate //gapSumme = gapSumme + GlobalObj[asn].GAPdurchschnitt } } let lastcut = cutoff cutoff = 0 BatBedarf = 0 let PVBedarf = 0 if ((NewValue - totalPV) > 0) BatBedarf = Math.floor(NewValue - totalPV) if ((totalPV - NewValue) > 0) { PVBedarf = NewValue } else { PVBedarf = totalPV } let PVfaktor = 0 if (totalPV != 0) { PVfaktor = PVBedarf / totalPV } let Batfaktor = 1 if (psBatSumme != 0) { Batfaktor = (BatBedarf / psBatSumme) } if (BatBedarf < 2 && lastcut > 0) { lastcut = 0 } let ueberschuss = (gapSumme) * -1 if (ueberschuss < 0) ueberschuss = 0 //*/ //logM = true mlog("*************************************") mlog("Lastcutoff: " + lastcut) mlog("gapSumme: " + gapSumme) mlog("Bedarf : " + NewValue) mlog("totalPV: " + totalPV) mlog("PVBedarf : " + (PVBedarf)) mlog("BatBedarf: " + BatBedarf) mlog("PVfaktor:" + PVfaktor) mlog("Batfaktor:" + Batfaktor) mlog("ueberschuss:" + ueberschuss) mlog("*************************************") //*/ setStateNE(ConfigData.statesPrefix + ".ExcessPower", ueberschuss) LastSetSumme = 0 if (ConfigData.RegulationMultiPsMode == 1) { // Serieller Regulationsmodus let calcValue = NewValue // Durchlaufen aller ps die reguliert werden sollen for (var i = 0; i < ConfigData.seriennummern.length; i++) { const asn = ConfigData.seriennummern[i].seriennummer if (ConfigData.seriennummern[i].typ == "PS" && ConfigData.seriennummern[i].seriennummer != "XXXXXXXXXXXXX" && GlobalObj[asn].regulieren) { let Setpower = calcValue //Maximale Abgebeleistung ermitteln let myMaxPower = GlobalObj[asn].TempMaxPower //Wird weniger Leistung als angefordert abgegeben, das hier berücksichtigen //mlog("LeiststungsGap PS:" + GlobalObj[asn].PsName + " : " + GlobalObj[asn].LeiststungsGap + " W") //mlog("gapSumme: " + gapSumme) if (GlobalObj[asn].GAPdurchschnitt < 20 && gapSumme > 0) { Setpower = Setpower + gapSumme mlog("Der ps: " + GlobalObj[asn].PsName + " bekommt die Gapsumme: " + gapSumme + " W Zusaetzlich") } if (Setpower > NewValue) Setpower = NewValue //if (GlobalObj[asn].LeiststungsGap > 10) { //myMaxPower = GlobalObj[asn].invOutputWatts //} if (Setpower > myMaxPower) Setpower = myMaxPower //Setpower = Setpower - GlobalObj[asn].dynamicWatts //mlog("GlobalObj[asn].dynamicWatts: " + GlobalObj[asn].dynamicWatts) Setpower = Math.floor(Setpower) if (GlobalObj[asn].OldNewValue != Setpower) { let dynset = Math.floor(Setpower - GlobalObj[asn].dynamicWatts) * 10 if (dynset < 0) dynset = 0 setAC(asn, dynset) mlog("Änderung für Einspeisung gesendet PS:" + GlobalObj[asn].PsName + " : " + Math.floor(Setpower) + " W") // } GlobalObj[asn].OldNewValue = Setpower calcValue = calcValue - Setpower if (calcValue <= 0) calcValue = 0; } } } else { // Balance Mode verteil die Leistung nach Batterie Ladestand, um gleichmäßige Entladung aller Batterien zu erreichen let calcValue = (NewValue) / psBatSumme for (var i = 0; i < ConfigData.seriennummern.length; i++) { const asn = ConfigData.seriennummern[i].seriennummer if (ConfigData.seriennummern[i].typ == "PS" && ConfigData.seriennummern[i].seriennummer != "XXXXXXXXXXXXX" && GlobalObj[asn].regulieren) { let Setpower = (GlobalObj[asn].sumPV * PVfaktor) Setpower = Setpower + (GlobalObj[asn].batstate * Batfaktor) //mlog("PS:" + GlobalObj[asn].PsName + " Setpower: " + Setpower) //let Setpower = (calcValue * GlobalObj[asn].batstate) //log("1 Setpower: " + Setpower + " cutoff: " + cutoff + " lastcut: " + lastcut) Setpower += lastcut lastcut = 0 if (Setpower > NewValue) Setpower = NewValue //Maximale Abgebeleistung ermitteln let myMaxPower = GlobalObj[asn].TempMaxPower //Wird weniger Leistung als angefordert abgegeben, das hier berücksichtigen //log("LeiststungsGap PS:" + GlobalObj[asn].PsName + " : " + GlobalObj[asn].LeiststungsGap + " W") if ((GlobalObj[asn].GAPdurchschnitt < 10 && gapSumme > 10) || GlobalObj[asn].gapWait > Date.now() - (1 * 60 * 1000)) { if (GlobalObj[asn].gapWait == 0) GlobalObj[asn].gapWait = Date.now() Setpower = Setpower + gapSumme - GlobalObj[asn].LeiststungsGap mlog("Der ps: " + GlobalObj[asn].PsName + " bekommt die Gapsumme: " + (gapSumme - GlobalObj[asn].LeiststungsGap) + " W Zusaetzlich") } else { if (GlobalObj[asn].gapWait != 0) mlog("Der ps: " + GlobalObj[asn].PsName + " gapWait zurückgesetzt") GlobalObj[asn].gapWait = 0 } //*/ if (Setpower > myMaxPower) { cutoff = Math.floor((Setpower - myMaxPower)) Setpower = myMaxPower } else { Setpower = Setpower + cutoff cutoff = 0 if (Setpower > myMaxPower) { cutoff = Math.floor((Setpower - myMaxPower)) Setpower = myMaxPower } } //Setpower = Setpower - GlobalObj[asn].dynamicWatts Setpower = Math.floor(Setpower) if (GlobalObj[asn].OldNewValue != Setpower || GlobalObj[asn].LastSetNewValue < (Date.now() - (60 * 1000))) { GlobalObj[asn].LastSetNewValue = Date.now() let dynset = (Math.floor(Setpower - GlobalObj[asn].dynamicWatts) * 10) if (dynset < 0) dynset = 0 setAC(asn, dynset) //setAC(asn, (Math.floor(Setpower) * 10)) mlog("Änderung für Einspeisung gesendet PS:" + GlobalObj[asn].PsName + " : " + Math.floor(dynset / 10) + " W") // } GlobalObj[asn].OldNewValue = Setpower LastSetSumme = LastSetSumme + Setpower //log("Der ps: " + GlobalObj[asn].PsName + " 2 Setpower: " + Setpower + " cutoff: " + cutoff + " lastcut: " + lastcut) } } } } }) .catch(error => { log("Fehler beim Abrufen des niedrigsten Werts: " + error); // }); } } function GetValAkt(id, minuten = 15, reset = true) { if (isState2(id)) { const state = getState(id) if (state.ts > Date.now() - minuten * 60 * 1000) { return state } else { if (typeof state.val === 'number') { if (reset && state.val != 0) setState(id, 0, true) } else { if (reset && state.val != "0") setState(id, "0", true) } state.val = 0 return state } } else { //log("Kein State: " + id + "lege an.") createState(id, "0", false) //return getState(id) const leerstate = {} leerstate.val = "0" leerstate.ts = Date.now() return leerstate } } async function createMyState(name, value = undefined) { const stateName = ConfigData.statesPrefix + '.' + name; if (!(await existsObjectAsync(stateName))) { const state = { name: name.split('.').pop(), role: 'state', //type: 'string', // 'number', 'boolean', usw. read: true, write: true, }; // @ts-ignore await createStateAsync(stateName, value, false, state); // Wenn der optionale Parameter value übergeben wurde, schreibe den Wert in den State } } function getStateCr(id, initValue, ack = false, common = {}, native = {}) { if (!isState2(id)) { let valueType = typeof initValue; let name = id.split('.').pop(); if (Object.keys(common).length === 0) { common = { name: name, type: valueType, role: 'state', read: true, write: true, }; } createState(id, initValue, false, common, native) //log("getStateCr: " + id) const leerstate = {} leerstate.val = initValue leerstate.ts = Date.now() return leerstate } else { return getState(id); } } function setStateNE(id, value, ack = false, common = {}, native = {}) { existsState(id, function (err, exists) { if (!exists) { let valueType = typeof value; let name = id.split('.').pop(); if (Object.keys(common).length === 0) { common = { name: name, type: valueType, role: 'state', read: true, write: true, }; } createState(id, value, false, common, native, function () { setState(id, value, ack); }); } else { setState(id, value, ack); } }); } // State RealPower anlegen wenn noch nicht vorhanden und History aktivieren if (isState2(ConfigData.SmartmeterID)) { if (!isState2(ConfigData.statesPrefix + ".RealPower")) { const stateObject = { name: "RealPower", role: "state", type: "number", read: true, write: true, custom: { "history.0": { enabled: true, aliasId: "", debounceTime: 0, blockTime: 0, changesOnly: false, changesRelogInterval: 0, changesMinDelta: 0, ignoreBelowNumber: "", disableSkippedValueLogging: true, retention: 86400, customRetentionDuration: 365, maxLength: 960, enableDebugLogs: false, debounce: 1000 } } }; createState(ConfigData.statesPrefix + ".RealPower", stateObject, function () { //* const stateId = ConfigData.statesPrefix + ".RealPower"; // Hier den ID des States angeben // Aktiviere die History-Funktion für den State const historyOptions = { id: stateId, options: { enabled: true // Setze den Wert auf true, um die History zu aktivieren } }; sendTo("history.0", "enableHistory", historyOptions, (result) => { if (result.error) { log("Fehler beim Aktivieren der History für " + stateId + ": " + result.error);// } else { log("History für " + stateId + " erfolgreich aktiviert");// } }); // }); } //Wert für den realen Verbrauch. Wird alle 5 Sekunden gesetzt, wenn sich die Werte vom Smartmeter ändern let WorkInProz = false const SECONDS_DELAY = 5; const DIVISION_FACTOR = 10; const TOLERANCE_PERIOD_FACTOR = 2; let LastRealPower = 0 on({ id: ConfigData.SmartmeterID, change: "any" }, function (obj) { if (!WorkInProz) { WorkInProz = true; setTimeout(function () { const Hausstrom = Number(getState(ConfigData.SmartmeterID).val); let Einspeisung = 0; for (const item of ConfigData.seriennummern) { if (item.typ == "PS" && item.seriennummer != "XXXXXXXXXXXXX") { const asn = item.seriennummer; const LastACset = getState(ConfigData.statesPrefix + '.app_' + mqttDaten.UserID + '_' + asn + '_thing_property_set.setAC').ts; const invOutputWattsState = GetValAkt(ConfigData.statesPrefix + ".app_device_property_" + asn + ".data.InverterHeartbeat.invOutputWatts", 50, true); const invOutputWatts = Math.floor(Number(invOutputWattsState.val) / DIVISION_FACTOR); const lastOutset = invOutputWattsState.ts; if ((Number(lastOutset) < Number(LastACset)) && invOutputWatts !== 0) { const lastRealset = getState(ConfigData.statesPrefix + ".RealPower").ts; if (Number(lastRealset) > Date.now() - ((ConfigData.MinValueMin * 1000 * 60) / TOLERANCE_PERIOD_FACTOR)) { mlog("RealPower Set Warte auf aktuelle Daten von: " + GlobalObj[asn].PsName + " lezter: " + new Date(lastOutset).toLocaleTimeString('de-DE') + " / ACset: " + new Date(LastACset).toLocaleTimeString('de-DE')); WorkInProz = false; return; } else { //log("Überspringe ab jetzt warten auf Daten von: " + asn + " und setzte Wert für Einspeisung auf 0 ") //setState(ConfigData.statesPrefix + ".app_device_property_" + GlobalObj[asn].PsName + ".data.InverterHeartbeat.invOutputWatts", "0") Einspeisung += invOutputWatts; } } else { Einspeisung += invOutputWatts; } } } Einspeisung = Number(Einspeisung.toFixed(0)) if (isState2(ConfigData.statesPrefix + '.AdditionalPowerSum')) { Einspeisung += Number(GetValAkt(ConfigData.statesPrefix + '.AdditionalPowerSum', 15).val) } const RealPower = Number((Hausstrom + Einspeisung).toFixed(0)) if (RealPower + 100 < LastRealPower) { mlog("PeakSkip Delta: " + (LastRealPower - RealPower)) } else { setState(ConfigData.statesPrefix + ".RealPower", RealPower); } LastRealPower = RealPower WorkInProz = false; }, SECONDS_DELAY * 1000); } }); } function getVarintByteSize(number) { let byteSize = 0; while (number >= 128) { byteSize++; number >>= 7; // Rechtsschiebeoperation um 7 Bits } byteSize++; byteSize++; return byteSize; } function mlog(text, force = 0) { if (logM || force == 1) log(text) } function isValidUnixTimestampAndConvert(n) { // Typüberprüfung if (typeof n !== 'number') { return false; } // Bereichsüberprüfung (optional) const unixEpoch = 0; // UNIX-Epoch: 1. Januar 1970 const currentTime = Math.floor(Date.now() / 1000); if (n < unixEpoch || n > currentTime) { return false; } // Granularität (optional) if (Math.floor(n) !== n) { return false; } // Konvertiere zu deutschem Zeitformat const date = new Date(n * 1000); const germanTimeFormat = date.toLocaleString('de-DE'); return germanTimeFormat; } /* Checks if a a given state or part of state is existing. * This is a workaround, as getObject() or getState() throw warnings in the log. * Set strict to true if the state shall match exactly. If it is false, it will add a wildcard * to the end. * See: https://forum.iobroker.net/topic/11354/ * @param {string} strStatePath Input string of state, like 'javascript.0.switches.Osram.Bedroom' * @param {boolean} [strict=false] Optional: if true, it will work strict, if false, it will add a wildcard * to the end of the string * @return {boolean} true if state exists, false if not */ function isState2(strStatePath, strict = true) { let mSelector; if (strict) { mSelector = $(strStatePath); } else { mSelector = $(strStatePath + "*"); } if (mSelector.length > 0) { return true; } else { return false; } } const fs = require('fs').promises; async function writeLog(logEntry, filename = "ECOTest2.csv") { // Get the current date and time const now = new Date(); const dateTime = now.toLocaleString(); // Format the log entry const formattedEntry = `${dateTime}\t${logEntry}\n`; try { // Check if the file already exists await fs.stat(logpath + filename); } catch (error) { if (error.code === 'ENOENT') { // If the file does not exist, we need to add the header const header = 'DateTime\tLog Entry\n'; await fs.writeFile(logpath + filename, header); log("neue datei erstellt"); } else { // Some other error occurred throw error; } } // Append the log entry to the file try { await fs.appendFile(logpath + filename, formattedEntry); //console.log('Log entry saved!'); } catch (error) { throw error; } } // Anwendung: //writeLog('This is a test entry.', 'Filename.csv').catch(error => console.error(error));