diff --git a/.github/workflows/build_display_theme_cards.yml b/.github/workflows/build_display_theme_cards.yml index 292e120de9..2b63cb8214 100644 --- a/.github/workflows/build_display_theme_cards.yml +++ b/.github/workflows/build_display_theme_cards.yml @@ -31,7 +31,7 @@ jobs: - name: Commit and Push Changes run: | git config user.name "${{ github.actor }}" - git config.user.email "${{ github.actor }}@users.noreply.github.com" + git config user.email "${{ github.actor }}@users.noreply.github.com" git add packages/modules/display_themes/cards/web git commit -m "Build Display Theme: Cards" git push diff --git a/data/config/apache/http-api-ssl.conf b/data/config/apache/http-api-ssl.conf new file mode 100644 index 0000000000..2a2546da76 --- /dev/null +++ b/data/config/apache/http-api-ssl.conf @@ -0,0 +1,141 @@ +# openwb-version:1 + + Listen 8443 + + + ServerAdmin webmaster@localhost + + DocumentRoot /var/www/html/openWB/runs/http-api + + # Available loglevels: trace8, ..., trace1, debug, info, notice, warn, + # error, crit, alert, emerg. + # It is also possible to configure the loglevel for particular + # modules, e.g. + #LogLevel info ssl:warn + + ErrorLog ${APACHE_LOG_DIR}/api-ssl-error.log + CustomLog ${APACHE_LOG_DIR}/api-ssl-access.log combined + + # For most configuration files from conf-available/, which are + # enabled or disabled at a global level, it is possible to + # include a line for only one particular virtual host. For example the + # following line enables the CGI configuration for this host only + # after it has been globally disabled with "a2disconf". + #Include conf-available/serve-cgi-bin.conf + + # SSL Engine Switch: + # Enable/Disable SSL for this virtual host. + SSLEngine on + + # A self-signed (snakeoil) certificate can be created by installing + # the ssl-cert package. See + # /usr/share/doc/apache2/README.Debian.gz for more info. + # If both key and certificate are stored in the same file, only the + # SSLCertificateFile directive is needed. + SSLCertificateFile /etc/ssl/certs/ssl-cert-snakeoil.pem + SSLCertificateKeyFile /etc/ssl/private/ssl-cert-snakeoil.key + + # Server Certificate Chain: + # Point SSLCertificateChainFile at a file containing the + # concatenation of PEM encoded CA certificates which form the + # certificate chain for the server certificate. Alternatively + # the referenced file can be the same as SSLCertificateFile + # when the CA certificates are directly appended to the server + # certificate for convinience. + #SSLCertificateChainFile /etc/apache2/ssl.crt/server-ca.crt + + # Certificate Authority (CA): + # Set the CA certificate verification path where to find CA + # certificates for client authentication or alternatively one + # huge file containing all of them (file must be PEM encoded) + # Note: Inside SSLCACertificatePath you need hash symlinks + # to point to the certificate files. Use the provided + # Makefile to update the hash symlinks after changes. + #SSLCACertificatePath /etc/ssl/certs/ + #SSLCACertificateFile /etc/apache2/ssl.crt/ca-bundle.crt + + # Certificate Revocation Lists (CRL): + # Set the CA revocation path where to find CA CRLs for client + # authentication or alternatively one huge file containing all + # of them (file must be PEM encoded) + # Note: Inside SSLCARevocationPath you need hash symlinks + # to point to the certificate files. Use the provided + # Makefile to update the hash symlinks after changes. + #SSLCARevocationPath /etc/apache2/ssl.crl/ + #SSLCARevocationFile /etc/apache2/ssl.crl/ca-bundle.crl + + # Client Authentication (Type): + # Client certificate verification type and depth. Types are + # none, optional, require and optional_no_ca. Depth is a + # number which specifies how deeply to verify the certificate + # issuer chain before deciding the certificate is not valid. + #SSLVerifyClient require + #SSLVerifyDepth 10 + + # SSL Engine Options: + # Set various options for the SSL engine. + # o FakeBasicAuth: + # Translate the client X.509 into a Basic Authorisation. This means that + # the standard Auth/DBMAuth methods can be used for access control. The + # user name is the `one line' version of the client's X.509 certificate. + # Note that no password is obtained from the user. Every entry in the user + # file needs this password: `xxj31ZMTZzkVA'. + # o ExportCertData: + # This exports two additional environment variables: SSL_CLIENT_CERT and + # SSL_SERVER_CERT. These contain the PEM-encoded certificates of the + # server (always existing) and the client (only existing when client + # authentication is used). This can be used to import the certificates + # into CGI scripts. + # o StdEnvVars: + # This exports the standard SSL/TLS related `SSL_*' environment variables. + # Per default this exportation is switched off for performance reasons, + # because the extraction step is an expensive operation and is usually + # useless for serving static content. So one usually enables the + # exportation for CGI and SSI requests only. + # o OptRenegotiate: + # This enables optimized SSL connection renegotiation handling when SSL + # directives are used in per-directory context. + #SSLOptions +FakeBasicAuth +ExportCertData +StrictRequire + + SSLOptions +StdEnvVars + + + SSLOptions +StdEnvVars + + + AllowOverride All + Require all granted + Options -Indexes + + # SSL Protocol Adjustments: + # The safe and default but still SSL/TLS standard compliant shutdown + # approach is that mod_ssl sends the close notify alert but doesn't wait for + # the close notify alert from client. When you need a different shutdown + # approach you can use one of the following variables: + # o ssl-unclean-shutdown: + # This forces an unclean shutdown when the connection is closed, i.e. no + # SSL close notify alert is send or allowed to received. This violates + # the SSL/TLS standard but is needed for some brain-dead browsers. Use + # this when you receive I/O errors because of the standard approach where + # mod_ssl sends the close notify alert. + # o ssl-accurate-shutdown: + # This forces an accurate shutdown when the connection is closed, i.e. a + # SSL close notify alert is send and mod_ssl waits for the close notify + # alert of the client. This is 100% SSL/TLS standard compliant, but in + # practice often causes hanging connections with brain-dead browsers. Use + # this only for browsers where you know that their SSL implementation + # works correctly. + # Notice: Most problems of broken clients are also related to the HTTP + # keep-alive facility, so you usually additionally want to disable + # keep-alive for those clients, too. Use variable "nokeepalive" for this. + # Similarly, one has to force some clients to use HTTP/1.0 to workaround + # their broken HTTP/1.1 implementation. Use variables "downgrade-1.0" and + # "force-response-1.0" for this. + # BrowserMatch "MSIE [2-6]" \ + # nokeepalive ssl-unclean-shutdown \ + # downgrade-1.0 force-response-1.0 + + + + +# vim: syntax=apache ts=4 sw=4 sts=4 sr noet diff --git a/data/config/mosquitto/mosquitto.acl b/data/config/mosquitto/mosquitto.acl index 80f2cb4777..c99f151a50 100644 --- a/data/config/mosquitto/mosquitto.acl +++ b/data/config/mosquitto/mosquitto.acl @@ -1,4 +1,4 @@ -# openwb-version:1 +# openwb-version:2 # allow publishing set topics topic write openWB/set/# # allow clearing system messages @@ -9,3 +9,5 @@ pattern write openWB/command/%c/messages/# topic read openWB/# # allow read access for remote support topics topic read openWB-remote/# +# allow brach "others" for devices other than openWB +topic readwrite others/# diff --git a/data/config/mosquitto/openwb_local.conf b/data/config/mosquitto/openwb_local.conf index 37201d3772..8909e9280f 100644 --- a/data/config/mosquitto/openwb_local.conf +++ b/data/config/mosquitto/openwb_local.conf @@ -1,4 +1,4 @@ -# openwb-version:13 +# openwb-version:14 listener 1886 localhost allow_anonymous true @@ -25,7 +25,6 @@ topic openWB/chargepoint/+/set/phases_to_use out 2 topic openWB/chargepoint/+/set/manual_lock out 2 topic openWB/chargepoint/+/set/autolock_state out 2 topic openWB/chargepoint/+/set/rfid out 2 -topic openWB/chargepoint/+/set/change_ev_permitted out 2 topic openWB/chargepoint/+/get/# out 2 topic openWB/chargepoint/+/config/# out 2 topic openWB/chargepoint/template/# out 2 diff --git a/data/config/ramdisk_config.txt b/data/config/ramdisk_config.txt new file mode 100644 index 0000000000..ed9fe59afc --- /dev/null +++ b/data/config/ramdisk_config.txt @@ -0,0 +1,5 @@ +# openwb - begin +# openwb-version:1 +# Do not edit this section! We need begin/end and version for proper updates! +tmpfs /var/www/html/openWB/ramdisk tmpfs nodev,nosuid,size=48M 0 0 +# openwb - end diff --git a/docs/Anzeige-Steuerung.md b/docs/Anzeige-Steuerung.md new file mode 100644 index 0000000000..4d60949873 --- /dev/null +++ b/docs/Anzeige-Steuerung.md @@ -0,0 +1,7 @@ +Die Kontrolle der openWB geschieht über einen Webbrowser. Aufruf geschieht über Eingabe der IP-Adresse der openWB. + +## Startseite +Die hier angezeigten Leistungen werden direkt aus den Leistungsdaten, welche vom Zähler übertragen werden, übernommen. + +## Auswertungen - Diagramme +In der Auswertung (und für's Langzeit-Logging) werden 5min-Intervalle der Zählerstände (kWh-Differenz/5min = kW) verwendet. \ No newline at end of file diff --git a/docs/Fehlersuche.md b/docs/Fehlersuche.md new file mode 100644 index 0000000000..96c5ab26e0 --- /dev/null +++ b/docs/Fehlersuche.md @@ -0,0 +1,43 @@ +# Fehlersuche +Es kann immer mal passieren, dass etwas nicht wie gedacht funktioniert. Das kann an einem Fehler im Programmcode, an einem Hardwaredefekt oder an einer fehlerhaften oder nicht zu den Gegebenheiten passenden Konfiguration liegen. + +Wenn nun eine Funktion nicht wie erwartet ausgeführt wird oder plötzlich ein Fehler auftritt, ist die erste Frage: +> Habe ich vor Kurzem etwas verändert? + +Dies betrifft ebenso Änderungen der Einstellungen im Fahrzeug, Wechselrichter (Softwareupdate?) oder der Hauselektrik. Unter Umständen kann das Problem so schon gelöst werden. + + +## Wo bekomme ich Hilfe? +### Hardwaresupport +Mit Problemen bei Inbetriebnahme / Anschluss oder Hardwareproblemen mit openWB-Hardware bitte direkt über die Support-Funktion unter System -> Support an openWB wenden (Notfalls auch per Mail an support@openwb.de). +Im Forum kann durchaus mal etwas untergehen. Das führt zu Frust und soll nicht sein. + +### Forum +Im [Forum](https://forum.openwb.de/index.php) findet man folgende Hilfestellung für Hilfesuchende. Die hier erbetene Herangehensweise dient dazu, den Fehler mit dem für alle niedrigsten Aufwand zu beheben. + +> Bitte keine Mehrfach-Meldung per Mail, Support-Ticket und Forum. +Das spart auf unserer Seite Supportzeit und bringt erfahrungsgemäß keine Beschleunigung des Vorgangs. +Bitte bei Problemen immer einen Logauszug posten: + +> Dazu unter System->Fehlersuche das Debuglevel auf Details stellen und mindestens zwei komplette Durchläufe von ``# *** Start***`` bis ``# ***Start***`` aus dem Main-Log kopieren, während das Problem auftritt. Sensible Daten wie Benutzernamen und Kennwörter unkenntlich machen. +Logauszüge bitte als Codeblock posten (Schaltfläche "" über dem Editor-Fenster). +Bei Problemen mit dem internen Ladepunkt zusätzlich einen Auszug aus dem Log des internen Ladepunkts, bei Problemen mit dem Soc aus dem Soc-Log posten. +Bei Problemen mit dem UI/Darstellung bitte ein Theme verwenden, das von openWB gepflegt wird (wird bei der Themeauswahl angezeigt). + +> Screenshots ersetzen keinen Logauszug! +Für Beiträge wie "Funktion XY geht nicht mehr! Woran kann das liegen?" ohne Logs gibt es von uns keine Hilfestellung. + +Formuliert Eure Frage freundlich, beschreibt was ihr tun wolltet und was anstelle dessen passiert ist. Weiterhin ist die verwendete Version von OpenWB wichtig. Diese findet ihr unter _Einstellungen -> System -> System_ im Feld _Versionsinformationen / Aktualisierungen_. +Da es schon Wechselwirkungen mit anderen Smarthome-Systemen gegeben hat, erwähnt ggf. weitere im Heimnetzwerk laufende SmartHome-Systeme. + +### Log-Erstellung +In der Standard-Einstellung des Logs werden nur Warnungen & Fehler erfasst. Außerdem wird bei einem Neustart der openWB der Fehlerlog gelöscht. + +Um aussagekräftige Logs zu erzeugen, müssen Log-Dateien im Debug-Modus erstellt werden. Hierzu ist folgende Schaltfläche zu aktivieren: + +![Debug-Einstellung](pictures/Fehlersuche_DebugLog.jpg) + +Aufgrund des detaillierten Loggings, ist die Dauer der Aufnahme ca. auf die letzten zwei Stunden begrenzt. Beachtet also, dass ihr sich der Fehler innerhalb des aufgezeichneten Abschnitts befindet. +Dann ist in den meisten Fällen das Main.log, aufzuklappen und mit der grünen Schaltfläche zu aktualisieren. Der entsprechende Auszug kann nun in eine Textdatei oder direkt in die Nachricht im Forum kopiert werden. + +![Main-Log](pictures/Fehlersuche_Main-Log.jpg) \ No newline at end of file diff --git "a/docs/Hausverbrauchs-Z\303\244hler.md" "b/docs/Hausverbrauchs-Z\303\244hler.md" index fb07aaa605..c5ddc5582a 100644 --- "a/docs/Hausverbrauchs-Z\303\244hler.md" +++ "b/docs/Hausverbrauchs-Z\303\244hler.md" @@ -1,4 +1,8 @@ -Einige Zähler, wie zB Solar-Log und Kostal Plenticore, werden im Hausverbrauchs-Zweig und nicht am EVU-Punkt installiert. Die für die Reglung erforderlichen Werte des EVU-Punkts werden mit einem virtuellen Zähler ermittelt. Dazu ein Virtuelles Gerät mit einem virtuellen Zähler anlegen. Die Komponenten müssen in der Hierarchie wie in den Abbildungen angeordnet werden: +Es gibt zwei mögliche Einbaupositionen für Zähler: EVU-Punkt und Hausverbrauchs-Zweig. +Ist der Zähler am EVU-Punkt installiert, misst er am EVU-Punkt (EVU=Elektrizitätsversorgungsunternehmen) Bezug und Einspeisung ins öffentliche Netz. Der Hausverbrauch wird dann aus den Werten der Ladepunkte, Wechselrichter und Speicher berechnet. +Ist der Zähler im Hausverbrauchs-Zweig installiert, misst er die Leistung der Ladepunkte und den Hausverbrauch. Bezug und Einspeisung ins öffentliche Netz werden dann aus den Werten des Zählers, Wechselrichter und Speicher berechnet. Dazu gibt es in openWB einen virtuellen Zähler. Dieser addiert die Werte aller in der Struktur dahinter angeordneten Komponenten. + +Zunächst ein Virtuelles Gerät mit einem virtuellen Zähler anlegen. Die Komponenten müssen in der Hierarchie wie in den Abbildungen unten angeordnet werden. In den Einstellungen für das Lastmanagement beim Punkt `Hausverbrauch` den Hausverbrauchs-Zähler auswählen. Der Hausverbrauch ist die Leistung des ausgewählten Zählers abzüglich der Ladeleistung. Misst der Zähler den Hausverbrauch, ergibt sich folgende Anordnung: diff --git a/docs/Huawei-Smartlogger.md b/docs/Huawei-Smartlogger.md new file mode 100644 index 0000000000..bbf082c46e --- /dev/null +++ b/docs/Huawei-Smartlogger.md @@ -0,0 +1,17 @@ +Im Smartlogger3000a müssen folgende Einstellungen festgelegt werden: + +1. Zunächst unter Einstell.-> Bef.-Param. -> Modbus TCP + Folgende Einstellungen festlegen: + Leitungseinstellungen: Akt.(Unbegrenzt) + Addressmodus: Logische Addresse + Logger-Addresse: z.B.4 (Muss eine freie ModBus ID sein, logische Addresse 2tes Bild.) + Schnelle Planung: Aktivieren + ![Huawei Smartlogger ModBusTCP](HuaweiSmartloggerModBusTCP.PNG) +2. Unter Wartung->Geräte-Mgmt.-> Geräte Liste + kann man jetzt die logische Adresse der einzelnen Geräte ablesen. Diese wird dann in der openWB in der Einstellung ModbusID eingetragen. + ![HuaweiSmartloggerLogischeAdressen](HuaweiSmartloggerLogischeAdressen.PNG) +4. in den Einstellungen der openWB das Modul Huawei Smartlogger auswählen. +5. Jetzt muss man die IP des Smartloggers und den Port 502 eintragen, außer dieser wurde geändert. +6. Jetzt die passenden Komponenten hinzufügen und die jeweilige ModbusID eintragen. +7. Zum Schluss auf Speichern drücken und unter dem Lastmanagement die passende Anordnung wählen. + ![Huawei Smartlogger Komponenten](HuaweiSmartloggerKomponenten.PNG) diff --git a/docs/HuaweiSmartloggerKomponenten.PNG b/docs/HuaweiSmartloggerKomponenten.PNG new file mode 100644 index 0000000000..2acc515423 Binary files /dev/null and b/docs/HuaweiSmartloggerKomponenten.PNG differ diff --git a/docs/HuaweiSmartloggerLogischeAdressen.PNG b/docs/HuaweiSmartloggerLogischeAdressen.PNG new file mode 100644 index 0000000000..ffb14ef7aa Binary files /dev/null and b/docs/HuaweiSmartloggerLogischeAdressen.PNG differ diff --git a/docs/HuaweiSmartloggerModBusTCP.PNG b/docs/HuaweiSmartloggerModBusTCP.PNG new file mode 100644 index 0000000000..ae0b4e61c8 Binary files /dev/null and b/docs/HuaweiSmartloggerModBusTCP.PNG differ diff --git a/docs/Ladeprofile.md b/docs/Ladeprofile.md index 2d75f080ca..f32aba2167 100644 --- a/docs/Ladeprofile.md +++ b/docs/Ladeprofile.md @@ -1,2 +1,5 @@ _Einstellungen -> Konfiguration -> Fahrzeuge -> Lade-Profile_ +Unter den Lade-Profilen werden die Einstellungen für das Ladeprofil verwaltet. Die Einstellungen auf der Hauptseite werden aus diesem Profil geladen und dorthin geschrieben. Ist nur ein Fahrzeug vorhanden, so wird in den meisten Fällen nur das Standard-Ladeprofil benötigt. Ausgenommen hiervon ist, wenn per RFID-Tag Ladevorgaben ausgewählt werden. + +In den fahrzeugspezifischen Einstellungen wird ein Ladeprofil einem Fahrzeug zugeordnet. Werden zwei Fahrzeuge geladen, empfiehlt es sich dazu ein zweites Ladeprofil anzulegen. \ No newline at end of file diff --git a/docs/Ladung nur nach Freischaltung.md b/docs/Ladung nur nach Freischaltung.md index 8741b9692f..2bb79e90c1 100644 --- a/docs/Ladung nur nach Freischaltung.md +++ b/docs/Ladung nur nach Freischaltung.md @@ -1,25 +1,67 @@ -Nach Abstecken des Fahrzeugs soll der Ladepunkt gesperrt werden und eine neue Ladung erst nach Freischalten durch: +Die openWB bietet die Möglichkeit den Ladepunkt gegen ungewollten Zugriff zu schützen. +Diese Option ist vor allem für öffentlich zugängliche Ladepunkte sinnvoll aber auch im privaten Bereich nützlich. +Dazu gibt es zwei grundlegende Konzepte: -- Eingeben einer PIN am openWB-Display (sofern mit Touchdisplay) oder -- Vorhalten eines RFID-Tags an der openWB mit RFID-Reader oder -- Direkt-tagging über den Ladestecker mit der openWB-Pro oder -- Auswahl eines Fahrzeugs im User Interface +#### **A** Nach Abstecken des Fahrzeugs wird der Ladepunkt gesperrt (Sperre nach Abstecken) +Ein neuer Ladevorgang erfolgt erst nach Freischalten durch: +- Eingeben einer **PIN** am openWB-Display (sofern mit Touchdisplay) +- Vorhalten eines **RFID-Tags** an der openWB mit RFID-Reader +- Fahrzeugerkennung über den **Ladestecker** mit der openWB-Pro +- händisches Entsperren des Ladepunktes im User Interface -gestartet werden. +#### **B** Nach Abstecken des Fahrzeugs wird auf das Standardfahrzeug (Standard nach Abstecken) mit Lademodus **Stop** gewechselt +Ein neuer Ladevorgang erfolgt erst durch: +- auswählen eines Fahrzeugs mit Lademodus Sofortladen, PV- oder Zielladen +- händischer Wechsel des Lademodus oder Fahrzeuges im User Interface +- vorhalten eines ID-Tags, welcher einem Fahrzeug zugeordnet ist -Hierzu ist folgendes zu konfigurieren: +#### Allgemeine Konfiguration +Wenn ID-Tags genutzt werden sollen, dann ist in der Navigationsbar unter **Einstellungen - Optionale Hardware** unter dem Punkt Identifikation von Fahrzeugen die Option **Identifikation aktivieren** auf **An** zustellen. -1. für jedes Fahrzeug mit Freischaltwunsch unter Einstellungen -> Konfiguration -> Fahrzeuge zusätzlich zum Standard-Fahrzeug **ein separates Fahrzeug** anlegen -2. unter Lade-Profile -> **Standard-Lade-Profil** (wird nur dem Standard-Fahrzeug zugeordnet) -> **Lademodus auf Stop** stellen -3. ein **neues Lade-Profil** für Fahrzeuge mit Freischaltwunsch anlegen (z.B. RFID-Lade-Profil) und dort -> **Standard nach Abstecken** aktivieren sowie bevorzugten Lademodus wählen -4. den Fahrzeugen mit Freischaltwunsch **das Lade-Profil** für Fahrzeuge mit Freischaltwunsch **zuweisen** (z.B. RFID-Lade-Profil) -5. **Speichern** nicht vergessen +**I.** Fahrzeuge +In der Navigationsbar auf Einstellungen klicken, dann den Reiter Konfiguration auswählen und **Fahrzeuge** aufrufen. Standardmäßig ist hier das **Standard-Fahrzeug** angelegt, welches die Option **Zugeordnete ID-Tags** beinhaltet. Hier müssen die ID-Tags eingetragen werden, welche ausschliesslich zur Zuordnung von Fahrzeugen verwendet werden. Hier zeigt sich auch die Stärke der openWB im Fahrzeugkonzept, welches die Möglichkeit bietet, verschiedene Fahrzeuge mit verschiedenen Fahrzeug- und Ladeprofilen über die ID-Kennung aufzurufen. -Wenn die Freischaltung mittels PIN, RFID oder MAC-Adresse erfolgen soll: +Hier muss für jedes Fahrzeug mit Freischaltwunsch ein **separates Fahrzeug** angelegt werden und die jeweiligen ID-Tags eingetragen werden, die zur Zuordnung genutzt werden. -- Einstellungen -> Optionale Hardware: **Identifikation aktivieren** + Speichern -- unter Konfiguration -> Fahrzeuge -> gewünschtes Fahrzeug -> Zugeordnete ID-Tags: dem jeweiligen Fahrzeug **den ID-Tag (PIN/RFID-Tag/MAC-Adresse) zuweisen** + Speichern -- unter Konfiguration -> Ladepunkte -> Ladepunkt-Profile -> im gewünschten Ladepunkt-Profil: **Freigabe durch ID-Tags aktiviere** + Speichern +Achtung: Lade-Profile müssen den Fahrzeugen unter Fahrzeuge zugeordnet werden! -Wenn die Tags an allen Ladepunkten genutzt werden dürfen, müssen die Tags nur bei den Fahrzeugen eingetragen werden. Wenn im Ladepunkt-Profil Tags eingetragen werden, können nur die eingetragenen Tags zur Fahrzeug-Zuordnung genutzt werden. Sind keine Tags eingetragen, wird nur die Zuordnung zum Fahrzeug geprüft. Durch die Begrenzung der Freischaltung auf bestimmte Tags, lassen sich zB Mitarbeiter- und Gäste-Parkplätze abbilden. -- unter Konfiguration -> Ladepunkte -> Ladepunkt-Profile -> im gewünschten Ladepunkt-Profil: **die gültigen ID-Tags zuordnen** + Speichern +**II.** Ladepunkte +In der Navigationsbar auf Einstellungen klicken, dann den Reiter Konfiguration auswählen und **Ladepunkte** aufrufen. +Standardmäßig ist hier das **Standard-Ladepunkt-Profil** angelegt, welches die Option **Sperre nach Abstecken** bietet. Wird diese Option aktiviert, dann ist das Feld **Zugeordnete ID-Tags** zugänglich. +Hier müssen die ID-Tags eingetragen werden, welche ausschliesslich zur Entsperrung des Ladepunktes verwendet werden. Sind mehrere Ladepunkte vorhanden (z.B. Duo oder mehrere ferngesteuerte openWBs) kann für jeden Ladepunkt ein eigenes Ladepunkt-Profil angelegt werden, wobei hier jeweils eine eigene ID-Kennung zur Freischaltung hinterlegbar ist. + +Achtung: Ladepunkt-Profile müssen den Ladepunkten unter Ladepunkte zugeordnet werden! + +##### Anmerkung: Bei allen Anpassungen der Einstellungen Speichern nicht vergessen! + +Zu **A. Sperre nach Abstecken** ist folgendes zu konfigurieren: +Die Option **Sperre nach Abstecken** ist im **Ladepunkt-Profil** bei Ladepunkte auswählbar und bewirkt, dass der Ladepunkt nach Abstecken eines Fahrzeugs gesperrt wird. Es gibt hier dann folgende zwei Möglichkeiten diesen Ladepunkt wieder zu entsperren: +1. die Option *Ladepunkt sperren* händisch im User Interface auf Nein setzen. Dadurch startet nach Anstecken eines Fahrzeugs das voreingestellte Fahrzeug den Ladevorgang. +2. ein ID-Tag vorhalten, der im Ladepunkt-Profil hinterlegt ist. Dadurch wird der Ladepunkt automatisch entsperrt und das voreingestellte Fahrzeug startet den Ladevorgang. Ist der ID-Tag, welcher im Ladepunkt-Profil hinterlegt wurde identisch mit einem ID-Tag, der einem Fahrzeug zugeordnet ist, dann findet hier auch direkt eine Zuordnung zu einem Fahrzeug statt. Dabei wird der Ladepunkt entsperrt und das dem ID-Tag zugeordnete Fahrzeug startet den Ladevorgang. +Wurden mehrere Fahrzeuge mit demselben ID-Tag angelegt, dann startet das Fahrzeug den Ladevorgang, welches auf der Liste der Fahrzeuge dem ID-Tag zuerst zugeordnet wurde. +Solange der Ladepunkt gesperrt ist, wird kein gültiger ausschliesslich einem Fahrzeug zugeordneter ID-Tag akzeptiert. +Nach Starten eines Ladevorgangs wird kein neuer ID-Tag akzeptiert. + +Anmerkung: Im Fall, dass zwei oder mehrere Fahrzeuge mit unterschiedlichen ID-Tags an demselben (gesperrten) Ladepunkt laden dürfen, müssen beide ID-Tags der Fahrzeuge auch im Ladepunkt-Profil hinterlegt werden. Dadurch wird erstens der Ladepunkt entsperrt und zweitens das dem Fahrzeug zugeordnete Lade-Profil ausgewählt, wodurch dann das damit verknüpfte Fahrzeug mit den dort hinterlegten Ladeeinstellungen den Ladevorgang startet. + +Zu **B. Standard nach Abstecken** ist folgendes zu konfigurieren: +Die Option **Standard nach Abstecken** ist im **Lade-Profil** bei Fahrzeuge auswählbar. Diese Option macht nur Sinn, wenn neben dem Standard-Lade-Profil mindestens ein weiteres Lade-Profil und mindestens ein weiteres Fahrzeug angelegt wurde. Dabei ist dem Standard-Fahrzeug das Standard-Lade-Profil und dem weiteren Fahrzeug das weitere Lade-Profil zuzuweisen. +Um diese Option sinnvoll zu nutzen, muss im Standard-Lade-Profil unter Allgemeine Optionen der aktive Lademodus auf **Stop** gestellt werden. +Weiterhin muss in einem anderen zu nutzenden Lade-Profil unter Allgemeine Optionen **Standard nach Abstecken** aktiviert werden. + +Insofern der Ladepunkt nicht gesperrt wurde, kann über einen ID-Tag der Ladevorgang für ein dem ID-Tag zugeordnetes Fahrzeug (entweder Standardfahrzeug oder ein anderes Fahrzeug mit einem Lade-Profil verschieden vom Standard-Lade-Profil) gestartet werden. Nach Abstecken wechselt die Auswahl dann auf Standardfahrzeug in den Lademodus **Stop** und der Ladepunkt startet keinen weiteren Ladevorgang bis die Auswahl entweder händisch über das User Interface oder automatisch per ID-Tag geändert wird. + +Startet ein Fahrzeug über einen ID-Tag den Ladevorgang und ist in diesem Fahrzeug Standard nach Abstecken aktiviert, dann wird nach Abstecken auf das Standardfahrzeug gewechselt, unabhängig davon, welches Fahrzeug vorher ausgewählt war. +Startet ein Fahrzeug über einen ID-Tag den Ladevorgang und ist in diesem Fahrzeug Standard nach Abstecken nicht aktiviert, dann wird nach Abstecken auf das letzte vorher ausgewählte Fahrzeug gewechselt. + +### Use Cases: + +#### Sperre nach Abstecken +Sperre nach Abstecken kann an einem Ladepunkt verwendet werden, welcher das Laden gegenüber fremden Zugriff sichert. Wird der ID-Tag nur zum Sperren/Entsperren des Ladepunktes verwendet, dann startet immer das ausgewählte Fahrzeug den Ladevorgang. Dies kann im privaten Bereich mit nur einem Fahrzeug sinnvoll sein, damit nur dieses Fahrzeug auch laden darf. Die Option ist aber auch für Ladeparks sinnvoll, bei denen die Ladepunkte nur für eine Gruppe von ID-Tags freischaltbar sind und dem ID-Tag zum Entsperren auch gleichzeitig zugeordnet sind. + +#### Standard nach Abstecken: +Standard nach Abstecken kann an einem Ladepunkt verwendet werden, welcher das Laden mehrere verschiedener Fahrzeuge ermöglichen soll. Werden mehrere Fahrzeuge mit verschiedenen Lade-Profilen und verschiedenen ID-Kennungen neben dem Standard-Fahrzeug angelegt, kann über die ID-Kennung zwischen den einzelnen Fahrzeugen gewechselt werden. Hier bietet sich beispielsweise ein privater Ladepunkt mit zwei Fahrzeugen an oder ein Ladepunkt in einer Firma mit verschiedenen Mitarbeitern. + +Standard nach Abstecken kann auch dazu verwendet werden, um beispielsweise zwischen zwei Fahrzeugen (und damit Fahrzeug-Profilen und Lade-Profilen) ohne ID-Tag zu wechseln, vor allem wenn nur eines der Fahrzeuge über die ID-Kennung zuverlässig erkannt wird. + +Stand 02. Juli 2024 \ No newline at end of file diff --git a/docs/MQTT.md b/docs/MQTT.md new file mode 100644 index 0000000000..5681b86447 --- /dev/null +++ b/docs/MQTT.md @@ -0,0 +1,71 @@ +# MQTT + +## Grundsätzliches +MQTT bedeutet: Message Queuing Telemetry Transport. Es handelt sich hierbei um ein M2M (Machine to Machine) Protokoll. +Für eine Kommunikation wird ein Broker (=Verwalter) benötigt, welcher die Nachrichten von den Sendern empfängt und an die Empfänger, welche sich für den Inhalt angemeldet haben, weiterleitet. Man spricht bei MQTT von publish und subscribe. Die Nachrichten werden in topics verschickt. + +openWB hat einen eigenen MQTT-Broker integriert, über den die Kommunikation läuft. Möchte man die Wallbox steuern oder Status-Nachrichten empfangen, sollte man sich als Client an diesem Broker anmelden. Der Broker läuft auf der IP der openWB unter Port 1883 ohne Nutzerauthentifizierung. + +## Zähler + +Als EVU-Zähler können auch Werte über MQTT empfangen werden. Die Integration ist im Abschnitt [Zähler](https://github.com/openWB/core/wiki/Zähler) beschrieben. + +## Smarthome + + +## Steuerbefehle + +Auf eigene Gefahr! Die folgenden Einstellungen und Kommunikationsmöglichkeiten sind nicht spezifiziert und nur von kundigen Nutzern mit entpsrechendem Fachwissen über die Konsequenzen durchzuführen. + +Bei den Steuerbefehlen ist # immer durch den entsprechenden Ladepunkt/Zähler/Fahrzeug zu ersetzen. Dies ist zwingend zu beachten, da ansonsten neue Fahrzeuge/Zähler etc. erstellt werden, wenn es nicht die ID eines Konfigurierten Gerätes ist. + +Lademodus auf "Sofortladen" +`openWB/set/vehicle/template/charge_template/#/chargemode/selected -> instant_charging` + +PV-Laden +`openWB/set/vehicle/template/charge_template/#/chargemode/selected -> pv_charging` + +"Minimal Stromstärke" im PV-Laden auf z.B. 6A +`openWB/set/vehicle/template/charge_template/#/chargemode/pv_charging/min_current -> 6` + +SoC-Limit auf z.B. 80% setzen +`openWB/set/vehicle/template/charge_template/#/chargemode/pv_charging/max_soc -> 80` + +Zielladen +`openWB/set/vehicle/template/charge_template/#/chargemode/selected -> scheduled_charging` + +Standby +`openWB/set/vehicle/template/charge_template/#/chargemode/selected -> standby` + +Stop +`openWB/set/vehicle/template/charge_template/#/chargemode/selected -> stop` + +_Work in Progress_ + +## Statusnachrichten + +Wo wird welcher nützliche Inhalt gesendet. + +Ladeprofil Status (verschachteltes JSON, muss entsprechend weiter decodiert werden...): +openWB/vehicle/template/charge_template/1 + +Setzen von min_Current für min+PV nachbauen: +`openWB/set/vehicle/template/charge_template/#/chargemode/pv_charging/min_current` + +Setzen des Lademodus: (Werte die zu senden sind: instant_charging, pv_charging, scheduled_charging, standby, stop) +`openWB/set/vehicle/template/charge_template/#/chargemode/selected` + +Ladepunkt sperren für Priosteuerung der LP: +`openWB/set/chargepoint/#/set/manual_lock` + +SoC Update triggern: +`openWB/set/vehicle/1#/get/force_soc_update` + +SoC im manuellen Modus setzen: +`openWB/set/vehicle/#/soc_module/calculated_soc_state/manual_soc` + +### Lademodus +Lademodus des angesteckten Auto wird in den LP geschrieben. Solange immer dasselbe Auto dran steckt ist das gleich, aber wenn Du ein anderes Auto ansteckst, bei mir z.b. ein Gastauto und Du nur den Lademodus deines normalen Auto ausliest und damit steuerst, ist der dortige Lademodus halt dann nicht der eigentliche des LP + + +_Work in Progress_ \ No newline at end of file diff --git "a/docs/Neues Ger\303\244t programmieren.md" "b/docs/Neues Ger\303\244t programmieren.md" deleted file mode 100644 index 777dcc8a70..0000000000 --- "a/docs/Neues Ger\303\244t programmieren.md" +++ /dev/null @@ -1,23 +0,0 @@ -Um die Programmierung neuer Geräte zu erleichtern, findet Ihr unter [docs/samples](https://github.com/openWB/core/tree/master/docs/samples?v30-12-2022) drei Muster: - -1. sample_modbus: Für Geräte, die per Modbus abgefragt werden. Dazu wird im Gerät ein Modbus-Client instanziiert, der dann an die Komponenten übergeben wird. -2. sample_request_per_component: Für Geräte, die per Http-Request abgefragt werden (lokal oder übers Internet) und bei denen jede Komponente eine eigene URL hat. -3. sample_request_per_device: Für Geräte, die per Http-Request abgefragt werden (lokal oder übers Internet) und bei denen alle Daten über eine URL abgefragt und dann je Komponente aus der Antwort geparst werden müssen. - -Die Muster sind nur als einheitlicher Ausgangspunkt zu verstehen! Es kann durchaus notwendig sein, Elemente der verschiedenen Muster zu kombinieren, weitere Einstellungs-Parameter hinzuzufügen oder bei einem Http-Request eine Authentifizierung durchzuführen. - -Nachdem Ihr das Muster, das am besten zu Eurem Gerät passt, ausgewählt habt, kopiert Ihr dieses in den _packages/modules/devies/\*Gerätename\*_-Ordner. Ordnername und Typ in config.py->Sample->type müssen identisch sein, damit das Gerät in der automatisch generierten Auswahlliste im UI angezeigt wird. -Wenn das Gerät nicht alle Komponenten unterstützt, löscht Ihr die nicht unterstützten Komponenten und die Referenzen darauf in config.py und device.py. -Wenn von der Komponente die Zählerstände für Import und Export gelesen werden können, können die Zeilen für simcount entfernt werden. - -Bei Hybrid-Systemen erfolgt die Verrechnung von Speicher-und PV-Leistung automatisiert, wenn Speicher und Wechselrichter in der Hierarchie wie [hier](https://github.com/openWB/core/wiki/Hybrid-System-aus-Wechselrichter-und-Speicher) beschrieben angeordnet sind. Wenn noch weitere spezifische Berechnungen erforderlich sind, müsst Ihr die Komponenten wie unter sample_request_per_device abfragen. Die update-Methode der Komponenten wird dann in eine get- und set-Methode aufgeteilt. Die get-Methode liefert den Component-State zurück, dieser wird in der update_components-Methode des Geräts verrechnet und dann die set-Methode der Komponente aufgerufen, die die store-Methode der Komponente aufruft. - -Das Speichern, Runden, Loggen und eine Plausibilitätsprüfung der Werte erfolgt zentral und muss daher nicht in jedem Modul implementiert werden. - -Wenn keine Einstellungsseiten in vue hinterlegt sind, sind die Einstellungen als json-Objekt editierbar. - -### Kompatibilität mit 1.9 - -Damit das Modul auch unter 1.9 lauffähig ist, müssen -wie bisher- die unter [docs/legacy](https://github.com/openWB/core/tree/master/docs/samples/legacy?v30-12-2022) angegebenen Ordner erstellt werden und das Modul im UI hinzugefügt werden. Außerdem muss in der device.py die read_legacy-Funktion so implementiert werden, dass anhand des übergebenen Komponenten-Typs das Update der entsprechenden Komponente getriggert wird. Beim zentralen Speichern der ausgelesenen Werte wird automatisch erkannt, ob diese in die ramdisk (1.9) oder in den Broker (2.x) geschrieben werden müssen. - -_Bei Fragen programmiert Ihr das Gerät vorerst, wie Ihr es versteht, und erstellt einen (Draft-)PR. Wir unterstützen Euch gerne per Review. diff --git a/docs/Neues Modul programmieren.md b/docs/Neues Modul programmieren.md new file mode 100644 index 0000000000..b856463bdc --- /dev/null +++ b/docs/Neues Modul programmieren.md @@ -0,0 +1,39 @@ +Um die Programmierung neuer Module zu erleichtern, findet Ihr unter [docs/samples](https://github.com/openWB/core/tree/master/docs/samples?v30-12-2022) Muster für neue Geräte, Fahrzeuge, Cloud-Sicherung und Stromanbieter für strompreisbasiertes Laden. +Die Muster sind nur als einheitlicher Ausgangspunkt zu verstehen! Es kann durchaus notwendig sein, Elemente der verschiedenen Muster zu kombinieren, weitere Einstellungs-Parameter hinzuzufügen oder bei einem Http-Request eine Authentifizierung durchzuführen. +Das Muster kopiert Ihr in den _packages/modules/\*Modul-Typ\*/\*Modul-Name\*_-Ordner. Ordnername und Typ in config.py->Sample->type müssen identisch sein, damit das Gerät in der automatisch generierten Auswahlliste im UI angezeigt wird. + +Wenn keine Einstellungsseiten in vue hinterlegt sind, sind die Einstellungen als json-Objekt editierbar. Muster für die Einstellungsseiten findet Ihr im Ordner [samples/samples_gui](https://github.com/openWB/core/tree/02b34ff216b0dfc83fdc56a53b63d52d5d9a79d2/docs/samples/samples_gui) + +Außer der modulspezifischen Abfrage erfolgt alles weitere zentral und muss daher nicht in jedem Modul implementiert werden: +* Speichern, Runden, Loggen und eine Plausibilitätsprüfung der Werte +* Prüfung, ob das Intervall zB zur SoC- oder Preis-Abfrage abgelaufen ist +* Behandlung von Exceptions + +Exceptions dürfen daher nur abgefangen werden, wenn sie +* behoben werden können. +* weitere Aktionen vorgenommen werden sollen. Danach mit `raise e` die Exception erneut werfen, damit sie weiterverarbeitet werden kann. + +Bei Modulen, die einen http-Request ausführen, get/post-Requests immer mit `req.get_http_session().get/post()` aus dem Ordner modules/common stellen. [get_http_session](https://github.com/openWB/core/blob/02b34ff216b0dfc83fdc56a53b63d52d5d9a79d2/packages/modules/common/req.py#L8) loggt die Antwort und prüft in einem Callback, ob ein Fehler aufgetreten ist und wirft eine Exception. Bei gängigen Fehlern wird diese in einen Text übersetzt, der auch für den Benutzer verständlich ist. + +Ein paar Hintergrund-Details, wie die Fehlerbehandlung umgesetzt ist: +Die update-Methode des Moduls wird immer mit dem [Kontextmanager](https://github.com/openWB/core/blob/02b34ff216b0dfc83fdc56a53b63d52d5d9a79d2/packages/modules/common/component_context.py#L11) aufgerufen. Dieser prüft nach dem Ende der Update-Methode, ob eine Exception aufgetreten ist und loggt diese und setzt die Topics `.../get/fault_state/` auf 2 und in `.../get/fault_str` den Text der Exception. fault_str wird dann im jeweiligen Modul auf der Status-Seite ausgegeben, um dem Benutzer eine Rückmeldung zu geben. + +### Neues Gerät programmieren +Für neue Geräte gibt es drei Muster: + +1. sample_modbus: Für Geräte, die per Modbus abgefragt werden. Dazu wird im Gerät ein Modbus-Client instanziiert, der dann an die Komponenten übergeben wird. +2. sample_request_per_component: Für Geräte, die per Http-Request abgefragt werden (lokal oder übers Internet) und bei denen jede Komponente eine eigene URL hat. +3. sample_request_per_device: Für Geräte, die per Http-Request abgefragt werden (lokal oder übers Internet) und bei denen alle Daten über eine URL abgefragt und dann je Komponente aus der Antwort geparst werden müssen. + +Wenn das Gerät nicht alle Komponenten unterstützt, löscht Ihr die nicht unterstützten Komponenten und die Referenzen darauf in config.py und device.py. +Wenn von der Komponente die Zählerstände für Import und Export gelesen werden können, können die Zeilen für simcount entfernt werden. + +Bei Hybrid-Systemen erfolgt die Verrechnung von Speicher-und PV-Leistung automatisiert, wenn Speicher und Wechselrichter in der Hierarchie wie [hier](https://github.com/openWB/core/wiki/Hybrid-System-aus-Wechselrichter-und-Speicher) beschrieben angeordnet sind. Wenn noch weitere spezifische Berechnungen erforderlich sind, müsst Ihr die Komponenten wie unter sample_request_per_device abfragen. Die update-Methode der Komponenten wird dann in eine get- und set-Methode aufgeteilt. Die get-Methode liefert den Component-State zurück, dieser wird in der update_components-Methode des Geräts verrechnet und dann die set-Methode der Komponente aufgerufen, die die store-Methode der Komponente aufruft. + +### Neues Fahrzeug programmieren +Beim Aufruf der _updater_-Funktion wird die Variable _vehicle_update_data_ übergeben. Darin sind aktuelle Daten aus der Regelung, wie zB Stecker-Status oder die geladene Energie seit Anstecken, enthalten, um Besonderheiten wie zB das Aufwecken des Fahrzeugs oder eine manuelle Berechnung während des Ladevorgangs umsetzen zu können. +Bei manchen Fahrzeugen kann der SoC nicht während der Ladung abgefragt werden. Damit dieser während der Ladung berechnet wird, muss in der soc.py bei der Instanziierung von `ConfigurableVehicle` der Parameter `calc_while_charging` auf `True` gesetzt werden. + +Nach dreimaliger fehlgeschlagener Abfrage wird der SoC auf 0% gesetzt, damit in jedem Fall geladen wird. + +_Bei Fragen programmiert Ihr das SoC-Modul vorerst, wie Ihr es versteht, und erstellt einen (Draft-)PR. Wir unterstützen Euch gerne per Review. diff --git a/docs/Neues Soc-Modul programmieren.md b/docs/Neues Soc-Modul programmieren.md deleted file mode 100644 index fc3dc74a30..0000000000 --- a/docs/Neues Soc-Modul programmieren.md +++ /dev/null @@ -1,12 +0,0 @@ -Um die Programmierung neuer SoC-Module zu erleichtern, findet Ihr unter [docs/samples](https://github.com/openWB/core/tree/master/docs/samples?v30-12-2022) ein Muster _sample_vehicle_. - -Die Muster sind nur als einheitlicher Ausgangspunkt zu verstehen! Es kann durchaus notwendig sein, weitere Einstellungs-Parameter hinzuzufügen oder bei einem Http-Request eine Authentifizierung durchzuführen. Beim Aufruf der _updater_-Funktion wird die Variable _vehicle_update_data_ übergeben. Darin sind aktuelle Daten aus der Regelung, wie zB Stecker-Status oder die geladene Energie seit Anstecken, enthalten, um Besonderheiten wie zB das Aufwecken des Fahrzeugs oder eine manuelle Berechnung während des Ladevorgangs umsetzen zu können. - -Das Muster kopiert Ihr in den _packages/modules/vehicles/\*Name\*_-Ordner. Ordnername und Typ in config.py->Sample->type müssen identisch sein, damit das Gerät in der automatisch generierten Auswahlliste im UI angezeigt wird. -Bei manchen Fahrzeugen kann der SoC nicht während der Ladung abgefragt werden. Damit dieser während der Ladung berechnet wird, muss in der soc.py bei der Instanziierung von `ConfigurableVehicle` der Parameter `calc_while_charging` auf `True` gesetzt werden. - -Das Speichern, Runden, Loggen und eine Plausibilitätsprüfung der Werte sowie die Prüfung, ob das Intervall zu SoC-Abfrage abgelaufen ist, erfolgt zentral und muss daher nicht in jedem Modul implementiert werden. - -Wenn keine Einstellungsseite in vue für das SoC-Modul hinterlegt ist, sind die Einstellungen als json-Objekt editierbar. - -_Bei Fragen programmiert Ihr das SoC-Modul vorerst, wie Ihr es versteht, und erstellt einen (Draft-)PR. Wir unterstützen Euch gerne per Review. diff --git a/docs/Tutorial b/docs/Tutorial new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/Typische-Anwendungsfaelle.md b/docs/Typische-Anwendungsfaelle.md new file mode 100644 index 0000000000..e24483be49 --- /dev/null +++ b/docs/Typische-Anwendungsfaelle.md @@ -0,0 +1,29 @@ +#Typische Anwendungsfälle +## Privater Haushalt, ein E-Auto und PV-Anlage +In diesem Szenario sind die Ziele meistens, das Auto morgens für den Weg zur Arbeit fahrbereit zu haben, aber bis dahin möglichst viel Energie aus der PV-Anlage zum Laden zu nutzen. +Hierfür ist die Funktion *Zielladen* zu nutzen. Auch, wenn es vom Namen her scheint, dass nur zu einem festen Zeitpunkt eine definierte Energiemenge in das EV geladen sein soll, wird dennoch bis zum Beginn dieses erzwingenen Ladevorgangs PV-Energie, sofern vorhanden, genutzt. + +![Zielladen](pictures/Anwendungsfaelle_zielladen.jpg) + +Einstellbar ist der Ziel SoC, der in vielen Fällen auf 80% eingestellt wird, da eine höhere Ladung den Akku des EV überproportional belastet. Dennoch sollte ab und zu der Akku auf 100% geladen und dort über kurze Zeit gehalten werden, damit die Zellen sich wieder balancieren können. +In dem oben gezeigten Beispiel ist der Ladestrom mit 13A eingestellt. Somit bleibt bei einem 3-phasigen 11kW-Lader noch Reserve, um die Stromstärke kurz vor Ende ggf. noch zu erhöhen. + +Als Zielzeit ist die Abfahrtzeit Abfahrt einzustellen. Die Regelung berechnet aus dem aktuellen SoC des Fahrzeugs, sowie aus den zwingend korrekt anzugebenden Maximalwerten der Ladeströme im Ladeprofil, den Zeitpunkt, an dem die Ladung starten muss. +Es empfielt sich den Ladestrom im Ladeprofil unter Zielladen etwas niedriger als die Möglichkeiten der Wallbox und des Fahrzeugs anzugeben, damit etwas Puffer vorhanden ist, falls das Auto zu spät angesteckt worden ist. + +Falls das EV durch eine Standheizung vor Fahrtbeginn vorgeheizt werden soll, kann hierfür ein Zeitslot mit _Laden nach Zeitplan_ konfiguriert werden. Zeitladen kann zusätzlich zum gewählten Lademodus, hier Zielladen, aktiviert werden. So wird der Akku durch die Standheizung nicht belastet, sondern der Strom kommt aus dem EVU-Netz. Hier kann dann ein minimaler Strom von z.B. 6A gewählt werden, da die Leistungsaufnahme für die Heizung meist nicht mehr als 1kW benötigt. + +###Außerplanmäßige Fahrt +Wird das Fahrzeug außer der Reihe benötigt und es soll kurzfristig viel Energie in den Akku geladen werden, ist die openWB auf der Startseite auf *Sofortladen* zu stellen. Hier ist es möglich, mit der maximal verfügbaren Leistung den Akku so schnell wie möglich aufzuladen. + +### Ausnutzen der PV-Anlage bei wechseldem Wetter +Insbesondere im Frühling und Herbst kann die PV-Leistung bei bewölktem Himmel stark schwanken. Um ein häufiges Beenden und Starten des Ladevorgangs zu vermeiden, kann bei dem Modus PV ein *Minimaler Dauerstrom* eingestellt werden. +Ist die Einstellung auf 0A, so wird ausschließlich mit solarem Überschuss geladen. Steigt die Netzeinspeisung über den in _Konfiguration->Ladeeinstellungen ->PV-Laden_ eingestellten Grenzwert, wird nach Ablauf der Einschaltverzögerung die Ladung gestartet. Mit der nächstgrößeren Einstellmöglichkeit 6A, wird (z.B. bei einphasigem Laden) kontinuierlich mit ~1,3kW geladen. Wird ins Netz eingespeist, wird die Ladeleistung hochgeregelt, sodass möglichst weder Strom eingespeist noch bezogen wird und, je nach Möglichkeiten des Fahrzeugs, auf 3-phasiges Laden umgeschaltet. + +![PV-Min](pictures/Anwendungsfaelle_minStrom.jpg) + +##Integration in Hausautomation +openWB eignet sich hervorragend zur Integration in eine bestehende Hausautomatins-Infrastruktur, da über den integrierten MQTT-Broker Befehle sowie Statusmeldungen ausgetauscht werden können. +### MQTT +Zum Debugging empfiehlt sich das Programm [MQTT-Explorer?](http://mqtt-explorer.com/). +Eine detailierte Erklärung ist auf der [MQTT-Seite](https://github.com/openWB/core/wiki/MQTT) zu finden. \ No newline at end of file diff --git a/docs/Wiki-Eintrag erstellen.md b/docs/Wiki-Eintrag erstellen.md index 7bec36028a..af529a2e6e 100644 --- a/docs/Wiki-Eintrag erstellen.md +++ b/docs/Wiki-Eintrag erstellen.md @@ -6,10 +6,12 @@ Der Name der Markdown-Datei ist der Titel der Wiki-Seite. Die Datei _Sidebar.md Wenn ihr euch am Wiki beteiligen wollt müsst ihr zunächst einen Github Account erstellen bzw. euch mit eurem anmelden. Dann geht ihr auf die [Projektseite](https://github.com/openWB/core) und erstellt einen Fork: -![Fork](pictures/Wiki-Eintrag erstellen_Fork.png) +![Fork](pictures/Wiki-Eintrag_erstellen_Fork.png) Dies ist nötig, da dem "normalen Mitarbeiter" das Projekt nicht gehört und man somit keine Schreibrechte im Projekt des OpenWB Accounts hat. Man erstellt also eine verknüfpte Kopie in seinem eigenen Account. Hier wird dann am besten ein Branch erstellt, den ihr sinnvoll benennt (z.B. Wiki oder ähnlich). In diesem Branch arbeitet ihr und ändert und ergänzt entsprechend euren Erfahrungen zu den Themen in denen ihr euch auskennt. Danach müsst ihr Änderungen mit *Commit* in die (lokale) Git-Umgebung übernehmen und mit *Push* zu Github übertragen. Dies beginnt ihr in eurem eigenen Branch und wählt im Menü oben Pull-Request aus und füllt die Felder mit einer Beschreibung was ihr gemacht habt. -![Pull](pictures/Wiki-Eintrag erstellen_Pull.png) -Für euren ersten Beitrag müsst ihr noch von einem Projektmitarbeiter freigeschaltet werden. Dies kann einige Zeit dauern. + +![Pull-Request](pictures/Wiki-Eintrag_erstellen_Pull.jpg) + +Für euren ersten Beitrag müsst ihr noch von einem Projektmitarbeiter freigeschaltet werden. Dies kann einige Zeit dauern. Eventuell werden auch noch Änderungen vorgeschlagen, die ihr dann diskutieren oder einfach annehmen könnt. diff --git a/docs/Zaehler.md b/docs/Zaehler.md new file mode 100644 index 0000000000..1abb80e036 --- /dev/null +++ b/docs/Zaehler.md @@ -0,0 +1,91 @@ + +# Zähler + +openWB benötigt zum erfolgreichen PV-Überschussladen die entsprechenden Zählerwerte am EVU-Punkt (EVU=Elektrizitätsversorgungsunternehmen), sprich dem Übergang ins öffentliche Netz. An dieser Stelle muss die Gesamtleistung saldierend erfasst werden. Für eine phasenbasierte Leistungsüberwachung sind auch die einzelnen Ströme und/oder Leistungen der drei Phasen notwendig. Bei einem Zähler im Hausverbrauchs-Zweig muss die Konfiguration wie [hier](https://github.com/openWB/core/wiki/Hausverbrauchs-Zähler) beschrieben erfolgen. + +Im einfachsten Fall geschieht dies durch Kauf und Einbau eines [EVU-Kits](##EVU-Kit). Sollten schon digital auslesbare Zähler vorhanden sein, so besteht die Möglichkeit diese Werte an openWB weiterzuleiten, auch mit Hilfe von Hausautomationsservern. + +Es gibt viele verschiedene Möglichkeiten, Zähler als auch Wechselrichter in das openWB-System einzufügen. Die Struktur der Zähler muss dann im [Lastmanagement](https://github.com/openWB/core/wiki/Lastmanagement-und-kaskadierte-Zähler) dem System bekanntgegeben werden. Hier können auch [virtuelle Zähler](##Virtuelle Zähler) hinzugefügt werden, welche openWB-intern die untergeordneten Zähler verrechnen. + + +## EVU-Kit + +Das [EVU-Kit (Link zum Shop)](https://openwb.de/shop/?product=openwb-evu-kit) ist die einfachste Art in der Software einen Zähler an die openWB zu integrieren. Der Zähler muss von einem Elektriker direkt hinter dem Zähler des EVU in den Zählerschrank integriert werden. Es gibt, je nach Kapazität des Hausanschlusses, verschiedene Messvarianten des Zählers, welche sich von der Integration unterscheiden. Dies betrifft aber nur die Arbeit des Elektrikers. +Der Zähler kommuniziert mit der openWB über Ethernet. Die Kits sind so vorkonfiguriert, dass sie von der openWB Software automatisch gefunden werden, wenn ein entsprechendes Gerät mit Zählerkomponente angelegt wird. Es gibt auch für Wechselrichter und Speicher entsprechende Kits. + +![EVU-Kit](pictures/EVU-PV-Speicher-Kit-689x1024.png) + +## MQTT + +openWB hat einen MQTT-Broker integriert, welcher unter Port 1883 (ohne Verschlüsselung) und Port 8883 (mit Verschlüsselung) erreichbar ist. Benutzerauthentifizierung ist deaktiviert und auch nicht aktivierbar. Ein Zähler, welcher die benötigten Daten liefert muss sich mit diesem Broker verbinden und dort die Werte unter den entsprechenden Topics publishen. + +Folgende Werte können dem MQTT-Zähler übergeben werden. Die ID ist individuell und wird beim Anlegen der MQTT-Komponente angezeigt. +Die folgenden Topics sind für einen reibungslosen Betrieb unbedingt erforderlich: +- **openWB/set/counter/id/get/power** + - **Beschreibung**: Bezugsleistung in Watt, Zahl mit oder ohne Nachkommastellen (Float, Integer) und einem Punkt als Dezimaltrennzeichen, positiv für Bezug, negativ für Einspeisung. + - **Beispiel**: `-123.45` + +- **openWB/set/counter/id/get/imported** + - **Beschreibung**: Bezogene Energie in Wh, Zahl mit oder ohne Nachkommastellen (Float, Integer) und einem Punkt als Dezimaltrennzeichen, nur positiv. + - **Beispiel**: `123.45` + +- **openWB/set/counter/id/get/exported** + - **Beschreibung**: Eingespeiste Energie in Wh, Zahl mit oder ohne Nachkommastellen (Float, Integer) und einem Punkt als Dezimaltrennzeichen, nur positiv. + - **Beispiel**: `123.45` + +Ströme je Phase sind für phasenbasiertes Lastmanagement unbedingt erforderlich, sonst erfolgt das Lastmanagement ausschließlich auf Basis der Gesamtleistung am EVU-Punkt: +- **openWB/set/counter/id/get/currents** + - **Beschreibung**: Array mit den Strömen je Phase in Ampere, mit Nachkommastellen (Float), positiv für Bezug, negativ für Einspeisung. + - **Beispiel**: `[1.2,2.3,-2.1]` + +Die Netzfrequenz, Spannungen, Leistungen und Leistungsfaktoren jeder Phase werden ausschließlich zu Anzeigezwecken verwendet: +- **openWB/set/counter/id/get/frequency** + - **Beschreibung**: Netzfrequenz in Hz, Zahl mit oder ohne Nachkommastellen (Float, Integer) und einem Punkt als Dezimaltrennzeichen. + - **Beispiel**: `50.12` + +- **openWB/set/counter/id/get/voltages** + - **Beschreibung**: Array mit den Spannungen je Phase in Volt, mit Nachkommastellen (Float). + - **Beispiel**: `[222.2,223.3,222.3]` + +- **openWB/set/counter/id/get/powers** + - **Beschreibung**: Array mit den Leistungen je Phase in Watt, mit Nachkommastellen (Float). + - **Beispiel**: `[12.3,23.4,-12.3]` + +- **openWB/set/counter/id/get/power_factors** + - **Beschreibung**: Array mit den Leistungsfaktoren je Phase, mit Nachkommastellen (Float), Wertebereich -1 bis 1. + - **Beispiel**: `[0.95,0.96,-0.95]` + +## Huawei Wechselrichter mit DTSU666-H 250A und SDongle + +Huawei Wechselrichter werden, in der Betriebsart mit Aufzeichnung des Hausverbraucht mit dem _DTSU666-H 250A_ Stromzähler direkt am EVU-Punkt betrieben. Die Kommunikation zwischen Zähler und Wechselrichter findet über RS485 statt. Sofern der Wechselrichter mit dem optionalen SmartDongle FE ausgestattet ist, können über diesen Daten des Wechselrichter ausgelesen werden. +Die Schnittstelle am Dongle ist Modbus-TCP. Dies muss mit Installer-Account am Wechselrichter auf "Unrestrictet" gestellt werden, damit die Daten extern abgerufen werden können. + +Der Huawei-Wechselrichter kann direkt über die openWB ausgelesen werden. +Eine weitere Möglichkeit des Datenabrufs wird im [openWB-Forum](https://openwb.de/forum/viewtopic.php?t=7029) entwickelt und ist auf [Github](https://github.com/AlexanderMetzger/huawei_openwb_bridge) sowie der [Homepage des Entwicklers](https://lebensraum-wohnraum.de/openwb-kommunikation-mit-dem-huawei-wechselrichter-sun-2000/) zu finden. Hierbei wird das Image auf die SD-karte eines Raspberry-Zero gespiegelt und der Raspberry mit dem Config-WLAN des Wechselrichters verbunden. Die Skripte ziehen sich die entsprechenden Werte in Echtzeit vom Wechselrichter und publishen diese auf die [MQTT](#MQTT) Schnittstelle der openWB. Der Zähler in der openWB muss dementsprechend als MQTT-Zähler eingerichtet sein. + +### Solaranzeige + +[Solaranzeige](https://solaranzeige.de) ist ebenfalls ein OpenSource Projekt, welches der Visualisierung, Speicherung und Weiterverarbeitung von PV-Daten dient. +Dieses Projekt unterstützt aktuell (Stand 2024-02) mehr Wechselrichter als openWB. Somit können hier mit etwas Aufwand existierende Wechselrichter eingebunden und die Daten weitergereicht werden, ohne Arbeiten am Zählerschrank durchzuführen zu lassen. +Die Software ist originär dafür vorgesehen auf einen Raspberry per Image installiert zu werden und nach wenigen Konfigurationsschritten lauffähig zu sein. Es gibt auch schon Portierungen für [Docker](https://github.com/DeBaschdi/docker.solaranzeige). +Solaranzeige kann mit vielen Wechselrichtern kommunizieren und auch teilweise die angeschlossenen Zähler auslesen. Eine zeitbasierte Datenbank (InfluxDb), Datenweitergabe über einen MQTT-Client sowie eine Visualisierung mit Grafana sind direkt integriert. Es kann aber auch bereits existierende Infrastruktur verwendet werden. +In dem Projekt wird (mit Stand von 2021) auch die Möglichkeit dokumentiert die Daten direkt an openWB weiterzuleiten. Dann kann jedoch kein weiterer MQTT-Broker bedient werden. +Alternativ können die Zählerwerte an eine Hausautomationsserver weitergegeben, dort ggf. vorzeichenkorrigiert und dann über einen zweiten MQTT-Client zur openWB geschickt werden. + +## Virtuelle Zähler + +Virtuelle Zähler sind, wie der Name schon sagt, nicht physikalisch vorhanden. Sie dienen in der Struktur des [Lastmanagement](https://github.com/openWB/core/wiki/Lastmanagement-und-kaskadierte-Zähler) dazu, Ströme und/oder Leistung zu begrenzen oder Werte aus untergeordneten einzelnen Zählern zu akkumulieren. + +## Timing +Das zyklische Senden bzw. Bereitstellen der Zählerwerte ist für eine funktionierende Regelung essentiell. Unter + +> Einstellungen - Allgemein - Hardware + +kann die Regelgeschwindigkeit ausgewählt werden. Es gibt die Intervalle: + - Normal + - Langsam (20s) + - Sehr langsam (60s) + +Hier muss eine Regelgeschwindigkeit entsprechend der Aktualisierungsrate der Zählerwerte angegeben werden, da ansonsten die Rückmeldung des Systems zu spät kommt und die openWB weiter versucht nachzuregeln. +Insbesondere bei der Verwendung von [Solaranzeige](### Solaranzeige) ist aufgrund der 20-30s dauerenden Aktualisierungsrate die Regelungsgeschwindigkeit anzupassen. + diff --git "a/docs/Z\303\244hler.md" "b/docs/Z\303\244hler.md" deleted file mode 100644 index c1fdeef89a..0000000000 --- "a/docs/Z\303\244hler.md" +++ /dev/null @@ -1,93 +0,0 @@ - -# Zähler - -OpenWB benötigt zum erfolgreichen PV-Überschussladen die entsprechenden Zählerwerte am EVU-Punkt (EVU=Elektrizitätsversorgungsunternehmen), sprich dem Übergang ins öffentliche Netz. An dieser Stelle muss die Gesamtleistung saldierend erfasst werden. Für eine phasenbasierte Leistungsüberwachung sind auch die einzelnen Ströme und/oder Leistungen der drei Phasen notwendig. - -Im einfachsten Fall geschieht dies durch Kauf und Einbau eines [EVU-Kits](##EVU-Kit). Sollten schon digital auslesbare Zähler vorhanden sein, so besteht die Möglichkeit diese Werte an OpenWB weiterzuleiten, auch mit Hilfe von Hausautomationsservern. - -Es gibt viele verschiedene Möglichkeiten, Zähler als auch Wechselrichter in das OpenWB-System einzufügen. Die Struktur der Zähler muss dann im [Lastmanagement](https://github.com/openWB/core/wiki/Lastmanagement-und-kaskadierte-Zähler) dem System bekanntgegeben werden. Hier können auch [virtuelle Zähler](##Virtuelle Zähler) hinzugefügt werden, welche OpenWB-intern die untergeordneten Zähler verrechnen. - - -## EVU-Kit - -Das [EVU-Kit (Link zum Shop)](https://openwb.de/shop/?product=openwb-evu-kit) ist die einfachste Art in der Software einen Zähler an die OpenWB zu integrieren. Der Zähler muss von einem Elektriker direkt hinter dem Zähler des EVU in den Zählerschrank integriert werden. Es gibt, je nach Kapazität des Hausanschlusses, verschiedene Messvarianten des Zählers, welche sich von der Integration unterscheiden. Dies betrifft aber nur die Arbeit des Elektrikers. -Der Zähler kommuniziert mit der OpenWB über Ethernet. Die Kits sind so vorkonfiguriert, dass sie von der openWB Software automatisch gefunden werden, wenn ein entsprechendes Gerät mit Zählerkomponente angelegt wird. Es gibt auch für Wechselrichter und Speicher entsprechende Kits. - -![EVU-Kit](pictures/EVU-PV-Speicher-Kit-689x1024.png) - -## MQTT - -OpenWB hat einen MQTT-Broker integriert, welcher unter Port 1883 (ohne Verschlüsselung) und Port 8883 (mit Verschlüsselung) erreichbar ist.Benutzerauthentifizierung ist deaktiviert und auch nicht aktivierbar. Ein Zähler, welcher die benötigten Daten liefert muss sich mit diesem Broker verbinden und dort die Werte unter den entsprechenden Topics publishen. - -Folgende Werte können dem MQTT-Zähler übergeben werden (Die Zahl in den Topics, hier "2", wird dynamisch erzeugt und ist den eigenen Gegebenheiten anzupassen. Wir ein neuer Zähler erstellt, wird diese Zahl inkrementiert): -``` -openWB/set/counter/2/get/power -Bezugsleistung in Watt, Zahl mit oder ohne Nachkommastellen (Float, Integer) und einem Punkt als Dezimaltrennzeichen, positiv Bezug, negativ Einspeisung -Beispiel: -123.45 - -openWB/set/counter/2/get/imported -Bezogene Energie in Wh, Zahl mit oder ohne Nachkommastellen (Float, Integer) und einem Punkt als Dezimaltrennzeichen, nur positiv -Beispiel: 123.45 - -openWB/set/counter/2/get/exported -Eingespeiste Energie in Wh, Zahl mit oder ohne Nachkommastellen (Float, Integer) und einem Punkt als Dezimaltrennzeichen, nur positiv -Beispiel: 123.45 - -openWB/set/counter/2/get/frequency -Netzfrequenz in Hz, Zahl mit oder ohne Nachkommastellen (Float, Integer) und einem Punkt als Dezimaltrennzeichen -Beispiel: 50.12 - -openWB/set/counter/2/get/currents -Array mit den Strömen je Phase in Ampere, mit Nachkommastellen (Float), positiv Bezug, negativ Einspeisung -Beispiel: [1.2,2.3,-2.1] - -openWB/set/counter/2/get/voltages -Array mit den Spannungen je Phase in Volt, mit Nachkommastellen (Float) -Beispiel: [222.2,223.3,222.3] - -openWB/set/counter/2/get/powers -Array mit den Leistungen je Phase in Watt, mit Nachkommastellen (Float) -Beispiel: [12.3,23.4,-12.3] - -openWB/set/counter/2/get/power_factors -Array mit den Leistungsfaktoren je Phase, mit Nachkommastellen (Float), Wertebereich -1 bis 1 -Beispiel: [0.95,0.96,-0.95] -``` - -### Optionale Werte -1. Phasenströme: _openWB/set/counter/2/get/voltages_: Werden nur benötigt, wenn in _Ladeeinstellungen - Übergreifendes_ die Begrenzung der Schieflast aktiviert ist. - - -## Huawei Wechselrichter mit DTSU666-H 250A und SDongle - -Huawei Wechselrichter werden, in der Betriebsart mit Aufzeichnung des Hausverbraucht mit dem _DTSU666-H 250A_ Stromzähler direkt am EVU-Punkt betrieben. Die Kommunikation zwischen Zähler und Wechselrichter findet über RS485 statt. Sofern der Wechselrichter mit dem optionalen SmartDongle FE ausgestattet ist, können über diesen Daten des Wechselrichter ausgelesen werden. -Die Schnittstelle am Dongle ist Modbus-TCP. Dies muss mit Installer-Account am Wechselrichter auf "Unrestrictet" gestellt werden, damit die Daten extern abgerufen werden können. - -Eine Möglichkeit des Datenabrufs wird im [OpenWB-Forum](https://openwb.de/forum/viewtopic.php?t=7029) entwickelt und ist auf [Github](https://github.com/AlexanderMetzger/huawei_openwb_bridge) sowie der [Homepage des Entwicklers](https://lebensraum-wohnraum.de/openwb-kommunikation-mit-dem-huawei-wechselrichter-sun-2000/) zu finden. Hierbei wird das Image auf die SD-karte eines Raspberry-Zero gespiegelt und der Raspberry mit dem Config-WLAN des Wechselrichters verbunden. Die Skripte ziehen sich die entsprechenden Werte in Echtzeit vom Wechselrichter und publishen diese auf die [MQTT](#MQTT) Schnittstelle der OpenWB. Der Zähler in der OpenWB muss dementsprechend als MQTT-Zähler eingerichtet sein. - -### Solaranzeige - -[Solaranzeige](https://solaranzeige.de) ist ebenfalls ein OpenSource Projekt, welches der Visualisierung, Speicherung und Weiterverarbeitung von PV-Daten dient. -Dieses Projekt unterstützt aktuell (Stand 2024-02) mehr Wechselrichter als OpenWB. Somit können hier mit etwas Aufwand existierende Wechselrichter eingebunden und die Daten weitergereicht werden, ohne Arbeiten am Zählerschrank durchzuführen zu lassen. -Die Software ist originär dafür vorgesehen auf einen Raspberry per Image installiert zu werden und nach wenigen Konfigurationsschritten lauffähig zu sein. Es gibt auch schon Portierungen für [Docker](https://github.com/DeBaschdi/docker.solaranzeige). -Solaranzeige kann mit vielen Wechselrichtern kommunizieren und auch teilweise die angeschlossenen Zähler auslesen. Eine zeitbasierte Datenbank (InfluxDb), Datenweitergabe über einen MQTT-Client sowie eine Visualisierung mit Grafana sind direkt integriert. Es kann aber auch bereits existierende Infrastruktur verwendet werden. -In dem Projekt wird (mit Stand von 2021) auch die Möglichkeit dokumentiert die Daten direkt an OpenWB weiterzuleiten. Dann kann jedoch kein weiterer MQTT-Broker bedient werden. -Alternativ können die Zählerwerte an eine Hausautomationsserver weitergegeben, dort ggf. vorzeichenkorrigiert und dann über einen zweiten MQTT-Client zur OpenWB geschickt werden. - -## Virtuelle Zähler - -Virtuelle Zähler sind, wie der Name schon sagt, nicht physikalisch vorhanden. Sie dienen in der Struktur des [Lastmanagement](https://github.com/openWB/core/wiki/Lastmanagement-und-kaskadierte-Zähler) dazu, Ströme und/oder Leistung zu begrenzen oder Werte aus untergeordneten einzelnen Zählern zu akkumulieren. - -## Timing -Das zyklische Senden bzw. Bereitstellen der Zählerwerte ist für eine funktionierende Regelung essentiell. Unter - -> Einstellungen - Allgemein - Hardware - -kann die Regelgeschwindigkeit ausgewählt werden. Es gibt die Intervalle: - - Normal - - Langsam (20s) - - Sehr langsam (60s) - -Hier muss eine Regelgeschwindigkeit entsprechend der Aktualisierungsrate der Zählerwerte angegeben werden, da ansonsten die Rückmeldung des Systems zu spät kommt und die OpenWB weiter versucht nachzuregeln. -Insbesondere bei der unter [Solaranzeige](### Solaranzeige) ist aufgrund der 20-30s dauerenden Aktualisierungsrate die Regelungsgeschwindigkeit anzupassen. - diff --git a/docs/_Sidebar.md b/docs/_Sidebar.md index 21f16c1d32..46a5e1bd78 100644 --- a/docs/_Sidebar.md +++ b/docs/_Sidebar.md @@ -4,19 +4,27 @@ * [Grundkonzept](https://github.com/openWB/core/wiki/Grundkonzept) * [Ladepunkte](https://github.com/openWB/core/wiki/Ladepunkte) * [Fahrzeuge](https://github.com/openWB/core/wiki/Fahrzeuge) -* [Zähler](https://github.com/openWB/core/wiki/Zaehler) +* Zähler + * [Grundsätzliches zu Zählern](https://github.com/openWB/core/wiki/Zaehler) * [Lastmanagement und kaskadierte Zähler](https://github.com/openWB/core/wiki/Lastmanagement-und-kaskadierte-Zähler) * [Hierarchie mit Hausverbrauchs-Zähler](https://github.com/openWB/core/wiki/Hausverbrauchs-Zähler) +* Oberfläche + * [Anzeige - Steuerung](https://github.com/openWB/core/wiki/Anzeige-Steuerung) * Szenarien + * [Typische Anwendungsfälle](https://github.com/openWB/core/wiki/Typische-Anwendungsfaelle) * [ID-Tag/Ladung nur nach Freischaltung](https://github.com/openWB/core/wiki/Ladung-nur-nach-Freischaltung) * [Hybrid-System aus Wechselrichter und Speicher](https://github.com/openWB/core/wiki/Hybrid-System-aus-Wechselrichter-und-Speicher) +* Integration in Heimautomation + * [MQTT](https://github.com/openWB/core/wiki/MQTT) * Mitarbeit am Projekt * [Wiki-Eintrag erstellen](https://github.com/openWB/core/wiki/Wiki-Eintrag_erstellen) * [Entwicklungsumgebung](https://github.com/openWB/core/wiki/Entwicklungsumgebung) - * [Neues Gerät programmieren](https://github.com/openWB/core/wiki/Neues-Gerät-programmieren) - * [Neues SoC-Modul programmieren](https://github.com/openWB/core/wiki/Neues-Soc-Modul-programmieren) + * [Neues Modul programmieren](https://github.com/openWB/core/wiki/Neues-Modul-programmieren) * [Einstellungs-Seite erstellen](https://github.com/openWB/core/wiki/Einstellungs-Seite-erstellen) * Konfiguration + * [Huawei Smartlogger 3000a](https://github.com/openWB/core/wiki/Huawei-Smartlogger) * [Cloud-Sicherung](https://github.com/openWB/core/wiki/Cloud-Sicherung) * [NextCloud](https://github.com/openWB/core/wiki/NextCloud-als-Sicherungs-Cloud-einrichten) * [Samba](https://github.com/openWB/core/wiki/Samba-als-Sicherung-einrichten) +* Sonstiges + * [Fehlersuche](https://github.com/openWB/core/wiki/Fehlersuche) diff --git a/docs/pictures/Anwendungsfaelle_minStrom.jpg b/docs/pictures/Anwendungsfaelle_minStrom.jpg new file mode 100644 index 0000000000..9ec32a627c Binary files /dev/null and b/docs/pictures/Anwendungsfaelle_minStrom.jpg differ diff --git a/docs/pictures/Anwendungsfaelle_zielladen.jpg b/docs/pictures/Anwendungsfaelle_zielladen.jpg new file mode 100644 index 0000000000..2fadb95194 Binary files /dev/null and b/docs/pictures/Anwendungsfaelle_zielladen.jpg differ diff --git a/docs/pictures/Fehlersuche_DebugLog.jpg b/docs/pictures/Fehlersuche_DebugLog.jpg new file mode 100644 index 0000000000..e8dbda94b0 Binary files /dev/null and b/docs/pictures/Fehlersuche_DebugLog.jpg differ diff --git a/docs/pictures/Fehlersuche_Main-Log.jpg b/docs/pictures/Fehlersuche_Main-Log.jpg new file mode 100644 index 0000000000..9f07623e51 Binary files /dev/null and b/docs/pictures/Fehlersuche_Main-Log.jpg differ diff --git a/docs/pictures/MQTT_Konsole_PV-Laden.jpg b/docs/pictures/MQTT_Konsole_PV-Laden.jpg new file mode 100644 index 0000000000..d192967f5f Binary files /dev/null and b/docs/pictures/MQTT_Konsole_PV-Laden.jpg differ diff --git a/docs/pictures/MQTT_Konsole_Sofortladen.jpg b/docs/pictures/MQTT_Konsole_Sofortladen.jpg new file mode 100644 index 0000000000..b4cf3e3523 Binary files /dev/null and b/docs/pictures/MQTT_Konsole_Sofortladen.jpg differ diff --git a/docs/pictures/Wiki-Eintrag erstellen_Pull.jpg b/docs/pictures/Wiki-Eintrag erstellen_Pull.jpg deleted file mode 100644 index c3e6c8ccb7..0000000000 Binary files a/docs/pictures/Wiki-Eintrag erstellen_Pull.jpg and /dev/null differ diff --git a/docs/pictures/Wiki-Eintrag erstellen_Fork.png b/docs/pictures/Wiki-Eintrag_erstellen_Fork.png similarity index 100% rename from docs/pictures/Wiki-Eintrag erstellen_Fork.png rename to docs/pictures/Wiki-Eintrag_erstellen_Fork.png diff --git a/docs/pictures/Wiki-Eintrag_erstellen_Pull.jpg b/docs/pictures/Wiki-Eintrag_erstellen_Pull.jpg new file mode 100644 index 0000000000..96d83ea5d8 Binary files /dev/null and b/docs/pictures/Wiki-Eintrag_erstellen_Pull.jpg differ diff --git a/docs/samples/sample_electricity_tariff/tariff.py b/docs/samples/sample_electricity_tariff/tariff.py index bebf1f5778..ea23689b6d 100644 --- a/docs/samples/sample_electricity_tariff/tariff.py +++ b/docs/samples/sample_electricity_tariff/tariff.py @@ -19,7 +19,7 @@ def fetch(config: SampleTariffConfiguration) -> None: def create_electricity_tariff(config: SampleTariff): def updater(): return fetch(config.configuration) - return ConfigurableTariff(config=config, component_updater=updater) + return updater device_descriptor = DeviceDescriptor(configuration_factory=SampleTariff) diff --git a/docs/samples/sample_modbus/device.py b/docs/samples/sample_modbus/device.py index 70b4372277..22b1957857 100644 --- a/docs/samples/sample_modbus/device.py +++ b/docs/samples/sample_modbus/device.py @@ -33,7 +33,7 @@ def update_components(components: Iterable[Union[SampleBat, SampleCounter, Sampl component.update(c) try: - client = ModbusTcpClient_(device_config.configuration.ip_address, port) + client = ModbusTcpClient_(device_config.configuration.ip_address, device_config.configuration.port) except Exception: log.exception("Fehler in create_device") return ConfigurableDevice( diff --git a/openwb-install.sh b/openwb-install.sh index 0de5f6de49..7933f35249 100755 --- a/openwb-install.sh +++ b/openwb-install.sh @@ -40,11 +40,11 @@ else fi echo -n "check for ramdisk... " -if grep -Fxq "tmpfs ${OPENWBBASEDIR}/ramdisk tmpfs nodev,nosuid,size=32M 0 0" /etc/fstab; then +if grep -Fq "tmpfs ${OPENWBBASEDIR}/ramdisk" /etc/fstab; then echo "ok" else mkdir -p "${OPENWBBASEDIR}/ramdisk" - echo "tmpfs ${OPENWBBASEDIR}/ramdisk tmpfs nodev,nosuid,size=32M 0 0" >> /etc/fstab + sudo tee -a "/etc/fstab" <"${OPENWBBASEDIR}/data/config/ramdisk_config.txt" >/dev/null mount -a echo "created" fi diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000000..9e36453c2a --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "core", + "lockfileVersion": 3, + "requires": true, + "packages": {} +} diff --git a/packages/conftest.py b/packages/conftest.py index a2700ddc9d..11bb3faba2 100644 --- a/packages/conftest.py +++ b/packages/conftest.py @@ -134,3 +134,43 @@ def data_() -> None: imported=14000, exported=18000), config=Mock(spec=CounterConfig, max_currents=[32]*3), set=Mock(spec=CounterSet, raw_currents_left=[31]*3)))}) + + +def hierarchy_hc_counter() -> CounterAll: + # counter0 + # | + # - counter6 + # | + # - cp3 + # - inverter1 + # - bat2 + c = CounterAll() + c.data.get.hierarchy = [{"id": 0, "type": "counter", + "children": [ + {"id": 6, "type": "counter", + "children": [ + {"id": 3, "type": "cp", "children": []}]}, + {"id": 1, "type": "inverter", "children": []}, + {"id": 2, "type": "bat", "children": []}]}] + return c + + +@pytest.fixture() +def data_hc_counter_() -> None: + data.data_init(Mock()) + data.data.cp_data = { + "cp3": Mock(spec=Chargepoint, data=Mock(spec=ChargepointData, + config=Mock(spec=Config, phase_1=1), + get=Mock(spec=Get, currents=[30, 0, 0], power=6900, + daily_imported=10000, daily_exported=0, imported=56000), + set=Mock(spec=Set, loadmanagement_available=True)))} + data.data.pv_data.update({"pv1": Mock(spec=Pv, data=Mock( + spec=PvData, get=Mock(spec=PvGet, power=-10000, daily_exported=6000, exported=27000, currents=None)))}) + data.data.counter_data.update({ + "counter0": Mock(spec=Counter, data=Mock(spec=CounterData, get=Mock( + spec=CounterGet, currents=[40]*3, power=-2000, daily_imported=45000, daily_exported=3000))), + "counter6": Mock(spec=Counter, data=Mock(spec=CounterData, get=Mock( + spec=CounterGet, currents=[25, 10, 25], power=8000, daily_imported=20000, daily_exported=0, + imported=14000, exported=18000), + config=Mock(spec=CounterConfig, max_currents=[32]*3), + set=Mock(spec=CounterSet, raw_currents_left=[31]*3)))}) diff --git a/packages/control/algorithm/additional_current.py b/packages/control/algorithm/additional_current.py index 0203122ed8..4b5e6ddc1e 100644 --- a/packages/control/algorithm/additional_current.py +++ b/packages/control/algorithm/additional_current.py @@ -1,5 +1,4 @@ import logging -from typing import List from control.algorithm import common from control.loadmanagement import LimitingValue, Loadmanagement @@ -13,12 +12,14 @@ class AdditionalCurrent: + CONSIDERED_CHARGE_MODES = common.CHARGEMODES[0:8] + def __init__(self) -> None: pass - def set_additional_current(self, mode_range: List[int]) -> None: - common.reset_current_by_chargemode(common.CHARGEMODES[0:8]) - for mode_tuple, counter in common.mode_and_counter_generator(mode_range): + def set_additional_current(self) -> None: + common.reset_current_by_chargemode(self.CONSIDERED_CHARGE_MODES) + for mode_tuple, counter in common.mode_and_counter_generator(self.CONSIDERED_CHARGE_MODES): preferenced_chargepoints, preferenced_cps_without_set_current = get_preferenced_chargepoint_charging( get_chargepoints_by_mode_and_counter(mode_tuple, f"counter{counter.num}")) if preferenced_chargepoints: diff --git a/packages/control/algorithm/algorithm.py b/packages/control/algorithm/algorithm.py index 72d46f8360..d795583339 100644 --- a/packages/control/algorithm/algorithm.py +++ b/packages/control/algorithm/algorithm.py @@ -31,14 +31,14 @@ def calc_current(self) -> None: self.min_current.set_min_current() log.info("**Sollstrom setzen**") common.reset_current_to_target_current() - self.additional_current.set_additional_current([0, 8]) + self.additional_current.set_additional_current() counter.limit_raw_power_left_to_surplus(self.evu_counter.calc_raw_surplus()) self.surplus_controlled.check_switch_on() if self.evu_counter.data.set.surplus_power_left > 0: log.info("**PV-geführten Strom setzen**") common.reset_current_to_target_current() self.surplus_controlled.set_required_current_to_max() - self.surplus_controlled.set_surplus_current([6, 12]) + self.surplus_controlled.set_surplus_current() else: log.info("**Keine Leistung für PV-geführtes Laden übrig.**") self.no_current.set_no_current() diff --git a/packages/control/algorithm/common.py b/packages/control/algorithm/common.py index 280ce0780a..9df5b2b9c6 100644 --- a/packages/control/algorithm/common.py +++ b/packages/control/algorithm/common.py @@ -47,13 +47,8 @@ def reset_current_by_chargemode(mode_tuple: Tuple[Optional[str], str, bool]) -> cp.data.set.current = None -def mode_range_list_factory() -> List[int]: - return [0, -1] - - -def mode_and_counter_generator( - mode_range: List[int] = mode_range_list_factory()) -> Iterable[Tuple[Tuple[Optional[str], str, bool], Counter]]: - for mode_tuple in CHARGEMODES[mode_range[0]: mode_range[1]]: +def mode_and_counter_generator(chargemodes: List) -> Iterable[Tuple[Tuple[Optional[str], str, bool], Counter]]: + for mode_tuple in chargemodes: levels = data.data.counter_all_data.get_list_of_elements_per_level() for level in reversed(levels): for element in level: diff --git a/packages/control/algorithm/filter_chargepoints.py b/packages/control/algorithm/filter_chargepoints.py index a53971bd6f..c87a86fa62 100644 --- a/packages/control/algorithm/filter_chargepoints.py +++ b/packages/control/algorithm/filter_chargepoints.py @@ -3,7 +3,6 @@ from typing import List, Optional, Tuple from control import data -from control.algorithm import common from control.chargepoint.chargepoint import Chargepoint log = logging.getLogger(__name__) @@ -54,19 +53,6 @@ def get_preferenced_chargepoint_charging( return preferenced_chargepoints_with_set_current, preferenced_chargepoints_without_set_current -def get_chargepoints_pv_charging() -> List[Chargepoint]: - chargepoints: List[Chargepoint] = [] - for mode in common.CHARGEMODES[8: 12]: - chargepoints.extend(get_chargepoints_by_mode(mode)) - return chargepoints - - -def get_chargepoints_surplus_controlled() -> List[Chargepoint]: - chargepoints: List[Chargepoint] = [] - for mode in common.CHARGEMODES[6: 12]: - chargepoints.extend(get_chargepoints_by_mode(mode)) - return chargepoints - # tested diff --git a/packages/control/algorithm/filter_chargepoints_test.py b/packages/control/algorithm/filter_chargepoints_test.py index 3debfd7340..f278afcf4f 100644 --- a/packages/control/algorithm/filter_chargepoints_test.py +++ b/packages/control/algorithm/filter_chargepoints_test.py @@ -163,28 +163,3 @@ def test_get_chargepoints_by_mode_and_counter(chargepoints_of_counter: List[str] # assertion assert valid_chargepoints == expected_chargepoints - - -@pytest.mark.parametrize( - "submode_1, submode_2, expected_chargepoints", - [ - pytest.param(Chargemode.PV_CHARGING, Chargemode.PV_CHARGING, [mock_cp2, mock_cp1]), - pytest.param(Chargemode.SCHEDULED_CHARGING, Chargemode.PV_CHARGING, [mock_cp2]), - pytest.param(Chargemode.SCHEDULED_CHARGING, Chargemode.INSTANT_CHARGING, []), - ]) -def test_get_chargepoints_submode_pv_charging(submode_1: Chargemode, - submode_2: Chargemode, - expected_chargepoints: List[Chargepoint]): - # setup - def setup_cp(cp: Chargepoint, submode: str) -> Chargepoint: - cp.data.set.charging_ev = Ev(0) - cp.data.control_parameter.submode = submode - return cp - data.data.cp_data = {"cp1": setup_cp(mock_cp1, submode_1), - "cp2": setup_cp(mock_cp2, submode_2)} - - # evaluation - chargepoints = filter_chargepoints.get_chargepoints_pv_charging() - - # assertion - assert chargepoints == expected_chargepoints diff --git a/packages/control/algorithm/integration_test/conftest.py b/packages/control/algorithm/integration_test/conftest.py index 98923f300e..ac2f44d16f 100644 --- a/packages/control/algorithm/integration_test/conftest.py +++ b/packages/control/algorithm/integration_test/conftest.py @@ -26,6 +26,7 @@ def data_() -> None: data.data.cp_data[f"cp{i}"].data.config.phase_1 = i-2 data.data.cp_data[f"cp{i}"].data.set.charging_ev = i data.data.cp_data[f"cp{i}"].data.set.charging_ev_data = Ev(i) + data.data.cp_data[f"cp{i}"].data.set.charging_ev_data.ev_template.data.max_current_single_phase = 32 data.data.cp_data[f"cp{i}"].data.get.plug_state = True data.data.cp_data[f"cp{i}"].data.set.plug_time = f"12/01/2022, 15:0{i}:11" data.data.cp_data[f"cp{i}"].data.set.charging_ev_data.ev_template.data.nominal_difference = 2 diff --git a/packages/control/algorithm/min_current.py b/packages/control/algorithm/min_current.py index f373789180..ac5af0f213 100644 --- a/packages/control/algorithm/min_current.py +++ b/packages/control/algorithm/min_current.py @@ -8,11 +8,13 @@ class MinCurrent: + CONSIDERED_CHARGE_MODES = common.CHARGEMODES[0:-1] + def __init__(self) -> None: pass def set_min_current(self) -> None: - for mode_tuple, counter in common.mode_and_counter_generator(): + for mode_tuple, counter in common.mode_and_counter_generator(self.CONSIDERED_CHARGE_MODES): preferenced_chargepoints = get_chargepoints_by_mode_and_counter(mode_tuple, f"counter{counter.num}") if preferenced_chargepoints: log.info(f"Mode-Tuple {mode_tuple[0]} - {mode_tuple[1]} - {mode_tuple[2]}, Zähler {counter.num}") diff --git a/packages/control/algorithm/surplus_controlled.py b/packages/control/algorithm/surplus_controlled.py index 9a7e7ca5d4..85c8fcda98 100644 --- a/packages/control/algorithm/surplus_controlled.py +++ b/packages/control/algorithm/surplus_controlled.py @@ -6,22 +6,25 @@ from control.loadmanagement import LimitingValue, Loadmanagement from control.counter import Counter from control.chargepoint.chargepoint import Chargepoint -from control.algorithm.filter_chargepoints import (get_chargepoints_by_mode_and_counter, - get_preferenced_chargepoint_charging, get_chargepoints_pv_charging, - get_chargepoints_surplus_controlled) +from control.algorithm.filter_chargepoints import (get_chargepoints_by_mode, get_chargepoints_by_mode_and_counter, + get_preferenced_chargepoint_charging) from control.chargepoint.chargepoint_state import ChargepointState, CHARGING_STATES from modules.common.utils.component_parser import get_component_name_by_id log = logging.getLogger(__name__) +CONSIDERED_CHARGE_MODES = common.CHARGEMODES[0:2] + common.CHARGEMODES[6:12] +CONSIDERED_CHARGE_MODES_PV = common.CHARGEMODES[8:12] + class SurplusControlled: + def __init__(self) -> None: pass - def set_surplus_current(self, mode_range) -> None: - common.reset_current_by_chargemode(common.CHARGEMODES[6:12]) - for mode_tuple, counter in common.mode_and_counter_generator(mode_range): + def set_surplus_current(self) -> None: + common.reset_current_by_chargemode(CONSIDERED_CHARGE_MODES) + for mode_tuple, counter in common.mode_and_counter_generator(CONSIDERED_CHARGE_MODES): preferenced_chargepoints, preferenced_cps_without_set_current = get_preferenced_chargepoint_charging( get_chargepoints_by_mode_and_counter(mode_tuple, f"counter{counter.num}")) cp_with_feed_in, cp_without_feed_in = self.filter_by_feed_in_limit(preferenced_chargepoints) @@ -127,13 +130,14 @@ def check_submode_pv_charging(self) -> None: def phase_switch_necessary() -> bool: return cp.cp_ev_chargemode_support_phase_switch() and cp.data.get.phases_in_use != 1 control_parameter = cp.data.control_parameter - if cp.data.set.charging_ev_data.chargemode_changed: + if cp.data.set.charging_ev_data.chargemode_changed or cp.data.set.charging_ev_data.submode_changed: if control_parameter.state == ChargepointState.CHARGING_ALLOWED: if (cp.data.set.charging_ev_data.ev_template.data.prevent_charge_stop is False and phase_switch_necessary() is False): threshold = evu_counter.calc_switch_off_threshold(cp)[0] if evu_counter.calc_raw_surplus() - cp.data.set.required_power < threshold: control_parameter.required_currents = [0]*3 + control_parameter.state = ChargepointState.NO_CHARGING_ALLOWED else: control_parameter.required_currents = [0]*3 else: @@ -168,3 +172,17 @@ def set_required_current_to_max(self) -> None: control_parameter.required_currents = [max_current if required_currents[i] != 0 else 0 for i in range(3)] control_parameter.required_current = max_current + + +def get_chargepoints_pv_charging() -> List[Chargepoint]: + chargepoints: List[Chargepoint] = [] + for mode in CONSIDERED_CHARGE_MODES_PV: + chargepoints.extend(get_chargepoints_by_mode(mode)) + return chargepoints + + +def get_chargepoints_surplus_controlled() -> List[Chargepoint]: + chargepoints: List[Chargepoint] = [] + for mode in CONSIDERED_CHARGE_MODES: + chargepoints.extend(get_chargepoints_by_mode(mode)) + return chargepoints diff --git a/packages/control/algorithm/surplus_controlled_test.py b/packages/control/algorithm/surplus_controlled_test.py index 006727fb4e..a364bdd460 100644 --- a/packages/control/algorithm/surplus_controlled_test.py +++ b/packages/control/algorithm/surplus_controlled_test.py @@ -2,8 +2,10 @@ from unittest.mock import Mock import pytest +from control import data from control.algorithm import surplus_controlled -from control.algorithm.surplus_controlled import SurplusControlled +from control.algorithm.surplus_controlled import SurplusControlled, get_chargepoints_pv_charging +from control.chargemode import Chargemode from control.chargepoint.chargepoint import Chargepoint, ChargepointData from control.chargepoint.chargepoint_data import Get, Set from control.chargepoint.control_parameter import ControlParameter @@ -70,8 +72,8 @@ def test_limit_adjust_current(new_current: float, expected_current: float, monke @pytest.mark.parametrize("phases, required_currents, expected_currents", [ - pytest.param(1, [10, 0, 0], [32, 0, 0]), - pytest.param(1, [0, 15, 0], [0, 32, 0]), + pytest.param(1, [10, 0, 0], [16, 0, 0]), + pytest.param(1, [0, 15, 0], [0, 16, 0]), pytest.param(3, [10]*3, [16]*3), ]) def test_set_required_current_to_max(phases: int, @@ -115,3 +117,29 @@ def test_add_unused_evse_current(evse_current: float, limited_current: float, ex # evaluation assert current == expected_current + + +@pytest.mark.parametrize( + "submode_1, submode_2, expected_chargepoints", + [ + pytest.param(Chargemode.PV_CHARGING, Chargemode.PV_CHARGING, [mock_cp1, mock_cp2]), + pytest.param(Chargemode.INSTANT_CHARGING, Chargemode.PV_CHARGING, [mock_cp2]), + pytest.param(Chargemode.INSTANT_CHARGING, Chargemode.INSTANT_CHARGING, []), + ]) +def test_get_chargepoints_submode_pv_charging(submode_1: Chargemode, + submode_2: Chargemode, + expected_chargepoints: List[Chargepoint]): + # setup + def setup_cp(cp: Chargepoint, submode: str) -> Chargepoint: + cp.data.set.charging_ev = Ev(0) + cp.data.control_parameter.chargemode = Chargemode.PV_CHARGING + cp.data.control_parameter.submode = submode + return cp + data.data.cp_data = {"cp1": setup_cp(mock_cp1, submode_1), + "cp2": setup_cp(mock_cp2, submode_2)} + + # evaluation + chargepoints = get_chargepoints_pv_charging() + + # assertion + assert chargepoints == expected_chargepoints diff --git a/packages/control/auto_phase_switch_test.py b/packages/control/auto_phase_switch_test.py index 97aa74456c..c206d33087 100644 --- a/packages/control/auto_phase_switch_test.py +++ b/packages/control/auto_phase_switch_test.py @@ -35,7 +35,6 @@ def __init__(self, phases_to_use: int, required_current: float, evu_surplus: int, - reserved_evu_overhang: int, get_currents: List[float], get_power: float, state: ChargepointState, @@ -50,7 +49,6 @@ def __init__(self, self.phases_to_use = phases_to_use self.required_current = required_current self.available_power = evu_surplus - self.reserved_evu_overhang = reserved_evu_overhang self.get_currents = get_currents self.get_power = get_power self.state = state @@ -63,60 +61,60 @@ def __init__(self, cases = [ Params("1to3, enough power, start timer", max_current_single_phase=16, timestamp_auto_phase_switch=None, - phases_to_use=1, required_current=6, evu_surplus=-800, reserved_evu_overhang=0, get_currents=[15.6, 0, 0], + phases_to_use=1, required_current=6, evu_surplus=800, get_currents=[15.6, 0, 0], get_power=3450, state=ChargepointState.CHARGING_ALLOWED, expected_phases_to_use=1, expected_current=6, - expected_message=Ev.PHASE_SWITCH_DELAY_TEXT.format("Umschaltung von 1 auf 3", "7 Min. 0 Sek."), + expected_message=Ev.PHASE_SWITCH_DELAY_TEXT.format("Umschaltung von 1 auf 3", "7 Min."), expected_timestamp_auto_phase_switch=1652683252.0, expected_state=ChargepointState.PHASE_SWITCH_DELAY), Params("1to3, not enough power, start timer", max_current_single_phase=16, timestamp_auto_phase_switch=None, - phases_to_use=1, required_current=6, evu_surplus=-300, reserved_evu_overhang=0, get_currents=[15.6, 0, 0], + phases_to_use=1, required_current=6, evu_surplus=300, get_currents=[15.6, 0, 0], get_power=3450, state=ChargepointState.CHARGING_ALLOWED, expected_phases_to_use=1, expected_current=6, expected_state=ChargepointState.CHARGING_ALLOWED), Params("1to3, enough power, timer not expired", max_current_single_phase=16, timestamp_auto_phase_switch=1652682952.0, phases_to_use=1, required_current=6, - evu_surplus=-1200, reserved_evu_overhang=460, get_currents=[15.6, 0, 0], get_power=3450, + evu_surplus=1460, get_currents=[15.6, 0, 0], get_power=3450, state=ChargepointState.PHASE_SWITCH_DELAY, expected_phases_to_use=1, expected_current=6, - expected_message=Ev.PHASE_SWITCH_DELAY_TEXT.format("Umschaltung von 1 auf 3", "2 Min. 0 Sek."), + expected_message=Ev.PHASE_SWITCH_DELAY_TEXT.format("Umschaltung von 1 auf 3", "2 Min."), expected_timestamp_auto_phase_switch=1652683252.0, expected_state=ChargepointState.PHASE_SWITCH_DELAY), Params("1to3, not enough power, timer not expired", max_current_single_phase=16, timestamp_auto_phase_switch=1652682952.0, phases_to_use=1, required_current=6, - evu_surplus=0, reserved_evu_overhang=460, get_currents=[15.6, 0, 0], get_power=3450, + evu_surplus=460, get_currents=[15.6, 0, 0], get_power=3450, state=ChargepointState.PHASE_SWITCH_DELAY, expected_phases_to_use=1, expected_current=6, expected_message=f"Verzögerung für die Umschaltung von 1 auf 3 Phasen abgebrochen{Ev.NOT_ENOUGH_POWER}", expected_timestamp_auto_phase_switch=1652683252.0, expected_state=ChargepointState.CHARGING_ALLOWED), Params("1to3, enough power, timer expired", max_current_single_phase=16, timestamp_auto_phase_switch=1652682772.0, phases_to_use=1, required_current=6, - evu_surplus=-1200, reserved_evu_overhang=460, get_currents=[15.6, 0, 0], get_power=3450, + evu_surplus=1640, get_currents=[15.6, 0, 0], get_power=3450, state=ChargepointState.PHASE_SWITCH_DELAY, expected_phases_to_use=3, expected_current=6, expected_state=ChargepointState.PHASE_SWITCH_DELAY_EXPIRED), Params("3to1, not enough power, start timer", max_current_single_phase=16, timestamp_auto_phase_switch=None, - phases_to_use=3, required_current=6, evu_surplus=0, reserved_evu_overhang=0, + phases_to_use=3, required_current=6, evu_surplus=0, get_currents=[4.5, 4.4, 5.8], get_power=3381, state=ChargepointState.CHARGING_ALLOWED, expected_phases_to_use=3, expected_current=6, - expected_message="Umschaltung von 3 auf 1 Phasen in 9 Min. 0 Sek..", + expected_message="Umschaltung von 3 auf 1 Phasen in 9 Min..", expected_timestamp_auto_phase_switch=1652683252.0, expected_state=ChargepointState.PHASE_SWITCH_DELAY), Params("3to1, not enough power, timer not expired", max_current_single_phase=16, timestamp_auto_phase_switch=1652682952.0, - phases_to_use=3, required_current=6, evu_surplus=0, reserved_evu_overhang=-460, + phases_to_use=3, required_current=6, evu_surplus=-460, get_currents=[4.5, 4.4, 5.8], get_power=3381, state=ChargepointState.PHASE_SWITCH_DELAY, expected_phases_to_use=3, expected_current=6, - expected_message="Umschaltung von 3 auf 1 Phasen in 4 Min. 0 Sek..", + expected_message="Umschaltung von 3 auf 1 Phasen in 4 Min..", expected_timestamp_auto_phase_switch=1652683252.0, expected_state=ChargepointState.PHASE_SWITCH_DELAY), Params("3to1, enough power, timer not expired", max_current_single_phase=16, timestamp_auto_phase_switch=1652682952.0, phases_to_use=3, required_current=6, - evu_surplus=-860, reserved_evu_overhang=0, get_currents=[4.5, 4.4, 5.8], + evu_surplus=860, get_currents=[4.5, 4.4, 5.8], get_power=3381, state=ChargepointState.PHASE_SWITCH_DELAY, expected_phases_to_use=3, expected_current=6, expected_message=f"Verzögerung für die Umschaltung von 3 auf 1 Phasen abgebrochen{Ev.ENOUGH_POWER}", expected_timestamp_auto_phase_switch=1652683252.0, expected_state=ChargepointState.CHARGING_ALLOWED), Params("3to1, not enough power, timer expired", max_current_single_phase=16, timestamp_auto_phase_switch=1652682592.0, phases_to_use=3, required_current=6, - evu_surplus=0, reserved_evu_overhang=-460, get_currents=[4.5, 4.4, 5.8], + evu_surplus=-460, get_currents=[4.5, 4.4, 5.8], get_power=3381, state=ChargepointState.PHASE_SWITCH_DELAY, expected_phases_to_use=1, expected_current=16, expected_state=ChargepointState.PHASE_SWITCH_DELAY_EXPIRED), ] @@ -126,12 +124,12 @@ def __init__(self, def test_auto_phase_switch(monkeypatch, vehicle: Ev, params: Params): # setup mock_evu = Mock(spec=Counter, data=Mock(spec=CounterData, - set=Mock(spec=Set, reserved_surplus=params.reserved_evu_overhang, + set=Mock(spec=Set, reserved_surplus=0, released_surplus=0))) mock_get_evu_counter = Mock(name="power_for_bat_charging", return_value=mock_evu) monkeypatch.setattr(data.data.counter_all_data, "get_evu_counter", mock_get_evu_counter) mock_evu_counter_surplus = Mock(return_value=params.available_power) - monkeypatch.setattr(mock_evu, "calc_surplus", mock_evu_counter_surplus) + monkeypatch.setattr(mock_evu, "get_usable_surplus", mock_evu_counter_surplus) vehicle.ev_template.data.max_current_single_phase = params.max_current_single_phase control_parameter = ControlParameter() diff --git a/packages/control/bat.py b/packages/control/bat.py index 9fb4529b87..0458e3e588 100644 --- a/packages/control/bat.py +++ b/packages/control/bat.py @@ -3,21 +3,23 @@ from typing import List from dataclass_utils.factories import currents_list_factory +from helpermodules.constants import NO_ERROR log = logging.getLogger(__name__) @dataclass class Get: - currents: List[float] = field(default_factory=currents_list_factory) - soc: float = 0 - daily_exported: float = 0 - daily_imported: float = 0 - imported: float = 0 - exported: float = 0 - fault_state: int = 0 - fault_str: str = "" - power: float = 0 + currents: List[float] = field(default_factory=currents_list_factory, metadata={ + "topic": "get/currents"}) + soc: float = field(default=0, metadata={"topic": "get/soc"}) + daily_exported: float = field(default=0, metadata={"topic": "get/daily_exported"}) + daily_imported: float = field(default=0, metadata={"topic": "get/daily_imported"}) + imported: float = field(default=0, metadata={"topic": "get/imported"}) + exported: float = field(default=0, metadata={"topic": "get/exported"}) + fault_state: int = field(default=0, metadata={"topic": "get/fault_state"}) + fault_str: str = field(default=NO_ERROR, metadata={"topic": "get/fault_str"}) + power: float = field(default=0, metadata={"topic": "get/power"}) def get_factory() -> Get: diff --git a/packages/control/bat_all.py b/packages/control/bat_all.py index 0afc0726de..a6b9e28edf 100644 --- a/packages/control/bat_all.py +++ b/packages/control/bat_all.py @@ -24,7 +24,6 @@ from control import data from control.bat import Bat from helpermodules.constants import NO_ERROR -from helpermodules.pub import Pub from modules.common.fault_state import FaultStateLevel log = logging.getLogger(__name__) @@ -38,7 +37,7 @@ class BatConsiderationMode(Enum): @dataclass class Config: - configured: bool = False + configured: bool = field(default=False, metadata={"topic": "config/configured"}) def config_factory() -> Config: @@ -47,14 +46,14 @@ def config_factory() -> Config: @dataclass class Get: - soc: float = field(default=0, metadata={"topic": "get/soc", "mutable_by_algorithm": True}) - daily_exported: float = field(default=0, metadata={"topic": "get/daily_exported", "mutable_by_algorithm": True}) - daily_imported: float = field(default=0, metadata={"topic": "get/daily_imported", "mutable_by_algorithm": True}) - fault_str: str = field(default=NO_ERROR, metadata={"topic": "get/fault_str", "mutable_by_algorithm": True}) - fault_state: int = field(default=0, metadata={"topic": "get/fault_state", "mutable_by_algorithm": True}) - imported: float = field(default=0, metadata={"topic": "get/imported", "mutable_by_algorithm": True}) - exported: float = field(default=0, metadata={"topic": "get/exported", "mutable_by_algorithm": True}) - power: float = field(default=0, metadata={"topic": "get/power", "mutable_by_algorithm": True}) + soc: float = field(default=0, metadata={"topic": "get/soc"}) + daily_exported: float = field(default=0, metadata={"topic": "get/daily_exported"}) + daily_imported: float = field(default=0, metadata={"topic": "get/daily_imported"}) + fault_str: str = field(default=NO_ERROR, metadata={"topic": "get/fault_str"}) + fault_state: int = field(default=0, metadata={"topic": "get/fault_state"}) + imported: float = field(default=0, metadata={"topic": "get/imported"}) + exported: float = field(default=0, metadata={"topic": "get/exported"}) + power: float = field(default=0, metadata={"topic": "get/power"}) def get_factory() -> Get: @@ -63,8 +62,9 @@ def get_factory() -> Get: @dataclass class Set: - charging_power_left: float = 0 - regulate_up: bool = False + charging_power_left: float = field( + default=0, metadata={"topic": "set/charging_power_left"}) + regulate_up: bool = field(default=False, metadata={"topic": "set/regulate_up"}) def set_factory() -> Set: @@ -90,7 +90,6 @@ def calc_power_for_all_components(self): try: if len(data.data.bat_data) >= 1: self.data.config.configured = True - Pub().pub("openWB/set/bat/config/configured", self.data.config.configured) # Summe für alle konfigurierten Speicher bilden exported = 0 imported = 0 @@ -128,7 +127,6 @@ def calc_power_for_all_components(self): self.data.get.soc = 0 else: self.data.config.configured = False - Pub().pub("openWB/set/bat/config/configured", self.data.config.configured) except Exception: log.exception("Fehler im Bat-Modul") @@ -148,10 +146,6 @@ def _max_bat_power_hybrid_system(self, battery: Bat) -> float: else: battery.data.get.fault_state = FaultStateLevel.ERROR.value battery.data.get.fault_str = self.ERROR_CONFIG_MAX_AC_OUT - Pub().pub(f"openWB/set/bat/{battery.num}/get/fault_state", - battery.data.get.fault_state) - Pub().pub(f"openWB/set/bat/{battery.num}/get/fault_str", - battery.data.get.fault_str) raise ValueError(self.ERROR_CONFIG_MAX_AC_OUT) else: # Kein Hybrid-WR @@ -193,8 +187,6 @@ def setup_bat(self): else: self.data.set.charging_power_left = 0 self.data.get.power = 0 - Pub().pub("openWB/set/bat/set/charging_power_left", self.data.set.charging_power_left) - Pub().pub("openWB/set/bat/set/regulate_up", self.data.set.regulate_up) except Exception: log.exception("Fehler im Bat-Modul") diff --git a/packages/control/chargelog/chargelog.py b/packages/control/chargelog/chargelog.py index 59ec3a66e6..6e026a49b7 100644 --- a/packages/control/chargelog/chargelog.py +++ b/packages/control/chargelog/chargelog.py @@ -7,19 +7,56 @@ from control import data from dataclass_utils import asdict -from helpermodules.measurement_logging.process_log import CalculationType, analyse_percentage, process_entry +from helpermodules.measurement_logging.process_log import (CalculationType, analyse_percentage, + get_log_from_date_until_now, process_entry) from helpermodules.measurement_logging.write_log import LegacySmartHomeLogData, LogType, create_entry from helpermodules.pub import Pub from helpermodules import timecheck +from helpermodules.utils.json_file_handler import write_and_check # alte Daten: Startzeitpunkt der Ladung, Endzeitpunkt, Geladene Reichweite, Energie, Leistung, Ladedauer, LP-Nummer, # Lademodus, ID-Tag -# json-Objekt: {"chargepoint": {"id": 1, "name": "Hof", "rfid": 1234}, -# "vehicle": { "id": 1, "name":"Model 3", "chargemode": "pv_charging", "prio": True }, -# "time": { "begin":"27.05.2021 07:43", "end": "27.05.2021 07:50", "time_charged": "1:34", -# "data": {"range_charged": 34, "imported_since_mode_switch": 3400, "imported_since_plugged": 5000, -# "power": 110000, "costs": 3,42} }} - +# json-Objekt: new_entry = { +# "chargepoint": +# { +# "id": 22, +# "name": "LP 22", +# "serial_number": "0123456," +# "imported_at_start": 1000, +# "imported_at_end": 2000, +# }, +# "vehicle": +# { +# "id": 1, +# "name": "Auto", +# "chargemode": instant_charging, +# "prio": False, +# "rfid": "123" +# "soc_at_start": 50, +# "soc_at_end": 60, +# "range_at_start": 100, +# "range_at_end": 125, +# }, +# "time": +# { +# "begin": "01.02.2024 15:00:00", +# "end": "01.02.2024 16:00:00", +# "time_charged": "1:00" +# }, +# "data": +# { +# "range_charged": 100, +# "imported_since_mode_switch": 1000, +# "imported_since_plugged": 1000, +# "power": 1000, +# "costs": 0.95, +# "energy_source": { +# "grid": 0.25, +# "pv": 0.25, +# "bat": 0.5, +# "cp": 0} +# } +# } log = logging.getLogger("chargelog") @@ -48,6 +85,9 @@ def collect_data(chargepoint): if chargepoint.data.get.charge_state: if log_data.timestamp_start_charging is None: log_data.timestamp_start_charging = timecheck.create_timestamp() + if charging_ev.soc_module: + log_data.range_at_start = charging_ev.data.get.range + log_data.soc_at_start = charging_ev.data.get.soc if chargepoint.data.control_parameter.submode == "time_charging": log_data.chargemode_log_entry = "time_charging" else: @@ -157,6 +197,8 @@ def _create_entry(chargepoint, charging_ev, immediately: bool = True): if duration > 0: power = get_value_or_default(lambda: round(log_data.imported_since_mode_switch / duration, 2)) calculate_charge_cost(chargepoint, True) + energy_source = get_value_or_default(lambda: analyse_percentage(get_log_from_date_until_now( + log_data.timestamp_start_charging)["totals"])["energy_source"]) costs = round(log_data.costs, 2) new_entry = { "chargepoint": @@ -173,7 +215,11 @@ def _create_entry(chargepoint, charging_ev, immediately: bool = True): "name": get_value_or_default(lambda: _get_ev_name(log_data.ev)), "chargemode": get_value_or_default(lambda: log_data.chargemode_log_entry), "prio": get_value_or_default(lambda: log_data.prio), - "rfid": get_value_or_default(lambda: log_data.rfid) + "rfid": get_value_or_default(lambda: log_data.rfid), + "soc_at_start": get_value_or_default(lambda: log_data.soc_at_start), + "soc_at_end": get_value_or_default(lambda: charging_ev.data.get.soc), + "range_at_start": get_value_or_default(lambda: log_data.range_at_start), + "range_at_end": get_value_or_default(lambda: charging_ev.data.get.range), }, "time": { @@ -189,7 +235,8 @@ def _create_entry(chargepoint, charging_ev, immediately: bool = True): "imported_since_mode_switch": log_data.imported_since_mode_switch, "imported_since_plugged": log_data.imported_since_plugged, "power": power, - "costs": costs + "costs": costs, + "power_source": energy_source } } return new_entry @@ -210,8 +257,7 @@ def write_new_entry(new_entry): # content = json.load(jsonFile) content = [] content.append(new_entry) - with open(filepath, "w", encoding="utf-8") as json_file: - json.dump(content, json_file) + write_and_check(filepath, content) log.debug(f"Neuer Ladelog-Eintrag: {new_entry}") @@ -403,7 +449,11 @@ def get_reference_time(cp, reference_position): elif reference_position == ReferenceTime.MIDDLE: return timecheck.create_timestamp() - 3540 elif reference_position == ReferenceTime.END: - return timecheck.create_unix_timestamp_current_full_hour() + 60 + # Wenn der Ladevorgang erst innerhalb der letzten Stunde gestartet wurde. + if timecheck.create_unix_timestamp_current_full_hour() <= cp.data.set.log.timestamp_start_charging: + return cp.data.set.log.timestamp_start_charging + else: + return timecheck.create_unix_timestamp_current_full_hour() + 60 else: raise TypeError(f"Unbekannter Referenz-Zeitpunkt {reference_position}") diff --git a/packages/control/chargelog/chargelog_test.py b/packages/control/chargelog/chargelog_test.py new file mode 100644 index 0000000000..1ab700af43 --- /dev/null +++ b/packages/control/chargelog/chargelog_test.py @@ -0,0 +1,154 @@ + +import datetime +from unittest.mock import Mock + +import pytest + +from control import data +from control.chargelog import chargelog +from control.chargelog.chargelog import calculate_charge_cost +from control.chargepoint.chargepoint import Chargepoint +from helpermodules import timecheck +from test_utils.test_environment import running_on_github + + +def mock_daily_log_with_charging(date: str, num_of_intervalls, monkeypatch): + """erzeugt ein daily_log, im ersten Eintrag gibt es keine Änderung, danach wird bis inklusive dem letzten Beitrag + geladen""" + bat_exported = pv_exported = cp_imported = counter_imported = 2000 + date = datetime.datetime.strptime(date, "%m/%d/%Y, %H:%M") + daily_log = {"entries": []} + for i in range(0, num_of_intervalls): + if i != 0: + bat_exported += 1000 + pv_exported += 500 + cp_imported += 2000 + counter_imported += 500 + daily_log["entries"].append({'bat': {'all': {'exported': bat_exported, 'imported': 2000, 'soc': 100}, + 'bat2': {'exported': bat_exported, 'imported': 2000, 'soc': 100}}, + 'counter': {'counter0': {'exported': 2000, + 'grid': True, + 'imported': counter_imported}}, + 'cp': {'all': {'exported': 0, 'imported': cp_imported}, + 'cp4': {'exported': 0, 'imported': cp_imported}}, + 'date': date.strftime("%H:%M"), + 'ev': {'ev0': {'soc': None}}, + 'hc': {'all': {'imported': 0}}, + 'pv': {'all': {'exported': pv_exported}, 'pv1': {'exported': pv_exported}}, + 'sh': {}, + 'timestamp': date.timestamp()}) + date += datetime.timedelta(minutes=5) + mock_todays_daily_log = Mock(return_value=daily_log) + monkeypatch.setattr(chargelog, "get_todays_daily_log", mock_todays_daily_log) + return daily_log + + +@pytest.fixture() +def mock_data() -> None: + data.data_init(Mock()) + data.data.optional_data.et_module = None + + +def mock_create_entry_reference_end(clock, daily_log, monkeypatch): + current_log = daily_log["entries"][-1] + current_log["cp"]["all"]["imported"] += 500 + current_log["cp"]["cp4"]["imported"] += 500 + current_log["counter"]["counter0"]["imported"] += 500 + current_log["date"] = clock + current_log["timestamp"] = datetime.datetime.strptime(f"05/16/2022, {clock}", "%m/%d/%Y, %H:%M").timestamp() + mock_create_entry = Mock(return_value=current_log) + monkeypatch.setattr(chargelog, "create_entry", mock_create_entry) + + +def init_cp(charged_energy, costs, start_hour, start_minute=47): + cp = Chargepoint(4, None) + cp.data.set.log.imported_since_plugged = cp.data.set.log.imported_since_mode_switch = charged_energy + cp.data.set.log.timestamp_start_charging = datetime.datetime(2022, 5, 16, start_hour, start_minute).timestamp() + cp.data.get.imported = charged_energy + 2000 + cp.data.set.log.costs = costs + return cp + + +def test_calc_charge_cost_no_hour_change_reference_end(mock_data, monkeypatch): + cp = init_cp(6500, 0, 10, start_minute=27) + daily_log = mock_daily_log_with_charging("05/16/2022, 10:25", 4, monkeypatch) + mock_create_entry_reference_end("10:42", daily_log, monkeypatch) + + calculate_charge_cost(cp, True) + + assert cp.data.set.log.costs == 1.425 + + +def test_calc_charge_cost_first_hour_change_reference_begin(mock_data, monkeypatch): + cp = init_cp(6000, 0, 7) + daily_log = mock_daily_log_with_charging("05/16/2022, 07:45", 4, monkeypatch) + current_log = daily_log["entries"][-1] + current_log["date"] = "08:00" + current_log["timestamp"] = datetime.datetime.strptime("05/16/2022, 08:00", "%m/%d/%Y, %H:%M").timestamp() + mock_create_entry = Mock(return_value=current_log) + monkeypatch.setattr(chargelog, "create_entry", mock_create_entry) + + calculate_charge_cost(cp, False) + + assert cp.data.set.log.costs == 1.275 + + +def test_calc_charge_cost_first_hour_change_reference_begin_day_change(mock_data, monkeypatch): + cp = init_cp(6000, 0, 23) + daily_log = mock_daily_log_with_charging("05/16/2022, 23:45", 4, monkeypatch) + current_log = daily_log["entries"][-1] + current_log["date"] = "00:00" + current_log["timestamp"] = datetime.datetime.strptime("05/17/2022, 00:00", "%m/%d/%Y, %H:%M").timestamp() + mock_create_entry = Mock(return_value=current_log) + monkeypatch.setattr(chargelog, "create_entry", mock_create_entry) + mock_today_timestamp = Mock(return_value=1652738421) + monkeypatch.setattr(timecheck, "create_timestamp", mock_today_timestamp) + + calculate_charge_cost(cp, False) + + assert cp.data.set.log.costs == 1.275 + + +def test_calc_charge_cost_one_hour_change_reference_end(mock_data, monkeypatch): + if running_on_github(): + # ToDo Zeitzonen berücksichtigen, damit Tests auf Github laufen + return + cp = init_cp(22500, 1.275, 7) + daily_log = mock_daily_log_with_charging("05/16/2022, 07:45", 12, monkeypatch) + mock_create_entry_reference_end("08:40", daily_log, monkeypatch) + + calculate_charge_cost(cp, True) + + assert cp.data.set.log.costs == 4.8248999999999995 + + +def test_calc_charge_cost_two_hour_change_reference_middle(mock_data, monkeypatch): + if running_on_github(): + # ToDo Zeitzonen berücksichtigen, damit Tests auf Github laufen + return + cp = init_cp(22500, 1.275, 6) + daily_log = mock_daily_log_with_charging("05/16/2022, 06:45", 16, monkeypatch) + current_log = daily_log["entries"][-1] + current_log["date"] = "08:00" + current_log["timestamp"] = datetime.datetime(2022, 5, 16, 8).timestamp() + mock_create_entry = Mock(return_value=current_log) + monkeypatch.setattr(chargelog, "create_entry", mock_create_entry) + mock_today_timestamp = Mock(return_value=1652680801) + monkeypatch.setattr(timecheck, "create_timestamp", mock_today_timestamp) + + calculate_charge_cost(cp, False) + + assert cp.data.set.log.costs == 6.375 + + +def test_calc_charge_cost_two_hour_change_reference_end(mock_data, monkeypatch): + if running_on_github(): + # ToDo Zeitzonen berücksichtigen, damit Tests auf Github laufen + return + cp = init_cp(46500, 6.375, 6) + daily_log = mock_daily_log_with_charging("05/16/2022, 06:45", 24, monkeypatch) + mock_create_entry_reference_end("08:40", daily_log, monkeypatch) + + calculate_charge_cost(cp, True) + + assert cp.data.set.log.costs == 9.924900000000001 diff --git a/packages/control/chargepoint/chargepoint.py b/packages/control/chargepoint/chargepoint.py index c2b25b1922..69e6178ea6 100644 --- a/packages/control/chargepoint/chargepoint.py +++ b/packages/control/chargepoint/chargepoint.py @@ -41,7 +41,7 @@ def get_chargepoint_config_default() -> dict: return { - "name": "Standard-Ladepunkt", + "name": "neuer Ladepunkt", "type": None, "ev": 0, "template": 0, @@ -101,8 +101,8 @@ def _is_grid_protection_inactive(self) -> Tuple[bool, Optional[str]]: general_data.grid_protection_random_stop): state = False message = "Ladepunkt gesperrt, da der Netzschutz aktiv ist." - Pub().pub("openWB/set/general/grid_protection_timestamp", None) - Pub().pub("openWB/set/general/grid_protection_random_stop", 0) + general_data.grid_protection_timestamp = None + general_data.grid_protection_random_stop = 0 else: state = False message = "Ladepunkt gesperrt, da der Netzschutz aktiv ist." @@ -149,8 +149,7 @@ def _is_autolock_inactive(self) -> Tuple[bool, Optional[str]]: state = True else: # Darf Autolock durch Tag überschrieben werden? - if (data.data.optional_data.data.rfid.active and - self.template.data.rfid_enabling): + if data.data.optional_data.data.rfid.active: if self.data.get.rfid is None and self.data.set.rfid is None: state = False message = ("Keine Ladung, da der Ladepunkt durch Autolock gesperrt ist und erst per ID-Tag " @@ -164,13 +163,15 @@ def _is_autolock_inactive(self) -> Tuple[bool, Optional[str]]: return state, message def _is_manual_lock_inactive(self) -> Tuple[bool, Optional[str]]: - if (self.data.set.manual_lock is False or - (self.template.data.rfid_enabling and - (self.data.get.rfid is not None or self.data.set.rfid is not None))): - if self.data.set.manual_lock: + if self.data.set.manual_lock and self.template.data.disable_after_unplug or self.data.set.manual_lock is False: + if ((self.data.get.rfid or self.data.set.rfid) in self.template.data.valid_tags + or self.data.set.manual_lock is False): Pub().pub(f"openWB/set/chargepoint/{self.num}/set/manual_lock", False) - charging_possible = True - message = None + charging_possible = True + message = None + else: + charging_possible = False + message = "Ladepunkt gesperrt, da kein zum Ladepunkt passender ID-Tag gefunden wurde." else: charging_possible = False message = "Keine Ladung, da der Ladepunkt gesperrt wurde." @@ -223,8 +224,7 @@ def _process_charge_stop(self) -> None: self.data.config.ev = 0 Pub().pub("openWB/set/chargepoint/"+str(self.num)+"/config/ev", 0) # Ladepunkt nach Abstecken sperren - if data.data.ev_data[ - "ev"+str(self.data.set.charging_ev_prev)].charge_template.data.disable_after_unplug: + if self.template.data.disable_after_unplug: self.data.set.manual_lock = True Pub().pub("openWB/set/chargepoint/"+str(self.num)+"/set/manual_lock", True) # Ev wurde noch nicht aktualisiert. @@ -458,11 +458,11 @@ def initiate_phase_switch(self): Pub().pub("openWB/set/chargepoint/"+str(self.num)+"/set/phases_to_use", self.data.control_parameter.phases) self.data.set.phases_to_use = self.data.control_parameter.phases - if self._is_phase_switch_required(): - # Wenn die Umschaltverzögerung aktiv ist, darf nicht umgeschaltet werden. - if (self.data.control_parameter.state != ChargepointState.PERFORMING_PHASE_SWITCH and - self.data.control_parameter.state != ChargepointState.WAIT_FOR_USING_PHASES): - if self.cp_ev_support_phase_switch(): + if self.cp_ev_support_phase_switch(): + if self._is_phase_switch_required(): + # Wenn die Umschaltverzögerung aktiv ist, darf nicht umgeschaltet werden. + if (self.data.control_parameter.state != ChargepointState.PERFORMING_PHASE_SWITCH and + self.data.control_parameter.state != ChargepointState.WAIT_FOR_USING_PHASES): log.debug( f"Lp {self.num}: Ladung aktiv halten " f"{charging_ev.ev_template.data.keep_charge_active_duration}s") @@ -490,13 +490,16 @@ def initiate_phase_switch(self): self.data.set.phases_to_use = self.data.control_parameter.phases self.data.control_parameter.state = ChargepointState.PERFORMING_PHASE_SWITCH else: - log.error( - "Phasenumschaltung an Ladepunkt" + str(self.num) + - " nicht möglich, da der Ladepunkt keine Phasenumschaltung unterstützt.") - else: - log.error("Phasenumschaltung an Ladepunkt" + str(self.num) + - " nicht möglich, da gerade eine Umschaltung im Gange ist.") - + log.error("Phasenumschaltung an Ladepunkt" + str(self.num) + + " nicht möglich, da gerade eine Umschaltung im Gange ist.") + elif self.data.control_parameter.state == ChargepointState.PHASE_SWITCH_DELAY_EXPIRED: + # Wenn keine Phasenumschaltung durchgeführt wird, Status auf CHARGING_ALLOWED setzen, sonst + # bleibt PHASE_SWITCH_DELAY_EXPIRED stehen. + self.data.control_parameter.state = ChargepointState.CHARGING_ALLOWED + else: + log.error( + "Phasenumschaltung an Ladepunkt" + str(self.num) + + " nicht möglich, da der Ladepunkt keine Phasenumschaltung unterstützt.") except Exception: log.exception("Fehler in der Ladepunkt-Klasse von "+str(self.num)) @@ -508,9 +511,13 @@ def get_phases_by_selected_chargemode(self) -> int: mode = "time_charging" else: mode = charging_ev.charge_template.data.chargemode.selected - chargemode = data.data.general_data.get_phases_chargemode(mode) + chargemode = data.data.general_data.get_phases_chargemode(mode, self.data.control_parameter.submode) - if chargemode is None: + if (chargemode is None or + (self.data.config.auto_phase_switch_hw is False and self.data.get.charge_state) or + self.data.control_parameter.failed_phase_switches > self.MAX_FAILED_PHASE_SWITCHES): + # Wenn keine Umschaltung verbaut ist, die Phasenzahl nehmen, mit der geladen wird. Damit werden zB auch + # einphasige EV an dreiphasigen openWBs korrekt berücksichtigt. phases = self.data.get.phases_in_use elif (chargemode == 0 and (self.data.set.phases_to_use == self.data.get.phases_in_use or self.data.get.phases_in_use == 0)): @@ -638,6 +645,7 @@ def update(self, ev_list: Dict[str, Ev]) -> None: required_current = self.check_min_max_current( required_current, self.data.control_parameter.phases) charging_ev.set_chargemode_changed(self.data.control_parameter, submode) + charging_ev.set_submode_changed(self.data.control_parameter, submode) self.set_control_parameter(submode, required_current) self.set_required_currents(required_current) @@ -773,11 +781,14 @@ def _pub_connected_vehicle(self, vehicle: Ev): def cp_ev_chargemode_support_phase_switch(self) -> bool: control_parameter = self.data.control_parameter pv_auto_switch = (control_parameter.chargemode == Chargemode.PV_CHARGING and - data.data.general_data.get_phases_chargemode(Chargemode.PV_CHARGING.value) == 0) + data.data.general_data.get_phases_chargemode( + Chargemode.PV_CHARGING.value, + control_parameter.submode) == 0) scheduled_auto_switch = ( control_parameter.chargemode == Chargemode.SCHEDULED_CHARGING and control_parameter.submode == Chargemode.PV_CHARGING and - data.data.general_data.get_phases_chargemode(Chargemode.SCHEDULED_CHARGING.value) == 0) + data.data.general_data.get_phases_chargemode(Chargemode.SCHEDULED_CHARGING.value, + control_parameter.submode) == 0) return (self.cp_ev_support_phase_switch() and self.data.get.charge_state and (pv_auto_switch or scheduled_auto_switch) and diff --git a/packages/control/chargepoint/chargepoint_all.py b/packages/control/chargepoint/chargepoint_all.py index 5f1600c18f..d2fd2e5075 100644 --- a/packages/control/chargepoint/chargepoint_all.py +++ b/packages/control/chargepoint/chargepoint_all.py @@ -3,7 +3,6 @@ from control import data from control.chargepoint.chargepoint_state import ChargepointState -from helpermodules.pub import Pub log = logging.getLogger(__name__) @@ -11,11 +10,11 @@ @dataclass class AllGet: - daily_imported: float = 0 - daily_exported: float = 0 - power: float = 0 - imported: float = 0 - exported: float = 0 + daily_imported: float = field(default=0, metadata={"topic": "get/daily_imported"}) + daily_exported: float = field(default=0, metadata={"topic": "get/daily_exported"}) + power: float = field(default=0, metadata={"topic": "get/power"}) + imported: float = field(default=0, metadata={"topic": "get/imported"}) + exported: float = field(default=0, metadata={"topic": "get/exported"}) def all_get_factory() -> AllGet: @@ -80,10 +79,7 @@ def get_cp_sum(self): except Exception: log.exception("Fehler in der allgemeinen Ladepunkt-Klasse für Ladepunkt "+cp) self.data.get.power = power - Pub().pub("openWB/set/chargepoint/get/power", power) self.data.get.imported = imported - Pub().pub("openWB/set/chargepoint/get/imported", imported) self.data.get.exported = exported - Pub().pub("openWB/set/chargepoint/get/exported", exported) except Exception: log.exception("Fehler in der allgemeinen Ladepunkt-Klasse") diff --git a/packages/control/chargepoint/chargepoint_data.py b/packages/control/chargepoint/chargepoint_data.py index 0388566936..ca4fdf1728 100644 --- a/packages/control/chargepoint/chargepoint_data.py +++ b/packages/control/chargepoint/chargepoint_data.py @@ -77,6 +77,10 @@ class Log: prio: bool = False rfid: Optional[str] = None serial_number: Optional[str] = None + soc_at_start: Optional[int] = None + soc_at_end: Optional[int] = None + range_at_start: Optional[float] = None + range_at_end: Optional[float] = None def connected_vehicle_factory() -> ConnectedVehicle: @@ -118,7 +122,6 @@ def log_factory() -> Log: @dataclass class Set: - change_ev_permitted: bool = False charging_ev: int = -1 charging_ev_prev: int = -1 current: float = 0 @@ -139,11 +142,11 @@ class Set: class Config: configuration: Dict = field(default_factory=empty_dict_factory) ev: int = 0 - name: str = "Standard-Ladepunkt" + name: str = "neuer Ladepunkt" type: Optional[str] = None template: int = 0 connected_phases: int = 3 - phase_1: int = 0 + phase_1: int = 1 auto_phase_switch_hw: bool = False control_pilot_interruption_hw: bool = False id: int = 0 diff --git a/packages/control/chargepoint/chargepoint_template.py b/packages/control/chargepoint/chargepoint_template.py index 736a0e6f48..c2591de2a1 100644 --- a/packages/control/chargepoint/chargepoint_template.py +++ b/packages/control/chargepoint/chargepoint_template.py @@ -36,21 +36,26 @@ def autolock_factory(): @dataclass class CpTemplateData: - autolock: Autolock = field(default_factory=autolock_factory) + autolock: Autolock = field(default_factory=autolock_factory, metadata={"topic": ""}) id: int = 0 max_current_multi_phases: int = 32 max_current_single_phase: int = 32 - name: str = "Standard Ladepunkt-Profil" - rfid_enabling: bool = False + name: str = "neues Ladepunkt-Profil" + disable_after_unplug: bool = False valid_tags: List = field(default_factory=empty_list_factory) +def cp_template_data_factory() -> CpTemplateData: + return CpTemplateData() + + +@dataclass class CpTemplate: """ Profil für einen Ladepunkt. """ - def __init__(self): - self.data: CpTemplateData = CpTemplateData() + data: CpTemplateData = field(default_factory=cp_template_data_factory, metadata={ + "topic": ""}) def is_locked_by_autolock(self, charge_state: bool) -> bool: if self.data.autolock.active: @@ -90,11 +95,11 @@ def get_ev(self, rfid: str, vehicle_id: str, assigned_ev: int) -> int: if data.data.optional_data.data.rfid.active and (rfid is not None or vehicle_id is not None): vehicle = ev_module.get_ev_to_rfid(rfid, vehicle_id) if vehicle is None: - if self.data.rfid_enabling: + if len(self.data.valid_tags) == 0: num = -1 message = ( f"Keine Ladung, da dem ID-Tag {rfid} kein Fahrzeug-Profil zugeordnet werden kann. Eine " - "Freischaltung ist nur mit gültigen ID-Tag möglich.") + "Freischaltung ist nur mit gültigem ID-Tag möglich.") else: num = assigned_ev else: diff --git a/packages/control/chargepoint/control_parameter.py b/packages/control/chargepoint/control_parameter.py index fc151b9e7b..de8daa2ed4 100644 --- a/packages/control/chargepoint/control_parameter.py +++ b/packages/control/chargepoint/control_parameter.py @@ -9,50 +9,28 @@ @dataclass class ControlParameter: - chargemode: Chargemode_enum = field( - default=Chargemode_enum.STOP, - metadata={"topic": "control_parameter/chargemode", "mutable_by_algorithm": True}) - current_plan: Optional[str] = field( - default=None, - metadata={"topic": "control_parameter/current_plan", "mutable_by_algorithm": True}) - failed_phase_switches: int = field( - default=0, - metadata={"topic": "control_parameter/failed_phase_switches", "mutable_by_algorithm": True}) + chargemode: Chargemode_enum = field(default=Chargemode_enum.STOP, metadata={ + "topic": "control_parameter/chargemode"}) + current_plan: Optional[str] = field(default=None, metadata={"topic": "control_parameter/current_plan"}) + failed_phase_switches: int = field(default=0, metadata={"topic": "control_parameter/failed_phase_switches"}) imported_at_plan_start: Optional[float] = field( - default=None, - metadata={"topic": "control_parameter/imported_at_plan_start", "mutable_by_algorithm": True}) + default=None, metadata={"topic": "control_parameter/imported_at_plan_start"}) imported_instant_charging: Optional[float] = field( - default=None, - metadata={"topic": "control_parameter/imported_instant_charging", "mutable_by_algorithm": True}) - limit: Optional[LimitingValue] = field( - default=None, - metadata={"topic": "control_parameter/limit", "mutable_by_algorithm": True}) - phases: int = field( - default=0, - metadata={"topic": "control_parameter/phases", "mutable_by_algorithm": True}) - prio: bool = field( - default=False, - metadata={"topic": "control_parameter/prio", "mutable_by_algorithm": True}) - required_current: float = field( - default=0, - metadata={"topic": "control_parameter/required_current", "mutable_by_algorithm": True}) - required_currents: List[float] = field( - default_factory=currents_list_factory) - state: ChargepointState = field( - default=ChargepointState.NO_CHARGING_ALLOWED, - metadata={"topic": "control_parameter/state", "mutable_by_algorithm": True}) - submode: Chargemode_enum = field( - default=Chargemode_enum.STOP, - metadata={"topic": "control_parameter/submode", "mutable_by_algorithm": True}) + default=None, metadata={"topic": "control_parameter/imported_instant_charging"}) + limit: Optional[LimitingValue] = field(default=None, metadata={"topic": "control_parameter/limit"}) + phases: int = field(default=0, metadata={"topic": "control_parameter/phases"}) + prio: bool = field(default=False, metadata={"topic": "control_parameter/prio"}) + required_current: float = field(default=0, metadata={"topic": "control_parameter/required_current"}) + required_currents: List[float] = field(default_factory=currents_list_factory) + state: ChargepointState = field(default=ChargepointState.NO_CHARGING_ALLOWED, + metadata={"topic": "control_parameter/state"}) + submode: Chargemode_enum = field(default=Chargemode_enum.STOP, metadata={"topic": "control_parameter/submode"}) timestamp_auto_phase_switch: Optional[float] = field( - default=None, - metadata={"topic": "control_parameter/timestamp_auto_phase_switch", "mutable_by_algorithm": True}) + default=None, metadata={"topic": "control_parameter/timestamp_auto_phase_switch"}) timestamp_perform_phase_switch: Optional[float] = field( - default=None, - metadata={"topic": "control_parameter/timestamp_perform_phase_switch", "mutable_by_algorithm": True}) + default=None, metadata={"topic": "control_parameter/timestamp_perform_phase_switch"}) timestamp_switch_on_off: Optional[float] = field( - default=None, - metadata={"topic": "control_parameter/timestamp_switch_on_off", "mutable_by_algorithm": True}) + default=None, metadata={"topic": "control_parameter/timestamp_switch_on_off"}) def control_parameter_factory() -> ControlParameter: diff --git a/packages/control/chargepoint/get_phases_test.py b/packages/control/chargepoint/get_phases_test.py index 1b4f7360f7..4f8f5ce47d 100644 --- a/packages/control/chargepoint/get_phases_test.py +++ b/packages/control/chargepoint/get_phases_test.py @@ -52,10 +52,13 @@ def __init__(self, cases = [ Params("continue using 3", connected_phases=3, auto_phase_switch_hw=False, prevent_phase_switch=True, chargemode_phases=3, phases_in_use=3, imported_since_plugged=0, - expected_phases=3), + expected_phases=3, charge_state=True), + Params("continue using 1, one phase car", connected_phases=3, auto_phase_switch_hw=False, + prevent_phase_switch=True, chargemode_phases=3, phases_in_use=1, imported_since_plugged=0, + expected_phases=1, charge_state=True), Params("continue using 1", connected_phases=1, auto_phase_switch_hw=False, prevent_phase_switch=True, chargemode_phases=1, phases_in_use=1, imported_since_plugged=0, - expected_phases=1), + expected_phases=1, charge_state=True), Params("don't change during phase switch", connected_phases=3, auto_phase_switch_hw=True, prevent_phase_switch=False, chargemode_phases=0, phases_in_use=1, imported_since_plugged=0, expected_phases=1, timestamp_perform_phase_switch="2022/05/11, 15:00:02"), diff --git a/packages/control/counter.py b/packages/control/counter.py index 6cf4252960..580b052dff 100644 --- a/packages/control/counter.py +++ b/packages/control/counter.py @@ -7,13 +7,14 @@ from typing import List, Tuple from control import data +from control.chargemode import Chargemode from control.ev import Ev from control.chargepoint.chargepoint import Chargepoint from control.chargepoint.chargepoint_state import ChargepointState from dataclass_utils.factories import currents_list_factory, voltages_list_factory from helpermodules import timecheck +from helpermodules.constants import NO_ERROR from helpermodules.phase_mapping import convert_cp_currents_to_evu_currents -from helpermodules.pub import Pub from modules.common.fault_state import FaultStateLevel log = logging.getLogger(__name__) @@ -32,8 +33,9 @@ class ControlRangeState(Enum): @dataclass class Config: - max_currents: List[float] = field(default_factory=currents_list_factory) - max_total_power: float = 0 + max_currents: List[float] = field(default_factory=currents_list_factory, metadata={ + "topic": "get/max_currents"}) + max_total_power: float = field(default=0, metadata={"topic": "get/max_total_power"}) def config_factory() -> Config: @@ -42,19 +44,22 @@ def config_factory() -> Config: @dataclass class Get: - powers: List[float] = field(default_factory=currents_list_factory) - currents: List[float] = field(default_factory=currents_list_factory) - voltages: List[float] = field(default_factory=voltages_list_factory) - power_factors: List[float] = field(default_factory=currents_list_factory) - unbalanced_load: float = 0 - frequency: float = 0 - daily_exported: float = 0 - daily_imported: float = 0 - imported: float = 0 - exported: float = 0 - fault_state: int = 0 - fault_str: str = "" - power: float = 0 + powers: List[float] = field(default_factory=currents_list_factory, metadata={ + "topic": "get/powers"}) + currents: List[float] = field(default_factory=currents_list_factory, metadata={ + "topic": "get/currents"}) + voltages: List[float] = field(default_factory=voltages_list_factory, metadata={ + "topic": "get/voltages"}) + power_factors: List[float] = field(default_factory=currents_list_factory, metadata={ + "topic": "get/power_factors"}) + frequency: float = field(default=0, metadata={"topic": "get/frequency"}) + daily_exported: float = field(default=0, metadata={"topic": "get/daily_exported"}) + daily_imported: float = field(default=0, metadata={"topic": "get/daily_imported"}) + imported: float = field(default=0, metadata={"topic": "get/imported"}) + exported: float = field(default=0, metadata={"topic": "get/exported"}) + fault_state: int = field(default=0, metadata={"topic": "get/fault_state"}) + fault_str: str = field(default=NO_ERROR, metadata={"topic": "get/fault_str"}) + power: float = field(default=0, metadata={"topic": "get/power"}) def get_factory() -> Get: @@ -63,13 +68,12 @@ def get_factory() -> Get: @dataclass class Set: - error_counter: int = 0 - reserved_surplus: float = 0 - released_surplus: float = 0 + error_counter: int = field(default=0, metadata={"topic": "set/error_counter"}) + reserved_surplus: float = field(default=0, metadata={"topic": "set/reserved_surplus"}) + released_surplus: float = field(default=0, metadata={"topic": "set/released_surplus"}) raw_power_left: float = 0 raw_currents_left: List[float] = field(default_factory=currents_list_factory) surplus_power_left: float = 0 - state_str: str = "" def set_factory() -> Set: @@ -121,7 +125,6 @@ def _set_loadmanagement_state(self) -> None: # auf True gesetzt werden. if loadmanagement_available is False: data.data.cp_data[cp].data.set.loadmanagement_available = loadmanagement_available - Pub().pub(f"openWB/set/counter/{self.num}/set/error_counter", self.data.set.error_counter) # tested @@ -183,25 +186,12 @@ def update_surplus_values_left(self, diffs) -> None: log.debug(f'Zähler {self.num}: {self.data.set.raw_currents_left}A verbleibende Ströme, ' f'{self.data.set.surplus_power_left}W verbleibender Überschuss') - def put_stats(self): - try: - if f'counter{self.num}' == data.data.counter_all_data.get_evu_counter_str(): - Pub().pub(f"openWB/set/counter/{self.num}/set/reserved_surplus", - self.data.set.reserved_surplus) - Pub().pub(f"openWB/set/counter/{self.num}/set/released_surplus", - self.data.set.released_surplus) - log.info(f'{self.data.set.reserved_surplus}W reservierte EVU-Leistung, ' - f'{self.data.set.released_surplus}W freigegebene EVU-Leistung') - except Exception: - log.exception("Fehler in der Zähler-Klasse von "+str(self.num)) - def calc_surplus(self): # reservierte Leistung wird nicht berücksichtigt, weil diese noch verwendet werden kann, bis die EV # eingeschaltet werden. Es darf bloß nicht für zu viele zB die Einschaltverzögerung gestartet werden. evu_counter = data.data.counter_all_data.get_evu_counter() bat_surplus = data.data.bat_all_data.power_for_bat_charging() surplus = evu_counter.data.get.power - bat_surplus - log.info(f"Überschuss zur PV-geführten Ladung: {surplus}W") return surplus def calc_raw_surplus(self): @@ -214,7 +204,6 @@ def calc_raw_surplus(self): max_power = evu_counter.data.config.max_total_power surplus = raw_power_left - max_power + bat_surplus + disengageable_smarthome_power ranged_surplus = surplus + self._control_range_offset() - log.info(f"Überschuss zur PV-geführten Ladung: {ranged_surplus}W") return ranged_surplus def get_control_range_state(self, feed_in_yield: int) -> ControlRangeState: @@ -249,11 +238,17 @@ def _control_range_offset(self): log.debug(f"Anpassen des Regelbereichs {range_offset}W") return range_offset + def get_usable_surplus(self, feed_in_yield: float) -> float: + # verbleibender EVU-Überschuss unter Berücksichtigung der Einspeisegrenze und Speicherleistung + return (-self.calc_surplus() - self.data.set.released_surplus + + self.data.set.reserved_surplus - feed_in_yield) + SWITCH_ON_FALLEN_BELOW = "Einschaltschwelle während der Einschaltverzögerung unterschritten." SWITCH_ON_WAITING = "Die Ladung wird gestartet, sobald in {} die Einschaltverzögerung abgelaufen ist." SWITCH_ON_NOT_EXCEEDED = ("Die Ladung kann nicht gestartet werden, da die Einschaltschwelle nicht erreicht " "wird.") SWITCH_ON_EXPIRED = "Einschaltschwelle für die Dauer der Einschaltverzögerung überschritten." + SWITCH_ON_MAX_PHASES = "Der Überschuss ist ausreichend, um direkt mit {} Phasen zu laden." def calc_switch_on_power(self, chargepoint: Chargepoint) -> Tuple[float, float]: surplus = self.data.set.surplus_power_left - self.data.set.reserved_surplus @@ -329,6 +324,19 @@ def switch_on_timer_expired(self, chargepoint: Chargepoint) -> None: self.data.set.reserved_surplus -= pv_config.switch_on_threshold*control_parameter.phases msg = self.SWITCH_ON_EXPIRED.format(pv_config.switch_on_threshold) control_parameter.state = ChargepointState.CHARGING_ALLOWED + + if chargepoint.data.set.charging_ev_data.charge_template.data.chargemode.pv_charging.feed_in_limit: + feed_in_yield = pv_config.feed_in_yield + else: + feed_in_yield = 0 + ev_template = chargepoint.data.set.charging_ev_data.ev_template + max_phases_power = ev_template.data.min_current * ev_template.data.max_phases * 230 + if (data.data.general_data.get_phases_chargemode(Chargemode.PV_CHARGING.value, + control_parameter.submode) == 0 and + chargepoint.cp_ev_support_phase_switch() and + self.get_usable_surplus(feed_in_yield) > max_phases_power): + control_parameter.phases = ev_template.data.max_phases + msg += self.SWITCH_ON_MAX_PHASES.format(ev_template.data.max_phases) chargepoint.set_state_and_log(msg) except Exception: log.exception("Fehler im allgemeinen PV-Modul") @@ -480,8 +488,6 @@ def reset_pv_data(self): """ setzt die Daten zurück, die über mehrere Regelzyklen genutzt werden. """ try: - Pub().pub(f"openWB/set/counter/{self.num}/set/reserved_surplus", 0) - Pub().pub(f"openWB/set/counter/{self.num}/set/released_surplus", 0) self.data.set.reserved_surplus = 0 self.data.set.released_surplus = 0 except Exception: diff --git a/packages/control/counter_all.py b/packages/control/counter_all.py index 3c737f563d..fa053f9ced 100644 --- a/packages/control/counter_all.py +++ b/packages/control/counter_all.py @@ -4,7 +4,7 @@ from dataclasses import dataclass, field import logging import re -from typing import Callable, Dict, List, Tuple, Union +from typing import Callable, Dict, List, Optional, Tuple, Union from control import data from control.counter import Counter @@ -20,7 +20,10 @@ @dataclass class Config: - reserve_for_not_charging: bool = False + home_consumption_source_id: Optional[str] = field( + default=None, metadata={"topic": "config/home_consumption_source_id"}) + reserve_for_not_charging: bool = field( + default=False, metadata={"topic": "config/reserve_for_not_charging"}) def config_factory() -> Config: @@ -29,18 +32,26 @@ def config_factory() -> Config: @dataclass class Set: - loadmanagement_active: bool = False - home_consumption: float = 0 - smarthome_power_excluded_from_home_consumption: float = 0 - invalid_home_consumption: int = 0 - daily_yield_home_consumption: float = 0 - imported_home_consumption: float = 0 - disengageable_smarthome_power: float = 0 + loadmanagement_active: bool = field( + default=False, metadata={"topic": "set/loadmanagement_active"}) + home_consumption: float = field(default=0, metadata={"topic": "set/home_consumption"}) + smarthome_power_excluded_from_home_consumption: float = field( + default=0, + metadata={"topic": "set/smarthome_power_excluded_from_home_consumption"}) + invalid_home_consumption: int = field( + default=0, metadata={"topic": "set/invalid_home_consumption"}) + daily_yield_home_consumption: float = field( + default=0, metadata={"topic": "set/daily_yield_home_consumption"}) + imported_home_consumption: float = field( + default=0, metadata={"topic": "set/imported_home_consumption"}) + disengageable_smarthome_power: float = field( + default=0, metadata={"topic": "set/disengageable_smarthome_power"}) @dataclass class Get: - hierarchy: List = field(default_factory=empty_list_factory) + hierarchy: List = field(default_factory=empty_list_factory, metadata={ + "topic": "get/hierarchy"}) def get_factory() -> Get: @@ -59,8 +70,7 @@ class CounterAllData: class CounterAll: - """ - """ + MISSING_EVU_COUNTER = "Bitte erst einen EVU-Zähler konfigurieren." def __init__(self): self.data = CounterAllData() @@ -89,51 +99,63 @@ def get_id_evu_counter(self) -> int: "möglich.") raise - def put_stats(self) -> None: - try: - Pub().pub("openWB/set/counter/set/loadmanagement_active", self.data.set.loadmanagement_active) - except Exception: - log.exception("Fehler in der allgemeinen Zähler-Klasse") - def set_home_consumption(self) -> None: try: + self._validate_home_consumption_counter() home_consumption, elements = self._calc_home_consumption() if home_consumption < 0: log.error( f"Ungültiger Hausverbrauch: {home_consumption}W, Berücksichtigte Komponenten neben EVU {elements}") - evu_counter_data = data.data.counter_data[self.get_evu_counter_str()].data - if evu_counter_data.get.fault_state == FaultStateLevel.NO_ERROR: - evu_counter_data.get.fault_state = FaultStateLevel.WARNING.value - evu_counter_data.get.fault_str = ("Der Wert für den Hausverbrauch ist nicht plausibel (negativ). " - "Bitte die Leistungen der Komponenten und die Anordnung in der " - "Hierarchie prüfen.") - evu_counter = self.get_id_evu_counter() - Pub().pub(f"openWB/set/counter/{evu_counter}/get/fault_state", - evu_counter_data.get.fault_state) - Pub().pub(f"openWB/set/counter/{evu_counter}/get/fault_str", - evu_counter_data.get.fault_str) + if self.data.config.home_consumption_source_id is None: + hc_counter_source = self.get_evu_counter_str() + else: + hc_counter_source = f"counter{self.data.config.home_consumption_source_id}" + hc_counter_data = data.data.counter_data[hc_counter_source].data + if hc_counter_data.get.fault_state == FaultStateLevel.NO_ERROR: + hc_counter_data.get.fault_state = FaultStateLevel.WARNING.value + hc_counter_data.get.fault_str = ("Hinweis: Es gibt mehr Stromerzeuger im Haus als in der openWB " + "eingetragen sind. Der Hausverbrauch kann nicht korrekt berechnet " + "werden. Dies hat auf die PV-Überschussladung keine negativen " + "Auswirkungen.") if self.data.set.invalid_home_consumption < 3: self.data.set.invalid_home_consumption += 1 - Pub().pub("openWB/set/counter/set/invalid_home_consumption", - self.data.set.invalid_home_consumption) return else: home_consumption = 0 else: self.data.set.invalid_home_consumption = 0 - Pub().pub("openWB/set/counter/set/invalid_home_consumption", - self.data.set.invalid_home_consumption) self.data.set.home_consumption = home_consumption - Pub().pub("openWB/set/counter/set/home_consumption", self.data.set.home_consumption) imported, _ = self.sim_counter.sim_count(self.data.set.home_consumption) - Pub().pub("openWB/set/counter/set/imported_home_consumption", imported) self.data.set.imported_home_consumption = imported except Exception: log.exception("Fehler in der allgemeinen Zähler-Klasse") + EVU_IS_HC_COUNTER_ERROR = ("Der EVU-Zähler kann nicht als Quelle für den Hausverbrauch verwendet werden. Meist ist " + "der Zähler am EVU-Punkt installiert, dann muss im Lastmanagement unter Hausverbrauch" + " 'von openWB berechnen' ausgewählt werden. Wenn der Zähler im Hausverbrauchszweig " + "installiert ist, einen virtuellen Zähler anlegen und im Lastmanagement ganz links " + "anordnen.") + + def _validate_home_consumption_counter(self): + if self.data.config.home_consumption_source_id is not None: + if self.data.config.home_consumption_source_id == self.get_id_evu_counter(): + hc_counter_data = data.data.counter_data[self.get_evu_counter_str()].data + hc_counter_data.get.fault_state = FaultStateLevel.ERROR.value + hc_counter_data.get.fault_str = self.EVU_IS_HC_COUNTER_ERROR + evu_counter = self.get_id_evu_counter() + Pub().pub(f"openWB/set/counter/{evu_counter}/get/fault_state", + hc_counter_data.get.fault_state) + Pub().pub(f"openWB/set/counter/{evu_counter}/get/fault_str", + hc_counter_data.get.fault_str) + raise Exception(self.EVU_IS_HC_COUNTER_ERROR) + def _calc_home_consumption(self) -> Tuple[float, List]: power = 0 - elements_to_sum_up = self.get_elements_for_downstream_calculation(self.get_id_evu_counter()) + if self.data.config.home_consumption_source_id is None: + id_source = self.get_id_evu_counter() + else: + id_source = self.data.config.home_consumption_source_id + elements_to_sum_up = self.get_elements_for_downstream_calculation(id_source) for element in elements_to_sum_up: if element["type"] == ComponentType.CHARGEPOINT.value: power += data.data.cp_data[f"cp{element['id']}"].data.get.power @@ -143,7 +165,7 @@ def _calc_home_consumption(self) -> Tuple[float, List]: power += data.data.counter_data[f"counter{element['id']}"].data.get.power elif element["type"] == ComponentType.INVERTER.value: power += data.data.pv_data[f"pv{element['id']}"].data.get.power - evu = data.data.counter_data[self.get_evu_counter_str()].data.get.power + evu = data.data.counter_data[f"counter{id_source}"].data.get.power return evu - power - self.data.set.smarthome_power_excluded_from_home_consumption, elements_to_sum_up def _add_hybrid_bat(self, id: int) -> List: @@ -290,7 +312,6 @@ def hierarchy_add_item_aside(self, new_id: int, new_type: ComponentType, id_to_f """ if self.__is_id_in_top_level(id_to_find): self.data.get.hierarchy.append({"id": new_id, "type": new_type.value, "children": []}) - Pub().pub("openWB/set/counter/get/hierarchy", self.data.get.hierarchy) else: if (self.__edit_element_in_hierarchy( self.data.get.hierarchy[0], @@ -301,7 +322,6 @@ def _add_item_aside( self, child: Dict, current_entry: Dict, id_to_find: int, new_id: int, new_type: ComponentType) -> bool: if id_to_find == child["id"]: current_entry["children"].append({"id": new_id, "type": new_type.value, "children": []}) - Pub().pub("openWB/set/counter/get/hierarchy", self.data.get.hierarchy) return True else: return False @@ -315,7 +335,6 @@ def hierarchy_remove_item(self, id_to_find: int, keep_children: bool = True) -> if keep_children: self.data.get.hierarchy.extend(item["children"]) self.data.get.hierarchy.remove(item) - Pub().pub("openWB/set/counter/get/hierarchy", self.data.get.hierarchy) else: if (self.__edit_element_in_hierarchy( self.data.get.hierarchy[0], @@ -327,18 +346,32 @@ def _remove_item(self, child: Dict, current_entry: Dict, id: str, keep_children: if keep_children: current_entry["children"].extend(child["children"]) current_entry["children"].remove(child) - Pub().pub("openWB/set/counter/get/hierarchy", self.data.get.hierarchy) return True else: return False + def hierarchy_add_item_below_evu(self, new_id: int, new_type: ComponentType) -> None: + try: + self.hierarchy_add_item_below(new_id, new_type, self.get_id_evu_counter()) + except (TypeError, IndexError): + if new_type == ComponentType.COUNTER: + # es gibt noch keinen EVU-Zähler + hierarchy = [{ + "id": new_id, + "type": ComponentType.COUNTER.value, + "children": self.data.get.hierarchy + }] + Pub().pub("openWB/set/counter/get/hierarchy", hierarchy) + self.data.get.hierarchy = hierarchy + else: + raise ValueError(self.MISSING_EVU_COUNTER) + def hierarchy_add_item_below(self, new_id: int, new_type: ComponentType, id_to_find: int) -> None: """ruft die rekursive Funktion zum Hinzufügen eines Elements als Kind des angegebenen Elements. """ item = self.__is_id_in_top_level(id_to_find) if item: item["children"].append({"id": new_id, "type": new_type.value, "children": []}) - Pub().pub("openWB/set/counter/get/hierarchy", self.data.get.hierarchy) else: if (self.__edit_element_in_hierarchy( self.data.get.hierarchy[0], @@ -349,7 +382,6 @@ def _add_item_below( self, child: Dict, current_entry: Dict, id_to_find: int, new_id: int, new_type: ComponentType) -> bool: if id_to_find == child["id"]: child["children"].append({"id": new_id, "type": new_type.value, "children": []}) - Pub().pub("openWB/set/counter/get/hierarchy", self.data.get.hierarchy) return True else: return False @@ -419,24 +451,21 @@ def check_and_add(type_name: ComponentType, data_structure): break else: try: - self.hierarchy_add_item_below(entry_num, type_name, self.get_evu_counter().num) - except (TypeError, IndexError): - # es gibt noch keinen EVU-Zähler - hierarchy = [{ - "id": entry_num, - "type": ComponentType.COUNTER.value, - "children": data.data.counter_all_data.data.get.hierarchy - }] - Pub().pub("openWB/set/counter/get/hierarchy", hierarchy) - data.data.counter_all_data.data.get.hierarchy = hierarchy + self.hierarchy_add_item_below_evu(entry_num, type_name) + except ValueError: + pub_system_message({}, "Die Struktur des Lastmanagements ist nicht plausibel. Bitte prüfe die " + "Konfiguration und Anordnung der Komponenten in der Hierarchie.", + MessageType.WARNING) pub_system_message({}, f"{component_type_to_readable_text(type_name)} mit ID {element['id']} wurde" - " in der Hierarchie hinzugefügt, da kein Eintrag in der Hierarchie gefunden " - "wurde. Bitte prüfe die Anordnung der Komponenten in der Hierarchie.", + " in der Struktur des Lastmanagements hinzugefügt, da kein Eintrag in der " + "Struktur gefunden wurde. Bitte prüfe die Anordnung der Komponenten in der " + "Struktur.", MessageType.WARNING) - check_and_add(ComponentType.BAT, data.data.bat_data) + # Falls EVU-Zähler fehlt, zuerst hinzufügen. check_and_add(ComponentType.COUNTER, data.data.counter_data) + check_and_add(ComponentType.BAT, data.data.bat_data) check_and_add(ComponentType.CHARGEPOINT, data.data.cp_data) check_and_add(ComponentType.INVERTER, data.data.pv_data) diff --git a/packages/control/counter_home_consumption_test.py b/packages/control/counter_home_consumption_test.py index b71a09e650..6303dd63e9 100644 --- a/packages/control/counter_home_consumption_test.py +++ b/packages/control/counter_home_consumption_test.py @@ -3,7 +3,7 @@ import pytest from control import data -from packages.conftest import hierarchy_standard, hierarchy_hybrid, hierarchy_nested +from packages.conftest import hierarchy_hc_counter, hierarchy_standard, hierarchy_hybrid, hierarchy_nested from control.counter_all import CounterAll from modules.common.fault_state import FaultStateLevel @@ -13,16 +13,18 @@ pytest.param(hierarchy_hybrid, id="hybrid"), pytest.param(hierarchy_nested, id="nested")]) def test_calc_home_consumption(counter_all: Callable[[], CounterAll], data_): - # c = counter_all() - - # execution home_consumption = c._calc_home_consumption()[0] - - # evaluation assert home_consumption == 500 +def test_calc_home_consumption_hc_counter(data_hc_counter_): + c = hierarchy_hc_counter() + c.data.config.home_consumption_source_id = 6 + home_consumption = c._calc_home_consumption()[0] + assert home_consumption == 1100 + + @pytest.mark.parametrize(["home_consumption", "invalid_home_consumption", "expected_home_consumption", @@ -50,3 +52,15 @@ def test_set_home_consumption(home_consumption: int, # evaluation assert c.data.set.invalid_home_consumption == expected_invalid_home_consumption assert c.data.set.home_consumption == expected_home_consumption + + +def test_validate_home_consumption_counter(monkeypatch): + c = CounterAll() + c.data.config.home_consumption_source_id = 0 + monkeypatch.setattr(c, "get_id_evu_counter", lambda: 0) + monkeypatch.setattr(c, "get_evu_counter_str", lambda: "counter0") + + with pytest.raises(Exception) as e: + c._validate_home_consumption_counter() + + assert str(e.value) == CounterAll.EVU_IS_HC_COUNTER_ERROR diff --git a/packages/control/ev.py b/packages/control/ev.py index 6c8e173294..71e1a74c4b 100644 --- a/packages/control/ev.py +++ b/packages/control/ev.py @@ -18,6 +18,7 @@ from dataclass_utils.factories import empty_dict_factory, empty_list_factory from helpermodules.abstract_plans import Limit, limit_factory, ScheduledChargingPlan, TimeChargingPlan from helpermodules import timecheck +from helpermodules.constants import NO_ERROR from modules.common.abstract_vehicle import VehicleUpdateData from modules.common.configurable_vehicle import ConfigurableVehicle @@ -28,30 +29,39 @@ def get_vehicle_default() -> dict: return { "charge_template": 0, "ev_template": 0, - "name": "Standard-Fahrzeug", + "name": "neues Fahrzeug", "tag_id": [], "get/soc": 0 } -def get_charge_template_default() -> dict: +def get_new_charge_template() -> dict: ct_default = asdict(ChargeTemplateData()) ct_default["chargemode"]["scheduled_charging"].pop("plans") ct_default["time_charging"].pop("plans") return ct_default + +def get_charge_template_default() -> dict: + ct_default = asdict(ChargeTemplateData(name="Lade-Profil")) + ct_default["chargemode"]["scheduled_charging"].pop("plans") + ct_default["time_charging"].pop("plans") + return ct_default + # Avoid anti-pattern: mutable default arguments @dataclass class ScheduledCharging: - plans: Dict[int, ScheduledChargingPlan] = field(default_factory=empty_dict_factory) + plans: Dict[int, ScheduledChargingPlan] = field(default_factory=empty_dict_factory, metadata={ + "topic": ""}) @dataclass class TimeCharging: active: bool = False - plans: Dict[int, TimeChargingPlan] = field(default_factory=empty_dict_factory) + plans: Dict[int, TimeChargingPlan] = field(default_factory=empty_dict_factory, metadata={ + "topic": ""}) @dataclass @@ -109,8 +119,7 @@ def et_factory() -> Et: @dataclass class ChargeTemplateData: - name: str = "Standard-Lade-Profil" - disable_after_unplug: bool = False + name: str = "neues Lade-Profil" prio: bool = False load_default: bool = False et: Et = field(default_factory=et_factory) @@ -118,9 +127,13 @@ class ChargeTemplateData: chargemode: Chargemode = field(default_factory=chargemode_factory) +def charge_template_data_factory() -> ChargeTemplateData: + return ChargeTemplateData() + + @dataclass class EvTemplateData: - name: str = "Standard-Fahrzeug-Profil" + name: str = "neues Fahrzeug-Profil" max_current_multi_phases: int = 16 max_phases: int = 3 phase_switch_pause: int = 2 @@ -130,7 +143,7 @@ class EvTemplateData: control_pilot_interruption_duration: int = 4 average_consump: float = 17000 min_current: int = 6 - max_current_single_phase: int = 32 + max_current_single_phase: int = 16 battery_capacity: float = 82000 efficiency: float = 90 nominal_difference: float = 1 @@ -146,7 +159,8 @@ class EvTemplate: """ Klasse mit den EV-Daten """ - data: EvTemplateData = field(default_factory=ev_template_data_factory) + data: EvTemplateData = field(default_factory=ev_template_data_factory, metadata={ + "topic": "config"}) et_num: int = 0 @@ -156,7 +170,8 @@ def ev_template_factory() -> EvTemplate: @dataclass class Set: - soc_error_counter: int = 0 + soc_error_counter: int = field( + default=0, metadata={"topic": "set/soc_error_counter"}) def set_factory() -> Set: @@ -165,12 +180,14 @@ def set_factory() -> Set: @dataclass class Get: - soc: Optional[int] = None - soc_timestamp: Optional[float] = None - force_soc_update: bool = False - range: Optional[float] = None - fault_state: int = 0 - fault_str: str = "" + soc: Optional[int] = field(default=None, metadata={"topic": "get/soc"}) + soc_timestamp: Optional[float] = field( + default=None, metadata={"topic": "get/soc_timestamp"}) + force_soc_update: bool = field(default=False, metadata={ + "topic": "get/force_soc_update"}) + range: Optional[float] = field(default=None, metadata={"topic": "get/range"}) + fault_state: int = field(default=0, metadata={"topic": "get/fault_state"}) + fault_str: str = field(default=NO_ERROR, metadata={"topic": "get/fault_str"}) def get_factory() -> Get: @@ -180,10 +197,11 @@ def get_factory() -> Get: @dataclass class EvData: set: Set = field(default_factory=set_factory) - charge_template: int = 0 - ev_template: int = 0 - name: str = "Standard-Fahrzeug" - tag_id: List[str] = field(default_factory=empty_list_factory) + charge_template: int = field(default=0, metadata={"topic": "charge_template"}) + ev_template: int = field(default=0, metadata={"topic": "ev_template"}) + name: str = field(default="neues Fahrzeug", metadata={"topic": "name"}) + tag_id: List[str] = field(default_factory=empty_list_factory, metadata={ + "topic": "tag_id"}) get: Get = field(default_factory=get_factory) @@ -197,6 +215,7 @@ def __init__(self, index: int): self.charge_template: ChargeTemplate = ChargeTemplate(0) self.soc_module: ConfigurableVehicle = None self.chargemode_changed = False + self.submode_changed = False self.num = index self.data = EvData() except Exception: @@ -342,6 +361,9 @@ def set_chargemode_changed(self, control_parameter: ControlParameter, submode: s else: self.chargemode_changed = False + def set_submode_changed(self, control_parameter: ControlParameter, submode: str) -> None: + self.submode_changed = (submode != control_parameter.submode) + def check_min_max_current(self, control_parameter: ControlParameter, required_current: float, @@ -378,8 +400,8 @@ def check_min_max_current(self, CURRENT_OUT_OF_NOMINAL_DIFFERENCE = (", da das Fahrzeug nicht mit der vorgegebenen Stromstärke +/- der erlaubten " + "Stromabweichung aus dem Fahrzeug-Profil lädt.") - ENOUGH_POWER = ", da ausreichend Leistung für mehrphasiges Laden zur Verfügung steht." - NOT_ENOUGH_POWER = ", da nicht ausreichend Leistung für mehrphasiges Laden zur Verfügung steht." + ENOUGH_POWER = ", da ausreichend Überschuss für mehrphasiges Laden zur Verfügung steht." + NOT_ENOUGH_POWER = ", da nicht ausreichend Überschuss für mehrphasiges Laden zur Verfügung steht." def _check_phase_switch_conditions(self, control_parameter: ControlParameter, @@ -398,26 +420,21 @@ def _check_phase_switch_conditions(self, feed_in_yield = pv_config.feed_in_yield else: feed_in_yield = 0 - evu_counter = data.data.counter_all_data.get_evu_counter() - # verbleibender EVU-Überschuss unter Berücksichtigung der Einspeisegrenze und Speicherleistung - all_surplus = (-evu_counter.calc_surplus() - evu_counter.data.set.released_surplus + - evu_counter.data.set.reserved_surplus - feed_in_yield) + all_surplus = data.data.counter_all_data.get_evu_counter().get_usable_surplus(feed_in_yield) + required_surplus = self.ev_template.data.min_current * max_phases_ev * 230 - get_power condition_1_to_3 = (((max(get_currents) > max_current and - all_surplus > self.ev_template.data.min_current * max_phases_ev * 230 - - get_power) or limit == LimitingValue.UNBALANCED_LOAD.value) and + all_surplus > required_surplus) or limit == LimitingValue.UNBALANCED_LOAD.value) and phases_in_use == 1) condition_3_to_1 = max(get_currents) < min_current and all_surplus <= 0 and phases_in_use > 1 if condition_1_to_3 or condition_3_to_1: return True, None else: - if ((phases_in_use > 1 and max(get_currents) > min_current) or - (phases_in_use == 1 and max(get_currents) < max_current)): - return False, self.CURRENT_OUT_OF_NOMINAL_DIFFERENCE + if phases_in_use > 1 and all_surplus > 0: + return False, self.ENOUGH_POWER + elif phases_in_use == 1 and all_surplus < required_surplus: + return False, self.NOT_ENOUGH_POWER else: - if phases_in_use > 1: - return False, self.ENOUGH_POWER - else: - return False, self.NOT_ENOUGH_POWER + return False, self.CURRENT_OUT_OF_NOMINAL_DIFFERENCE PHASE_SWITCH_DELAY_TEXT = '{} Phasen in {}.' @@ -435,17 +452,15 @@ def auto_phase_switch(self, phases_to_use = control_parameter.phases phases_in_use = control_parameter.phases pv_config = data.data.general_data.data.chargemode_config.pv_charging + cm_config = data.data.general_data.data.chargemode_config if self.charge_template.data.chargemode.pv_charging.feed_in_limit: feed_in_yield = pv_config.feed_in_yield else: feed_in_yield = 0 - evu_counter = data.data.counter_all_data.get_evu_counter() - # verbleibender EVU-Überschuss unter Berücksichtigung der Einspeisegrenze und Speicherleistung - all_surplus = (-evu_counter.calc_surplus() - evu_counter.data.set.released_surplus + - evu_counter.data.set.reserved_surplus - feed_in_yield) + all_surplus = data.data.counter_all_data.get_evu_counter().get_usable_surplus(feed_in_yield) if phases_in_use == 1: direction_str = f"Umschaltung von 1 auf {max_phases}" - delay = pv_config.phase_switch_delay * 60 + delay = cm_config.phase_switch_delay * 60 required_reserved_power = (self.ev_template.data.min_current * max_phases * 230 - self.ev_template.data.max_current_single_phase * 230) @@ -453,7 +468,7 @@ def auto_phase_switch(self, new_current = self.ev_template.data.min_current else: direction_str = f"Umschaltung von {max_phases} auf 1" - delay = (16 - pv_config.phase_switch_delay) * 60 + delay = (16 - cm_config.phase_switch_delay) * 60 # Es kann einphasig mit entsprechend niedriger Leistung gestartet werden. required_reserved_power = 0 new_phase = 1 @@ -481,6 +496,8 @@ def auto_phase_switch(self, direction_str, timecheck.convert_timestamp_delta_to_time_string(timestamp_auto_phase_switch, delay)) control_parameter.state = ChargepointState.PHASE_SWITCH_DELAY + elif condition_msg: + log.debug(f"Keine Phasenumschaltung{condition_msg}") else: if condition: # Timer laufen lassen @@ -554,16 +571,17 @@ class SelectedPlan: num: int = 0 +@dataclass class ChargeTemplate: """ Klasse der Lade-Profile """ + ct_num: int + data: ChargeTemplateData = field(default_factory=charge_template_data_factory, metadata={ + "topic": ""}) + BUFFER = -1200 # nach mehr als 20 Min Überschreitung wird der Termin als verpasst angesehen CHARGING_PRICE_EXCEEDED = "Keine Ladung, da der aktuelle Strompreis über dem maximalen Strompreis liegt." - def __init__(self, index): - self.data: ChargeTemplateData = ChargeTemplateData() - self.ct_num = index - TIME_CHARGING_NO_PLAN_CONFIGURED = "Keine Ladung, da keine Zeitfenster für Zeitladen konfiguriert sind." TIME_CHARGING_NO_PLAN_ACTIVE = "Keine Ladung, da kein Zeitfenster für Zeitladen aktiv ist." TIME_CHARGING_SOC_REACHED = "Kein Zeitladen, da der Soc bereits erreicht wurde." @@ -679,7 +697,8 @@ def scheduled_charging_recent_plan(self, Ladestrom ein. Um etwas mehr Puffer zu haben, wird bis 20 Min nach dem Zieltermin noch geladen, wenn dieser nicht eingehalten werden konnte. """ - if phase_switch_supported and data.data.general_data.get_phases_chargemode("scheduled_charging") == 0: + if phase_switch_supported and data.data.general_data.get_phases_chargemode("scheduled_charging", + "instant_charging") == 0: max_current = ev_template.data.max_current_multi_phases plan_data = self.search_plan(max_current, soc, ev_template, max_phases, used_amount) if plan_data: @@ -755,7 +774,11 @@ def calculate_duration(self, duration = missing_amount/(plan.current * phases*230) * 3600 return duration, missing_amount - SCHEDULED_CHARGING_REACHED_LIMIT_SOC = "Kein Zielladen, da der Ziel-Soc und das SoC-Limit bereits erreicht wurden." + SCHEDULED_REACHED_LIMIT_SOC = ("Kein Zielladen, da noch Zeit bis zum Zieltermin ist. " + "Kein Zielladen mit Überschuss, da das SoC-Limit für Überschuss-Laden " + + "erreicht wurde.") + SCHEDULED_CHARGING_REACHED_LIMIT_SOC = ("Kein Zielladen, da das Limit für Fahrzeug Laden mit Überschuss (SoC-Limit)" + " sowie der Fahrzeug-SoC (Ziel-SoC) bereits erreicht wurde.") SCHEDULED_CHARGING_REACHED_AMOUNT = "Kein Zielladen, da die Energiemenge bereits erreicht wurde." SCHEDULED_CHARGING_REACHED_SCHEDULED_SOC = ("Falls vorhanden wird mit EVU-Überschuss geladen, da der Ziel-Soc " "für Zielladen bereits erreicht wurde.") @@ -767,7 +790,8 @@ def calculate_duration(self, "Es wird bis max. 20 Minuten nach dem angegebenen Zieltermin geladen.") SCHEDULED_CHARGING_LIMITED_BY_SOC = 'einen SoC von {}%' SCHEDULED_CHARGING_LIMITED_BY_AMOUNT = '{}kWh geladene Energie' - SCHEDULED_CHARGING_IN_TIME = 'Zielladen mit {}A, um {} um {} zu erreichen.' + SCHEDULED_CHARGING_IN_TIME = ('Zielladen mit mindestens {}A, um {} um {} zu erreichen. Falls vorhanden wird ' + 'zusätzlich EVU-Überschuss geladen.') SCHEDULED_CHARGING_CHEAP_HOUR = "Zielladen, da ein günstiger Zeitpunkt zum preisbasierten Laden ist." SCHEDULED_CHARGING_EXPENSIVE_HOUR = ("Zielladen ausstehend, da jetzt kein günstiger Zeitpunkt zum preisbasierten " "Laden ist. Falls vorhanden, wird mit Überschuss geladen.") @@ -780,22 +804,22 @@ def scheduled_charging_calc_current(self, min_current: int, soc_request_intervall_offset: int) -> Tuple[float, str, str, int]: current = 0 - mode = "stop" + submode = "stop" if plan_data is None: if len(self.data.chargemode.scheduled_charging.plans) == 0: - return current, mode, self.SCHEDULED_CHARGING_NO_PLANS_CONFIGURED, control_parameter_phases + return current, submode, self.SCHEDULED_CHARGING_NO_PLANS_CONFIGURED, control_parameter_phases else: - return current, mode, self.SCHEDULED_CHARGING_NO_DATE_PENDING, control_parameter_phases + return current, submode, self.SCHEDULED_CHARGING_NO_DATE_PENDING, control_parameter_phases current_plan = self.data.chargemode.scheduled_charging.plans[plan_data.num] limit = current_plan.limit phases = plan_data.phases log.debug("Verwendeter Plan: "+str(current_plan.name)) - if limit.selected == "soc" and soc >= limit.soc_limit: + if limit.selected == "soc" and soc >= limit.soc_limit and soc >= limit.soc_scheduled: message = self.SCHEDULED_CHARGING_REACHED_LIMIT_SOC elif limit.selected == "soc" and limit.soc_scheduled <= soc < limit.soc_limit: message = self.SCHEDULED_CHARGING_REACHED_SCHEDULED_SOC current = min_current - mode = "pv_charging" + submode = "pv_charging" # bei Überschuss-Laden mit der Phasenzahl aus den control_parameter laden, # um die Umschaltung zu berücksichtigen. phases = control_parameter_phases @@ -810,7 +834,7 @@ def scheduled_charging_calc_current(self, message = self.SCHEDULED_CHARGING_IN_TIME.format( plan_data.available_current, limit_string, current_plan.time) current = plan_data.available_current - mode = "instant_charging" + submode = "instant_charging" # weniger als die berechnete Zeit verfügbar # Ladestart wurde um maximal 20 Min verpasst. elif plan_data.remaining_time <= 0 - soc_request_intervall_offset: @@ -820,7 +844,7 @@ def scheduled_charging_calc_current(self, current = min(plan_data.missing_amount/((plan_data.duration + plan_data.remaining_time) / 3600)/(phases*230), plan_data.max_current) message = self.SCHEDULED_CHARGING_MAX_CURRENT.format(round(current, 2)) - mode = "instant_charging" + submode = "instant_charging" else: # Wenn Elektronische Tarife aktiv sind, prüfen, ob jetzt ein günstiger Zeitpunkt zum Laden # ist. @@ -830,18 +854,24 @@ def scheduled_charging_calc_current(self, if timecheck.is_list_valid(hourlist): message = self.SCHEDULED_CHARGING_CHEAP_HOUR current = plan_data.available_current - mode = "instant_charging" - else: + submode = "instant_charging" + elif soc <= limit.soc_limit: message = self.SCHEDULED_CHARGING_EXPENSIVE_HOUR current = min_current - mode = "pv_charging" + submode = "pv_charging" phases = control_parameter_phases + else: + message = self.SCHEDULED_REACHED_LIMIT_SOC else: - message = self.SCHEDULED_CHARGING_USE_PV - current = min_current - mode = "pv_charging" - phases = control_parameter_phases - return current, mode, message, phases + # Wenn SoC-Limit erreicht wurde, soll nicht mehr mit Überschuss geladen werden + if soc >= limit.soc_limit: + message = self.SCHEDULED_REACHED_LIMIT_SOC + else: + message = self.SCHEDULED_CHARGING_USE_PV + current = min_current + submode = "pv_charging" + phases = control_parameter_phases + return current, submode, message, phases def standby(self) -> Tuple[int, str, str]: return 0, "standby", "Keine Ladung, da der Lademodus Standby aktiv ist." diff --git a/packages/control/ev_charge_template_test.py b/packages/control/ev_charge_template_test.py index a990f892a2..22be9e628f 100644 --- a/packages/control/ev_charge_template_test.py +++ b/packages/control/ev_charge_template_test.py @@ -26,24 +26,24 @@ def data_module() -> None: pytest.param({"0": TimeChargingPlan()}, 0, 0, None, (0, "stop", ChargeTemplate.TIME_CHARGING_NO_PLAN_ACTIVE, None), id="no plan active"), pytest.param({"0": TimeChargingPlan()}, 0, 0, TimeChargingPlan(), - (16, "time_charging", None, "Zeitladen-Standard"), id="plan active"), + (16, "time_charging", None, "neuer Zeitladen-Plan"), id="plan active"), pytest.param({"0": TimeChargingPlan(limit=Limit(selected="soc"))}, 100, 0, TimeChargingPlan(limit=Limit(selected="soc")), - (0, "stop", ChargeTemplate.TIME_CHARGING_SOC_REACHED, "Zeitladen-Standard"), + (0, "stop", ChargeTemplate.TIME_CHARGING_SOC_REACHED, "neuer Zeitladen-Plan"), id="plan active, soc is reached"), pytest.param({"0": TimeChargingPlan(limit=Limit(selected="soc"))}, 40, 0, TimeChargingPlan(limit=Limit(selected="soc")), - (16, "time_charging", None, "Zeitladen-Standard"), id="plan active, soc is not reached"), + (16, "time_charging", None, "neuer Zeitladen-Plan"), id="plan active, soc is not reached"), pytest.param({"0": TimeChargingPlan(limit=Limit(selected="soc"))}, None, 0, TimeChargingPlan(limit=Limit(selected="soc")), - (16, "time_charging", None, "Zeitladen-Standard"), id="plan active, soc is not defined"), + (16, "time_charging", None, "neuer Zeitladen-Plan"), id="plan active, soc is not defined"), pytest.param({"0": TimeChargingPlan(limit=Limit(selected="amount"))}, 0, 1500, TimeChargingPlan(limit=Limit(selected="amount")), - (0, "stop", ChargeTemplate.TIME_CHARGING_AMOUNT_REACHED, "Zeitladen-Standard"), + (0, "stop", ChargeTemplate.TIME_CHARGING_AMOUNT_REACHED, "neuer Zeitladen-Plan"), id="plan active, used_amount_time_charging is reached"), pytest.param({"0": TimeChargingPlan(limit=Limit(selected="amount"))}, 0, 500, TimeChargingPlan(limit=Limit(selected="amount")), - (16, "time_charging", None, "Zeitladen-Standard"), + (16, "time_charging", None, "neuer Zeitladen-Plan"), id="plan active, used_amount_time_charging is not reached"), pytest.param({"0": TimeChargingPlan()}, 0, 0, None, (0, "stop", ChargeTemplate.TIME_CHARGING_NO_PLAN_ACTIVE, None), id="plan defined but not found"), diff --git a/packages/control/general.py b/packages/control/general.py index 157f5b8659..3b7d4f3418 100644 --- a/packages/control/general.py +++ b/packages/control/general.py @@ -4,12 +4,11 @@ from enum import Enum import logging import random -from typing import List, Optional +from typing import Dict, List, Optional from control import data from control.bat_all import BatConsiderationMode from helpermodules.constants import NO_ERROR -from helpermodules.pub import Pub from helpermodules import timecheck from modules.common.configurable_ripple_control_receiver import ConfigurableRcr from modules.ripple_control_receivers.gpio.config import GpioRcr @@ -20,7 +19,8 @@ @dataclass class InstantCharging: - phases_to_use: int = 1 + phases_to_use: int = field(default=1, metadata={ + "topic": "chargemode_config/instant_charging/phases_to_use"}) def instant_charging_factory() -> InstantCharging: @@ -33,20 +33,34 @@ def control_range_factory() -> List: @dataclass class PvCharging: - bat_power_reserve: int = 2000 - bat_power_reserve_active: bool = False - control_range: List = field(default_factory=control_range_factory) - feed_in_yield: int = 15000 - phase_switch_delay: int = 7 - phases_to_use: int = 1 - bat_power_discharge: int = 1500 - bat_power_discharge_active: bool = False - min_bat_soc: int = 50 - bat_mode: BatConsiderationMode = BatConsiderationMode.EV_MODE.value - switch_off_delay: int = 60 - switch_off_threshold: int = 5 - switch_on_delay: int = 30 - switch_on_threshold: int = 1500 + bat_power_reserve: int = field(default=2000, metadata={ + "topic": "chargemode_config/pv_charging/bat_power_reserve"}) + bat_power_reserve_active: bool = field(default=False, metadata={ + "topic": "chargemode_config/pv_charging/bat_power_reserve_active"}) + control_range: List = field(default_factory=control_range_factory, metadata={ + "topic": "chargemode_config/pv_charging/control_range"}) + feed_in_yield: int = field(default=15000, metadata={ + "topic": "chargemode_config/pv_charging/feed_in_yield"}) + phase_switch_delay: int = field(default=7, metadata={ + "topic": "chargemode_config/pv_charging/phase_switch_delay"}) + phases_to_use: int = field(default=1, metadata={ + "topic": "chargemode_config/pv_charging/phases_to_use"}) + bat_power_discharge: int = field(default=1500, metadata={ + "topic": "chargemode_config/pv_charging/bat_power_discharge"}) + bat_power_discharge_active: bool = field(default=False, metadata={ + "topic": "chargemode_config/pv_charging/bat_power_discharge_active"}) + min_bat_soc: int = field(default=50, metadata={ + "topic": "chargemode_config/pv_charging/min_bat_soc"}) + bat_mode: BatConsiderationMode = field(default=BatConsiderationMode.EV_MODE.value, metadata={ + "topic": "chargemode_config/pv_charging/bat_mode"}) + switch_off_delay: int = field(default=60, metadata={ + "topic": "chargemode_config/pv_charging/switch_off_delay"}) + switch_off_threshold: int = field(default=5, metadata={ + "topic": "chargemode_config/pv_charging/switch_off_threshold"}) + switch_on_delay: int = field(default=30, metadata={ + "topic": "chargemode_config/pv_charging/switch_on_delay"}) + switch_on_threshold: int = field(default=1500, metadata={ + "topic": "chargemode_config/pv_charging/switch_on_threshold"}) def pv_charging_factory() -> PvCharging: @@ -55,7 +69,10 @@ def pv_charging_factory() -> PvCharging: @dataclass class ScheduledCharging: - phases_to_use: int = 0 + phases_to_use: int = field(default=0, metadata={ + "topic": "chargemode_config/scheduled_charging/phases_to_use"}) + phases_to_use_pv: int = field(default=0, metadata={ + "topic": "chargemode_config/scheduled_charging/phases_to_use_pv"}) def scheduled_charging_factory() -> ScheduledCharging: @@ -64,7 +81,8 @@ def scheduled_charging_factory() -> ScheduledCharging: @dataclass class TimeCharging: - phases_to_use: int = 1 + phases_to_use: int = field(default=1, metadata={ + "topic": "chargemode_config/time_charging/phases_to_use"}) def time_charging_factory() -> TimeCharging: @@ -74,12 +92,18 @@ def time_charging_factory() -> TimeCharging: @dataclass class ChargemodeConfig: instant_charging: InstantCharging = field(default_factory=instant_charging_factory) + phase_switch_delay: int = field(default=7, metadata={ + "topic": "chargemode_config/phase_switch_delay"}) pv_charging: PvCharging = field(default_factory=pv_charging_factory) - retry_failed_phase_switches: bool = False + retry_failed_phase_switches: bool = field( + default=False, + metadata={"topic": "chargemode_config/retry_failed_phase_switches"}) scheduled_charging: ScheduledCharging = field(default_factory=scheduled_charging_factory) time_charging: TimeCharging = field(default_factory=time_charging_factory) - unbalanced_load_limit: int = 18 - unbalanced_load: bool = False + unbalanced_load_limit: int = field( + default=18, metadata={"topic": "chargemode_config/unbalanced_load_limit"}) + unbalanced_load: bool = field(default=False, metadata={ + "topic": "chargemode_config/unbalanced_load"}) def chargemode_config_factory() -> ChargemodeConfig: @@ -88,9 +112,12 @@ def chargemode_config_factory() -> ChargemodeConfig: @dataclass class RippleControlReceiverGet: - fault_state: int = 0 - fault_str: str = NO_ERROR - override_value: float = 100 + fault_state: int = field(default=0, metadata={ + "topic": "ripple_control_receiver/get/fault_state"}) + fault_str: str = field(default=NO_ERROR, metadata={ + "topic": "ripple_control_receiver/get/fault_str"}) + override_value: float = field(default=100, metadata={ + "topic": "ripple_control_receiver/get/override_value"}) def rcr_get_factory() -> RippleControlReceiverGet: @@ -109,8 +136,10 @@ class OverrideReference(Enum): @dataclass class RippleControlReceiver: get: RippleControlReceiverGet = field(default_factory=rcr_get_factory) - module: ConfigurableRcr = field(default_factory=gpio_rcr_factory) - overrice_reference: OverrideReference = OverrideReference.CHARGEPOINT + module: Optional[Dict] = field(default=None, metadata={ + "topic": "ripple_control_receiver/module"}) + override_reference: OverrideReference = field(default=OverrideReference.CHARGEPOINT, metadata={ + "topic": "ripple_control_receiver/override_reference"}) def ripple_control_receiver_factory() -> RippleControlReceiver: @@ -119,10 +148,10 @@ def ripple_control_receiver_factory() -> RippleControlReceiver: @dataclass class Prices: - bat: float = 0.0002 - cp: float = 0 - grid: float = 0.0003 - pv: float = 0.00015 + bat: float = field(default=0.0002, metadata={"topic": "prices/bat"}) + cp: float = field(default=0, metadata={"topic": "prices/cp"}) + grid: float = field(default=0.0003, metadata={"topic": "prices/grid"}) + pv: float = field(default=0.00015, metadata={"topic": "prices/pv"}) def prices_factory() -> Prices: @@ -132,14 +161,22 @@ def prices_factory() -> Prices: @dataclass class GeneralData: chargemode_config: ChargemodeConfig = field(default_factory=chargemode_config_factory) - control_interval: int = 10 - extern_display_mode: str = "primary" - extern: bool = False - external_buttons_hw: bool = False - grid_protection_active: bool = False - grid_protection_configured: bool = True - grid_protection_random_stop: int = 0 - grid_protection_timestamp: Optional[float] = "" + control_interval: int = field(default=10, metadata={"topic": "control_interval"}) + extern_display_mode: str = field(default="primary", metadata={ + "topic": "extern_display_mode"}) + extern: bool = field(default=False, metadata={"topic": "extern"}) + external_buttons_hw: bool = field( + default=False, metadata={"topic": "external_buttons_hw"}) + grid_protection_active: bool = field( + default=False, metadata={"topic": "grid_protection_active"}) + grid_protection_configured: bool = field( + default=True, metadata={"topic": "grid_protection_configured"}) + grid_protection_random_stop: int = field( + default=0, metadata={"topic": "grid_protection_random_stop"}) + grid_protection_timestamp: Optional[float] = field( + default=None, metadata={"topic": "grid_protection_timestamp"}) + http_api: bool = field( + default=False, metadata={"topic": "http_api"}) mqtt_bridge: bool = False prices: Prices = field(default_factory=prices_factory) range_unit: str = "km" @@ -152,8 +189,9 @@ class General: def __init__(self): self.data: GeneralData = GeneralData() + self.ripple_control_receiver: ConfigurableRcr = None - def get_phases_chargemode(self, chargemode: str) -> Optional[int]: + def get_phases_chargemode(self, chargemode: str, submode: str) -> Optional[int]: """ gibt die Anzahl Phasen zurück, mit denen im jeweiligen Lademodus geladen wird. Wenn der Lademodus Stop oder Standby ist, wird 0 zurückgegeben, da in diesem Fall die bisher genutzte Phasenzahl weiter genutzt wird, bis der Algorithmus eine Umschaltung vorgibt. @@ -162,6 +200,9 @@ def get_phases_chargemode(self, chargemode: str) -> Optional[int]: if chargemode == "stop" or chargemode == "standby": # bei diesen Lademodi kann die bisherige Phasenzahl beibehalten werden. return None + elif chargemode == "scheduled_charging" and submode == "pv_charging": + # Phasenumschaltung bei PV-Ueberschuss nutzen + return getattr(self.data.chargemode_config, chargemode).phases_to_use_pv else: return getattr(self.data.chargemode_config, chargemode).phases_to_use except Exception: @@ -185,36 +226,20 @@ def grid_protection(self): self.data.grid_protection_timestamp = timecheck.create_timestamp( ) self.data.grid_protection_active = True - Pub().pub("openWB/set/general/grid_protection_timestamp", - self.data.grid_protection_timestamp) - Pub().pub("openWB/set/general/grid_protection_random_stop", - self.data.grid_protection_random_stop) - Pub().pub("openWB/set/general/grid_protection_active", - self.data.grid_protection_active) log.info("Netzschutz aktiv! Frequenz: " + str(data.data.counter_data[evu_counter].data.get.frequency)+"Hz") if 5180 < frequency < 5300: self.data.grid_protection_random_stop = 0 self.data.grid_protection_timestamp = None self.data.grid_protection_active = True - Pub().pub("openWB/set/general/grid_protection_timestamp", - self.data.grid_protection_timestamp) - Pub().pub("openWB/set/general/grid_protection_random_stop", - self.data.grid_protection_random_stop) - Pub().pub("openWB/set/general/grid_protection_active", - self.data.grid_protection_active) log.info("Netzschutz aktiv! Frequenz: " + str(data.data.counter_data[evu_counter].data.get.frequency)+"Hz") else: if 4962 < frequency < 5100: self.data.grid_protection_active = False - Pub().pub("openWB/set/general/grid_protection_active", - self.data.grid_protection_active) log.info("Netzfrequenz wieder im normalen Bereich. Frequenz: " + str(data.data.counter_data[evu_counter].data.get.frequency)+"Hz") - Pub().pub( - "openWB/set/general/grid_protection_timestamp", None) - Pub().pub( - "openWB/set/general/grid_protection_random_stop", 0) + self.data.grid_protection_timestamp = None + self.data.grid_protection_random_stop = 0 except Exception: log.exception("Fehler im General-Modul") diff --git a/packages/control/hierarchy_test.py b/packages/control/hierarchy_test.py index 2e142e7ec5..297a9d7a3b 100644 --- a/packages/control/hierarchy_test.py +++ b/packages/control/hierarchy_test.py @@ -367,7 +367,23 @@ def test_delete_obsolete_entries(hierarchy, data_): {"id": 5, "type": "cp", "children": []}]}, {"id": 2, "type": "bat", "children": []}, {"id": 1, "type": "inverter", "children": []} - ]}], id="add inverter 1") + ]}], id="add inverter 1"), + pytest.param([{"id": 3, "type": "cp", "children": []}, + {"id": 6, "type": "counter", + "children": [ + {"id": 4, "type": "cp", "children": []}, + {"id": 5, "type": "cp", "children": []}]}, + {"id": 2, "type": "bat", "children": []}], + [{"id": 0, "type": "counter", + "children": [ + {"id": 3, "type": "cp", "children": []}, + {"id": 6, "type": "counter", + "children": [ + {"id": 4, "type": "cp", "children": []}, + {"id": 5, "type": "cp", "children": []}]}, + {"id": 2, "type": "bat", "children": []}, + {"id": 1, "type": "inverter", "children": []} + ]}], id="add evu counter 0") ] ) def test_add_missing_entries(hierarchy, expected_hierarchy, data_, monkeypatch): diff --git a/packages/control/prepare.py b/packages/control/prepare.py index 2bbc9ca383..ee32f486ee 100644 --- a/packages/control/prepare.py +++ b/packages/control/prepare.py @@ -18,6 +18,8 @@ def setup_algorithm(self) -> None: """ bereitet die Daten für den Algorithmus vor und startet diesen. """ try: + data.data.pv_all_data.calc_power_for_all_components() + data.data.bat_all_data.calc_power_for_all_components() for cp in data.data.cp_data.values(): cp.setup_values_at_start() data.data.bat_all_data.setup_bat() diff --git a/packages/control/process.py b/packages/control/process.py index bf34f8579f..e89076e16c 100644 --- a/packages/control/process.py +++ b/packages/control/process.py @@ -69,8 +69,6 @@ def process_algorithm_results(self) -> None: log.error( thread.name + " konnte nicht innerhalb des Timeouts die Werte senden.") - - data.data.counter_all_data.get_evu_counter().put_stats() except Exception: log.exception("Fehler im Process-Modul") diff --git a/packages/dataclass_utils/_dataclass_asdict.py b/packages/dataclass_utils/_dataclass_asdict.py index eee6178f67..5d43362065 100644 --- a/packages/dataclass_utils/_dataclass_asdict.py +++ b/packages/dataclass_utils/_dataclass_asdict.py @@ -20,5 +20,4 @@ def asdict(value): return [None if v is None else asdict(v) for v in value] if not isinstance(value, dict): value = vars(value) - log.debug(value) return {key: None if value is None else asdict(value) for key, value in value.items()} diff --git a/packages/helpermodules/abstract_plans.py b/packages/helpermodules/abstract_plans.py index 77fa3752d4..0e2967cfe8 100644 --- a/packages/helpermodules/abstract_plans.py +++ b/packages/helpermodules/abstract_plans.py @@ -62,18 +62,18 @@ class TimeframePlan(PlanBase): @dataclass class ScheduledChargingPlan(PlanBase): current: int = 14 - name: str = "Zielladen-Standard" + name: str = "neuer Zielladen-Plan" limit: ScheduledLimit = field(default_factory=scheduled_limit_factory) time: str = "07:00" # ToDo: aktuelle Zeit verwenden @dataclass class TimeChargingPlan(TimeframePlan): - name: str = "Zeitladen-Standard" + name: str = "neuer Zeitladen-Plan" current: int = 16 limit: Limit = field(default_factory=limit_factory) @dataclass class AutolockPlan(TimeframePlan): - name: str = "Standard Autolock-Plan" + name: str = "neuer Autolock-Plan" diff --git a/packages/helpermodules/changed_values_handler.py b/packages/helpermodules/changed_values_handler.py index 2a8b093255..5f202e1980 100644 --- a/packages/helpermodules/changed_values_handler.py +++ b/packages/helpermodules/changed_values_handler.py @@ -14,9 +14,8 @@ # In den Metadaten wird unter dem Key der Topic-Suffix ab "openWB/ev/2/" angegeben. Der Topic-Prefix ("openWB/ev/2/") # wird automatisch ermittelt. -# Der Key mutable_by_algorithm ist True, wenn die Werte im Algorithmus geändert werden können. Nur diese Werte werden -# automatisiert gepublished. Werte, die an anderer Stelle gepublished werden, wie zB Zählerstände, werden mit False -# gekennzeichnet. Um eine Dokumentation der Topics zu erhalten, sollten die Metadaten dennoch ausgefüllt werden. +# Der Kontextmanager muss immer verwendet werden, wenn in den Funktionen Werte geändert werden, die nicht gepublished +# werden. # Metadaten werden nur für Felder erzeugt, die gepublished werden sollen, dh bei ganzen Klassen für das Feld der # jeweiligen Klasse. Wenn Werte aus einer instanziierten Klasse gepublished werden sollen, erhält die übergeordnete # Klasse keine Metadaten (siehe Beispiel unten). @@ -34,8 +33,8 @@ # @dataclass # class SampleNested: -# parameter1: bool = field(default=False, metadata={"topic": "get/nested1", "mutable_by_algorithm": True}) -# parameter2: int = field(default=0, metadata={"topic": "get/nested2", "mutable_by_algorithm": True}) +# parameter1: bool = field(default=False, metadata={"topic": "get/nested1"}) +# parameter2: int = field(default=0, metadata={"topic": "get/nested2"}) # def sample_nested() -> SampleNested: @@ -48,12 +47,12 @@ # diese Klasse eingetragen. Die Felder der Konfigurationsklasse bekommen keine Metadaten, da diese nicht einzeln # gepublished werden. # sample_field_class: SampleClass = field( -# default_factory=sample_class, metadata={"topic": "get/field_class", "mutable_by_algorithm": True}) -# sample_field_int: int = field(default=0, metadata={"topic": "get/field_int", "mutable_by_algorithm": True}) +# default_factory=sample_class, metadata={"topic": "get/field_class"}) +# sample_field_int: int = field(default=0, metadata={"topic": "get/field_int"}) # sample_field_immutable: float = field( -# default=0, metadata={"topic": "get/field_immutable", "mutable_by_algorithm": False}) +# default=0, metadata={"topic": "get/field_immutable"}) # sample_field_list: List = field(default_factory=currents_list_factory, metadata={ -# "topic": "get/field_list", "mutable_by_algorithm": True}) +# "topic": "get/field_list"}) # # Bei verschachtelten Klassen, wo der zu publishende Wert auf einer tieferen Ebene liegt, werden nur für den zu # publishenden Wert Metadaten erzeugt. # sample_field_nested: SampleNested = field(default_factory=sample_nested) @@ -78,48 +77,69 @@ def store_initial_values(self): def pub_changed_values(self): try: # publishen der geänderten Werte - self._update_value("openWB/set/bat/", self.prev_data.bat_all_data.data.get, data.data.bat_all_data.data.get) + self._update_value("openWB/set/bat/", self.prev_data.bat_all_data.data, data.data.bat_all_data.data) + self._update_value("openWB/set/chargepoint/", self.prev_data.cp_all_data.data.get, + data.data.cp_all_data.data.get) + self._update_value("openWB/set/counter/", self.prev_data.counter_all_data.data, + data.data.counter_all_data.data) for key, value in data.data.cp_data.items(): + self._update_value(f"openWB/set/chargepoint/{value.num}/", self.prev_data.cp_data[key].data, value.data) + for key, value in data.data.bat_data.items(): + self._update_value(f"openWB/set/bat/{value.num}/", self.prev_data.bat_data[key].data, value.data) + for key, value in data.data.counter_data.items(): + self._update_value(f"openWB/set/counter/{value.num}/", + self.prev_data.counter_data[key].data, value.data) + # chargepoint, ev template, autolock, time and scheduled charging plans mutable_by_algorithm immer false + except Exception as e: + log.exception(e) + + def _update_value(self, topic_prefix, data_inst_previous, data_inst): + try: + for f in fields(data_inst): try: - self._update_value(f"openWB/set/chargepoint/{value.num}/", - self.prev_data.cp_data[key].data, value.data) + changed = False + value = getattr(data_inst, f.name) + if isinstance(value, Enum): + value = value.value + previous_value = getattr(data_inst_previous, f.name) + if isinstance(previous_value, Enum): + previous_value = previous_value.value + if hasattr(f, "metadata"): + if f.metadata.get("topic"): + if isinstance(value, (str, int, float, Dict, List, Tuple)): + if previous_value != value: + changed = True + elif isinstance(value, (bool, type(None))): + if previous_value is not value: + changed = True + else: + dict_prev = dict(asdict(previous_value)) + dict_current = dict(asdict(value)) + if dict_prev != dict_current: + changed = True + value = asdict(value) + previous_value = asdict(previous_value) + + if changed: + topic = f"{topic_prefix}{f.metadata['topic']}" + Pub().pub(topic, value) + log.debug(f"Topic {topic}, Payload {value}, vorherige Payload: {previous_value}") + continue + if is_dataclass(value): + self._update_value(topic_prefix, previous_value, value) except Exception as e: log.exception(e) except Exception as e: log.exception(e) - def _update_value(self, topic_prefix, data_inst_previous, data_inst): - for f in fields(data_inst): - try: - changed = False - value = getattr(data_inst, f.name) - if isinstance(value, Enum): - value = value.value - previous_value = getattr(data_inst_previous, f.name) - if isinstance(previous_value, Enum): - previous_value = previous_value.value - if hasattr(f, "metadata"): - if f.metadata.get("mutable_by_algorithm"): - if isinstance(value, (str, int, float, Dict, List, Tuple)): - if previous_value != value: - changed = True - elif isinstance(value, (bool, type(None))): - if previous_value is not value: - changed = True - else: - dict_prev = dict(asdict(previous_value)) - dict_current = dict(asdict(value)) - if dict_prev != dict_current: - changed = True - value = asdict(value) - previous_value = asdict(previous_value) - - if changed: - topic = f"{topic_prefix}{f.metadata['topic']}" - Pub().pub(topic, value) - log.debug(f"Topic {topic}, Payload {value}, vorherige Payload: {previous_value}") - continue - if is_dataclass(value): - self._update_value(topic_prefix, previous_value, value) - except Exception as e: - log.exception(e) + +class ChangedValuesContext: + def __init__(self, event_module_update_completed: threading.Event): + self.changed_values_handler = ChangedValuesHandler(event_module_update_completed) + + def __enter__(self): + self.changed_values_handler.store_initial_values() + + def __exit__(self, exception_type, exception, exception_traceback) -> bool: + self.changed_values_handler.pub_changed_values() + return False diff --git a/packages/helpermodules/changed_values_handler_test.py b/packages/helpermodules/changed_values_handler_test.py index d42524dbe4..2e9225f435 100644 --- a/packages/helpermodules/changed_values_handler_test.py +++ b/packages/helpermodules/changed_values_handler_test.py @@ -23,8 +23,8 @@ def sample_class() -> SampleClass: @dataclass class SampleNested: - parameter1: bool = field(default=False, metadata={"topic": "get/nested1", "mutable_by_algorithm": True}) - parameter2: int = field(default=0, metadata={"topic": "get/nested2", "mutable_by_algorithm": True}) + parameter1: bool = field(default=False, metadata={"topic": "get/nested1"}) + parameter2: int = field(default=0, metadata={"topic": "get/nested2"}) def sample_nested() -> SampleNested: @@ -41,25 +41,25 @@ def sample_tuple_factory() -> Tuple: @dataclass class SampleData: - sample_field_bool: bool = field(default=False, metadata={"topic": "get/field_bool", "mutable_by_algorithm": True}) + sample_field_bool: bool = field(default=False, metadata={"topic": "get/field_bool"}) sample_field_class: SampleClass = field( - default_factory=sample_class, metadata={"topic": "get/field_class", "mutable_by_algorithm": True}) + default_factory=sample_class, metadata={"topic": "get/field_class"}) sample_field_dict: Dict = field(default_factory=sample_dict_factory, metadata={ - "topic": "get/field_dict", "mutable_by_algorithm": True}) + "topic": "get/field_dict"}) sample_field_enum: ChargepointState = field(default=ChargepointState.CHARGING_ALLOWED, metadata={ - "topic": "get/field_enum", "mutable_by_algorithm": True}) - sample_field_float: float = field(default=0, metadata={"topic": "get/field_float", "mutable_by_algorithm": True}) - sample_field_int: int = field(default=0, metadata={"topic": "get/field_int", "mutable_by_algorithm": True}) + "topic": "get/field_enum"}) + sample_field_float: float = field(default=0, metadata={"topic": "get/field_float"}) + sample_field_int: int = field(default=0, metadata={"topic": "get/field_int"}) sample_field_immutable: float = field( - default=0, metadata={"topic": "get/field_immutable", "mutable_by_algorithm": False}) + default=0, metadata={"topic": "get/field_immutable"}) sample_field_list: List = field(default_factory=currents_list_factory, metadata={ - "topic": "get/field_list", "mutable_by_algorithm": True}) + "topic": "get/field_list"}) sample_field_nested: SampleNested = field(default_factory=sample_nested) sample_field_none: Optional[str] = field( - default="Hi", metadata={"topic": "get/field_none", "mutable_by_algorithm": True}) - sample_field_str: str = field(default="Hi", metadata={"topic": "get/field_str", "mutable_by_algorithm": True}) + default="Hi", metadata={"topic": "get/field_none"}) + sample_field_str: str = field(default="Hi", metadata={"topic": "get/field_str"}) sample_field_tuple: Tuple = field(default_factory=sample_tuple_factory, metadata={ - "topic": "get/field_tuple", "mutable_by_algorithm": True}) + "topic": "get/field_tuple"}) @dataclass @@ -83,7 +83,6 @@ class Params: expected_pub_call=("openWB/get/field_float", 2.5)), Params(name="change int", sample_data=SampleData(sample_field_int=2), expected_pub_call=("openWB/get/field_int", 2)), - Params(name="immutable", sample_data=SampleData(sample_field_immutable=2), expected_calls=0), Params(name="change list", sample_data=SampleData(sample_field_list=[ 10, 0, 0]), expected_pub_call=("openWB/get/field_list", [10, 0, 0])), Params(name="change nested", sample_data=SampleData(sample_field_nested=SampleNested( diff --git a/packages/helpermodules/command.py b/packages/helpermodules/command.py index c296478a15..b36264d845 100644 --- a/packages/helpermodules/command.py +++ b/packages/helpermodules/command.py @@ -16,13 +16,14 @@ from control.chargepoint.chargepoint_template import get_autolock_plan_default, get_chargepoint_template_default # ToDo: move to module commands if implemented +from helpermodules.utils.run_command import run_command from modules.backup_clouds.onedrive.api import generateMSALAuthCode, retrieveMSALTokens from helpermodules.broker import InternalBrokerClient from helpermodules.data_migration.data_migration import MigrateData from helpermodules.measurement_logging.process_log import get_daily_log, get_monthly_log, get_yearly_log from helpermodules.messaging import MessageType, pub_user_message -from helpermodules.parse_send_debug import parse_send_debug_data +from helpermodules.create_debug import create_debug_log from helpermodules.pub import Pub, pub_single from helpermodules.subdata import SubData from helpermodules.utils.topic_parser import decode_payload @@ -168,7 +169,6 @@ def addChargepoint(self, connection_id: str, payload: dict) -> None: def setup_added_chargepoint(): Pub().pub(f'openWB/chargepoint/{new_id}/config', chargepoint_config) Pub().pub(f'openWB/chargepoint/{new_id}/set/manual_lock', False) - Pub().pub(f'openWB/chargepoint/{new_id}/set/change_ev_permitted', False) {Pub().pub(f"openWB/chargepoint/{new_id}/get/"+k, v) for (k, v) in asdict(chargepoint.Get()).items()} self.max_id_hierarchy = self.max_id_hierarchy + 1 Pub().pub("openWB/set/command/max_id/hierarchy", self.max_id_hierarchy) @@ -221,7 +221,7 @@ def setup_added_chargepoint(): MAX_NUM_REACHED = ("Es kann maximal ein interner Ladepunkt für eine openWB Series 1/2 Buchse, Custom, " "Standard oder Standard+ konfiguriert werden. Wenn ein zweiter Ladepunkt für eine " "Duo hinzugefügt werden soll, muss auch für den ersten Ladepunkt Bauart 'Duo' " - "gewählt werden.") + "gewählt und gespeichert werden.") def _check_max_num_of_internal_chargepoints(self, config: Dict) -> Optional[str]: if config["type"] == 'internal_openwb': @@ -328,7 +328,7 @@ def addChargeTemplate(self, connection_id: str, payload: dict) -> None: """ sendet das Topic, zu dem ein neues Lade-Profil erstellt werden soll. """ new_id = self.max_id_charge_template + 1 - charge_template_default = ev.get_charge_template_default() + charge_template_default = ev.get_new_charge_template() Pub().pub("openWB/set/vehicle/template/charge_template/" + str(new_id), charge_template_default) self.max_id_charge_template = new_id @@ -344,7 +344,7 @@ def removeChargeTemplate(self, connection_id: str, payload: dict) -> None: pub_user_message(payload, connection_id, "Die ID ist größer als die maximal vergebene ID.", MessageType.ERROR) if payload["data"]["id"] > 0: - Pub().pub(f'openWB/vehicle/template/charge_template/{payload["data"]["id"]}', "") + ProcessBrokerBranch(f'vehicle/template/charge_template/{payload["data"]["id"]}/').remove_topics() pub_user_message( payload, connection_id, f'Lade-Profil mit ID \'{payload["data"]["id"]}\' gelöscht.', @@ -434,22 +434,10 @@ def set_default(topic: str, defaults: dict): component_default["id"] = new_id general_type = special_to_general_type_mapping(payload["data"]["type"]) try: - data.data.counter_all_data.hierarchy_add_item_below( - new_id, general_type, data.data.counter_all_data.get_id_evu_counter()) - except (TypeError, IndexError): - if general_type == ComponentType.COUNTER: - # es gibt noch keinen EVU-Zähler - hierarchy = [{ - "id": new_id, - "type": ComponentType.COUNTER.value, - "children": data.data.counter_all_data.data.get.hierarchy - }] - Pub().pub("openWB/set/counter/get/hierarchy", hierarchy) - data.data.counter_all_data.data.get.hierarchy = hierarchy - else: - pub_user_message(payload, connection_id, - "Bitte erst einen EVU-Zähler konfigurieren!", MessageType.ERROR) - return + data.data.counter_all_data.hierarchy_add_item_below_evu(new_id, general_type) + except ValueError: + pub_user_message(payload, connection_id, counter_all.CounterAll.MISSING_EVU_COUNTER, MessageType.ERROR) + return # Bei Zählern müssen noch Standardwerte veröffentlicht werden. if general_type == ComponentType.BAT: topic = f"openWB/set/bat/{new_id}" @@ -503,7 +491,7 @@ def removeEvTemplate(self, connection_id: str, payload: dict) -> None: pub_user_message(payload, connection_id, "Die ID ist größer als die maximal vergebene ID.", MessageType.ERROR) if payload["data"]["id"] > 0: - Pub().pub(f'openWB/vehicle/template/ev_template/{payload["data"]["id"]}', "") + ProcessBrokerBranch(f'vehicle/template/ev_template/{payload["data"]["id"]}/').remove_topics() pub_user_message( payload, connection_id, f'Fahrzeug-Profil mit ID \'{payload["data"]["id"]}\' gelöscht.', MessageType.SUCCESS) @@ -548,10 +536,8 @@ def removeVehicle(self, connection_id: str, payload: dict) -> None: def sendDebug(self, connection_id: str, payload: dict) -> None: pub_user_message(payload, connection_id, "Systembericht wird erstellt...", MessageType.INFO) - parent_file = Path(__file__).resolve().parents[2] previous_log_level = SubData.system_data["system"].data["debug_level"] - subprocess.run([str(parent_file / "runs" / "send_debug.sh"), - json.dumps(payload["data"]), parse_send_debug_data()]) + create_debug_log(payload["data"]) Pub().pub("openWB/set/system/debug_level", previous_log_level) pub_user_message(payload, connection_id, "Systembericht wurde versandt.", MessageType.SUCCESS) @@ -572,21 +558,17 @@ def getYearlyLog(self, connection_id: str, payload: dict) -> None: def initCloud(self, connection_id: str, payload: dict) -> None: parent_file = Path(__file__).resolve().parents[2] - try: - result = subprocess.check_output( - ["php", "-f", str(parent_file / "runs" / "cloudRegister.php"), json.dumps(payload["data"])] - ) - # exit status = 0 is success, std_out contains json: {"username", "password"} - result_dict = json.loads(result) - connect_payload = { - "data": result_dict - } - connect_payload["data"]["partner"] = payload["data"]["partner"] - self.connectCloud(connection_id, connect_payload) - pub_user_message(payload, connection_id, "Verbindung zur Cloud wurde eingerichtet.", MessageType.SUCCESS) - except subprocess.CalledProcessError as error: - # exit status = 1 is failure, std_out contains error message - pub_user_message(payload, connection_id, error.output.decode("utf-8", MessageType.ERROR)) + result = run_command( + ["php", "-f", str(parent_file / "runs" / "cloudRegister.php"), json.dumps(payload["data"])] + ) + # exit status = 0 is success, std_out contains json: {"username", "password"} + result_dict = json.loads(result) + connect_payload = { + "data": result_dict + } + connect_payload["data"]["partner"] = payload["data"]["partner"] + self.connectCloud(connection_id, connect_payload) + pub_user_message(payload, connection_id, "Verbindung zur Cloud wurde eingerichtet.", MessageType.SUCCESS) def connectCloud(self, connection_id: str, payload: dict) -> None: cloud_config = bridge.get_cloud_config() @@ -623,27 +605,27 @@ def removeMqttBridge(self, connection_id: str, payload: dict) -> None: def systemReboot(self, connection_id: str, payload: dict) -> None: pub_user_message(payload, connection_id, "Neustart wird ausgeführt.", MessageType.INFO) parent_file = Path(__file__).resolve().parents[2] - subprocess.run([str(parent_file / "runs" / "reboot.sh")]) + run_command([str(parent_file / "runs" / "reboot.sh")]) def systemShutdown(self, connection_id: str, payload: dict) -> None: pub_user_message(payload, connection_id, "openWB wird heruntergefahren.", MessageType.INFO) parent_file = Path(__file__).resolve().parents[2] - subprocess.run([str(parent_file / "runs" / "shutdown.sh")]) + run_command([str(parent_file / "runs" / "shutdown.sh")]) def systemUpdate(self, connection_id: str, payload: dict) -> None: log.info("Update requested") # notify system about running update, notify about end update in script Pub().pub("openWB/system/update_in_progress", True) - if SubData.system_data["system"].data["backup_before_update"]: + if SubData.system_data["system"].data["backup_cloud"]["backup_before_update"]: try: self.createCloudBackup(connection_id, {}) except Exception: pub_user_message(payload, connection_id, - ("Fehler beim Erstellen der Cloud-Sicherung." - f" {traceback.format_exc()}
Update abgebrochen!" - "Bitte Fehlerstatus überprüfen!. " + - "Option Sicherung vor System Update kann unter Datenverwaltung deaktiviert werden."), - MessageType.WARNING) + ("Fehler beim Erstellen der Cloud-Sicherung. Update abgebrochen! " + "Bitte die Cloud-Konfiguration überprüfen! Die Option " + + "Sicherung vor System Update kann unter Datenverwaltung deaktiviert werden."), + MessageType.ERROR) + log.exception("Fehler beim Erstellen der Cloud-Sicherung: ") Pub().pub("openWB/system/update_in_progress", False) return parent_file = Path(__file__).resolve().parents[2] @@ -652,13 +634,13 @@ def systemUpdate(self, connection_id: str, payload: dict) -> None: payload, connection_id, f'Wechsel auf Zweig \'{payload["data"]["branch"]}\' Tag \'{payload["data"]["tag"]}\' gestartet.', MessageType.SUCCESS) - subprocess.run([ + run_command([ str(parent_file / "runs" / "update_self.sh"), str(payload["data"]["branch"]), str(payload["data"]["tag"])]) else: pub_user_message(payload, connection_id, "Update gestartet.", MessageType.INFO) - subprocess.run([ + run_command([ str(parent_file / "runs" / "update_self.sh"), SubData.system_data["system"].data["current_branch"]]) @@ -666,31 +648,20 @@ def systemFetchVersions(self, connection_id: str, payload: dict) -> None: log.info("Fetch versions requested") pub_user_message(payload, connection_id, "Versionsliste wird aktualisiert...", MessageType.INFO) parent_file = Path(__file__).resolve().parents[2] - result = subprocess.run([str(parent_file / "runs" / "update_available_versions.sh")]) - if result.returncode == 0: - pub_user_message(payload, connection_id, "Versionsliste erfolgreich aktualisiert.", MessageType.SUCCESS) - else: - pub_user_message( - payload, connection_id, - f'Version-Status: {result.returncode}
Meldung: {result.stdout.decode("utf-8", MessageType.ERROR)}') + run_command([str(parent_file / "runs" / "update_available_versions.sh")]) + pub_user_message(payload, connection_id, "Versionsliste erfolgreich aktualisiert.", MessageType.SUCCESS) def createBackup(self, connection_id: str, payload: dict) -> None: pub_user_message(payload, connection_id, "Backup wird erstellt...", MessageType.INFO) parent_file = Path(__file__).resolve().parents[2] - result = subprocess.run( + result = run_command( [str(parent_file / "runs" / "backup.sh"), - "1" if "use_extended_filename" in payload["data"] and payload["data"]["use_extended_filename"] else "0"], - stdout=subprocess.PIPE) - if result.returncode == 0: - file_name = result.stdout.decode("utf-8").rstrip('\n') - file_link = "/openWB/data/backup/" + file_name - pub_user_message(payload, connection_id, - "Backup erfolgreich erstellt.
" - f'Jetzt herunterladen.', MessageType.SUCCESS) - else: - pub_user_message(payload, connection_id, - f'Backup-Status: {result.returncode}
Meldung: {result.stdout.decode("utf-8")}', - MessageType.ERROR) + "1" if "use_extended_filename" in payload["data"] and payload["data"]["use_extended_filename"] else "0"]) + file_name = result.rstrip('\n') + file_link = "/openWB/data/backup/" + file_name + pub_user_message(payload, connection_id, + "Backup erfolgreich erstellt.
" + f'Jetzt herunterladen.', MessageType.SUCCESS) def createCloudBackup(self, connection_id: str, payload: dict) -> None: if SubData.system_data["system"].backup_cloud is not None: @@ -705,18 +676,11 @@ def createCloudBackup(self, connection_id: str, payload: dict) -> None: def restoreBackup(self, connection_id: str, payload: dict) -> None: parent_file = Path(__file__).resolve().parents[2] - result = subprocess.run( - [str(parent_file / "runs" / "prepare_restore.sh")], - stdout=subprocess.PIPE) - if result.returncode == 0: - pub_user_message(payload, connection_id, - "Wiederherstellung wurde vorbereitet. openWB wird jetzt zum Abschluss neu gestartet.", - MessageType.INFO) - self.systemReboot(connection_id, payload) - else: - pub_user_message(payload, connection_id, - f'Restore-Status: {result.returncode}
Meldung: {result.stdout.decode("utf-8")}', - MessageType.ERROR) + run_command([str(parent_file / "runs" / "prepare_restore.sh")]) + pub_user_message(payload, connection_id, + "Wiederherstellung wurde vorbereitet. openWB wird jetzt zum Abschluss neu gestartet.", + MessageType.INFO) + self.systemReboot(connection_id, payload) # ToDo: move to module commands if implemented def requestMSALAuthCode(self, connection_id: str, payload: dict) -> None: @@ -771,7 +735,14 @@ def __enter__(self): def __exit__(self, exception_type, exception, exception_traceback) -> bool: if isinstance(exception, Exception): pub_user_message(self.payload, self.connection_id, - f'Es ist ein interner Fehler aufgetreten: {traceback.format_exc()}', MessageType.ERROR) + f'Es ist ein interner Fehler aufgetreten: {exception}', MessageType.ERROR) + log.error({traceback.format_exc()}) + return True + elif isinstance(exception, subprocess.CalledProcessError): + log.debug(exception.stdout) + pub_user_message(self.payload, self.connection_id, + f'Fehler-Status: {exception.returncode}
Meldung: {exception.stderr}', + MessageType.ERROR) return True else: return False @@ -835,28 +806,52 @@ def on_connect(self, client, userdata, flags, rc): client.subscribe(f'openWB/set/{self.topic_str}#', 2) def __on_message_rm(self, client, userdata, msg): - if decode_payload(msg.payload) != '': - log.debug(f'Gelöschtes Topic: {msg.topic}') - Pub().pub(msg.topic, "") - if "openWB/system/device/" in msg.topic and "component" in msg.topic and "config" in msg.topic: - payload = decode_payload(msg.payload) - topic = type_to_topic_mapping(payload["type"]) - data.data.counter_all_data.hierarchy_remove_item(payload["id"]) - client.subscribe(f'openWB/{topic}/{payload["id"]}/#', 2) - elif re.search("openWB/chargepoint/[0-9]+/config$", msg.topic) is not None: - payload = decode_payload(msg.payload) - if payload["type"] == "external_openwb": - pub_single( - f'openWB/set/internal_chargepoint/{payload["configuration"]["duo_num"]}/data/parent_cp', - None, - hostname=payload["configuration"]["ip_address"]) + try: + if decode_payload(msg.payload) != '': + log.debug(f'Gelöschtes Topic: {msg.topic}') + Pub().pub(msg.topic, "") + if "openWB/system/device/" in msg.topic and "component" in msg.topic and "config" in msg.topic: + payload = decode_payload(msg.payload) + topic = type_to_topic_mapping(payload["type"]) + data.data.counter_all_data.hierarchy_remove_item(payload["id"]) + client.subscribe(f'openWB/{topic}/{payload["id"]}/#', 2) + elif re.search("openWB/chargepoint/[0-9]+/config$", msg.topic) is not None: + payload = decode_payload(msg.payload) + if payload["type"] == "external_openwb": + pub_single( + f'openWB/set/internal_chargepoint/{payload["configuration"]["duo_num"]}/data/parent_cp', + None, + hostname=payload["configuration"]["ip_address"]) + elif re.search("openWB/chargepoint/template/[0-9]+$", msg.topic) is not None: + for cp in SubData.cp_data.values(): + if cp.chargepoint.data.config.template == int(msg.topic.split("/")[-1]): + pub_single(f'openWB/set/chargepoint/{cp.chargepoint.num}/config/template', 0) + elif re.search("openWB/vehicle/template/charge_template/[0-9]+$", msg.topic) is not None: + for vehicle in SubData.ev_data.values(): + if vehicle.data.charge_template == int(msg.topic.split("/")[-1]): + pub_single(f'openWB/set/vehicle/{vehicle.num}/charge_template', 0) + elif re.search("openWB/vehicle/template/ev_template/[0-9]+$", msg.topic) is not None: + for vehicle in SubData.ev_data.values(): + if vehicle.data.ev_template == int(msg.topic.split("/")[-1]): + pub_single(f'openWB/set/vehicle/{vehicle.num}/ev_template', 0) + except Exception: + log.exception("Fehler in ProcessBrokerBranch") def __on_message_max_id(self, client, userdata, msg): - self.received_topics.append(msg.topic) + try: + self.received_topics.append(msg.topic) + except Exception: + log.exception("Fehler in ProcessBrokerBranch") def __get_payload(self, client, userdata, msg): - self.payload = msg.payload + try: + self.payload = msg.payload + except Exception: + log.exception("Fehler in ProcessBrokerBranch") def __on_message_mqtt_bridge_exists(self, client, userdata, msg): - if decode_payload(msg.payload)["name"] == self.name: - self.mqtt_bridge_exists = True + try: + if decode_payload(msg.payload)["name"] == self.name: + self.mqtt_bridge_exists = True + except Exception: + log.exception("Fehler in ProcessBrokerBranch") diff --git a/packages/helpermodules/create_debug.py b/packages/helpermodules/create_debug.py new file mode 100644 index 0000000000..9b2455a51c --- /dev/null +++ b/packages/helpermodules/create_debug.py @@ -0,0 +1,279 @@ +import os +import time +import logging +from pathlib import Path +import pprint +from typing import Any, Optional +import requests + +from control import data +from control.chargepoint.chargepoint import Chargepoint +import dataclass_utils +from helpermodules import subdata +from helpermodules.broker import InternalBrokerClient +from helpermodules.pub import Pub +from helpermodules.utils.run_command import run_command +from helpermodules.utils.topic_parser import decode_payload +from modules.common import req +from modules.common.abstract_device import AbstractDevice + +log = logging.getLogger(__name__) + + +def config_and_state(): + parsed_data = "" + try: + secondary = subdata.SubData.general_data.data.extern + except Exception: + secondary = False + + with ErrorHandlingContext(): + parent_file = Path(__file__).resolve().parents[2] + with open(f"{parent_file}/web/version", "r") as f: + version = f.read().strip() + with open(f"{parent_file}/web/lastcommit", "r") as f: + lastcommit = f.read().strip() + parsed_data += f"# Version\n{version}\n{lastcommit}\n\n" + with ErrorHandlingContext(): + parsed_data += f"# Cloud/Brücken\n{BrokerContent().get_bridges()}" + with ErrorHandlingContext(): + chargemode_config = data.data.general_data.data.chargemode_config + parsed_data += "\n# Allgemein\n" + if secondary is False: + parsed_data += (f"Modus: Primary\nHausverbrauch: {data.data.counter_all_data.data.set.home_consumption}W\n" + f"Phasenvorgabe: Sofortladen {chargemode_config.instant_charging.phases_to_use}, Zielladen " + f"{chargemode_config.scheduled_charging.phases_to_use}, Zeitladen: " + f"{chargemode_config.time_charging.phases_to_use}, PV-Laden: " + f"{chargemode_config.pv_charging.phases_to_use}, Einschaltschwelle: " + f"{chargemode_config.pv_charging.switch_on_threshold}W, Ausschaltschwelle: " + f"{chargemode_config.pv_charging.switch_off_threshold}W\n" + f"Regelintervall: {data.data.general_data.data.control_interval}s, ") + else: + parsed_data += "Modus: Secondary\n" + parsed_data += f"Display aktiviert: {data.data.optional_data.data.int_display.active}\n" + + if secondary is False: + with ErrorHandlingContext(): + pretty_hierarchy = pprint.pformat(data.data.counter_all_data.data.get.hierarchy, + indent=4, compact=True, sort_dicts=False, width=100) + parsed_data += f"\n# Hierarchie\n{pretty_hierarchy}\n" + + with ErrorHandlingContext(): + if secondary: + with ErrorHandlingContext(): + parsed_data += "\n# Ladepunkte\n" + for cp in subdata.SubData.cp_data.values(): + parsed_data += get_parsed_cp_data(cp.chargepoint) + else: + parsed_data += "\n# Geräte und Komponenten\n" + for key, value in data.data.system_data.items(): + with ErrorHandlingContext(): + if isinstance(value, AbstractDevice): + parsed_data += f"{key}: {dataclass_utils.asdict(value.device_config)}\n" + for comp_key, comp_value in value.components.items(): + parsed_data += f"{comp_key}: {dataclass_utils.asdict(comp_value.component_config)}\n" + if "bat" in comp_value.component_config.type: + component_data = data.data.bat_data[f"bat{comp_value.component_config.id}"] + elif "counter" in comp_value.component_config.type: + component_data = data.data.counter_data[f"counter{comp_value.component_config.id}"] + elif "inverter" in comp_value.component_config.type: + component_data = data.data.pv_data[f"pv{comp_value.component_config.id}"] + if "bat" in comp_value.component_config.type: + parsed_data += (f"Leistung: {component_data.data.get.power/1000}kW, " + f"SoC: {component_data.data.get.soc}%, " + f"Fehlerstatus: {component_data.data.get.fault_str}\n") + elif "inverter" in comp_value.component_config.type: + parsed_data += (f"Leistung: {component_data.data.get.power/1000}kW, " + f"Fehlerstatus: {component_data.data.get.fault_str}\n") + else: + counter_all_data = data.data.counter_all_data + if counter_all_data.get_evu_counter_str() == f"counter{component_data.num}": + parsed_data += (f"{comp_key}: EVU-Zähler -> max. Leistung " + f"{component_data.data.config.max_total_power}, " + f"max. Ströme {component_data.data.config.max_currents}; ") + elif counter_all_data.data.config.home_consumption_source_id == component_data.num: + parsed_data += (f"{comp_key}: Hausverbrauchszähler -> max. Leistung " + f"{component_data.data.config.max_total_power}, " + f"max. Ströme {component_data.data.config.max_currents}; ") + else: + parsed_data += f"{key}: max. Ströme {component_data.data.config.max_currents}" + parsed_data += (f"Leistung: {component_data.data.get.power/1000}kW, Ströme: " + f"{component_data.data.get.currents}A, Fehlerstatus: " + f"{component_data.data.get.fault_str}\n") + with ErrorHandlingContext(): + parsed_data += "\n# Ladepunkte\n" + parsed_data += f"Ladeleistung aller Ladepunkte {data.data.cp_all_data.data.get.power / 1000}kW\n" + for cp in data.data.cp_data.values(): + parsed_data += get_parsed_cp_data(cp) + return parsed_data + + +def get_parsed_cp_data(cp: Chargepoint) -> str: + parsed_data = "" + with ErrorHandlingContext(): + if hasattr(cp.chargepoint_module.config.configuration, "ip_address"): + ip = cp.chargepoint_module.config.configuration.ip_address + else: + ip = None + parsed_data += (f"LP{cp.num}: Typ: {cp.chargepoint_module.config.type}; IP: " + f"{ip}; Stecker-Status: {cp.data.get.plug_state}, Leistung: " + f"{cp.data.get.power/1000}kW, {cp.data.get.currents}A, {cp.data.get.voltages}V, Lademodus: " + f"{cp.data.control_parameter.chargemode}, Submode: " + f"{cp.data.control_parameter.submode}, Sollstrom: " + f"{cp.data.set.current}A, Status: {cp.data.get.state_str}, " + f"Fehlerstatus: {cp.data.get.fault_str}\n") + if cp.chargepoint_module.config.type == "openwb_pro": + try: + parsed_data += f"{req.get_http_session().get(f'http://{ip}/connect.php', timeout=5).text}\n" + except requests.Timeout: + parsed_data += "Timeout beim Abrufen der Daten\n" + return parsed_data + + +openwb_base_dir = Path(__file__).resolve().parents[2] +ramdisk_dir = openwb_base_dir / 'ramdisk' +debug_file = ramdisk_dir / 'debug.log' + + +def merge_log_files(log_name, num_lines): + log_files = [f"{ramdisk_dir}/{log_name}.log.{i}" for i in range(5, 1)] + log_files.append(f"{ramdisk_dir}/{log_name}.log") + + lines = [] + try: + for log_file in log_files: + if os.path.isfile(log_file): + with open(log_file, 'r') as file: + lines += file.readlines() + except Exception as e: + log.exception(f"Fehler beim Lesen der Logdateien: {e}") + return ''.join(lines[-num_lines:]) + + +def get_uuids(): + try: + with open(openwb_base_dir / 'data/log/uuid', 'r') as uuid_file: + return (uuid_file.read()) + except Exception as e: + log.exception(f"Error reading UUID file: {e}") + + +def create_debug_log(input_data): + def write_to_file(file_handler, func, default: Optional[Any] = None): + try: + file_handler.write(func()+"\n") + except Exception as e: + log.exception(f"Error getting value for chargelog: {func}. Setting to default {default}.") + file_handler.write(f"Error getting value for chargelog: {func}. Setting to default {default}.\n" + f"Error: {e}\n") + + try: + broker = BrokerContent() + debug_email = input_data.get('email', '') + header = (f"{input_data['message']}\n{debug_email}\n{input_data['serialNumber']}\n" + f"{input_data['installedComponents']}\n{input_data['vehicles']}\n") + with open(debug_file, 'w+') as df: + write_to_file(df, lambda: header) + write_to_file(df, lambda: f"## section: configuration and state ##\n{config_and_state()}\n") + write_to_file(df, lambda: f'## section: system ##\n{run_command(["uptime"])}{run_command(["free"])}\n') + write_to_file(df, lambda: f"## section: uuids ##\n{get_uuids()}\n") + write_to_file(df, lambda: f'## section: network ##\n{run_command(["ifconfig"])}\n') + write_to_file(df, lambda: f'## section: storage ##\n{run_command(["df", "-h"])}\n') + write_to_file(df, lambda: f"## section: broker essentials ##\n{broker.get_broker_essentials()}\n") + write_to_file( + df, lambda: f"## section: retained log ##\n{merge_log_files('main', 500)}") + write_to_file(df, lambda: "## section: info log ##\n") + Pub().pub('openWB/set/system/debug_level', 20) + time.sleep(60) + write_to_file(df, lambda: merge_log_files("main", 1000)) + write_to_file(df, lambda: "## section: debug log ##\n") + Pub().pub('openWB/set/system/debug_level', 10) + time.sleep(60) + write_to_file(df, lambda: merge_log_files("main", 2500)) + write_to_file( + df, + lambda: f'## section: internal chargepoint log ##\n{merge_log_files("internal_chargepoint", 1000)}\n') + write_to_file(df, lambda: f'## section: mqtt log ##\n{merge_log_files("mqtt", 1000)}\n') + write_to_file(df, lambda: f'## section: soc log ##\n{merge_log_files("soc", 1000)}\n') + write_to_file(df, lambda: f'## section: charge log ##\n{merge_log_files("chargelog", 1000)}\n') + write_to_file(df, lambda: f"## section: broker ##\n{broker.get_broker()}") + + log.info("***** uploading debug log...") + with open(debug_file, 'rb') as f: + data = f.read() + req.get_http_session().put("https://openwb.de/tools/debug2.php", + data=data, + params={'debugemail': debug_email}) + + log.info("***** cleanup...") + os.remove(debug_file) + log.info("***** debug log end") + except Exception as e: + log.exception(f"Error creating debug log: {e}") + + +class BrokerContent: + def __init__(self) -> None: + self.content = "" + + def get_broker(self): + InternalBrokerClient("processBrokerBranch", self.__on_connect_broker, self.__get_content).start_finite_loop() + return self.content + + def __on_connect_broker(self, client, userdata, flags, rc): + client.subscribe("openWB/#", 2) + + def __get_content(self, client, userdata, msg): + self.content += f"{msg.topic} {decode_payload(msg.payload)}\n" + + def get_broker_essentials(self): + InternalBrokerClient("processBrokerBranch", self.__on_connect_broker_essentials, + self.__get_content).start_finite_loop() + return self.content + + def __on_connect_broker_essentials(self, client, userdata, flags, rc): + client.subscribe("openWB/system/ip_address", 2) + client.subscribe("openWB/system/current_commit", 2) + client.subscribe("openWB/system/boot_done", 2) + client.subscribe("openWB/system/update_in_progress", 2) + client.subscribe("openWB/system/device/#", 2) + client.subscribe("openWB/system/time", 2) + client.subscribe("openWB/chargepoint/#", 2) + client.subscribe("openWB/internal_chargepoint/#", 2) + client.subscribe("openWB/vehicle/#", 2) + client.subscribe("openWB/counter/#", 2) + client.subscribe("openWB/pv/#", 2) + client.subscribe("openWB/bat/#", 2) + client.subscribe("openWB/optional/et/provider", 2) + + def get_bridges(self): + InternalBrokerClient("processBrokerBranch", self.__on_connect_bridges, self.__get_bridges).start_finite_loop() + return self.content + + def __on_connect_bridges(self, client, userdata, flags, rc): + client.subscribe("openWB/system/mqtt/#", 2) + + def __get_bridges(self, client, userdata, msg): + if "openWB/system/mqtt/bridge" in msg.topic: + payload = decode_payload(msg.payload) + self.content += (f"Name: {payload['name']}, aktiv: {payload['active']}, " + f"openWB-Cloud: {payload['remote']['is_openwb_cloud']}") + if payload['remote'].get("is_openwb_cloud"): + self.content += (f", BN: {payload['remote']['username']}, PW: {payload['remote']['password']}, " + f"Partnerzugang: {payload['access']['partner']}") + self.content += "\n" + elif "openWB/system/mqtt/valid_partner_ids": + self.content += f"Partner-IDs: {decode_payload(msg.payload)}\n" + + +class ErrorHandlingContext: + def __init__(self): + pass + + def __enter__(self): + return None + + def __exit__(self, exception_type, exception, exception_traceback) -> bool: + if isinstance(exception, Exception): + log.exception("Fehler beim Parsen der Daten für das Support-Ticket") + return True diff --git a/packages/helpermodules/data_migration/data_migration.py b/packages/helpermodules/data_migration/data_migration.py index e8075f50a2..e42418d486 100644 --- a/packages/helpermodules/data_migration/data_migration.py +++ b/packages/helpermodules/data_migration/data_migration.py @@ -28,6 +28,7 @@ from helpermodules.timecheck import convert_timedelta_to_time_string, get_difference from helpermodules.utils import thread_handler from helpermodules.pub import Pub +from helpermodules.utils.json_file_handler import write_and_check from modules.ripple_control_receivers.gpio.config import GpioRcr import re @@ -154,8 +155,7 @@ def convert(old_file_name: str) -> None: except FileNotFoundError: pass new_entries.extend(content) - with open(filepath, "w") as jsonFile: - json.dump(new_entries, jsonFile) + write_and_check(filepath, new_entries) except Exception: log.exception(f"Fehler beim Konvertieren des Lade-Logs vom {old_file_name}") @@ -278,8 +278,7 @@ def convert(old_file_name: str) -> None: with open(filepath, "r") as jsonFile: content = json.load(jsonFile) except FileNotFoundError: - with open(filepath, "w+") as jsonFile: - json.dump({"entries": [], "totals": {}}, jsonFile) + write_and_check(filepath, {"entries": [], "totals": {}}) with open(filepath, "r") as jsonFile: content = json.load(jsonFile) entries = content["entries"] @@ -288,8 +287,7 @@ def convert(old_file_name: str) -> None: content["totals"] = get_totals(merged_entries) content["entries"] = merged_entries content["names"] = get_names(content["totals"], LegacySmartHomeLogData().sh_names) - with open(filepath, "w") as jsonFile: - json.dump(content, jsonFile) + write_and_check(filepath, content) except Exception: log.exception(f"Fehler beim Konvertieren des Logs vom {old_file_name}") @@ -559,8 +557,7 @@ def _migrate_settings_from_openwb_conf(self): def _move_serial_number(self) -> None: serial_number = self._get_openwb_conf_value("snnumber") if serial_number is not None: - with open("/home/openwb/snnumber", "w") as file: - file.write(f"snnumber={serial_number}") + write_and_check("/home/openwb/snnumber", f"snnumber={serial_number}") def _move_cloud_data(self) -> None: cloud_user = self._get_openwb_conf_value("clouduser") @@ -583,8 +580,7 @@ def _move_max_c_socket(self): def _move_pddate(self) -> None: pddate = self._get_openwb_conf_value("pddate") if pddate is not None: - with open("/home/openwb/pddate", "w") as file: - file.write(f"pddate={pddate}") + write_and_check("/home/openwb/pddate", f"pddate={pddate}") NOT_CONFIGURED = " wurde in openWB software2 nicht konfiguriert." diff --git a/packages/helpermodules/exceptions/os.py b/packages/helpermodules/exceptions/os.py index 3e9971ce5e..e39607da10 100644 --- a/packages/helpermodules/exceptions/os.py +++ b/packages/helpermodules/exceptions/os.py @@ -3,7 +3,7 @@ def handle_os_error(e: OSError): code = e.errno - if code == 113: + if code == 113 or e.args[0] == "timed out": return "Die Verbindung zum Host ist fehlgeschlagen. Überprüfe Adresse und Netzwerk." return "OSError {}: Unbekannter Fehler {}".format(code, e.strerror) diff --git a/packages/helpermodules/graph.py b/packages/helpermodules/graph.py index 3a13f7a01f..f18a9567ec 100644 --- a/packages/helpermodules/graph.py +++ b/packages/helpermodules/graph.py @@ -1,13 +1,13 @@ from dataclasses import dataclass, field import json from pathlib import Path -import subprocess import time import datetime import logging from control import data from helpermodules.pub import Pub +from helpermodules.utils.run_command import run_command from modules.common.fault_state import FaultStateLevel log = logging.getLogger(__name__) @@ -64,7 +64,7 @@ def _convert_to_kW(value): return round(value/1000, 3) Pub().pub("openWB/set/system/lastlivevaluesJson", data_line) with open(str(Path(__file__).resolve().parents[2] / "ramdisk"/"graph_live.json"), "a") as f: f.write(f"{json.dumps(data_line, separators=(',', ':'))}\n") - subprocess.run([str(Path(__file__).resolve().parents[2] / "runs"/"graphing.sh"), - str(self.data.config.duration*6)]) + run_command([str(Path(__file__).resolve().parents[2] / "runs"/"graphing.sh"), + str(self.data.config.duration*6)]) except Exception: log.exception("Fehler im Graph-Modul") diff --git a/packages/helpermodules/hardware_configuration.py b/packages/helpermodules/hardware_configuration.py index 6ad16782c8..cb81289693 100644 --- a/packages/helpermodules/hardware_configuration.py +++ b/packages/helpermodules/hardware_configuration.py @@ -2,24 +2,22 @@ import sys from typing import Dict +from helpermodules.utils.json_file_handler import write_and_check + HARDWARE_CONFIGURATION_FILE = "/home/openwb/configuration.json" def update_hardware_configuration(new_setting: Dict) -> None: with open(HARDWARE_CONFIGURATION_FILE, "r") as f: data = json.loads(f.read()) - with open(HARDWARE_CONFIGURATION_FILE, "w") as f: - data.update(new_setting) - f.write(json.dumps(data)) + write_and_check(HARDWARE_CONFIGURATION_FILE, data.update(new_setting)) def remove_setting_hardware_configuration(obsolet_setting: str) -> None: with open(HARDWARE_CONFIGURATION_FILE, "r") as f: data = json.loads(f.read()) if obsolet_setting in data: - with open(HARDWARE_CONFIGURATION_FILE, "w") as f: - data.pop(obsolet_setting) - f.write(json.dumps(data)) + write_and_check(HARDWARE_CONFIGURATION_FILE, data.pop(obsolet_setting)) def get_hardware_configuration_setting(name: str, default=None): diff --git a/packages/helpermodules/logger.py b/packages/helpermodules/logger.py index a1eb806a95..435088fa73 100644 --- a/packages/helpermodules/logger.py +++ b/packages/helpermodules/logger.py @@ -2,6 +2,8 @@ import logging from logging.handlers import RotatingFileHandler from pathlib import Path +import sys +import threading import typing_extensions FORMAT_STR_DETAILED = '%(asctime)s - {%(name)s:%(lineno)s} - {%(levelname)s:%(threadName)s} - %(message)s' @@ -25,7 +27,8 @@ def filter_pos(name: str, record) -> bool: def setup_logging() -> None: def mb_to_bytes(megabytes: int) -> int: return megabytes * 1000000 - main_file_handler = RotatingFileHandler(RAMDISK_PATH + 'main.log', maxBytes=mb_to_bytes(4), backupCount=1) + # Mehrere kleine Dateien verwenden, damit nicht zu viel verworfen wird, wenn die Datei voll ist. + main_file_handler = RotatingFileHandler(RAMDISK_PATH + 'main.log', maxBytes=mb_to_bytes(5.5), backupCount=4) main_file_handler.setFormatter(logging.Formatter(FORMAT_STR_DETAILED)) logging.basicConfig(level=logging.DEBUG, handlers=[main_file_handler]) logging.getLogger().handlers[0].addFilter(functools.partial(filter_neg, "soc")) @@ -35,14 +38,14 @@ def mb_to_bytes(megabytes: int) -> int: chargelog_log = logging.getLogger("chargelog") chargelog_log.propagate = False chargelog_file_handler = RotatingFileHandler( - RAMDISK_PATH + 'chargelog.log', maxBytes=mb_to_bytes(3), backupCount=1) + RAMDISK_PATH + 'chargelog.log', maxBytes=mb_to_bytes(2), backupCount=1) chargelog_file_handler.setFormatter(logging.Formatter(FORMAT_STR_SHORT)) chargelog_log.addHandler(chargelog_file_handler) data_migration_log = logging.getLogger("data_migration") data_migration_log.propagate = False data_migration_file_handler = RotatingFileHandler( - PERSISTENT_LOG_PATH + 'data_migration.log', maxBytes=mb_to_bytes(3), backupCount=1) + PERSISTENT_LOG_PATH + 'data_migration.log', maxBytes=mb_to_bytes(1), backupCount=1) data_migration_file_handler.setFormatter(logging.Formatter(FORMAT_STR_SHORT)) data_migration_log.addHandler(data_migration_file_handler) @@ -52,7 +55,7 @@ def mb_to_bytes(megabytes: int) -> int: mqtt_file_handler.setFormatter(logging.Formatter(FORMAT_STR_SHORT)) mqtt_log.addHandler(mqtt_file_handler) - smarthome_log_handler = RotatingFileHandler(RAMDISK_PATH + 'smarthome.log', maxBytes=mb_to_bytes(2), backupCount=1) + smarthome_log_handler = RotatingFileHandler(RAMDISK_PATH + 'smarthome.log', maxBytes=mb_to_bytes(1), backupCount=1) smarthome_log_handler.setFormatter(logging.Formatter(FORMAT_STR_SHORT)) smarthome_log_handler.addFilter(functools.partial(filter_pos, "smarthome")) logging.getLogger().addHandler(smarthome_log_handler) @@ -63,7 +66,7 @@ def mb_to_bytes(megabytes: int) -> int: logging.getLogger().addHandler(soc_log_handler) internal_chargepoint_log_handler = RotatingFileHandler(RAMDISK_PATH + 'internal_chargepoint.log', - maxBytes=mb_to_bytes(4), + maxBytes=mb_to_bytes(1), backupCount=1) internal_chargepoint_log_handler.setFormatter(logging.Formatter(FORMAT_STR_DETAILED)) internal_chargepoint_log_handler.addFilter(functools.partial(filter_pos, "Internal Chargepoint")) @@ -71,7 +74,7 @@ def mb_to_bytes(megabytes: int) -> int: urllib3_log = logging.getLogger("urllib3.connectionpool") urllib3_log.propagate = True - urllib3_file_handler = RotatingFileHandler(RAMDISK_PATH + 'soc.log', maxBytes=mb_to_bytes(5), backupCount=1) + urllib3_file_handler = RotatingFileHandler(RAMDISK_PATH + 'soc.log', maxBytes=mb_to_bytes(2), backupCount=1) urllib3_file_handler.setFormatter(logging.Formatter(FORMAT_STR_DETAILED)) urllib3_file_handler.addFilter(functools.partial(filter_pos, "soc")) urllib3_log.addHandler(urllib3_file_handler) @@ -79,6 +82,15 @@ def mb_to_bytes(megabytes: int) -> int: logging.getLogger("pymodbus").setLevel(logging.WARNING) logging.getLogger("uModbus").setLevel(logging.WARNING) + def threading_excepthook(args): + logging.getLogger(__name__).error("Uncaught exception in threading.excepthook:", exc_info=( + args.exc_type, args.exc_value, args.exc_traceback)) + threading.excepthook = threading_excepthook + + def handle_unhandled_exception(exc_type, exc_value, exc_traceback): + logging.getLogger(__name__).error("Uncaught exception", exc_info=(exc_type, exc_value, exc_traceback)) + sys.excepthook = handle_unhandled_exception + log = logging.getLogger(__name__) diff --git a/packages/helpermodules/measurement_logging/process_log.py b/packages/helpermodules/measurement_logging/process_log.py index 5aec2e33f2..594c5e58b8 100644 --- a/packages/helpermodules/measurement_logging/process_log.py +++ b/packages/helpermodules/measurement_logging/process_log.py @@ -1,9 +1,10 @@ +import datetime from decimal import Decimal from enum import Enum import json import logging from pathlib import Path -from typing import Dict, List, Tuple +from typing import Dict, List, Tuple, Union from helpermodules import timecheck from helpermodules.measurement_logging.write_log import (LegacySmartHomeLogData, LogType, create_entry, @@ -181,14 +182,14 @@ def get_monthly_log(date: str): def _collect_monthly_log_data(date: str): try: - with open(str(Path(__file__).resolve().parents[3] / "data"/"monthly_log"/(date+".json")), "r") as jsonFile: + with open(f"{_get_data_folder_path()}/monthly_log/{date}.json", "r") as jsonFile: log_data = json.load(jsonFile) this_month = timecheck.create_timestamp_YYYYMM() if date == this_month: # add last entry of current day, if current month is requested try: today = timecheck.create_timestamp_YYYYMMDD() - with open(str(Path(__file__).resolve().parents[3] / "data" / "daily_log"/(today+".json")), + with open(f"{_get_data_folder_path()}/daily_log/{today}.json", "r") as todayJsonFile: today_log_data = json.load(todayJsonFile) if len(today_log_data["entries"]) > 0: @@ -199,7 +200,7 @@ def _collect_monthly_log_data(date: str): # add first entry of next month try: next_date = timecheck.get_relative_date_string(date, month_offset=1) - with open(str(Path(__file__).resolve().parents[3] / "data"/"monthly_log"/(next_date+".json")), + with open(f"{_get_data_folder_path()}/monthly_log/{next_date}.json", "r") as nextJsonFile: next_log_data = json.load(nextJsonFile) log_data["entries"].append(next_log_data["entries"][0]) @@ -218,6 +219,66 @@ def get_yearly_log(year: str): return data +def get_log_from_date_until_now(timestamp: int): + data = {} + try: + entries = _collect_log_data_from_date_until_now(timestamp) + data["entries"] = _process_entries(entries, CalculationType.ENERGY) + data["totals"] = get_totals(data["entries"], False) + data = _analyse_energy_source(data) + except Exception: + log.exception(f"Fehler beim Zusammenstellen der Logdaten von {timestamp}") + finally: + return data + + +def _collect_log_data_from_date_until_now(timestamp: int): + def add_to_list(log_data: List, data: Union[Dict, List]): + if isinstance(data, list): + log_data.extend(data) + else: + log_data.append(data) + return log_data + log_data = [] + try: + date = datetime.datetime.fromtimestamp(timestamp).strftime("%Y%m%d") + try: + with open(f"{_get_data_folder_path()}/daily_log/{date}.json", "r") as jsonFile: + entries = json.load(jsonFile)["entries"] + except FILE_ERRORS: + pass + for index, entry in enumerate(entries): + if entry["timestamp"] > timestamp: + log_data = add_to_list(log_data, entries[index:]) + break + else: + try: + # Wenn der Ladevorgang nicht über volle 5 Minuten ging, wurde während dem Laden kein Eintrag ins + # daily-log geschrieben. + log_data = add_to_list(log_data, entries[-1]) + except KeyError: + log.exception(f"Fehler beim Zusammenstellen der Logdaten. Bitte Logdatei daily_log/{date}.json prüfen.") + # Das Teillog vom ersten Tag wurde bereits ermittelt. + start_date = datetime.datetime.fromtimestamp(timestamp) + datetime.timedelta(days=1) + end_date = datetime.datetime.now() + current_date = start_date + date_list = [] + while current_date <= end_date: + date_list.append(current_date.strftime('%Y%m%d')) + current_date += datetime.timedelta(days=1) + for date_str in date_list: + try: + with open(f"{_get_data_folder_path()}/daily_log/{date_str}.json", "r") as jsonFile: + log_data = add_to_list(log_data, json.load(jsonFile)["entries"]) + except FILE_ERRORS: + pass + log_data = add_to_list(log_data, create_entry(LogType.DAILY, LegacySmartHomeLogData(), log_data[-1])) + except Exception: + log.exception(f"Fehler beim Zusammenstellen der Logdaten von {timestamp}") + finally: + return log_data + + def _collect_yearly_log_data(year: str): def add_monthly_log(month: str, check_next_month: bool = False) -> None: monthly_log_path = Path(__file__).resolve().parents[3]/"data"/"monthly_log" @@ -238,8 +299,7 @@ def add_monthly_log(month: str, check_next_month: bool = False) -> None: def add_daily_log(day: str) -> None: try: - with open(str(Path(__file__).resolve().parents[3] / "data" / "daily_log"/(day+".json")), - "r") as dayJsonFile: + with open(f"{_get_data_folder_path()}/daily_log/{day}.json", "r") as dayJsonFile: day_log_data = json.load(dayJsonFile) if len(day_log_data["entries"]) > 0: entries.append(day_log_data["entries"][-1]) @@ -473,3 +533,7 @@ def _calculate_average_power(time_diff: float, current_imported: float = 0, next power = power.quantize(Decimal('0.001')) # limit precision power = f'{power: f}' return string_to_float(power) if "." in power else string_to_int(power) + + +def _get_data_folder_path() -> str: + return str(Path(__file__).resolve().parents[3] / "data") diff --git a/packages/helpermodules/measurement_logging/update_yields.py b/packages/helpermodules/measurement_logging/update_yields.py index c5fefb9bf4..73961bae15 100644 --- a/packages/helpermodules/measurement_logging/update_yields.py +++ b/packages/helpermodules/measurement_logging/update_yields.py @@ -4,13 +4,11 @@ from typing import Dict, List from control import data -from control.bat_all import BatAll +from control.chargepoint.chargepoint import Chargepoint +from control.pv_all import PvAll from helpermodules import timecheck from helpermodules.measurement_logging.process_log import get_totals from helpermodules.pub import Pub -from control.bat import Bat -from control.chargepoint.chargepoint import Chargepoint -from control.counter import Counter from control.ev import Ev from control.pv import Pv @@ -24,7 +22,6 @@ def update_daily_yields(entries): totals = get_totals(entries) [update_module_yields(type, totals) for type in ("bat", "counter", "cp", "pv")] data.data.counter_all_data.data.set.daily_yield_home_consumption = totals["hc"]["all"]["energy_imported"] - Pub().pub("openWB/set/counter/set/daily_yield_home_consumption", totals["hc"]["all"]["energy_imported"]) except Exception: log.exception("Fehler beim Veröffentlichen der Tageserträge.") @@ -38,10 +35,10 @@ def update_imported_exported(daily_imported: float, daily_exported: float) -> No topic = "chargepoint" else: topic = module - if isinstance(module_data, (Ev, Chargepoint, Pv, Bat, Counter)): + if isinstance(module_data, (Ev, Pv, Chargepoint)): Pub().pub(f"openWB/set/{topic}/{module_data.num}/get/daily_imported", daily_imported) Pub().pub(f"openWB/set/{topic}/{module_data.num}/get/daily_exported", daily_exported) - elif not isinstance(module_data, BatAll): + elif isinstance(module_data, PvAll): # wird im changed_values_handler an den Broker gesendet Pub().pub(f"openWB/set/{topic}/get/daily_imported", daily_imported) Pub().pub(f"openWB/set/{topic}/get/daily_exported", daily_exported) @@ -78,21 +75,26 @@ def update_pv_monthly_yearly_yields(): def _update_pv_monthly_yields(): """ veröffentlicht die monatlichen Erträge für PV + für pv_all nicht die Differenz aus den Logs nehmen, sondern die Summe der Module. Wenn im laufenden Monat ein Modul + gelöscht wurde und keins oder eines mit niedrigerem Zählerstand hinzugefügt wird, wird sonst ein negativer Wert + ermittelt. """ try: + pv_all_monthly_yield = 0 with open(f"data/monthly_log/{timecheck.create_timestamp_YYYYMM()}.json", "r") as f: monthly_log = json.load(f) - monthly_yield = data.data.pv_all_data.data.get.exported - monthly_log["entries"][0]["pv"]["all"]["exported"] - Pub().pub("openWB/set/pv/get/monthly_exported", monthly_yield) for pv_module in data.data.pv_data.values(): - for i in range(0, len(monthly_log["entries"])): - # erster Eintrag im Monat, in dem das PV-Modul existiert (falls ein Modul im laufenden Monat hinzugefügt - # wurde) - if monthly_log["entries"][i]["pv"].get(f"pv{pv_module.num}"): + for entry in monthly_log["entries"]: + if entry["pv"].get(f"pv{pv_module.num}"): monthly_yield = data.data.pv_data[f"pv{pv_module.num}"].data.get.exported - \ - monthly_log["entries"][i]["pv"][f"pv{pv_module.num}"]["exported"] + entry["pv"][f"pv{pv_module.num}"]["exported"] + pv_all_monthly_yield += monthly_yield Pub().pub(f"openWB/set/pv/{pv_module.num}/get/monthly_exported", monthly_yield) break + Pub().pub("openWB/set/pv/get/monthly_exported", pv_all_monthly_yield) + except FileNotFoundError: + # am Tag der Ersteinrichtung gibt es noch kein Monatslog-File, das wird erst um Mitternacht erstellt. + log.debug("No monthly logfile found for calculation of monthly yield") except Exception: log.exception("Fehler beim Veröffentlichen der monatlichen Erträge für PV") @@ -101,29 +103,45 @@ def pub_yearly_module_yield(sorted_path_list: List[str], pv_module: Pv): for path in sorted_path_list: with open(path, "r") as f: monthly_log = json.load(f) - for i in range(0, len(monthly_log["entries"])): - # erster Eintrag im Jahr, in dem das PV-Modul existiert (falls ein Modul im laufenden Jahr hinzugefügt - # wurde) - if monthly_log["entries"][i]["pv"].get(f"pv{pv_module.num}"): + for entry in monthly_log["entries"]: + # erster Eintrag mit PV im Jahr,falls WR erst im laufenden Jahr hinzugefügt wurden + if entry["pv"].get(f"pv{pv_module.num}"): yearly_yield = data.data.pv_data[f"pv{pv_module.num}"].data.get.exported - \ - monthly_log["entries"][i]["pv"][f"pv{pv_module.num}"]["exported"] + entry["pv"][f"pv{pv_module.num}"]["exported"] Pub().pub(f"openWB/set/pv/{pv_module.num}/get/yearly_exported", yearly_yield) return def _update_pv_yearly_yields(): """ veröffentlicht die jährlichen Erträge für PV + für pv_all nicht die Differenz aus den Logs nehmen, sondern die Summe der Module. Wenn unterjährig ein Modul + gelöscht wurde und keins oder eines mit niedrigerem Zählerstand hinzugefügt wird, wird sonst ein negativer Wert + ermittelt. """ try: + pv_all_yearly_yield = 0 path_list = list(Path(_get_parent_path()/"data"/"monthly_log").glob(f"{timecheck.create_timestamp_YYYY()}*")) sorted_path_list = sorted([str(p) for p in path_list]) - with open(sorted_path_list[0], "r") as f: - monthly_log = json.load(f) - yearly_yield = data.data.pv_all_data.data.get.exported - monthly_log["entries"][0]["pv"]["all"]["exported"] - Pub().pub("openWB/set/pv/get/yearly_exported", yearly_yield) - log.debug(f"sorted_path_list{sorted_path_list}") for pv_module in data.data.pv_data.values(): - pub_yearly_module_yield(sorted_path_list, pv_module) + found_pv = False + for path in sorted_path_list: + with open(path, "r") as f: + monthly_log = json.load(f) + for entry in monthly_log["entries"]: + # erster Eintrag mit PV im Jahr, falls WR erst im laufenden Jahr hinzugefügt wurden + if entry["pv"].get(f"pv{pv_module.num}"): + yearly_yield = data.data.pv_data[f"pv{pv_module.num}"].data.get.exported - \ + entry["pv"][f"pv{pv_module.num}"]["exported"] + pv_all_yearly_yield += yearly_yield + Pub().pub(f"openWB/set/pv/{pv_module.num}/get/yearly_exported", yearly_yield) + found_pv = True + break + if found_pv: + break + else: + # am Tag der Ersteinrichtung gibt es noch kein Monatslog-File, das wird erst um Mitternacht erstellt. + log.debug("No monthly logfile found for calculation of yearly yield") + Pub().pub("openWB/set/pv/get/yearly_exported", pv_all_yearly_yield) except Exception: log.exception("Fehler beim Veröffentlichen der jährlichen Erträge für PV") diff --git a/packages/helpermodules/measurement_logging/update_yields_test.py b/packages/helpermodules/measurement_logging/update_yields_test.py index 4411cd23af..35a3fd7b81 100644 --- a/packages/helpermodules/measurement_logging/update_yields_test.py +++ b/packages/helpermodules/measurement_logging/update_yields_test.py @@ -1,5 +1,4 @@ -import pytest - +from control import data from helpermodules.measurement_logging.update_yields import update_module_yields @@ -8,28 +7,17 @@ def test_update_module_yields(daily_log_totals, mock_pub): [update_module_yields(type, daily_log_totals) for type in ("bat", "counter", "cp", "pv")] # evaluation - expected = { - "openWB/set/bat/2/get/daily_imported": 0.0, - "openWB/set/bat/2/get/daily_exported": 550.0, - "openWB/set/counter/0/get/daily_imported": 1492.0, - "openWB/set/counter/0/get/daily_exported": 0.0, - "openWB/set/chargepoint/get/daily_imported": 1920.0, - "openWB/set/chargepoint/get/daily_exported": 0.0, - "openWB/set/chargepoint/4/get/daily_imported": 384.0, - "openWB/set/chargepoint/4/get/daily_exported": 0.0, - "openWB/set/chargepoint/5/get/daily_imported": 192.0, - "openWB/set/chargepoint/5/get/daily_exported": 0.0, - "openWB/set/chargepoint/6/get/daily_imported": 0.0, - "openWB/set/chargepoint/6/get/daily_exported": 0.0, - "openWB/set/pv/get/daily_exported": 251.0, - "openWB/set/pv/1/get/daily_exported": 251.0} - for topic, value in expected.items(): - for call in mock_pub.mock_calls: - try: - if call.args[0] == topic: - assert value == call.args[1] - break - except IndexError: - pass - else: - pytest.fail(f"Topic {topic} is missing") + data.data.bat_data["bat2"].data.get.daily_imported = 0.0 + data.data.bat_data["bat2"].data.get.daily_exported = 550.0 + data.data.counter_data["counter0"].data.get.daily_imported = 1492.0 + data.data.counter_data["counter0"].data.get.daily_exported = 0.0 + data.data.cp_all_data.data.get.daily_imported = 1920.0 + data.data.cp_all_data.data.get.daily_exported = 0.0 + data.data.cp_data["cp4"].data.get.daily_imported = 384.0 + data.data.cp_data["cp4"].data.get.daily_exported = 0.0 + data.data.cp_data["cp5"].data.get.daily_imported = 192.0 + data.data.cp_data["cp5"].data.get.daily_exported = 0.0 + data.data.cp_data["cp6"].data.get.daily_imported = 0.0 + data.data.cp_data["cp6"].data.get.daily_exported = 0.0 + data.data.pv_all_data.data.get.daily_exported = 251.0 + data.data.pv_data["pv1"].data.get.daily_exported = 251.0 diff --git a/packages/helpermodules/measurement_logging/write_log.py b/packages/helpermodules/measurement_logging/write_log.py index 32e41128f3..40aeaa5f69 100644 --- a/packages/helpermodules/measurement_logging/write_log.py +++ b/packages/helpermodules/measurement_logging/write_log.py @@ -10,6 +10,7 @@ from control import data from helpermodules.broker import InternalBrokerClient from helpermodules import timecheck +from helpermodules.utils.json_file_handler import write_and_check from helpermodules.utils.topic_parser import decode_payload, get_index from modules.common.utils.component_parser import get_component_name_by_id @@ -153,8 +154,7 @@ def save_log(log_type: LogType): entries = content["entries"] entries.append(new_entry) content["names"] = get_names(content["entries"][-1], sh_log_data.sh_names) - with open(filepath, "w") as jsonFile: - json.dump(content, jsonFile) + write_and_check(filepath, content) return content["entries"] except Exception: log.exception("Fehler beim Speichern des Log-Eintrags") @@ -209,14 +209,15 @@ def create_entry(log_type: LogType, sh_log_data: LegacySmartHomeLogData, previou log.exception("Fehler im Werte-Logging-Modul für EV "+str(ev)) counter_dict = {} - for counter in data.data.counter_data: + for counter in data.data.counter_data.values(): try: - if "counter" in counter: + home_consumption_source_id = data.data.counter_all_data.data.config.home_consumption_source_id + if (home_consumption_source_id is None or counter.num != home_consumption_source_id): counter_dict.update( - {counter: { - "imported": data.data.counter_data[counter].data.get.imported, - "exported": data.data.counter_data[counter].data.get.exported, - "grid": True if data.data.counter_all_data.get_evu_counter_str() == counter else False}}) + {f"counter{counter.num}": { + "imported": counter.data.get.imported, + "exported": counter.data.get.exported, + "grid": True if data.data.counter_all_data.get_id_evu_counter() == counter.num else False}}) except Exception: log.exception("Fehler im Werte-Logging-Modul für Zähler "+str(counter)) diff --git a/packages/helpermodules/measurement_logging/write_log_test.py b/packages/helpermodules/measurement_logging/write_log_test.py index 64d05f7a76..a491716c85 100644 --- a/packages/helpermodules/measurement_logging/write_log_test.py +++ b/packages/helpermodules/measurement_logging/write_log_test.py @@ -15,9 +15,9 @@ def test_get_names(daily_log_totals, monkeypatch): assert names == {'bat2': "Speicher", 'counter0': "Zähler", 'cp3': "cp3", - 'cp4': "Standard-Ladepunkt", - 'cp5': "Standard-Ladepunkt", - 'cp6': "Standard-Ladepunkt", + 'cp4': "neuer Ladepunkt", + 'cp5': "neuer Ladepunkt", + 'cp6': "neuer Ladepunkt", 'pv1': "Wechselrichter", "sh1": "Smarthome1"} diff --git a/packages/helpermodules/parse_send_debug.py b/packages/helpermodules/parse_send_debug.py deleted file mode 100644 index e3d55734d2..0000000000 --- a/packages/helpermodules/parse_send_debug.py +++ /dev/null @@ -1,74 +0,0 @@ -import logging -import pprint -from control import data -import dataclass_utils -from modules.common.abstract_device import AbstractDevice - -log = logging.getLogger(__name__) - - -def parse_send_debug_data(): - parsed_data = "# Hierarchie\n" - pretty_hierarchy = pprint.pformat(data.data.counter_all_data.data.get.hierarchy, - indent=4, compact=True, sort_dicts=False, width=100) - parsed_data += f"{pretty_hierarchy}\n" - parsed_data += "\n# Geräte und Komponenten\n" - for key, value in data.data.system_data.items(): - try: - if isinstance(value, AbstractDevice): - parsed_data += f"{key}: {dataclass_utils.asdict(value.device_config)}\n" - for comp_key, comp_value in value.components.items(): - parsed_data += f"{comp_key}: {dataclass_utils.asdict(comp_value.component_config)}\n" - if "bat" in comp_value.component_config.type: - component_data = data.data.bat_data[f"bat{comp_value.component_config.id}"] - elif "counter" in comp_value.component_config.type: - component_data = data.data.counter_data[f"counter{comp_value.component_config.id}"] - elif "inverter" in comp_value.component_config.type: - component_data = data.data.pv_data[f"pv{comp_value.component_config.id}"] - if "bat" in comp_value.component_config.type: - parsed_data += (f"Leistung: {component_data.data.get.power/1000}kW, " - f"SoC: {component_data.data.get.soc}%, " - f"Fehlerstatus: {component_data.data.get.fault_str}\n") - elif "inverter" in comp_value.component_config.type: - parsed_data += (f"Leistung: {component_data.data.get.power/1000}kW, " - f"Fehlerstatus: {component_data.data.get.fault_str}\n") - else: - if data.data.counter_all_data.get_evu_counter_str() == f"counter{component_data.num}": - parsed_data += (f"{key}: EVU-Zähler -> max. Leistung " - f"{component_data.data.config.max_total_power}, " - f"max. Ströme {component_data.data.config.max_currents}; ") - else: - parsed_data += f"{key}: max. Ströme {component_data.data.config.max_currents}" - parsed_data += (f"Leistung: {component_data.data.get.power/1000}kW, Ströme: " - f"{component_data.data.get.currents}A, Fehlerstatus: " - f"{component_data.data.get.fault_str}\n") - except Exception: - log.exception("Fehler beim Parsen der Daten für das Support-Ticket") - parsed_data += f"Hausverbrauch: {data.data.counter_all_data.data.set.home_consumption}W\n" - chargemode_config = data.data.general_data.data.chargemode_config - parsed_data += (f"Phasenvorgabe: Sofortladen {chargemode_config.instant_charging.phases_to_use}, Zielladen " - f"{chargemode_config.scheduled_charging.phases_to_use}, Zeitladen: " - f"{chargemode_config.time_charging.phases_to_use}, PV-Laden: " - f"{chargemode_config.pv_charging.phases_to_use}, Einschaltschwelle: " - f"{chargemode_config.pv_charging.switch_on_threshold}W, Ausschaltschwelle: " - f"{chargemode_config.pv_charging.switch_off_threshold}W\n") - - parsed_data += "\n# Ladepunkte\n" - parsed_data += f"Ladeleistung aller Ladepunkte {data.data.cp_all_data.data.get.power / 1000}kW\n" - for cp in data.data.cp_data.values(): - try: - if hasattr(cp.chargepoint_module.config.configuration, "ip_address"): - ip = cp.chargepoint_module.config.configuration.ip_address - else: - ip = None - parsed_data += (f"LP{cp.num}: Typ: {cp.chargepoint_module.config.type}; IP: " - f"{ip}; Stecker-Status: {cp.data.get.plug_state}, Leistung: " - f"{cp.data.get.power/1000}kW, {cp.data.get.currents}A, {cp.data.get.voltages}V, Lademodus: " - f"{cp.data.control_parameter.chargemode}, Submode: " - f"{cp.data.control_parameter.submode}, Sollstrom: " - f"{cp.data.set.current}A, Status: {cp.data.get.state_str}, " - f"Fehlerstatus: {cp.data.get.fault_str}\n") - except Exception: - log.exception("Fehler beim Parsen der Daten für das Support-Ticket") - - return parsed_data diff --git a/packages/helpermodules/setdata.py b/packages/helpermodules/setdata.py index c21c4b3ee1..ddc0329ea3 100644 --- a/packages/helpermodules/setdata.py +++ b/packages/helpermodules/setdata.py @@ -446,7 +446,6 @@ def _subprocess_vehicle_chargemode_topic(self, msg: mqtt.MQTTMessage): if "/name" in msg.topic: self._validate_value(msg, str, pub_json=True) elif ("/load_default" in msg.topic or - "/disable_after_unplug" in msg.topic or "/prio" in msg.topic): self._validate_value(msg, bool, pub_json=True) elif "/chargemode/selected" in msg.topic: @@ -511,6 +510,8 @@ def process_chargepoint_topic(self, msg: mqtt.MQTTMessage): "openWB/set/chargepoint/get/daily_imported" in msg.topic or "openWB/set/chargepoint/get/daily_exported" in msg.topic): self._validate_value(msg, float, [(0, float("inf"))]) + elif re.search("chargepoint/[0-9]+/config/template$", msg.topic) is not None: + self._validate_value(msg, int, pub_json=True) elif "template" in msg.topic: self._validate_value(msg, "json") elif re.search("chargepoint/[0-9]+/config$", msg.topic) is not None: @@ -538,8 +539,6 @@ def process_chargepoint_topic(self, msg: mqtt.MQTTMessage): self._validate_value(msg, float) elif "/set/log" in msg.topic: self._validate_value(msg, "json") - elif "/set/change_ev_permitted" in msg.topic: - self._validate_value(msg, "json") elif "/config/ev" in msg.topic: self._validate_value( msg, int, [(0, float("inf"))], pub_json=True) @@ -567,6 +566,8 @@ def process_chargepoint_topic(self, msg: mqtt.MQTTMessage): self._validate_value(msg, float, [(0, float("inf"))]) elif "/control_parameter/state" in msg.topic: self._validate_value(msg, int, [(0, 7)]) + elif "/disable_after_unplug" in msg.topic: + self._validate_value(msg, bool, pub_json=True) else: self.__unknown_topic(msg) else: @@ -729,7 +730,8 @@ def process_general_topic(self, msg: mqtt.MQTTMessage): try: if "openWB/set/general/extern_display_mode" in msg.topic: self._validate_value(msg, str) - elif ("openWB/set/general/modbus_control" in msg.topic or + elif ("openWB/set/general/http_api" in msg.topic or + "openWB/set/general/modbus_control" in msg.topic or "openWB/set/general/extern" in msg.topic): self._validate_value(msg, bool) elif "openWB/set/general/control_interval" in msg.topic: @@ -750,12 +752,13 @@ def process_general_topic(self, msg: mqtt.MQTTMessage): self._validate_value(msg, int, [(0, float("inf"))]) elif "openWB/set/general/chargemode_config/pv_charging/switch_off_threshold" in msg.topic: self._validate_value(msg, float) - elif "openWB/set/general/chargemode_config/pv_charging/phase_switch_delay" in msg.topic: + elif "openWB/set/general/chargemode_config/phase_switch_delay" in msg.topic: self._validate_value(msg, int, [(1, 15)]) elif "openWB/set/general/chargemode_config/pv_charging/control_range" in msg.topic: self._validate_value(msg, int, collection=list) elif (("openWB/set/general/chargemode_config/pv_charging/phases_to_use" in msg.topic or - "openWB/set/general/chargemode_config/scheduled_charging/phases_to_use" in msg.topic)): + "openWB/set/general/chargemode_config/scheduled_charging/phases_to_use" in msg.topic or + "openWB/set/general/chargemode_config/scheduled_charging/phases_to_use_pv" in msg.topic)): self._validate_value(msg, int, [(0, 0), (1, 1), (3, 3)]) elif "openWB/set/general/chargemode_config/pv_charging/min_bat_soc" in msg.topic: self._validate_value(msg, int, [(0, 100)]) @@ -873,12 +876,12 @@ def process_counter_topic(self, msg: mqtt.MQTTMessage): self._validate_value(msg, float, [(0, float("inf"))]) elif "openWB/set/counter/get/hierarchy" in msg.topic: self._validate_value(msg, None) + elif "openWB/set/counter/config/home_consumption_source_id" in msg.topic: + self._validate_value(msg, int) elif "openWB/set/counter/set/simulation" in msg.topic: self._validate_value(msg, "json") elif "/set/consumption_left" in msg.topic: self._validate_value(msg, float) - elif "/config/selected" in msg.topic: - self._validate_value(msg, str) elif "/module" in msg.topic: self._validate_value(msg, "json") elif "/config/max_currents" in msg.topic: @@ -897,7 +900,6 @@ def process_counter_topic(self, msg: mqtt.MQTTMessage): self._validate_value( msg, float, [(-1, 1)], collection=list) elif ("/get/power_average" in msg.topic - or "/get/unbalanced_load" in msg.topic or "/get/frequency" in msg.topic or "/get/daily_exported" in msg.topic or "/get/daily_imported" in msg.topic @@ -909,8 +911,7 @@ def process_counter_topic(self, msg: mqtt.MQTTMessage): self._validate_value(msg, int, [(0, 2)]) elif "/set/error_counter" in msg.topic: self._validate_value(msg, int, [(0, float("inf"))]) - elif ("/get/fault_str" in msg.topic or - "/set/state_str" in msg.topic): + elif "/get/fault_str" in msg.topic: self._validate_value(msg, str) elif "/get/power" in msg.topic: self._validate_value( @@ -984,6 +985,7 @@ def process_system_topic(self, msg: mqtt.MQTTMessage): "openWB/set/system/wizard_done" in msg.topic or "openWB/set/system/update_in_progress" in msg.topic or "openWB/set/system/backup_cloud/backup_before_update" in msg.topic or + "openWB/set/system/installAssistantDone" in msg.topic or "openWB/set/system/dataprotection_acknowledged" in msg.topic or "openWB/set/system/usage_terms_acknowledged" in msg.topic or "openWB/set/system/update_config_completed" in msg.topic): diff --git a/packages/helpermodules/subdata.py b/packages/helpermodules/subdata.py index b7d3f23487..d8bc88df2b 100644 --- a/packages/helpermodules/subdata.py +++ b/packages/helpermodules/subdata.py @@ -23,6 +23,7 @@ from helpermodules.abstract_plans import AutolockPlan from helpermodules.broker import InternalBrokerClient from helpermodules.messaging import MessageType, pub_system_message +from helpermodules.utils.run_command import run_command from helpermodules.utils.topic_parser import decode_payload, get_index, get_second_index from control import optional from helpermodules.pub import Pub @@ -30,6 +31,9 @@ from control import pv from dataclass_utils import dataclass_from_dict from modules.common.abstract_vehicle import CalculatedSocState, GeneralVehicleConfig +from modules.common.configurable_backup_cloud import ConfigurableBackupCloud +from modules.common.configurable_ripple_control_receiver import ConfigurableRcr +from modules.common.configurable_tariff import ConfigurableElectricityTariff from modules.common.simcount.simcounter_state import SimCounterState from modules.internal_chargepoint_handler.internal_chargepoint_handler_config import ( GlobalHandlerData, InternalChargepoint, RfidData) @@ -571,11 +575,14 @@ def process_general_topic(self, var: general.General, msg: mqtt.MQTTMessage): config_dict = decode_payload(msg.payload) if config_dict["type"] is None: var.data.ripple_control_receiver.module = None + var.ripple_control_receiver = None else: mod = importlib.import_module(".ripple_control_receivers." + config_dict["type"]+".ripple_control_receiver", "modules") config = dataclass_from_dict(mod.device_descriptor.configuration_factory, config_dict) - var.data.ripple_control_receiver.module = mod.create_ripple_control_receiver(config) + var.data.ripple_control_receiver.module = config_dict + var.ripple_control_receiver = ConfigurableRcr( + config=config, component_initialiser=mod.create_ripple_control_receiver) elif re.search("/general/ripple_control_receiver/get/", msg.topic) is not None: self.set_json_payload_class(var.data.ripple_control_receiver.get, msg) elif re.search("/general/ripple_control_receiver/", msg.topic) is not None: @@ -602,12 +609,24 @@ def process_general_topic(self, var: general.General, msg: mqtt.MQTTMessage): # 5 Min Handler bis auf Heartbeat, Cleanup, ... beenden self.event_jobs_running.clear() self.set_json_payload_class(var.data, msg) - subprocess.run([ + run_command([ str(Path(__file__).resolve().parents[2] / "runs" / "setup_network.sh") - ]) + ], process_exception=True) elif "openWB/general/modbus_control" == msg.topic: if decode_payload(msg.payload) and self.general_data.data.extern: self.event_modbus_server.set() + elif "openWB/general/http_api" == msg.topic: + if ( + self.event_subdata_initialized.is_set() and + self.general_data.data.http_api != decode_payload(msg.payload) + ): + pub_system_message( + msg.payload, + "Bitte die openWB " + "neu starten, damit die Änderungen an der HTTP-API wirksam werden.", + MessageType.SUCCESS + ) + self.set_json_payload_class(var.data, msg) else: self.set_json_payload_class(var.data, msg) except Exception: @@ -633,9 +652,9 @@ def process_optional_topic(self, var: optional.Optional, msg: mqtt.MQTTMessage): self.set_json_payload_class(var.data.int_display, msg) if re.search("/(standby|active|rotation)$", msg.topic) is not None: # some topics require an update of the display manager or boot settings - subprocess.run([ + run_command([ str(Path(__file__).resolve().parents[2] / "runs" / "update_local_display.sh") - ]) + ], process_exception=True) elif re.search("/optional/et/", msg.topic) is not None: if re.search("/optional/et/get/prices", msg.topic) is not None: var.data.et.get.prices = decode_payload(msg.payload) @@ -649,7 +668,7 @@ def process_optional_topic(self, var: optional.Optional, msg: mqtt.MQTTMessage): mod = importlib.import_module( f".electricity_tariffs.{config_dict['type']}.tariff", "modules") config = dataclass_from_dict(mod.device_descriptor.configuration_factory, config_dict) - var.et_module = mod.create_electricity_tariff(config) + var.et_module = ConfigurableElectricityTariff(config, mod.create_electricity_tariff) var.et_get_prices() else: self.set_json_payload_class(var.data.et, msg) @@ -759,12 +778,14 @@ def process_system_topic(self, client: mqtt.Client, var: dict, msg: mqtt.MQTTMes if self.event_subdata_initialized.is_set(): index = get_index(msg.topic) parent_file = Path(__file__).resolve().parents[2] - result = subprocess.run( - ["php", "-f", str(parent_file / "runs" / "save_mqtt.php"), index, msg.payload], - stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True) - if len(result.stdout) > 0: - pub_system_message(msg.payload, result.stdout, - MessageType.SUCCESS if result.returncode == 0 else MessageType.ERROR) + try: + result = run_command( + ["php", "-f", str(parent_file / "runs" / "save_mqtt.php"), index, msg.payload]) + pub_system_message(msg.payload, result, MessageType.SUCCESS) + except subprocess.CalledProcessError as e: + log.debug(e.stdout) + pub_system_message(msg.payload, f'Fehler-Status: {e.returncode}
Meldung: {e.stderr}', + MessageType.ERROR) else: log.debug("skipping mqtt bridge message on startup") elif "mqtt" and "valid_partner_ids" in msg.topic: @@ -779,8 +800,8 @@ def process_system_topic(self, client: mqtt.Client, var: dict, msg: mqtt.MQTTMes token = splitted[0] port = splitted[1] if len(splitted) > 1 else "2223" user = splitted[2] if len(splitted) > 2 else "getsupport" - subprocess.run([str(Path(__file__).resolve().parents[2] / "runs" / "start_remote_support.sh"), - token, port, user]) + run_command([str(Path(__file__).resolve().parents[2] / "runs" / "start_remote_support.sh"), + token, port, user], process_exception=True) elif "openWB/system/backup_cloud/config" in msg.topic: config_dict = decode_payload(msg.payload) if config_dict["type"] is None: @@ -788,9 +809,9 @@ def process_system_topic(self, client: mqtt.Client, var: dict, msg: mqtt.MQTTMes else: mod = importlib.import_module(".backup_clouds."+config_dict["type"]+".backup_cloud", "modules") config = dataclass_from_dict(mod.device_descriptor.configuration_factory, config_dict) - var["system"].backup_cloud = mod.create_backup_cloud(config) + var["system"].backup_cloud = ConfigurableBackupCloud(config, mod.create_backup_cloud) elif "openWB/system/backup_cloud/backup_before_update" in msg.topic: - self.set_json_payload(var["system"].data, msg) + self.set_json_payload(var["system"].data["backup_cloud"], msg) else: if "module_update_completed" in msg.topic: self.event_module_update_completed.set() diff --git a/packages/helpermodules/system.py b/packages/helpermodules/system.py index 340e10aecd..c49889e550 100644 --- a/packages/helpermodules/system.py +++ b/packages/helpermodules/system.py @@ -11,6 +11,7 @@ from helpermodules import pub from control import data +from helpermodules.utils.run_command import run_command from modules.common.configurable_backup_cloud import ConfigurableBackupCloud log = logging.getLogger(__name__) @@ -21,7 +22,8 @@ def __init__(self): """ """ self.data = {"update_in_progress": False, - "perform_update": False} + "perform_update": False, + "backup_cloud": {}} self.backup_cloud: Optional[ConfigurableBackupCloud] = None def perform_update(self): @@ -39,8 +41,8 @@ def perform_update(self): self._trigger_ext_update(train) time.sleep(15) # aktuell soll kein Update für den Master durchgeführt werden. - # subprocess.run([str(Path(__file__).resolve().parents[2]/"runs"/"update_self.sh"), train]) - subprocess.run(str(Path(__file__).resolve().parents[2]/"runs"/"atreboot.sh")) + # run_command([str(Path(__file__).resolve().parents[2]/"runs"/"update_self.sh"), train]) + run_command(str(Path(__file__).resolve().parents[2]/"runs"/"atreboot.sh"), process_exception=True) except Exception: log.exception("Fehler im System-Modul") @@ -98,12 +100,13 @@ def create_backup_and_send_to_cloud(self): log.debug('Nächtliche Sicherung erstellt und hochgeladen.') def create_backup(self) -> str: - result = subprocess.run([str(self._get_parent_file() / "runs" / "backup.sh"), "1"], stdout=subprocess.PIPE) - if result.returncode == 0: - file_name = result.stdout.decode("utf-8").rstrip('\n') + try: + result = run_command([str(self._get_parent_file() / "runs" / "backup.sh"), "1"]) + file_name = result.rstrip('\n') return file_name - else: - raise Exception(f'Backup-Status: {result.returncode}, Meldung: {result.stdout.decode("utf-8")}') + except subprocess.CalledProcessError as e: + log.debug(e.stdout) + raise Exception(f'Backup-Status: {e.returncode}, Meldung: {e.stderr}') def _get_parent_file(self) -> Path: return Path(__file__).resolve().parents[2] diff --git a/packages/helpermodules/timecheck.py b/packages/helpermodules/timecheck.py index 97f51fc6b2..27d6847483 100644 --- a/packages/helpermodules/timecheck.py +++ b/packages/helpermodules/timecheck.py @@ -3,7 +3,6 @@ import copy import logging import datetime -import math from dateutil.relativedelta import relativedelta from typing import Dict, List, Optional, Tuple, TypeVar, Union @@ -344,6 +343,12 @@ def convert_timedelta_to_time_string(timedelta_obj: datetime.timedelta) -> str: def convert_timestamp_delta_to_time_string(timestamp: int, delta: int) -> str: - diff = delta - (create_timestamp() - timestamp) - minute_diff = int(diff/60) - return f"{f'{minute_diff} Min. ' if minute_diff > 0 else ''}{math.ceil(diff%60)} Sek." + diff = int(delta - (create_timestamp() - timestamp)) + seconds_diff = diff % 60 + minute_diff = int((diff - seconds_diff) / 60) + if minute_diff > 0 and seconds_diff > 0: + return f"{minute_diff} Min. {seconds_diff} Sek." + elif minute_diff > 0: + return f"{minute_diff} Min." + elif seconds_diff > 0: + return f"{seconds_diff} Sek." diff --git a/packages/helpermodules/timecheck_test.py b/packages/helpermodules/timecheck_test.py index da47c316db..e8a0ce920e 100644 --- a/packages/helpermodules/timecheck_test.py +++ b/packages/helpermodules/timecheck_test.py @@ -151,12 +151,21 @@ def test_check_timeframe(plan: Union[AutolockPlan, TimeChargingPlan], now: str, assert state == expected_state -def test_convert_timestamp_delta_to_time_string(): +@pytest.mark.parametrize("timestamp, expected", + [ + pytest.param(1652683202, "40 Sek."), + pytest.param(1652683222, "1 Min."), + pytest.param(1652683221.8, "59 Sek."), + pytest.param(1652683222.2, "1 Min."), + pytest.param(1652683232, "1 Min. 10 Sek.") + ] + ) +def test_convert_timestamp_delta_to_time_string(timestamp, expected): # setup delta = 90 # execution - time_string = timecheck.convert_timestamp_delta_to_time_string(1652683202, delta) + time_string = timecheck.convert_timestamp_delta_to_time_string(timestamp, delta) # evaluation - assert time_string == "40 Sek." + assert time_string == expected diff --git a/packages/helpermodules/update_config.py b/packages/helpermodules/update_config.py index 8290151aa0..8e7cd57256 100644 --- a/packages/helpermodules/update_config.py +++ b/packages/helpermodules/update_config.py @@ -5,7 +5,6 @@ import logging from pathlib import Path import re -import subprocess import time from typing import List, Optional from paho.mqtt.client import Client as MqttClient, MQTTMessage @@ -23,6 +22,8 @@ from helpermodules.measurement_logging.write_log import get_names from helpermodules.messaging import MessageType, pub_system_message from helpermodules.pub import Pub +from helpermodules.utils.json_file_handler import write_and_check +from helpermodules.utils.run_command import run_command from helpermodules.utils.topic_parser import decode_payload, get_index, get_second_index from control import counter_all from control import ev @@ -33,6 +34,7 @@ from modules.display_themes.cards.config import CardsDisplayTheme from modules.ripple_control_receivers.gpio.config import GpioRcr from modules.web_themes.standard_legacy.config import StandardLegacyWebTheme +from modules.devices.good_we.version import GoodWeVersion log = logging.getLogger(__name__) @@ -40,7 +42,7 @@ class UpdateConfig: - DATASTORE_VERSION = 44 + DATASTORE_VERSION = 54 valid_topic = [ "^openWB/bat/config/configured$", "^openWB/bat/set/charging_power_left$", @@ -74,6 +76,7 @@ class UpdateConfig: "^openWB/chargepoint/[0-9]+/control_parameter/chargemode$", "^openWB/chargepoint/[0-9]+/control_parameter/current_plan$", "^openWB/chargepoint/[0-9]+/control_parameter/imported_at_plan_start$", + "^openWB/chargepoint/[0-9]+/control_parameter/imported_instant_charging$", "^openWB/chargepoint/[0-9]+/control_parameter/limit$", "^openWB/chargepoint/[0-9]+/control_parameter/prio$", "^openWB/chargepoint/[0-9]+/control_parameter/required_current$", @@ -85,6 +88,7 @@ class UpdateConfig: "^openWB/chargepoint/[0-9]+/control_parameter/state$", "^openWB/chargepoint/[0-9]+/get/charge_state$", "^openWB/chargepoint/[0-9]+/get/currents$", + "^openWB/chargepoint/[0-9]+/get/evse_current$", "^openWB/chargepoint/[0-9]+/get/fault_state$", "^openWB/chargepoint/[0-9]+/get/fault_str$", "^openWB/chargepoint/[0-9]+/get/frequency$", @@ -97,7 +101,11 @@ class UpdateConfig: "^openWB/chargepoint/[0-9]+/get/power$", "^openWB/chargepoint/[0-9]+/get/powers$", "^openWB/chargepoint/[0-9]+/get/power_factors$", + "^openWB/chargepoint/[0-9]+/get/vehicle_id$", "^openWB/chargepoint/[0-9]+/get/voltages$", + "^openWB/chargepoint/[0-9]+/get/serial_number$", + "^openWB/chargepoint/[0-9]+/get/soc$", + "^openWB/chargepoint/[0-9]+/get/soc_timestamp$", "^openWB/chargepoint/[0-9]+/get/state_str$", "^openWB/chargepoint/[0-9]+/get/connected_vehicle/soc$", "^openWB/chargepoint/[0-9]+/get/connected_vehicle/info$", @@ -111,7 +119,6 @@ class UpdateConfig: "^openWB/chargepoint/[0-9]+/set/plug_state_prev$", "^openWB/chargepoint/[0-9]+/set/plug_time$", "^openWB/chargepoint/[0-9]+/set/rfid$", - "^openWB/chargepoint/[0-9]+/set/change_ev_permitted$", "^openWB/chargepoint/[0-9]+/set/log$", "^openWB/chargepoint/[0-9]+/set/phases_to_use$", "^openWB/chargepoint/[0-9]+/set/charging_ev_prev$", @@ -130,6 +137,7 @@ class UpdateConfig: "^openWB/command/todo$", "^openWB/counter/config/reserve_for_not_charging$", + "^openWB/counter/config/home_consumption_source_id$", "^openWB/counter/get/hierarchy$", "^openWB/counter/set/disengageable_smarthome_power$", "^openWB/counter/set/imported_home_consumption$", @@ -153,7 +161,6 @@ class UpdateConfig: "^openWB/counter/[0-9]+/set/error_counter$", "^openWB/counter/[0-9]+/set/released_surplus$", "^openWB/counter/[0-9]+/set/reserved_surplus$", - "^openWB/counter/[0-9]+/set/state_str$", "^openWB/counter/[0-9]+/config/max_currents$", "^openWB/counter/[0-9]+/config/max_total_power$", @@ -163,6 +170,7 @@ class UpdateConfig: "^openWB/general/external_buttons_hw$", "^openWB/general/grid_protection_configured$", "^openWB/general/grid_protection_active$", + "^openWB/general/http_api$", "^openWB/general/modbus_control$", "^openWB/general/mqtt_bridge$", "^openWB/general/grid_protection_timestamp$", @@ -188,7 +196,7 @@ class UpdateConfig: "^openWB/general/chargemode_config/pv_charging/switch_on_delay$", "^openWB/general/chargemode_config/pv_charging/switch_off_threshold$", "^openWB/general/chargemode_config/pv_charging/switch_off_delay$", - "^openWB/general/chargemode_config/pv_charging/phase_switch_delay$", + "^openWB/general/chargemode_config/phase_switch_delay$", "^openWB/general/chargemode_config/pv_charging/control_range$", "^openWB/general/chargemode_config/pv_charging/phases_to_use$", "^openWB/general/chargemode_config/pv_charging/min_bat_soc$", @@ -198,6 +206,7 @@ class UpdateConfig: "^openWB/general/chargemode_config/pv_charging/bat_power_reserve_active$", "^openWB/general/chargemode_config/retry_failed_phase_switches$", "^openWB/general/chargemode_config/scheduled_charging/phases_to_use$", + "^openWB/general/chargemode_config/scheduled_charging/phases_to_use_pv$", "^openWB/general/chargemode_config/instant_charging/phases_to_use$", "^openWB/general/chargemode_config/time_charging/phases_to_use$", # obsolet, Daten hieraus müssen nach prices/ überführt werden @@ -238,6 +247,8 @@ class UpdateConfig: "^openWB/pv/config/configured$", "^openWB/pv/get/exported$", + "^openWB/pv/get/fault_state$", + "^openWB/pv/get/fault_str$", "^openWB/pv/get/power$", "^openWB/pv/get/daily_exported$", "^openWB/pv/get/monthly_exported$", @@ -352,6 +363,8 @@ class UpdateConfig: "^openWB/LegacySmartHome/config/get/Devices/[0-9]+/device_updatesec$", "^openWB/LegacySmartHome/config/get/Devices/[0-9]+/device_username$", "^openWB/LegacySmartHome/config/set/Devices/[0-9]+/mode$", + "^openWB/LegacySmartHome/Devices/[0-9]+/device_manual_control$", + "^openWB/LegacySmartHome/Devices/[0-9]+/mode$", "^openWB/LegacySmartHome/Devices/[0-9]+/WHImported_temp$", "^openWB/LegacySmartHome/Devices/[0-9]+/RunningTimeToday$", "^openWB/LegacySmartHome/Devices/[0-9]+/oncountnor$", @@ -381,6 +394,7 @@ class UpdateConfig: "^openWB/system/current_commit", "^openWB/system/current_missing_commits", "^openWB/system/dataprotection_acknowledged$", + "^openWB/system/installAssistantDone$", "^openWB/system/datastore_version", "^openWB/system/debug_level$", "^openWB/system/device/[0-9]+/component/[0-9]+/config$", @@ -393,7 +407,6 @@ class UpdateConfig: "^openWB/system/device/module_update_completed$", "^openWB/system/ip_address$", "^openWB/system/lastlivevaluesJson$", - "^openWB/system/messages/[0-9]+$", "^openWB/system/mqtt/bridge/[0-9]+$", "^openWB/system/mqtt/valid_partner_ids$", "^openWB/system/release_train$", @@ -403,20 +416,22 @@ class UpdateConfig: "^openWB/system/version$", ] default_topic = ( + ("openWB/bat/config/configured", False), ("openWB/bat/get/fault_state", 0), ("openWB/bat/get/fault_str", NO_ERROR), ("openWB/chargepoint/get/power", 0), ("openWB/chargepoint/template/0", get_chargepoint_template_default()), ("openWB/counter/get/hierarchy", []), ("openWB/counter/config/reserve_for_not_charging", counter_all.Config().reserve_for_not_charging), - ("openWB/vehicle/0/name", ev.EvData().name), + ("openWB/counter/config/home_consumption_source_id", counter_all.Config().home_consumption_source_id), + ("openWB/vehicle/0/name", "Standard-Fahrzeug"), ("openWB/vehicle/0/charge_template", ev.Ev(0).charge_template.ct_num), ("openWB/vehicle/0/soc_module/config", NO_MODULE), ("openWB/vehicle/0/soc_module/general_config", dataclass_utils.asdict(GeneralVehicleConfig())), ("openWB/vehicle/0/ev_template", ev.Ev(0).ev_template.et_num), ("openWB/vehicle/0/tag_id", ev.Ev(0).data.tag_id), ("openWB/vehicle/0/get/soc", ev.Ev(0).data.get.soc), - ("openWB/vehicle/template/ev_template/0", asdict(ev.EvTemplateData(min_current=10))), + ("openWB/vehicle/template/ev_template/0", asdict(ev.EvTemplateData(name="Fahrzeug-Profil", min_current=10))), ("openWB/vehicle/template/charge_template/0", ev.get_charge_template_default()), ("openWB/general/chargemode_config/instant_charging/phases_to_use", 3), ("openWB/general/chargemode_config/pv_charging/bat_mode", BatConsiderationMode.EV_MODE.value), @@ -431,11 +446,12 @@ class UpdateConfig: ("openWB/general/chargemode_config/pv_charging/switch_on_delay", 30), ("openWB/general/chargemode_config/pv_charging/switch_on_threshold", 1500), ("openWB/general/chargemode_config/pv_charging/feed_in_yield", 0), - ("openWB/general/chargemode_config/pv_charging/phase_switch_delay", 7), + ("openWB/general/chargemode_config/phase_switch_delay", 7), ("openWB/general/chargemode_config/pv_charging/phases_to_use", 0), ("openWB/general/chargemode_config/retry_failed_phase_switches", ChargemodeConfig().retry_failed_phase_switches), ("openWB/general/chargemode_config/scheduled_charging/phases_to_use", 0), + ("openWB/general/chargemode_config/scheduled_charging/phases_to_use_pv", 0), ("openWB/general/chargemode_config/time_charging/phases_to_use", 1), ("openWB/general/chargemode_config/unbalanced_load", False), ("openWB/general/chargemode_config/unbalanced_load_limit", 18), @@ -444,6 +460,7 @@ class UpdateConfig: ("openWB/general/extern_display_mode", "primary"), ("openWB/general/external_buttons_hw", False), ("openWB/general/grid_protection_configured", True), + ("openWB/general/http_api", False), ("openWB/general/modbus_control", False), ("openWB/general/notifications/selected", "none"), ("openWB/general/notifications/plug", False), @@ -473,6 +490,7 @@ class UpdateConfig: ("openWB/optional/rfid/active", False), ("openWB/system/backup_cloud/config", NO_MODULE), ("openWB/system/backup_cloud/backup_before_update", True), + ("openWB/system/installAssistantDone", False), ("openWB/system/dataprotection_acknowledged", False), ("openWB/system/datastore_version", DATASTORE_VERSION), ("openWB/system/usage_terms_acknowledged", False), @@ -767,12 +785,12 @@ def upgrade(topic: str, payload) -> Optional[dict]: def upgrade_datastore_4(self) -> None: moved_file = False for path in Path("/etc/mosquitto/conf.d").glob('99-bridge-openwb-*.conf'): - subprocess.run(["sudo", "mv", str(path), str(path).replace("conf.d", "conf_local.d")]) + run_command(["sudo", "mv", str(path), str(path).replace("conf.d", "conf_local.d")], process_exception=True) moved_file = True self.__update_topic("openWB/system/datastore_version", 5) if moved_file: time.sleep(1) - subprocess.run([str(self.base_path / "runs" / "reboot.sh")]) + run_command([str(self.base_path / "runs" / "reboot.sh")], process_exception=True) def upgrade_datastore_5(self) -> None: def upgrade(topic: str, payload) -> Optional[dict]: @@ -1058,16 +1076,11 @@ def upgrade(topic: str, payload) -> None: bridge_configuration = decode_payload(payload) if bridge_configuration["remote"]["is_openwb_cloud"]: index = get_index(topic) - result = subprocess.run( - ["php", "-f", str(self.base_path / "runs" / "save_mqtt.php"), index, payload], - stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True) - if result.returncode == 0: - log.info("successfully updated configuration of bridge " - f"'{bridge_configuration['name']}' ({index})") - pub_system_message(payload, result.stdout, MessageType.SUCCESS) - else: - log.error("update of configuration for bridge " - f"'{bridge_configuration['name']}' ({index}) failed! {result.stdout}") + result = run_command(["php", "-f", str(self.base_path / "runs" / "save_mqtt.php"), index, payload], + process_exception=True) + log.info("successfully updated configuration of bridge " + f"'{bridge_configuration['name']}' ({index})") + pub_system_message(payload, result, MessageType.SUCCESS) self._loop_all_received_topics(upgrade) self.__update_topic("openWB/system/datastore_version", 24) @@ -1491,3 +1504,150 @@ def upgrade(topic: str, payload) -> None: self._loop_all_received_topics(upgrade) Pub().pub("openWB/system/datastore_version", 44) + + def upgrade_datastore_44(self) -> None: + try: + corrupt_days = ["20240620", "20240619", "20240618"] + for topic, payload in self.all_received_topics.items(): + if topic == "openWB/counter/get/hierarchy": + top_entry = decode_payload(payload)[0] + if top_entry["type"] != "counter": + raise Exception("First item in hierarchy must be a counter") + evu_counter_str = f"counter{top_entry['id']}" + for corrupt_day in corrupt_days: + try: + filepath = f"{self.base_path}/data/daily_log/{corrupt_day}.json" + with open(filepath, "r") as jsonFile: + content = json.load(jsonFile) + for entry in content["entries"]: + for counter_entry in entry["counter"]: + if evu_counter_str == counter_entry and entry["counter"][counter_entry]["grid"] is False: + entry["counter"][counter_entry]["grid"] = True + break + else: + log.debug("all grid: False-bug does not exist in this installation") + return + write_and_check(filepath, content) + except Exception: + log.exception(f"Logdatei '{filepath}' konnte nicht konvertiert werden.") + try: + filepath = f"{self.base_path}/data/monthly_log/202406.json" + with open(filepath, "r") as jsonFile: + content = json.load(jsonFile) + for entry in content["entries"]: + if entry["date"] in corrupt_days: + for counter_entry in entry["counter"]: + if evu_counter_str == counter_entry: + entry["counter"][counter_entry]["grid"] = True + break + write_and_check(filepath, content) + except Exception: + log.exception(f"Logdatei '{filepath}' konnte nicht konvertiert werden.") + except Exception: + log.exception("Fehler beim Konvertieren der Logdateien") + self.__update_topic("openWB/system/datastore_version", 45) + + def upgrade_datastore_45(self) -> None: + def upgrade(topic: str, payload) -> Optional[dict]: + if re.search("^openWB/general/chargemode_config/pv_charging/phase_switch_delay$", topic) is not None: + delay = decode_payload(payload) + return { + "openWB/general/chargemode_config/phase_switch_delay": delay, + } + self._loop_all_received_topics(upgrade) + self.__update_topic("openWB/system/datastore_version", 46) + + def upgrade_datastore_46(self) -> None: + def upgrade(topic: str, payload) -> Optional[dict]: + if re.search("openWB/vehicle/template/charge_template/[0-9]+$", topic) is not None: + payload = decode_payload(payload) + if "disable_after_unplug" in payload: + updated_payload = payload + payload.pop("disable_after_unplug") + return {topic: updated_payload} + if re.search("openWB/chargepoint/template/[0-9]+$", topic) is not None: + payload = decode_payload(payload) + if "rfid_enabling" in payload: + updated_payload = payload + updated_payload["rfid_enabling"] = {} + payload.pop("rfid_enabling") + return {topic: updated_payload} + self._loop_all_received_topics(upgrade) + self.__update_topic("openWB/system/datastore_version", 47) + + def upgrade_datastore_47(self) -> None: + def upgrade(topic: str, payload) -> Optional[dict]: + if re.search("openWB/chargepoint/template/[0-9]+$", topic) is not None: + payload = decode_payload(payload) + if "disable_after_unplug" not in payload: + updated_payload = payload + updated_payload.update({"disable_after_unplug": False}) + return {topic: updated_payload} + self._loop_all_received_topics(upgrade) + self.__update_topic("openWB/system/datastore_version", 48) + + def upgrade_datastore_48(self) -> None: + def upgrade(topic: str, payload) -> None: + if re.search("openWB/system/device/[0-9]+", topic) is not None: + payload = decode_payload(payload) + # update version and firmware of GoodWe + if payload.get("type") == "good_we" and "version" not in payload["configuration"]: + payload["configuration"].update({"firmware": 8}) + payload["configuration"].update({"version": GoodWeVersion.V_1_7}) + Pub().pub(topic, payload) + self._loop_all_received_topics(upgrade) + self.__update_topic("openWB/system/datastore_version", 49) + + def upgrade_datastore_49(self) -> None: + Pub().pub("openWB/system/installAssistantDone", True) + Pub().pub("openWB/system/datastore_version", 50) + + def upgrade_datastore_50(self) -> None: + # es gibt noch Topics von gelöschten Komponenten unter openWB/(counter|pv|bat)/[0-9], aber keine Konfiguration + # zu den Komponenten. + def upgrade(topic: str, payload) -> Optional[dict]: + if re.search("openWB/(counter|pv|bat)/[0-9]+", topic) is not None: + for component_topic in self.all_received_topics.keys(): + if re.search("openWB/system/device/[0-9]+/component/[0-9]+/config$", component_topic) is not None: + if get_second_index(component_topic) == get_index(topic): + return + else: + return {topic: ""} + self._loop_all_received_topics(upgrade) + self.__update_topic("openWB/system/datastore_version", 51) + + def upgrade_datastore_51(self) -> None: + def upgrade(topic: str, payload) -> None: + if re.search("openWB/system/device/[0-9]+", topic) is not None: + payload = decode_payload(payload) + # update version and firmware of GoodWe + if payload.get("type") == "deye" and "device_type" in payload["configuration"]: + payload["configuration"].pop("device_type") + Pub().pub(topic, payload) + self._loop_all_received_topics(upgrade) + self.__update_topic("openWB/system/datastore_version", 52) + + def upgrade_datastore_52(self) -> None: + # PR reverted + self.__update_topic("openWB/system/datastore_version", 53) + + def upgrade_datastore_53(self) -> None: + def upgrade(topic: str, payload) -> Optional[dict]: + if "openWB/optional/int_display/theme" == topic: + configuration_payload = decode_payload(payload) + if configuration_payload.get("type") == "cards": + if configuration_payload["configuration"].get("enable_energy_flow_view") is None: + configuration_payload["configuration"].update({ + "enable_energy_flow_view": True, + }) + if configuration_payload["configuration"].get("enable_dashboard_card_vehicles") is None: + configuration_payload["configuration"].update({ + "enable_dashboard_card_vehicles": True, + }) + if configuration_payload["configuration"].get("simple_charge_point_view") is None: + configuration_payload["configuration"].update({ + "simple_charge_point_view": True, + }) + return {topic: configuration_payload} + self._loop_all_received_topics(upgrade) + self.__update_topic("openWB/system/datastore_version", 54) diff --git a/packages/helpermodules/update_config_test.py b/packages/helpermodules/update_config_test.py index 749283171f..79405ebd4f 100644 --- a/packages/helpermodules/update_config_test.py +++ b/packages/helpermodules/update_config_test.py @@ -5,13 +5,13 @@ 'openWB/chargepoint/5/get/voltages': b'[230.2,230.2,230.2]', 'openWB/chargepoint/3/get/state_str': b'"Keine Ladung, da kein Auto angesteckt ist."', 'openWB/chargepoint/3/config': (b'{"name": "Standard-Ladepunkt", "type": "mqtt", "ev": 0, "template": 0,' - b'"connected_phases": 3, "phase_1": 0, "auto_phase_switch_hw": false, ' + b'"connected_phases": 3, "phase_1": 1, "auto_phase_switch_hw": false, ' b'"control_pilot_interruption_hw": false, "id": 3, "connection_module": ' b'{"type": "mqtt", "name": "MQTT-Ladepunkt", "configuration": {}}, ' b'"power_module": {}}'), 'openWB/chargepoint/get/power': b'0', 'openWB/chargepoint/template/0': (b'{"autolock": {"active": false, "plans": {}, "wait_for_charging_end": false}, ' - b'"name": "Standard Ladepunkt-Profil", "rfid_enabling": false, ' + b'"name": "Standard Ladepunkt-Profil" ' b'"valid_tags": [], "id": 0}'), 'openWB/optional/int_display/theme': b'"cards"'} diff --git a/packages/helpermodules/utils/json_file_handler.py b/packages/helpermodules/utils/json_file_handler.py new file mode 100644 index 0000000000..e953b0473d --- /dev/null +++ b/packages/helpermodules/utils/json_file_handler.py @@ -0,0 +1,49 @@ +import json +import logging +import os +import shutil + +log = logging.getLogger(__name__) + + +def write_and_check(file_path, content): + """ + Schreibt den Inhalt in die Datei und überprüft, ob der Inhalt erfolgreich geschrieben wurde. + Falls nicht, wird die Sicherung wiederhergestellt und der Schreibvorgang einmalig erneut durchgeführt. + Schlägt der Schreibvorgang wieder fehl, wird die Sicherung wiederhergestellt. + """ + def _write_and_check(): + with open(file_path, 'w', encoding="utf-8") as file: + json.dump(content, file) + with open(file_path, 'r', encoding="utf-8") as file: + written_content = json.load(file) + if content != written_content: + raise ValueError("Der geschriebene Inhalt stimmt nicht mit dem erwarteten Inhalt überein.") + + def restore_backup(): + shutil.copyfile(backup_path, file_path) + log.debug("Sicherung erfolgreich wiederhergestellt.") + + def handle_broken_file(): + restore_backup() + try: + _write_and_check() + except Exception: + log.exception("Fehler beim Wiederherstellen und erneuten Schreiben der Datei. " + "Wiederherstellen der Sicherung.") + restore_backup() + + try: + backup_path = file_path + '.bak' + if os.path.exists(file_path): + shutil.copyfile(file_path, backup_path) + _write_and_check() + else: + with open(file_path, 'w', encoding="utf-8") as file: + json.dump(content, file) + except Exception: + log.exception("Fehler beim Schreiben der Datei. Wiederherstellen der Sicherung.") + handle_broken_file() + finally: + if os.path.exists(backup_path): + os.remove(backup_path) diff --git a/packages/helpermodules/utils/json_file_handler_test.py b/packages/helpermodules/utils/json_file_handler_test.py new file mode 100644 index 0000000000..30ae0409eb --- /dev/null +++ b/packages/helpermodules/utils/json_file_handler_test.py @@ -0,0 +1,34 @@ +import json +import os +import tempfile +from unittest.mock import Mock + +from helpermodules.utils import json_file_handler +from helpermodules.utils.json_file_handler import write_and_check + +import pytest + + +@pytest.mark.parametrize("load_return, expected_content", [ + ([{"new_key": "new_value"}], {"new_key": "new_value"}), + ([ValueError("Ungültige Daten"), {"new_key": "new_value"}], {"new_key": "new_value"}), + ([ValueError("Ungültige Daten"), ValueError("Ungültige Daten")], {"key": "value"}) +]) +def test_backup_restore_and_corrupt_data_handling(load_return, expected_content, monkeypatch): + mock_json_load = Mock(side_effect=load_return) + monkeypatch.setattr(json_file_handler.json, "load", mock_json_load) + with tempfile.NamedTemporaryFile(delete=False) as temp_file: + file_path = temp_file.name + try: + with open(file_path, 'w') as file: + json.dump({"key": "value"}, file) + write_and_check(file_path, {"new_key": "new_value"}) + with open(file_path, 'r') as file: + # mocked auch hier json.load, obwohl monkeypatch für json_file_handler.json.load + restored_content = json.loads(file.read()) + assert restored_content == expected_content + finally: + # Löschen Sie die temporären Dateien + os.remove(file_path) + if os.path.exists(file_path + ".bak"): + os.remove(file_path + ".bak") diff --git a/packages/helpermodules/utils/run_command.py b/packages/helpermodules/utils/run_command.py new file mode 100644 index 0000000000..4bb4acdf7a --- /dev/null +++ b/packages/helpermodules/utils/run_command.py @@ -0,0 +1,23 @@ +import logging +import subprocess + +log = logging.getLogger(__name__) + + +def run_command(command, process_exception: bool = False): + # if return is non-zero a CalledProcessError is raised + try: + result = subprocess.run( + command, + check=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True + ) + return result.stdout + except subprocess.CalledProcessError as e: + if process_exception: + log.debug(e.stdout) + log.exception(e.stderr) + else: + raise e diff --git a/packages/main.py b/packages/main.py index ed9cf2925b..0ecff398cc 100755 --- a/packages/main.py +++ b/packages/main.py @@ -1,7 +1,13 @@ #!/usr/bin/env python3 """Starten der benötigten Prozesse """ +# flake8: noqa: F402 import logging +from helpermodules import logger +# als erstes logging initalisieren, damit auch ImportError geloggt werden +logger.setup_logging() +log = logging.getLogger() + from pathlib import Path from random import randrange import schedule @@ -11,7 +17,7 @@ from threading import Thread from control.chargelog.chargelog import calculate_charge_cost -from helpermodules.changed_values_handler import ChangedValuesHandler +from helpermodules.changed_values_handler import ChangedValuesContext from helpermodules.measurement_logging.update_yields import update_daily_yields, update_pv_monthly_yearly_yields from helpermodules.measurement_logging.write_log import LogType, save_log from modules import loadvars @@ -19,7 +25,6 @@ from helpermodules import timecheck, update_config from helpermodules import subdata from helpermodules import setdata -from helpermodules import logger from helpermodules import command from helpermodules.modbusserver import start_modbus_server from helpermodules.pub import Pub @@ -34,9 +39,6 @@ from modules.utils import wait_for_module_update_completed from smarthome.smarthome import readmq, smarthome_handler -logger.setup_logging() -log = logging.getLogger() - class HandlerAlgorithm: def __init__(self): @@ -52,23 +54,21 @@ def handler_with_control_interval(): if (data.data.general_data.data.control_interval / 10) == self.interval_counter: data.data.copy_data() loadvars_.get_values() - changed_values_handler.pub_changed_values() wait_for_module_update_completed(loadvars_.event_module_update_completed, "openWB/set/system/device/module_update_completed") data.data.copy_data() - changed_values_handler.store_initial_values() - self.heartbeat = True - if data.data.system_data["system"].data["perform_update"]: - data.data.system_data["system"].perform_update() - return - elif data.data.system_data["system"].data["update_in_progress"]: - log.info("Regelung pausiert, da ein Update durchgeführt wird.") - event_global_data_initialized.set() - prep.setup_algorithm() - control.calc_current() - proc.process_algorithm_results() - data.data.graph_data.pub_graph_data() - changed_values_handler.pub_changed_values() + with ChangedValuesContext(loadvars_.event_module_update_completed): + self.heartbeat = True + if data.data.system_data["system"].data["perform_update"]: + data.data.system_data["system"].perform_update() + return + elif data.data.system_data["system"].data["update_in_progress"]: + log.info("Regelung pausiert, da ein Update durchgeführt wird.") + event_global_data_initialized.set() + prep.setup_algorithm() + control.calc_current() + proc.process_algorithm_results() + data.data.graph_data.pub_graph_data() self.interval_counter = 1 else: self.interval_counter = self.interval_counter + 1 @@ -86,14 +86,13 @@ def handler5MinAlgorithm(self): ausführt, die nur alle 5 Minuten ausgeführt werden müssen. """ try: - changed_values_handler.store_initial_values() - totals = save_log(LogType.DAILY) - update_daily_yields(totals) - update_pv_monthly_yearly_yields() - data.data.general_data.grid_protection() - data.data.optional_data.et_get_prices() - data.data.counter_all_data.validate_hierarchy() - changed_values_handler.pub_changed_values() + with ChangedValuesContext(loadvars_.event_module_update_completed): + totals = save_log(LogType.DAILY) + update_daily_yields(totals) + update_pv_monthly_yearly_yields() + data.data.general_data.grid_protection() + data.data.optional_data.et_get_prices() + data.data.counter_all_data.validate_hierarchy() except KeyboardInterrupt: log.critical("Ausführung durch exit_after gestoppt: "+traceback.format_exc()) except Exception: @@ -126,8 +125,8 @@ def handler5Min(self): general_internal_chargepoint_handler.event_start.set() else: general_internal_chargepoint_handler.internal_chargepoint_handler.heartbeat = False - - sub.system_data["system"].update_ip_address() + with ChangedValuesContext(loadvars_.event_module_update_completed): + sub.system_data["system"].update_ip_address() except KeyboardInterrupt: log.critical("Ausführung durch exit_after gestoppt: "+traceback.format_exc()) except Exception: @@ -154,8 +153,9 @@ def handler_random_nightly(self): @exit_after(10) def handler_hour(self): try: - for cp in data.data.cp_data.values(): - calculate_charge_cost(cp) + with ChangedValuesContext(loadvars_.event_module_update_completed): + for cp in data.data.cp_data.values(): + calculate_charge_cost(cp) except KeyboardInterrupt: log.critical("Ausführung durch exit_after gestoppt: "+traceback.format_exc()) except Exception: @@ -193,7 +193,6 @@ def schedule_jobs(): prep = prepare.Prepare() general_internal_chargepoint_handler = GeneralInternalChargepointHandler() rfid = RfidReader() - changed_values_handler = ChangedValuesHandler(loadvars_.event_module_update_completed) event_ev_template = threading.Event() event_ev_template.set() event_charge_template = threading.Event() @@ -254,7 +253,6 @@ def schedule_jobs(): event_update_config_completed.wait(300) Pub().pub("openWB/set/system/boot_done", True) Path(Path(__file__).resolve().parents[1]/"ramdisk"/"bootdone").touch() - changed_values_handler.store_initial_values() schedule_jobs() except Exception: log.exception("Fehler im Main-Modul") diff --git a/packages/modules/backup_clouds/nextcloud/backup_cloud.py b/packages/modules/backup_clouds/nextcloud/backup_cloud.py index 2d1cfd4960..cee8d87290 100644 --- a/packages/modules/backup_clouds/nextcloud/backup_cloud.py +++ b/packages/modules/backup_clouds/nextcloud/backup_cloud.py @@ -5,14 +5,13 @@ from modules.backup_clouds.nextcloud.config import NextcloudBackupCloud, NextcloudBackupCloudConfiguration from modules.common import req from modules.common.abstract_device import DeviceDescriptor -from modules.common.configurable_backup_cloud import ConfigurableBackupCloud log = logging.getLogger(__name__) def upload_backup(config: NextcloudBackupCloudConfiguration, backup_filename: str, backup_file: bytes) -> None: if config.user is None: - url_match = re.fullmatch(r'(http[s]?):\/\/([^/]+)\/(?:index.php\/)?s\/(.+)', config.ip_address) + url_match = re.fullmatch(r'(http[s]?):\/\/([\S^/]+)\/(?:index.php\/)?s\/(.+)', config.ip_address) if not url_match: raise ValueError(f"URL '{config.ip_address}' hat nicht die erwartete Form " "'https://server/index.php/s/user_token' oder 'https://server/s/user_token'") @@ -34,7 +33,7 @@ def upload_backup(config: NextcloudBackupCloudConfiguration, backup_filename: st def create_backup_cloud(config: NextcloudBackupCloud): def updater(backup_filename: str, backup_file: bytes): upload_backup(config.configuration, backup_filename, backup_file) - return ConfigurableBackupCloud(config=config, component_updater=updater) + return updater device_descriptor = DeviceDescriptor(configuration_factory=NextcloudBackupCloud) diff --git a/packages/modules/backup_clouds/nfs/backup_cloud.py b/packages/modules/backup_clouds/nfs/backup_cloud.py index 6099461e6d..6e84f3c088 100644 --- a/packages/modules/backup_clouds/nfs/backup_cloud.py +++ b/packages/modules/backup_clouds/nfs/backup_cloud.py @@ -5,7 +5,6 @@ from modules.backup_clouds.nfs.config import NfsBackupCloud, NfsBackupCloudConfiguration from modules.common.abstract_device import DeviceDescriptor -from modules.common.configurable_backup_cloud import ConfigurableBackupCloud log = logging.getLogger(__name__) nfs_mount = '/mnt/nfs_mount' @@ -67,7 +66,7 @@ def upload_backup(config: NfsBackupCloudConfiguration, backup_filename: str, bac def create_backup_cloud(config: NfsBackupCloud): def updater(backup_filename: str, backup_file: bytes): upload_backup(config.configuration, backup_filename, backup_file) - return ConfigurableBackupCloud(config=config, component_updater=updater) + return updater device_descriptor = DeviceDescriptor(configuration_factory=NfsBackupCloud) diff --git a/packages/modules/backup_clouds/onedrive/backup_cloud.py b/packages/modules/backup_clouds/onedrive/backup_cloud.py index 8f2a4d5f96..d45bb70778 100644 --- a/packages/modules/backup_clouds/onedrive/backup_cloud.py +++ b/packages/modules/backup_clouds/onedrive/backup_cloud.py @@ -7,7 +7,6 @@ from modules.backup_clouds.onedrive.api import get_tokens from modules.backup_clouds.onedrive.config import OneDriveBackupCloud, OneDriveBackupCloudConfiguration from modules.common.abstract_device import DeviceDescriptor -from modules.common.configurable_backup_cloud import ConfigurableBackupCloud log = logging.getLogger(__name__) @@ -36,7 +35,7 @@ def upload_backup(config: OneDriveBackupCloudConfiguration, backup_filename: str def create_backup_cloud(config: OneDriveBackupCloud): def updater(backup_filename: str, backup_file: bytes): upload_backup(config.configuration, backup_filename, backup_file) - return ConfigurableBackupCloud(config=config, component_updater=updater) + return updater device_descriptor = DeviceDescriptor(configuration_factory=OneDriveBackupCloud) diff --git a/packages/modules/backup_clouds/samba/backup_cloud.py b/packages/modules/backup_clouds/samba/backup_cloud.py index fb9e4854b5..32a0d6b8f9 100644 --- a/packages/modules/backup_clouds/samba/backup_cloud.py +++ b/packages/modules/backup_clouds/samba/backup_cloud.py @@ -8,7 +8,6 @@ from modules.backup_clouds.samba.config import SambaBackupCloud, SambaBackupCloudConfiguration from modules.common.abstract_device import DeviceDescriptor -from modules.common.configurable_backup_cloud import ConfigurableBackupCloud log = logging.getLogger(__name__) @@ -57,7 +56,7 @@ def upload_backup(config: SambaBackupCloudConfiguration, backup_filename: str, b def create_backup_cloud(config: SambaBackupCloud): def updater(backup_filename: str, backup_file: bytes): upload_backup(config.configuration, backup_filename, backup_file) - return ConfigurableBackupCloud(config=config, component_updater=updater) + return updater device_descriptor = DeviceDescriptor(configuration_factory=SambaBackupCloud) diff --git a/packages/modules/backup_clouds/samba/config.py b/packages/modules/backup_clouds/samba/config.py index 5f207e9ad5..41b7c1f98b 100644 --- a/packages/modules/backup_clouds/samba/config.py +++ b/packages/modules/backup_clouds/samba/config.py @@ -2,7 +2,8 @@ class SambaBackupCloudConfiguration: - def __init__(self, smb_path: Optional[str] = None, + def __init__(self, + smb_path: Optional[str] = "/", smb_server: Optional[str] = None, smb_share: Optional[str] = None, smb_user: Optional[str] = None, diff --git a/packages/modules/chargepoints/openwb_series2_satellit/chargepoint_module.py b/packages/modules/chargepoints/openwb_series2_satellit/chargepoint_module.py index ccfc1712ff..768bc0a93f 100644 --- a/packages/modules/chargepoints/openwb_series2_satellit/chargepoint_module.py +++ b/packages/modules/chargepoints/openwb_series2_satellit/chargepoint_module.py @@ -10,7 +10,6 @@ from modules.common.abstract_device import DeviceDescriptor from modules.common.component_context import SingleComponentUpdateContext from modules.common.fault_state import ComponentInfo, FaultState -from modules.common.hardware_check_context import SeriesHardwareCheckContext from modules.common.store import get_chargepoint_value_store from modules.common.component_state import ChargepointState from modules.common.version_by_telnet import get_version_by_telnet @@ -75,29 +74,30 @@ def get_values(self) -> None: if self.version is not None: with self.__client_error_context: try: - with SeriesHardwareCheckContext(self._client): + self.delay_second_cp(self.CP0_DELAY) + with self._client.client: + self._client.check_hardware(self.fault_state) if self.version is False: raise ValueError( "Firmware des openWB Satellit ist nicht mit openWB 2 kompatibel. " "Bitte den Support kontaktieren.") - self.delay_second_cp(self.CP0_DELAY) - with self._client.client: - currents = self._client.meter_client.get_currents() - phases_in_use = sum(1 for current in currents if current > 3) - plug_state, charge_state, _ = self._client.evse_client.get_plug_charge_state() + currents = self._client.meter_client.get_currents() + phases_in_use = sum(1 for current in currents if current > 3) + plug_state, charge_state, _ = self._client.evse_client.get_plug_charge_state() - chargepoint_state = ChargepointState( - power=self._client.meter_client.get_power()[1], - currents=currents, - imported=self._client.meter_client.get_imported(), - exported=0, - voltages=self._client.meter_client.get_voltages(), - plug_state=plug_state, - charge_state=charge_state, - phases_in_use=phases_in_use, - serial_number=self._client.meter_client.get_serial_number() - ) - self.store.set(chargepoint_state) + chargepoint_state = ChargepointState( + power=self._client.meter_client.get_power()[1], + currents=currents, + imported=self._client.meter_client.get_imported(), + exported=0, + voltages=self._client.meter_client.get_voltages(), + plug_state=plug_state, + charge_state=charge_state, + phases_in_use=phases_in_use, + serial_number=self._client.meter_client.get_serial_number() + ) + self.store.set(chargepoint_state) + self.__client_error_context.reset_error_counter() except AttributeError: self._create_client() self._validate_version() @@ -107,16 +107,18 @@ def get_values(self) -> None: def set_current(self, current: float) -> None: if self.version is not None: + if self.__client_error_context.error_counter_exceeded(): + current = 0 with SingleComponentUpdateContext(self.fault_state, update_always=False): with self.__client_error_context: try: - with SeriesHardwareCheckContext(self._client): - self.delay_second_cp(self.CP0_DELAY) - with self._client.client: - if self.version: - self._client.evse_client.set_current(int(current)) - else: - self._client.evse_client.set_current(0) + self.delay_second_cp(self.CP0_DELAY) + with self._client.client: + self._client.check_hardware(self.fault_state) + if self.version: + self._client.evse_client.set_current(int(current)) + else: + self._client.evse_client.set_current(0) except AttributeError: self._create_client() self._validate_version() @@ -126,20 +128,20 @@ def switch_phases(self, phases_to_use: int, duration: int) -> None: with SingleComponentUpdateContext(self.fault_state, update_always=False): with self.__client_error_context: try: - with SeriesHardwareCheckContext(self._client): - with self._client.client: - if phases_to_use == 1: - self._client.client.delegate.write_register( - 0x0001, 256, unit=self.ID_PHASE_SWITCH_UNIT) - time.sleep(1) - self._client.client.delegate.write_register( - 0x0001, 512, unit=self.ID_PHASE_SWITCH_UNIT) - else: - self._client.client.delegate.write_register( - 0x0002, 512, unit=self.ID_PHASE_SWITCH_UNIT) - time.sleep(1) - self._client.client.delegate.write_register( - 0x0002, 256, unit=self.ID_PHASE_SWITCH_UNIT) + with self._client.client: + self._client.check_hardware(self.fault_state) + if phases_to_use == 1: + self._client.client.delegate.write_register( + 0x0001, 256, unit=self.ID_PHASE_SWITCH_UNIT) + time.sleep(1) + self._client.client.delegate.write_register( + 0x0001, 512, unit=self.ID_PHASE_SWITCH_UNIT) + else: + self._client.client.delegate.write_register( + 0x0002, 512, unit=self.ID_PHASE_SWITCH_UNIT) + time.sleep(1) + self._client.client.delegate.write_register( + 0x0002, 256, unit=self.ID_PHASE_SWITCH_UNIT) except AttributeError: self._create_client() self._validate_version() diff --git a/packages/modules/common/component_state.py b/packages/modules/common/component_state.py index 44a1b3a84b..afcd76d0bf 100644 --- a/packages/modules/common/component_state.py +++ b/packages/modules/common/component_state.py @@ -192,6 +192,7 @@ def __init__(self, self.prices = prices +@auto_str class RcrState: def __init__(self, override_value: float) -> None: self.override_value = override_value diff --git a/packages/modules/common/component_type.py b/packages/modules/common/component_type.py index 20ce20d93b..896f1f5737 100644 --- a/packages/modules/common/component_type.py +++ b/packages/modules/common/component_type.py @@ -3,6 +3,7 @@ class ComponentType(Enum): + BACKUP_CLOUD = "backup_cloud" BAT = "bat" CHARGEPOINT = "cp" COUNTER = "counter" diff --git a/packages/modules/common/configurable_backup_cloud.py b/packages/modules/common/configurable_backup_cloud.py index 1514d85f2b..c29ba2c764 100644 --- a/packages/modules/common/configurable_backup_cloud.py +++ b/packages/modules/common/configurable_backup_cloud.py @@ -1,5 +1,9 @@ from typing import TypeVar, Generic, Callable +from modules.common.component_context import SingleComponentUpdateContext +from modules.common.component_type import ComponentType +from modules.common.fault_state import ComponentInfo, FaultState + T_BACKUP_CLOUD_CONFIG = TypeVar("T_BACKUP_CLOUD_CONFIG") @@ -7,9 +11,14 @@ class ConfigurableBackupCloud(Generic[T_BACKUP_CLOUD_CONFIG]): def __init__(self, config: T_BACKUP_CLOUD_CONFIG, - component_updater: Callable[[str, bytes], None]) -> None: - self.__component_updater = component_updater + component_initialiser: Callable[[], float]) -> None: self.config = config + self.fault_state = FaultState(ComponentInfo(None, self.config.name, + ComponentType.BACKUP_CLOUD.value)) + with SingleComponentUpdateContext(self.fault_state): + self._component_updater = component_initialiser(config) def update(self, backup_filename: str, backup_file: bytes): - self.__component_updater(backup_filename, backup_file) + if hasattr(self, "_component_updater"): + # Wenn beim Initialisieren etwas schief gelaufen ist, ursprüngliche Fehlermeldung beibehalten + self._component_updater(backup_filename, backup_file) diff --git a/packages/modules/common/configurable_ripple_control_receiver.py b/packages/modules/common/configurable_ripple_control_receiver.py index ff94bd51ed..d238fccec5 100644 --- a/packages/modules/common/configurable_ripple_control_receiver.py +++ b/packages/modules/common/configurable_ripple_control_receiver.py @@ -12,13 +12,16 @@ class ConfigurableRcr(Generic[T_RCR_CONFIG]): def __init__(self, config: T_RCR_CONFIG, - component_updater: Callable[[], float]) -> None: - self.__component_updater = component_updater + component_initialiser: Callable[[], float]) -> None: self.config = config self.fault_state = FaultState(ComponentInfo(None, self.config.name, ComponentType.RIPPLE_CONTROL_RECEIVER.value)) + with SingleComponentUpdateContext(self.fault_state): + self._component_updater = component_initialiser(config) self.store = store.get_ripple_control_receiver_value_store() def update(self): - with SingleComponentUpdateContext(self.fault_state): - self.store.set(self.__component_updater()) + if hasattr(self, "_component_updater"): + # Wenn beim Initialisieren etwas schief gelaufen ist, ursprüngliche Fehlermeldung beibehalten + with SingleComponentUpdateContext(self.fault_state): + self.store.set(self._component_updater()) diff --git a/packages/modules/common/configurable_tariff.py b/packages/modules/common/configurable_tariff.py index 65d16dd4d8..6c83747861 100644 --- a/packages/modules/common/configurable_tariff.py +++ b/packages/modules/common/configurable_tariff.py @@ -13,22 +13,25 @@ class ConfigurableElectricityTariff(Generic[T_TARIFF_CONFIG]): def __init__(self, config: T_TARIFF_CONFIG, - component_updater: Callable[[], None]) -> None: - self.__component_updater = component_updater + component_initialiser: Callable[[], float]) -> None: self.config = config self.store = store.get_electricity_tariff_value_store() self.fault_state = FaultState(ComponentInfo(None, self.config.name, ComponentType.ELECTRICITY_TARIFF.value)) + with SingleComponentUpdateContext(self.fault_state): + self._component_updater = component_initialiser(config) def update(self): - with SingleComponentUpdateContext(self.fault_state): - tariff_state = self.__component_updater() - current_hour = create_unix_timestamp_current_full_hour() - self.store.set(tariff_state) - self.store.update() - for timestamp in tariff_state.prices.keys(): - if timestamp < current_hour: - self.fault_state.warning('Die Preisliste startet nicht mit der aktuellen Stunde.') - if len(tariff_state.prices) < 24: - self.fault_state.no_error(f'Die Preisliste hat nicht 24, sondern {len(tariff_state.prices)} Einträge. ' - 'Die Strompreise werden vom Anbieter erst um 14:00 für den Folgetag ' - 'aktualisiert.') + if hasattr(self, "_component_updater"): + # Wenn beim Initialisieren etwas schief gelaufen ist, ursprüngliche Fehlermeldung beibehalten + with SingleComponentUpdateContext(self.fault_state): + tariff_state = self._component_updater() + current_hour = create_unix_timestamp_current_full_hour() + self.store.set(tariff_state) + self.store.update() + for timestamp in tariff_state.prices.keys(): + if timestamp < current_hour: + self.fault_state.warning('Die Preisliste startet nicht mit der aktuellen Stunde.') + if len(tariff_state.prices) < 24: + self.fault_state.no_error( + f'Die Preisliste hat nicht 24, sondern {len(tariff_state.prices)} Einträge. ' + 'Die Strompreise werden vom Anbieter erst um 14:00 für den Folgetag aktualisiert.') diff --git a/packages/modules/common/hardware_check.py b/packages/modules/common/hardware_check.py index 8d620c75f4..a4700baba7 100644 --- a/packages/modules/common/hardware_check.py +++ b/packages/modules/common/hardware_check.py @@ -10,19 +10,16 @@ OPEN_TICKET = (" Bitte nehme bei anhaltenden Problemen über die Support-Funktion in den Einstellungen Kontakt mit " + "uns auf.") RS485_ADPATER_BROKEN = ("Auslesen von Zähler UND Evse nicht möglich. Vermutlich ist {} defekt oder zwei " - f"Busteilnehmer haben die gleiche Modbus-ID. Bitte die Zähler-ID prüfen. {OPEN_TICKET}") + "Busteilnehmer haben die gleiche Modbus-ID. Bitte die Zähler-ID prüfen.") USB_ADAPTER_BROKEN = RS485_ADPATER_BROKEN.format('der USB-Adapter') LAN_ADAPTER_BROKEN = (f"{RS485_ADPATER_BROKEN.format('der LAN-Konverter abgestürzt,')} " "Bitte den openWB series2 satellit stromlos machen.") -METER_PROBLEM = ("Der Zähler konnte nicht ausgelesen werden. " - f"Vermutlich ist der Zähler falsch konfiguriert oder defekt. {OPEN_TICKET}") -METER_BROKEN = ("Die Spannungen des Zählers konnten nicht korrekt ausgelesen werden. " - f"Der Zähler ist defekt. {OPEN_TICKET}") +METER_PROBLEM = "Der Zähler konnte nicht ausgelesen werden. Vermutlich ist der Zähler falsch konfiguriert oder defekt." +METER_BROKEN = "Die Spannungen des Zählers konnten nicht korrekt ausgelesen werden: {}V Der Zähler ist defekt." METER_NO_SERIAL_NUMBER = ("Die Seriennummer des Zählers für das Ladelog kann nicht ausgelesen werden. Wenn Sie die " "Seriennummer für Abrechnungszwecke benötigen, wenden Sie sich bitte an unseren Support. Die " "Funktionalität wird dadurch nicht beeinträchtigt!") -EVSE_BROKEN = ("Auslesen der EVSE nicht möglich. " - f"Vermutlich ist die EVSE defekt oder hat eine unbekannte Modbus-ID. {OPEN_TICKET}") +EVSE_BROKEN = "Auslesen der EVSE nicht möglich. Vermutlich ist die EVSE defekt oder hat eine unbekannte Modbus-ID." def check_meter_values(voltages: List[float]) -> Optional[str]: @@ -33,7 +30,7 @@ def valid_voltage(voltage) -> bool: (valid_voltage(voltages[0]) and valid_voltage(voltages[1]) and valid_voltage((voltages[2])))): return None else: - return METER_BROKEN + return METER_BROKEN.format(voltages) class ClientHandlerProtocol(Protocol): @@ -63,7 +60,7 @@ def handle_exception(self: ClientHandlerProtocol, exception: Exception): else: return False - def check_hardware(self: ClientHandlerProtocol): + def check_hardware(self: ClientHandlerProtocol, fault_state: FaultState): try: if self.evse_client.get_firmware_version() > EVSE_MIN_FIRMWARE: @@ -73,17 +70,23 @@ def check_hardware(self: ClientHandlerProtocol): except Exception as e: evse_check_passed = self.handle_exception(e) meter_check_passed, meter_error_msg = self.check_meter() - if meter_check_passed is False and evse_check_passed is False: - if isinstance(self.client, ModbusTcpClient_): - raise Exception(LAN_ADAPTER_BROKEN) - else: - raise Exception(USB_ADAPTER_BROKEN) if meter_check_passed is False: - raise Exception(meter_error_msg) - elif meter_check_passed and meter_error_msg is not None: - self.fault_state.warning(meter_error_msg) + if evse_check_passed is False: + if isinstance(self.client, ModbusTcpClient_): + raise Exception(LAN_ADAPTER_BROKEN + OPEN_TICKET) + else: + raise Exception(USB_ADAPTER_BROKEN + OPEN_TICKET) + else: + raise Exception(meter_error_msg + OPEN_TICKET) + elif evse_check_passed and meter_check_passed and meter_error_msg is not None: + if meter_error_msg != METER_NO_SERIAL_NUMBER: + meter_error_msg += OPEN_TICKET + fault_state.warning(meter_error_msg) if evse_check_passed is False: - raise Exception(EVSE_BROKEN) + if meter_error_msg is not None: + raise Exception(EVSE_BROKEN + " " + meter_error_msg + OPEN_TICKET) + else: + raise Exception(EVSE_BROKEN + OPEN_TICKET) def check_meter(self: ClientHandlerProtocol) -> Tuple[bool, Optional[str]]: try: diff --git a/packages/modules/common/hardware_check_context.py b/packages/modules/common/hardware_check_context.py deleted file mode 100644 index 112f5ee939..0000000000 --- a/packages/modules/common/hardware_check_context.py +++ /dev/null @@ -1,13 +0,0 @@ -from modules.internal_chargepoint_handler.clients import ClientHandler - - -class SeriesHardwareCheckContext: - def __init__(self, client: ClientHandler): - self.client = client - - def __enter__(self): - self.client.check_hardware() - return None - - def __exit__(self, exception_type, exception, exception_traceback) -> bool: - return True diff --git a/packages/modules/common/hardware_check_test.py b/packages/modules/common/hardware_check_test.py index 6e96898556..27bc555174 100644 --- a/packages/modules/common/hardware_check_test.py +++ b/packages/modules/common/hardware_check_test.py @@ -1,13 +1,14 @@ +import re from typing import List, Optional, Tuple, Union -from unittest.mock import Mock, patch +from unittest.mock import MagicMock, Mock, patch import pytest from modules.common import sdm from modules.common.evse import Evse from modules.common.hardware_check import ( - EVSE_BROKEN, LAN_ADAPTER_BROKEN, METER_BROKEN, METER_NO_SERIAL_NUMBER, METER_PROBLEM, USB_ADAPTER_BROKEN, - SeriesHardwareCheckMixin, check_meter_values) -from modules.common.modbus import NO_CONNECTION, ModbusSerialClient_, ModbusTcpClient_ + EVSE_BROKEN, LAN_ADAPTER_BROKEN, METER_BROKEN, METER_NO_SERIAL_NUMBER, METER_PROBLEM, OPEN_TICKET, + USB_ADAPTER_BROKEN, SeriesHardwareCheckMixin, check_meter_values) +from modules.common.modbus import NO_CONNECTION, ModbusClient, ModbusSerialClient_, ModbusTcpClient_ from modules.conftest import SAMPLE_IP, SAMPLE_PORT from modules.internal_chargepoint_handler.clients import ClientHandler @@ -17,6 +18,9 @@ "handle_exception_return_value, client_spec, expected_error_msg"), [pytest.param(Exception("Modbus"), None, None, [230]*3, None, False, ModbusSerialClient_, EVSE_BROKEN, id="EVSE defekt"), + pytest.param(Exception("Modbus"), None, None, [230, 0, 230], None, False, ModbusSerialClient_, + EVSE_BROKEN + " " + METER_BROKEN.format([230, 0, 230]) + OPEN_TICKET, + id="EVSE defekt und Zähler eine Phase defekt"), pytest.param(None, 18, Exception("Modbus"), None, None, None, ModbusSerialClient_, METER_PROBLEM, id="Zähler verkonfiguriert"), pytest.param(Exception("Modbus"), None, Exception("Modbus"), None, None, False, ModbusSerialClient_, @@ -51,9 +55,12 @@ def test_hardware_check_fails(evse_side_effect, handle_exception_mock = Mock(side_effect=handle_exception_side_effect, return_value=handle_exception_return_value) monkeypatch.setattr(SeriesHardwareCheckMixin, "handle_exception", handle_exception_mock) + mock_modbus_client = MagicMock(spec=client_spec, address=SAMPLE_IP, port=SAMPLE_PORT) + mock_modbus_client.__enter__.return_value = mock_modbus_client + # execution and evaluation - with pytest.raises(Exception, match=expected_error_msg): - ClientHandler(0, Mock(spec=client_spec, address=SAMPLE_IP, port=SAMPLE_PORT), [1], Mock()) + with pytest.raises(Exception, match=re.escape(expected_error_msg)): + ClientHandler(0, mock_modbus_client, [1], Mock()) def test_hardware_check_succeeds(monkeypatch): @@ -66,9 +73,12 @@ def test_hardware_check_succeeds(monkeypatch): mock_find_meter_client = Mock(spec=sdm.Sdm630, return_value=mock_meter_client) monkeypatch.setattr(ClientHandler, "find_meter_client", mock_find_meter_client) + mock_modbus_client = MagicMock(spec=ModbusClient) + mock_modbus_client.__enter__.return_value = mock_modbus_client + # execution and evaluation # keine Exception - ClientHandler(0, Mock(), [1], Mock()) + ClientHandler(0, mock_modbus_client, [1], Mock()) @pytest.mark.parametrize( @@ -87,7 +97,7 @@ def test_check_meter_values(voltages, expected_msg, monkeypatch): msg = check_meter_values(voltages) # assert - assert msg == expected_msg + assert msg == expected_msg if expected_msg is None else expected_msg.format(voltages) @patch('modules.common.hardware_check.ClientHandlerProtocol') diff --git a/packages/modules/common/simcount/_simcount.py b/packages/modules/common/simcount/_simcount.py index 66f14d3452..5c07af4a44 100644 --- a/packages/modules/common/simcount/_simcount.py +++ b/packages/modules/common/simcount/_simcount.py @@ -2,12 +2,13 @@ Berechnet die importierte und exportierte Leistung, wenn der Zähler / PV-Modul / Speicher diese nicht liefert. """ import logging +import math import time from control import data as data_module from modules.common.simcount._calculate import calculate_import_export from modules.common.simcount.simcounter_state import SimCounterState -from modules.common.simcount._simcounter_store import get_sim_counter_store +from modules.common.simcount._simcounter_store import get_sim_counter_store, restore_last_energy log = logging.getLogger(__name__) @@ -30,12 +31,21 @@ def sim_count(power_present: float, topic: str = "", data: SimCounterState = Non timestamp_present = time.time() previous_state = store.load(prefix, topic) if data is None else data + if math.isnan(power_present): + raise ValueError("power_present is NaN.") + if isinstance(power_present, (int, float)): if previous_state is None: log.debug("No previous state found. Starting new simulation.") return store.initialize(prefix, topic, power_present, timestamp_present) else: log.debug("Previous state: %s", previous_state) + if math.isnan(previous_state.imported): + log.error("imported is NaN. Reset simcount state.") + previous_state.imported = restore_last_energy(topic, "imported") + if math.isnan(previous_state.exported): + log.error("exported is NaN. Reset simcount state.") + previous_state.exported = restore_last_energy(topic, "exported") control_interval = data_module.data.general_data.data.control_interval if 2 * control_interval < timestamp_present - previous_state.timestamp: log.warning("Time difference between previous state and current state is too large. " @@ -44,6 +54,8 @@ def sim_count(power_present: float, topic: str = "", data: SimCounterState = Non else: hours_since_previous = (timestamp_present - previous_state.timestamp) / 3600 imported, exported = calculate_import_export(hours_since_previous, previous_state.power, power_present) + if math.isnan(imported) or math.isnan(exported): + raise ValueError("imported or exported is NaN. Retain previous state.") current_state = SimCounterState( timestamp_present, power_present, diff --git a/packages/modules/common/simcount/_simcounter_store.py b/packages/modules/common/simcount/_simcounter_store.py index aaaf7b84fe..587a8d1245 100644 --- a/packages/modules/common/simcount/_simcounter_store.py +++ b/packages/modules/common/simcount/_simcounter_store.py @@ -6,7 +6,10 @@ from paho.mqtt.client import Client as MqttClient, MQTTMessage +from control import data from helpermodules import pub, compatibility +from helpermodules.utils.topic_parser import get_index, get_second_index +from modules.common.component_type import type_to_topic_mapping from modules.common.simcount.simcounter_state import SimCounterState from modules.common.store import ramdisk_write, ramdisk_read_float from modules.common.store.ramdisk.io import RamdiskReadError @@ -182,5 +185,14 @@ def save(self, prefix: str, topic: str, state: SimCounterState): pub.Pub().pub(topic + "simulation", vars(state)) +def restore_last_energy(topic: str, value: str): + device_id = get_index(topic) + component_id = get_second_index(topic) + module_type = type_to_topic_mapping( + data.data.system_data[f"device{device_id}"].components[f"component{component_id}"].component_config.type) + module = getattr(data.data, f"{module_type}_data")[f"{module_type}{get_second_index(topic)}"].data.get + return getattr(module, value) + + def get_sim_counter_store() -> SimCounterStore: return SimCounterStoreRamdisk() if compatibility.is_ramdisk_in_use() else SimCounterStoreBroker() diff --git a/packages/modules/common/store/_api.py b/packages/modules/common/store/_api.py index 1c02aef108..8afca62477 100644 --- a/packages/modules/common/store/_api.py +++ b/packages/modules/common/store/_api.py @@ -27,8 +27,14 @@ def set(self, state: T) -> None: self.delegate.set(state) def update(self) -> None: - log.info("Saving %s", self.delegate.state) - self.delegate.update() + try: + log.info("Saving %s", self.delegate.state) + self.delegate.update() + except AttributeError: + # Wenn keine Daten ausgelesen werden, fehlt das state-Attribut. + pass + except Exception: + log.exception("Error while publishing module data") def update_values(component): diff --git a/packages/modules/common/store/_counter.py b/packages/modules/common/store/_counter.py index efd0e9bd86..a66430b247 100644 --- a/packages/modules/common/store/_counter.py +++ b/packages/modules/common/store/_counter.py @@ -93,17 +93,11 @@ def add_exported(element): for element in elements: if element["type"] == ComponentType.CHARGEPOINT.value: chargepoint = data.data.cp_data[f"cp{element['id']}"] - try: - self.currents = list(map(add, - self.currents, - convert_cp_currents_to_evu_currents( - chargepoint.data.config.phase_1, - chargepoint.data.get.currents))) - except KeyError: - raise KeyError("Für den virtuellen Zähler muss der Anschluss der Phasen von Ladepunkt" - f" {chargepoint.data.config.name} an die Phasen der EVU angegeben " - "werden.") - + self.currents = list(map(add, + self.currents, + convert_cp_currents_to_evu_currents( + chargepoint.data.config.phase_1, + chargepoint.data.get.currents))) self.power += chargepoint.data.get.power self.imported += chargepoint.data.get.imported elif element["type"] == ComponentType.BAT.value: diff --git a/packages/modules/devices/alpha_ess/counter.py b/packages/modules/devices/alpha_ess/counter.py index 42c7fb7a69..2a9466905b 100644 --- a/packages/modules/devices/alpha_ess/counter.py +++ b/packages/modules/devices/alpha_ess/counter.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 import time -from typing import Dict, Union +from typing import Callable, Dict, Union from dataclass_utils import dataclass_from_dict from modules.devices.alpha_ess.config import AlphaEssConfiguration, AlphaEssCounterSetup @@ -28,22 +28,23 @@ def __init__(self, def update(self): time.sleep(0.1) - counter_state = self.__get_values_factory() + factory_method = self.__get_values_factory() + counter_state = factory_method(self.__modbus_id) self.store.set(counter_state) - def __get_values_factory(self) -> CounterState: + def __get_values_factory(self) -> Callable[[int], CounterState]: if self.__device_config.source == 0 and self.__device_config.version == 0: return self.__get_values_before_v123 else: return self.__get_values_since_v123 - def __get_values_before_v123(self) -> CounterState: + def __get_values_before_v123(self, unit: int) -> CounterState: power, exported, imported = self.__tcp_client.read_holding_registers( - 0x6, [modbus.ModbusDataType.INT_32] * 3, unit=self.__modbus_id) + 0x6, [modbus.ModbusDataType.INT_32] * 3, unit=unit) exported *= 10 imported *= 10 currents = [val / 230 for val in self.__tcp_client.read_holding_registers( - 0x0000, [ModbusDataType.INT_32]*3, unit=self.__modbus_id)] + 0x0000, [ModbusDataType.INT_32]*3, unit=unit)] counter_state = CounterState( currents=currents, @@ -53,14 +54,14 @@ def __get_values_before_v123(self) -> CounterState: ) return counter_state - def __get_values_since_v123(self) -> CounterState: - power = self.__tcp_client.read_holding_registers(0x0021, ModbusDataType.INT_32, unit=self.__modbus_id) + def __get_values_since_v123(self, unit: int) -> CounterState: + power = self.__tcp_client.read_holding_registers(0x0021, ModbusDataType.INT_32, unit=unit) exported, imported = [ val * 10 for val in self.__tcp_client.read_holding_registers( - 0x0010, [ModbusDataType.INT_32] * 2, unit=self.__modbus_id + 0x0010, [ModbusDataType.INT_32] * 2, unit=unit )] currents = [val / 1000 for val in self.__tcp_client.read_holding_registers( - 0x0017, [ModbusDataType.INT_16]*3, unit=self.__modbus_id)] + 0x0017, [ModbusDataType.INT_16]*3, unit=unit)] counter_state = CounterState( currents=currents, diff --git a/packages/modules/devices/byd/bat.py b/packages/modules/devices/byd/bat.py index 330bc83316..c5c9cbfd03 100644 --- a/packages/modules/devices/byd/bat.py +++ b/packages/modules/devices/byd/bat.py @@ -3,7 +3,6 @@ from html.parser import HTMLParser from typing import Dict, List, Union, Tuple -from dataclass_utils import dataclass_from_dict from modules.devices.byd.config import BYDBatSetup from modules.common import req from modules.common.component_state import BatState @@ -20,7 +19,7 @@ def __init__(self, component_config: Union[Dict, BYDBatSetup], device_config) -> None: self.__device_config = device_config - self.component_config = dataclass_from_dict(BYDBatSetup, component_config) + self.component_config = component_config self.sim_counter = SimCounter(self.__device_config.id, self.component_config.id, prefix="speicher") self.store = get_bat_value_store(self.component_config.id) self.fault_state = FaultState(ComponentInfo.from_component_config(self.component_config)) diff --git a/packages/modules/devices/byd/device.py b/packages/modules/devices/byd/device.py index 503bc6d26d..116c7244e2 100644 --- a/packages/modules/devices/byd/device.py +++ b/packages/modules/devices/byd/device.py @@ -1,90 +1,25 @@ #!/usr/bin/env python3 import logging -from typing import Dict, Optional, List, Union -from dataclass_utils import dataclass_from_dict -from helpermodules.cli import run_using_positional_cli_args -from modules.devices.byd.config import BYD, BYDBatSetup, BYDConfiguration +from modules.common.configurable_device import ComponentFactoryByType, ConfigurableDevice, IndependentComponentUpdater +from modules.devices.byd.config import BYD, BYDBatSetup from modules.devices.byd import bat -from modules.common.abstract_device import AbstractDevice, DeviceDescriptor -from modules.common.component_context import SingleComponentUpdateContext +from modules.common.abstract_device import DeviceDescriptor log = logging.getLogger(__name__) -class Device(AbstractDevice): - COMPONENT_TYPE_TO_CLASS = { - "bat": bat.BYDBat - } +def create_device(device_config: BYD): + def create_bat_component(component_config: BYDBatSetup): + return bat.BYDBat(component_config, device_config) - def __init__(self, device_config: Union[Dict, BYD]) -> None: - self.components = {} # type: Dict[str, bat.BYDBat] - try: - self.device_config = dataclass_from_dict(BYD, device_config) - except Exception: - log.exception("Fehler im Modul "+self.device_config.name) - - def add_component(self, component_config: Union[Dict, BYDBatSetup]) -> None: - if isinstance(component_config, Dict): - component_type = component_config["type"] - else: - component_type = component_config.type - component_config = dataclass_from_dict(COMPONENT_TYPE_TO_MODULE[ - component_type].component_descriptor.configuration_factory, component_config) - if component_type in self.COMPONENT_TYPE_TO_CLASS: - self.components["component"+str(component_config.id)] = (self.COMPONENT_TYPE_TO_CLASS[component_type]( - component_config, - self.device_config)) - else: - raise Exception( - "illegal component type " + component_type + ". Allowed values: " + - ','.join(self.COMPONENT_TYPE_TO_CLASS.keys()) - ) - - def update(self) -> None: - log.debug("Start device reading " + str(self.components)) - if self.components: - for component in self.components: - # Auch wenn bei einer Komponente ein Fehler auftritt, sollen alle anderen noch ausgelesen werden. - with SingleComponentUpdateContext(self.components[component].fault_state): - self.components[component].update() - else: - log.warning( - self.device_config.name + - ": Es konnten keine Werte gelesen werden, da noch keine Komponenten konfiguriert wurden." - ) - - -COMPONENT_TYPE_TO_MODULE = { - "bat": bat -} - - -def read_legacy(component_type: str, - ip_address: str, - username: str, - password: str, - num: Optional[int] = None) -> None: - dev = Device(BYD(configuration=BYDConfiguration(user=username, password=password, ip_address=ip_address))) - if component_type in COMPONENT_TYPE_TO_MODULE: - component_config = COMPONENT_TYPE_TO_MODULE[component_type].component_descriptor.configuration_factory() - else: - raise Exception( - "illegal component type " + component_type + ". Allowed values: " + - ','.join(COMPONENT_TYPE_TO_MODULE.keys()) - ) - component_config.id = num - dev.add_component(component_config) - - log.debug('byd IP-Adresse: ' + ip_address) - log.debug('byd Benutzer: ' + username) - log.debug('byd Passwort: ' + password) - - dev.update() - - -def main(argv: List[str]): - run_using_positional_cli_args(read_legacy, argv) + return ConfigurableDevice( + device_config=device_config, + component_factory=ComponentFactoryByType( + bat=create_bat_component + ), + component_updater=IndependentComponentUpdater(lambda component: component.update()) + ) device_descriptor = DeviceDescriptor(configuration_factory=BYD) diff --git a/packages/modules/devices/carlo_gavazzi/counter.py b/packages/modules/devices/carlo_gavazzi/counter.py index dc25dabe37..efa07162e4 100644 --- a/packages/modules/devices/carlo_gavazzi/counter.py +++ b/packages/modules/devices/carlo_gavazzi/counter.py @@ -3,7 +3,6 @@ from pymodbus.constants import Endian -from dataclass_utils import dataclass_from_dict from modules.devices.carlo_gavazzi.config import CarloGavazziCounterSetup from modules.common import modbus from modules.common.component_state import CounterState @@ -21,7 +20,7 @@ def __init__(self, tcp_client: modbus.ModbusTcpClient_, modbus_id: int) -> None: self.__device_id = device_id - self.component_config = dataclass_from_dict(CarloGavazziCounterSetup, component_config) + self.component_config = component_config self.__tcp_client = tcp_client self.__modbus_id = modbus_id self.sim_counter = SimCounter(self.__device_id, self.component_config.id, prefix="bezug") diff --git a/packages/modules/devices/carlo_gavazzi/device.py b/packages/modules/devices/carlo_gavazzi/device.py index 18623efe52..e29dfe17bc 100644 --- a/packages/modules/devices/carlo_gavazzi/device.py +++ b/packages/modules/devices/carlo_gavazzi/device.py @@ -1,89 +1,39 @@ #!/usr/bin/env python3 import logging -from typing import Dict, Optional, List, Union +from typing import Iterable -from dataclass_utils import dataclass_from_dict -from helpermodules.cli import run_using_positional_cli_args +from modules.common.configurable_device import ComponentFactoryByType, ConfigurableDevice, MultiComponentUpdater from modules.devices.carlo_gavazzi import counter from modules.devices.carlo_gavazzi.config import CarloGavazzi, CarloGavazziCounterSetup from modules.common import modbus -from modules.common.abstract_device import AbstractDevice, DeviceDescriptor +from modules.common.abstract_device import DeviceDescriptor from modules.common.component_context import SingleComponentUpdateContext log = logging.getLogger(__name__) -class Device(AbstractDevice): - COMPONENT_TYPE_TO_CLASS = { - "counter": counter.CarloGavazziCounter, - } - - def __init__(self, device_config: Union[Dict, CarloGavazzi]) -> None: - self.components = {} # type: Dict[str, counter.CarloGavazziCounter] - try: - self.device_config = dataclass_from_dict(CarloGavazzi, device_config) - self.client = modbus.ModbusTcpClient_( - self.device_config.configuration.ip_address, self.device_config.configuration.port) - except Exception: - log.exception("Fehler im Modul "+self.device_config.name) - - def add_component(self, component_config: Union[Dict, CarloGavazziCounterSetup]) -> None: - if isinstance(component_config, Dict): - component_type = component_config["type"] - else: - component_type = component_config.type - component_config = dataclass_from_dict(COMPONENT_TYPE_TO_MODULE[ - component_type].component_descriptor.configuration_factory, component_config) - if component_type in self.COMPONENT_TYPE_TO_CLASS: - self.components["component"+str(component_config.id)] = self.COMPONENT_TYPE_TO_CLASS[component_type]( - self.device_config.id, component_config, self.client, - self.device_config.configuration.modbus_id) - else: - raise Exception( - "illegal component type " + component_type + ". Allowed values: " + - ','.join(self.COMPONENT_TYPE_TO_CLASS.keys()) - ) - - def update(self) -> None: - log.debug("Start device reading " + str(self.components)) - if self.components: - for component in self.components: - # Auch wenn bei einer Komponente ein Fehler auftritt, sollen alle anderen noch ausgelesen werden. - with SingleComponentUpdateContext(self.components[component].fault_state): - self.components[component].update() - else: - log.warning( - self.device_config.name + - ": Es konnten keine Werte gelesen werden, da noch keine Komponenten konfiguriert wurden." - ) - - -COMPONENT_TYPE_TO_MODULE = { - "counter": counter -} - - -def read_legacy(component_type: str, ip_address: str, num: Optional[int] = None) -> None: - device_config = CarloGavazzi() - device_config.configuration.ip_address = ip_address - dev = Device(device_config) - if component_type in COMPONENT_TYPE_TO_MODULE: - component_config = COMPONENT_TYPE_TO_MODULE[component_type].component_descriptor.configuration_factory() - else: - raise Exception( - "illegal component type " + component_type + ". Allowed values: " + - ','.join(COMPONENT_TYPE_TO_MODULE.keys()) - ) - component_config.id = num - dev.add_component(component_config) - - log.debug('carlo gavazzi IP-Adresse: ' + str(ip_address)) - - dev.update() - - -def main(argv: List[str]): - run_using_positional_cli_args(read_legacy, argv) +def create_device(device_config: CarloGavazzi): + def create_counter_component(component_config: CarloGavazziCounterSetup): + return counter.CarloGavazziCounter(device_config.id, component_config, client, + device_config.configuration.modbus_id) + + def update_components(components: Iterable[counter.CarloGavazziCounter]): + with client: + for component in components: + with SingleComponentUpdateContext(component.fault_state): + component.update() + + try: + client = modbus.ModbusTcpClient_(device_config.configuration.ip_address, device_config.configuration.port) + except Exception: + log.exception("Fehler in create_device") + return ConfigurableDevice( + device_config=device_config, + component_factory=ComponentFactoryByType( + counter=create_counter_component + ), + component_updater=MultiComponentUpdater(update_components) + ) device_descriptor = DeviceDescriptor(configuration_factory=CarloGavazzi) diff --git a/packages/modules/devices/deye/bat.py b/packages/modules/devices/deye/bat.py index 632b848842..aa52ea47c6 100644 --- a/packages/modules/devices/deye/bat.py +++ b/packages/modules/devices/deye/bat.py @@ -14,35 +14,41 @@ class DeyeBat: - def __init__(self, device_id: int, component_config: DeyeBatSetup) -> None: + def __init__(self, device_id: int, component_config: DeyeBatSetup, device_type: DeviceType) -> None: self.component_config = dataclass_from_dict(DeyeBatSetup, component_config) self.store = get_bat_value_store(self.component_config.id) self.fault_state = FaultState(ComponentInfo.from_component_config(self.component_config)) self.__device_id = device_id self.sim_counter = SimCounter(self.__device_id, self.component_config.id, prefix="speicher") + self.device_type = device_type - def update(self, client: ModbusTcpClient_, device_type: DeviceType) -> None: + def update(self, client: ModbusTcpClient_) -> None: unit = self.component_config.configuration.modbus_id - if device_type == DeviceType.THREE_PHASE: - power = client.read_holding_registers(590, ModbusDataType.INT_16, unit=unit) * -1 - soc = client.read_holding_registers(588, ModbusDataType.INT_16, unit=unit) - # 516: Geladen in kWh * 0,1 - imported = client.read_holding_registers(516, ModbusDataType.UINT_16, unit=unit) * 100 - # 518: Entladen in kWh * 0,1 - exported = client.read_holding_registers(518, ModbusDataType.UINT_16, unit=unit) * 100 - elif device_type == DeviceType.SINGLE_PHASE_STRING or device_type == DeviceType.SINGLE_PHASE_HYBRID: + if self.device_type == DeviceType.SINGLE_PHASE_STRING or self.device_type == DeviceType.SINGLE_PHASE_HYBRID: power = client.read_holding_registers(190, ModbusDataType.INT_16, unit=unit) * -1 soc = client.read_holding_registers(184, ModbusDataType.INT_16, unit=unit) - if device_type == DeviceType.SINGLE_PHASE_HYBRID: + if self.device_type == DeviceType.SINGLE_PHASE_HYBRID: # 516: Geladen in kWh * 0,1 imported = client.read_holding_registers(72, ModbusDataType.UINT_16, unit=unit) * 100 # 518: Entladen in kWh * 0,1 exported = client.read_holding_registers(74, ModbusDataType.UINT_16, unit=unit) * 100 - elif device_type == DeviceType.SINGLE_PHASE_STRING: + + elif self.device_type == DeviceType.SINGLE_PHASE_STRING: imported, exported = self.sim_counter.sim_count(power) + else: # THREE_PHASE_LV (0x0500, 0x0005), THREE_PHASE_HV (0x0006) + power = client.read_holding_registers(590, ModbusDataType.INT_16, unit=unit) * -1 + + if self.device_type == DeviceType.THREE_PHASE_HV: + power = power * 10 + soc = client.read_holding_registers(588, ModbusDataType.INT_16, unit=unit) + # 516: Geladen in kWh * 0,1 + imported = client.read_holding_registers(516, ModbusDataType.UINT_16, unit=unit) * 100 + # 518: Entladen in kWh * 0,1 + exported = client.read_holding_registers(518, ModbusDataType.UINT_16, unit=unit) * 100 + bat_state = BatState( power=power, soc=soc, diff --git a/packages/modules/devices/deye/config.py b/packages/modules/devices/deye/config.py index 95211f3155..38793440da 100644 --- a/packages/modules/devices/deye/config.py +++ b/packages/modules/devices/deye/config.py @@ -7,11 +7,9 @@ class DeyeConfiguration: def __init__(self, ip_address: Optional[str] = None, - port: int = 8899, - device_type: str = "three_phase"): + port: int = 8899): self.ip_address = ip_address self.port = port - self.device_type = device_type class Deye: diff --git a/packages/modules/devices/deye/counter.py b/packages/modules/devices/deye/counter.py index 925c5b29c6..1f6fc66bfe 100644 --- a/packages/modules/devices/deye/counter.py +++ b/packages/modules/devices/deye/counter.py @@ -11,44 +11,46 @@ class DeyeCounter: - def __init__(self, device_id: int, component_config: DeyeCounterSetup) -> None: + def __init__(self, device_id: int, component_config: DeyeCounterSetup, device_type: DeviceType) -> None: self.component_config = dataclass_from_dict(DeyeCounterSetup, component_config) self.store = get_counter_value_store(self.component_config.id) self.fault_state = FaultState(ComponentInfo.from_component_config(self.component_config)) self.__device_id = device_id self.sim_counter = SimCounter(self.__device_id, self.component_config.id, prefix="bezug") + self.device_type = device_type - def update(self, client: ModbusTcpClient_, device_type: DeviceType): + def update(self, client: ModbusTcpClient_): unit = self.component_config.configuration.modbus_id - if device_type == DeviceType.THREE_PHASE: - currents = [c / 100 for c in client.read_holding_registers(613, [ModbusDataType.INT_16]*3, unit=unit)] - voltages = [v / 10 for v in client.read_holding_registers(644, [ModbusDataType.INT_16]*3, unit=unit)] - powers = client.read_holding_registers(616, [ModbusDataType.INT_16]*3, unit=unit) - power = sum(powers) - frequency = client.read_holding_registers(187, ModbusDataType.INT_32, unit=unit) * 100 - - # Wenn der Import/export Netz in wh gerechnet wird => *100 !! kommt in kw/h *0.1 - imported = client.read_holding_registers(522, ModbusDataType.INT_16, unit=unit) * 100 - exported = client.read_holding_registers(524, ModbusDataType.INT_16, unit=unit) * 100 - - elif device_type == DeviceType.SINGLE_PHASE_STRING or device_type == DeviceType.SINGLE_PHASE_HYBRID: + if self.device_type == DeviceType.SINGLE_PHASE_STRING or self.device_type == DeviceType.SINGLE_PHASE_HYBRID: frequency = client.read_holding_registers(79, ModbusDataType.INT_16, unit=unit) * 100 - if device_type == DeviceType.SINGLE_PHASE_HYBRID: + if self.device_type == DeviceType.SINGLE_PHASE_HYBRID: powers = [0]*3 currents = [0]*3 voltages = [0]*3 power = [0] # High und low word vom import sind nicht in aufeinanderfolgenden Registern imported, exported = self.sim_counter.sim_count(power) - elif device_type == DeviceType.SINGLE_PHASE_STRING: + + elif self.device_type == DeviceType.SINGLE_PHASE_STRING: currents = [c / 100 for c in client.read_holding_registers(76, [ModbusDataType.INT_16]*3, unit=unit)] voltages = [v / 10 for v in client.read_holding_registers(70, [ModbusDataType.INT_16]*3, unit=unit)] powers = [currents[i] * voltages[i] for i in range(0, 3)] power = sum(powers) imported, exported = self.sim_counter.sim_count(power) + else: # THREE_PHASE_LV (0x0500, 0x0005), THREE_PHASE_HV (0x0006) + currents = [c / 100 for c in client.read_holding_registers(613, [ModbusDataType.INT_16]*3, unit=unit)] + voltages = [v / 10 for v in client.read_holding_registers(644, [ModbusDataType.INT_16]*3, unit=unit)] + powers = client.read_holding_registers(616, [ModbusDataType.INT_16]*3, unit=unit) + power = client.read_holding_registers(625, ModbusDataType.INT_16, unit=unit) + frequency = client.read_holding_registers(609, ModbusDataType.INT_16, unit=unit) / 100 + + # Wenn der Import/export Netz in wh gerechnet wird => *100 !! kommt in kw/h *0.1 + imported = client.read_holding_registers(522, ModbusDataType.INT_16, unit=unit) * 100 + exported = client.read_holding_registers(524, ModbusDataType.INT_16, unit=unit) * 100 + counter_state = CounterState( currents=currents, imported=imported, diff --git a/packages/modules/devices/deye/device.py b/packages/modules/devices/deye/device.py index 422c200919..18cef2eb42 100644 --- a/packages/modules/devices/deye/device.py +++ b/packages/modules/devices/deye/device.py @@ -6,10 +6,9 @@ from modules.common.abstract_device import DeviceDescriptor from modules.common.component_context import SingleComponentUpdateContext from modules.common.configurable_device import ConfigurableDevice, ComponentFactoryByType, MultiComponentUpdater -from modules.common.modbus import ModbusTcpClient_ +from modules.common.modbus import ModbusDataType, ModbusTcpClient_ from modules.devices.deye.bat import DeyeBat from modules.devices.deye.counter import DeyeCounter -from modules.devices.deye.device_type import DeviceType from modules.devices.deye.inverter import DeyeInverter from modules.devices.deye import bat, counter, inverter from modules.devices.deye.config import Deye, DeyeBatSetup, DeyeConfiguration, DeyeCounterSetup, DeyeInverterSetup @@ -19,19 +18,25 @@ def create_device(device_config: Deye): def create_bat_component(component_config: DeyeBatSetup): - return DeyeBat(device_config.id, component_config) + device_type = client.read_holding_registers( + 0, ModbusDataType.INT_16, unit=component_config.configuration.modbus_id) + return DeyeBat(device_config.id, component_config, device_type) def create_counter_component(component_config: DeyeCounterSetup): - return DeyeCounter(device_config.id, component_config) + device_type = client.read_holding_registers( + 0, ModbusDataType.INT_16, unit=component_config.configuration.modbus_id) + return DeyeCounter(device_config.id, component_config, device_type) def create_inverter_component(component_config: DeyeInverterSetup): - return DeyeInverter(device_config.id, component_config) + device_type = client.read_holding_registers( + 0, ModbusDataType.INT_16, unit=component_config.configuration.modbus_id) + return DeyeInverter(device_config.id, component_config, device_type) def update_components(components: Iterable[Union[DeyeBat, DeyeCounter, DeyeInverter]]): with client as c: for component in components: with SingleComponentUpdateContext(component.fault_state): - component.update(c, DeviceType(device_config.configuration.device_type)) + component.update(c) try: client = ModbusTcpClient_(device_config.configuration.ip_address, device_config.configuration.port) diff --git a/packages/modules/devices/deye/device_type.py b/packages/modules/devices/deye/device_type.py index 3baef0a7ba..6e38c65bab 100644 --- a/packages/modules/devices/deye/device_type.py +++ b/packages/modules/devices/deye/device_type.py @@ -2,6 +2,8 @@ class DeviceType(Enum): - SINGLE_PHASE_HYBRID = "single_phase_hybrid" - SINGLE_PHASE_STRING = "single_phase_string" - THREE_PHASE = "three_phase" + SINGLE_PHASE_STRING = 0x0200 + SINGLE_PHASE_HYBRID = 0x0300 + THREE_PHASE_LV_0 = 0x0500 + THREE_PHASE_LV_1 = 0x0005 + THREE_PHASE_HV = 0x0006 diff --git a/packages/modules/devices/deye/inverter.py b/packages/modules/devices/deye/inverter.py index b45979c46d..9d60ebc30d 100644 --- a/packages/modules/devices/deye/inverter.py +++ b/packages/modules/devices/deye/inverter.py @@ -13,24 +13,30 @@ class DeyeInverter: - def __init__(self, device_id: int, component_config: Union[Dict, DeyeInverterSetup]) -> None: + def __init__(self, device_id: int, + component_config: Union[Dict, DeyeInverterSetup], + device_type: DeviceType) -> None: self.component_config = dataclass_from_dict(DeyeInverterSetup, component_config) self.store = get_inverter_value_store(self.component_config.id) self.fault_state = FaultState(ComponentInfo.from_component_config(self.component_config)) self.__device_id = device_id self.sim_counter = SimCounter(self.__device_id, self.component_config.id, prefix="pv") + self.device_type = device_type - def update(self, client: ModbusTcpClient_, device_type: DeviceType) -> None: + def update(self, client: ModbusTcpClient_) -> None: unit = self.component_config.configuration.modbus_id - if device_type == DeviceType.THREE_PHASE: - # Wechselrichter hat 2 mppt Tracker + if self.device_type == DeviceType.SINGLE_PHASE_STRING or self.device_type == DeviceType.SINGLE_PHASE_HYBRID: + power = sum(client.read_holding_registers(186, [ModbusDataType.INT_16]*4, unit=unit)) * -1 + exported = self.sim_counter.sim_count(power)[1] + + else: # THREE_PHASE_LV (0x0500, 0x0005), THREE_PHASE_HV (0x0006) power = sum(client.read_holding_registers(672, [ModbusDataType.INT_16]*2, unit=unit)) * -1 + + if self.device_type == DeviceType.THREE_PHASE_HV: + power = power * 10 # 534: Gesamt Produktion Wechselrichter unsigned integer in kWh * 0,1 exported = client.read_holding_registers(534, ModbusDataType.UINT_16, unit=unit) * 100 - elif device_type == DeviceType.SINGLE_PHASE_STRING or device_type == DeviceType.SINGLE_PHASE_HYBRID: - power = sum(client.read_holding_registers(186, [ModbusDataType.INT_16]*4, unit=unit)) * -1 - exported = self.sim_counter.sim_count(power)[1] inverter_state = InverterState( power=power, diff --git a/packages/modules/devices/fox_ess/bat.py b/packages/modules/devices/fox_ess/bat.py new file mode 100644 index 0000000000..fa49a1c7b6 --- /dev/null +++ b/packages/modules/devices/fox_ess/bat.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python3 +import logging +from dataclass_utils import dataclass_from_dict +from modules.common.component_state import BatState +from modules.common.component_type import ComponentDescriptor +from modules.common.fault_state import ComponentInfo, FaultState +from modules.common.modbus import ModbusDataType, ModbusTcpClient_ +from modules.common.store import get_bat_value_store +from modules.devices.fox_ess.config import FoxEssBatSetup + +log = logging.getLogger(__name__) + + +class FoxEssBat: + def __init__(self, component_config: FoxEssBatSetup) -> None: + self.component_config = dataclass_from_dict(FoxEssBatSetup, component_config) + self.store = get_bat_value_store(self.component_config.id) + self.fault_state = FaultState(ComponentInfo.from_component_config(self.component_config)) + + def update(self, client: ModbusTcpClient_) -> None: + unit = self.component_config.configuration.modbus_id + + power = client.read_holding_registers(31036, ModbusDataType.INT_16, unit=unit) * -1 + soc = client.read_holding_registers(31038, ModbusDataType.UINT_16, unit=unit) + # Geladen in kWh * 0,1 + imported = client.read_holding_registers(32003, ModbusDataType.UINT_32, unit=unit) * 100 + # Entladen in kWh * 0,1 + exported = client.read_holding_registers(32006, ModbusDataType.UINT_32, unit=unit) * 100 + + bat_state = BatState( + power=power, + soc=soc, + imported=imported, + exported=exported + ) + self.store.set(bat_state) + + +component_descriptor = ComponentDescriptor(configuration_factory=FoxEssBatSetup) diff --git a/packages/modules/devices/fox_ess/config.py b/packages/modules/devices/fox_ess/config.py new file mode 100644 index 0000000000..fd0b7a8019 --- /dev/null +++ b/packages/modules/devices/fox_ess/config.py @@ -0,0 +1,72 @@ +from typing import Optional +from helpermodules.auto_str import auto_str + +from modules.common.component_setup import ComponentSetup + + +class FoxEssConfiguration: + def __init__(self, + ip_address: Optional[str] = None, + port: int = 502): + self.ip_address = ip_address + self.port = port + + +class FoxEss: + def __init__(self, + name: str = "FoxESS", + type: str = "fox_ess", + id: int = 0, + configuration: FoxEssConfiguration = None) -> None: + self.name = name + self.type = type + self.id = id + self.configuration = configuration or FoxEssConfiguration() + + +@auto_str +class FoxEssBatConfiguration: + def __init__(self, modbus_id: int = 1): + self.modbus_id = modbus_id + + +@auto_str +class FoxEssBatSetup(ComponentSetup[FoxEssBatConfiguration]): + def __init__(self, + name: str = "FoxESS Speicher", + type: str = "bat", + id: int = 0, + configuration: FoxEssBatConfiguration = None) -> None: + super().__init__(name, type, id, configuration or FoxEssBatConfiguration()) + + +@auto_str +class FoxEssCounterConfiguration: + def __init__(self, modbus_id: int = 1): + self.modbus_id = modbus_id + + +@auto_str +class FoxEssCounterSetup(ComponentSetup[FoxEssCounterConfiguration]): + def __init__(self, + name: str = "FoxESS Zähler", + type: str = "counter", + id: int = 0, + configuration: FoxEssCounterConfiguration = None) -> None: + super().__init__(name, type, id, configuration or FoxEssCounterConfiguration()) + + +@auto_str +class FoxEssInverterConfiguration: + def __init__(self, modbus_id: int = 1): + self.modbus_id = modbus_id + + +@auto_str +class FoxEssInverterSetup(ComponentSetup[FoxEssInverterConfiguration]): + def __init__(self, + name: str = "FoxESS Wechselrichter", + type: str = "inverter", + id: int = 0, + configuration: FoxEssInverterConfiguration = None) -> None: + super().__init__(name, type, id, configuration or FoxEssInverterConfiguration()) diff --git a/packages/modules/devices/fox_ess/counter.py b/packages/modules/devices/fox_ess/counter.py new file mode 100644 index 0000000000..849548a0b7 --- /dev/null +++ b/packages/modules/devices/fox_ess/counter.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python3 +from dataclass_utils import dataclass_from_dict +from modules.common.component_state import CounterState +from modules.common.component_type import ComponentDescriptor +from modules.common.fault_state import ComponentInfo, FaultState +from modules.common.modbus import ModbusDataType, ModbusTcpClient_ +from modules.common.store import get_counter_value_store +from modules.devices.fox_ess.config import FoxEssCounterSetup + + +class FoxEssCounter: + def __init__(self, component_config: FoxEssCounterSetup) -> None: + self.component_config = dataclass_from_dict(FoxEssCounterSetup, component_config) + self.store = get_counter_value_store(self.component_config.id) + self.fault_state = FaultState(ComponentInfo.from_component_config(self.component_config)) + + def update(self, client: ModbusTcpClient_): + unit = self.component_config.configuration.modbus_id + + powers = client.read_holding_registers(31026, [ModbusDataType.INT_16]*3, unit=unit) + power = sum(powers) + frequency = client.read_holding_registers(31015, ModbusDataType.UINT_16, unit=unit) / 100 + imported = client.read_holding_registers(32018, ModbusDataType.UINT_32, unit=unit) * 100 + exported = client.read_holding_registers(32015, ModbusDataType.UINT_32, unit=unit) * 100 + + counter_state = CounterState( + imported=imported, + exported=exported, + power=power, + powers=powers, + frequency=frequency + ) + self.store.set(counter_state) + + +component_descriptor = ComponentDescriptor(configuration_factory=FoxEssCounterSetup) diff --git a/packages/modules/devices/fox_ess/device.py b/packages/modules/devices/fox_ess/device.py new file mode 100644 index 0000000000..70592f4312 --- /dev/null +++ b/packages/modules/devices/fox_ess/device.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python3 +import logging +from typing import Iterable, Union + +from modules.common.abstract_device import DeviceDescriptor +from modules.common.component_context import SingleComponentUpdateContext +from modules.common.configurable_device import ConfigurableDevice, ComponentFactoryByType, MultiComponentUpdater +from modules.common.modbus import ModbusTcpClient_ +from modules.devices.fox_ess.bat import FoxEssBat +from modules.devices.fox_ess.counter import FoxEssCounter +from modules.devices.fox_ess.inverter import FoxEssInverter +from modules.devices.fox_ess.config import FoxEss, FoxEssBatSetup, FoxEssCounterSetup, FoxEssInverterSetup + +log = logging.getLogger(__name__) + + +def create_device(device_config: FoxEss): + def create_bat_component(component_config: FoxEssBatSetup): + return FoxEssBat(component_config) + + def create_counter_component(component_config: FoxEssCounterSetup): + return FoxEssCounter(component_config) + + def create_inverter_component(component_config: FoxEssInverterSetup): + return FoxEssInverter(component_config) + + def update_components(components: Iterable[Union[FoxEssBat, FoxEssCounter, FoxEssInverter]]): + with client as c: + for component in components: + with SingleComponentUpdateContext(component.fault_state): + component.update(c) + + try: + client = ModbusTcpClient_(device_config.configuration.ip_address, device_config.configuration.port) + except Exception: + log.exception("Fehler in create_device") + return ConfigurableDevice( + device_config=device_config, + component_factory=ComponentFactoryByType( + bat=create_bat_component, + counter=create_counter_component, + inverter=create_inverter_component, + ), + component_updater=MultiComponentUpdater(update_components) + ) + + +device_descriptor = DeviceDescriptor(configuration_factory=FoxEss) diff --git a/packages/modules/devices/fox_ess/inverter.py b/packages/modules/devices/fox_ess/inverter.py new file mode 100644 index 0000000000..3a03811e5d --- /dev/null +++ b/packages/modules/devices/fox_ess/inverter.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python3 +from typing import Dict, Union + +from dataclass_utils import dataclass_from_dict +from modules.common.component_state import InverterState +from modules.common.component_type import ComponentDescriptor +from modules.common.fault_state import ComponentInfo, FaultState +from modules.common.modbus import ModbusDataType, ModbusTcpClient_ +from modules.common.store import get_inverter_value_store +from modules.devices.fox_ess.config import FoxEssInverterSetup + + +class FoxEssInverter: + def __init__(self, component_config: Union[Dict, FoxEssInverterSetup]) -> None: + self.component_config = dataclass_from_dict(FoxEssInverterSetup, component_config) + self.store = get_inverter_value_store(self.component_config.id) + self.fault_state = FaultState(ComponentInfo.from_component_config(self.component_config)) + + def update(self, client: ModbusTcpClient_) -> None: + unit = self.component_config.configuration.modbus_id + # PV1 + PV2 Power + power = sum([self.__tcp_client.read_holding_registers( + reg, ModbusDataType.INT_16, unit=self.__modbus_id) + for reg in [31002, 31005]]) * -1 + # Gesamt Produktion Wechselrichter unsigned integer in kWh * 0,1 + exported = client.read_holding_registers(32000, ModbusDataType.UINT_32, unit=unit) * 100 + + inverter_state = InverterState( + power=power, + exported=exported, + ) + self.store.set(inverter_state) + + +component_descriptor = ComponentDescriptor(configuration_factory=FoxEssInverterSetup) diff --git a/packages/modules/devices/good_we/bat.py b/packages/modules/devices/good_we/bat.py index fb3592b39d..cdcc5109bd 100644 --- a/packages/modules/devices/good_we/bat.py +++ b/packages/modules/devices/good_we/bat.py @@ -9,14 +9,19 @@ from modules.common.fault_state import ComponentInfo, FaultState from modules.common.store import get_bat_value_store from modules.devices.good_we.config import GoodWeBatSetup +from modules.devices.good_we.version import GoodWeVersion class GoodWeBat: def __init__(self, modbus_id: int, + version: GoodWeVersion, + firmware: int, component_config: Union[Dict, GoodWeBatSetup], tcp_client: modbus.ModbusTcpClient_) -> None: self.__modbus_id = modbus_id + self.version = version + self.firmware = firmware self.component_config = dataclass_from_dict(GoodWeBatSetup, component_config) self.__tcp_client = tcp_client self.store = get_bat_value_store(self.component_config.id) @@ -24,7 +29,10 @@ def __init__(self, def update(self) -> None: with self.__tcp_client: - power = self.__tcp_client.read_holding_registers(35183, ModbusDataType.INT_16, unit=self.__modbus_id)*-1 + if self.version == GoodWeVersion.V_1_7: + power = self.__tcp_client.read_holding_registers(35183, ModbusDataType.INT_16, unit=self.__modbus_id)*-1 + else: + power = self.__tcp_client.read_holding_registers(35182, ModbusDataType.INT_32, unit=self.__modbus_id)*-1 soc = self.__tcp_client.read_holding_registers(37007, ModbusDataType.UINT_16, unit=self.__modbus_id) imported = self.__tcp_client.read_holding_registers( 35206, ModbusDataType.UINT_32, unit=self.__modbus_id) * 100 diff --git a/packages/modules/devices/good_we/config.py b/packages/modules/devices/good_we/config.py index db232d7833..32548da5b1 100644 --- a/packages/modules/devices/good_we/config.py +++ b/packages/modules/devices/good_we/config.py @@ -1,18 +1,25 @@ from typing import Optional from modules.common.component_setup import ComponentSetup +from modules.devices.good_we.version import GoodWeVersion class GoodWeConfiguration: - def __init__(self, ip_address: Optional[str] = None, modbus_id: int = 247, port: int = 502): + def __init__(self, ip_address: Optional[str] = None, + modbus_id: int = 247, + port: int = 502, + version: GoodWeVersion = GoodWeVersion.V_1_7, + firmware: int = 8): self.ip_address = ip_address self.modbus_id = modbus_id self.port = port + self.version = version + self.firmware = firmware class GoodWe: def __init__(self, - name: str = "GoodWe ET-Serie (5-10kW)", + name: str = "GoodWe ET-Serie", type: str = "good_we", id: int = 0, configuration: GoodWeConfiguration = None) -> None: diff --git a/packages/modules/devices/good_we/counter.py b/packages/modules/devices/good_we/counter.py index 8ff6feb683..f1c3c899a7 100644 --- a/packages/modules/devices/good_we/counter.py +++ b/packages/modules/devices/good_we/counter.py @@ -7,40 +7,61 @@ from modules.common.component_type import ComponentDescriptor from modules.common.fault_state import ComponentInfo, FaultState from modules.common.modbus import ModbusDataType +from modules.common.simcount import SimCounter from modules.common.store import get_counter_value_store from modules.devices.good_we.config import GoodWeCounterSetup +from modules.devices.good_we.version import GoodWeVersion class GoodWeCounter: def __init__(self, + device_id: int, modbus_id: int, + version: GoodWeVersion, + firmware: int, component_config: Union[Dict, GoodWeCounterSetup], tcp_client: modbus.ModbusTcpClient_) -> None: + self.__device_id = device_id self.__modbus_id = modbus_id + self.version = version + self.firmware = firmware self.component_config = dataclass_from_dict(GoodWeCounterSetup, component_config) + self.sim_counter = SimCounter(self.__device_id, self.component_config.id, prefix="bezug") self.__tcp_client = tcp_client self.store = get_counter_value_store(self.component_config.id) self.fault_state = FaultState(ComponentInfo.from_component_config(self.component_config)) def update(self): with self.__tcp_client: - power_factors = [ - val / 1000 for val in self.__tcp_client.read_holding_registers(36010, - [ModbusDataType.UINT_16]*3, - unit=self.__modbus_id)] - exported = self.__tcp_client.read_holding_registers( - 36015, ModbusDataType.FLOAT_32, unit=self.__modbus_id) - imported = self.__tcp_client.read_holding_registers( - 36017, ModbusDataType.FLOAT_32, unit=self.__modbus_id) - powers = [ - val * -1 for val in self.__tcp_client.read_holding_registers(36005, - [ModbusDataType.INT_16]*3, - unit=self.__modbus_id)] - power = self.__tcp_client.read_holding_registers(36008, ModbusDataType.INT_16, unit=self.__modbus_id) * -1 + if self.firmware < 9: + power = self.__tcp_client.read_holding_registers( + 36008, ModbusDataType.INT_16, unit=self.__modbus_id) * -1 + powers = [ + val * -1 for val in self.__tcp_client.read_holding_registers( + 36005, [ModbusDataType.INT_16]*3, unit=self.__modbus_id)] + else: + power = self.__tcp_client.read_holding_registers( + 36025, ModbusDataType.INT_32, unit=self.__modbus_id) * -1 + powers = [ + val * -1 for val in self.__tcp_client.read_holding_registers( + 36019, [ModbusDataType.INT_32]*3, unit=self.__modbus_id)] + power_factors = [ + val / 1000 for val in self.__tcp_client.read_holding_registers( + 36010, [ModbusDataType.INT_16]*3, unit=self.__modbus_id)] frequency = self.__tcp_client.read_holding_registers( 36014, ModbusDataType.UINT_16, unit=self.__modbus_id) / 100 + if self.version == GoodWeVersion.V_1_7: + exported = self.__tcp_client.read_holding_registers( + 36015, ModbusDataType.FLOAT_32, unit=self.__modbus_id) + imported = self.__tcp_client.read_holding_registers( + 36017, ModbusDataType.FLOAT_32, unit=self.__modbus_id) + else: + # v1.0 und v1.1 liefern keine Werte zurueck obwohl Register laut Doku gleich + # Alternative Register für die BTC Serie liefern statische Werte + imported, exported = self.sim_counter.sim_count(power) + counter_state = CounterState( powers=powers, imported=imported, diff --git a/packages/modules/devices/good_we/device.py b/packages/modules/devices/good_we/device.py index cb90cb2f50..661331c8b6 100644 --- a/packages/modules/devices/good_we/device.py +++ b/packages/modules/devices/good_we/device.py @@ -1,100 +1,56 @@ #!/usr/bin/env python3 import logging -from typing import Dict, List, Union, Optional +from typing import Iterable, Union -from dataclass_utils import dataclass_from_dict -from helpermodules.cli import run_using_positional_cli_args from modules.common import modbus -from modules.common.abstract_device import AbstractDevice, DeviceDescriptor +from modules.common.abstract_device import DeviceDescriptor from modules.common.component_context import SingleComponentUpdateContext +from modules.common.configurable_device import ComponentFactoryByType, ConfigurableDevice, MultiComponentUpdater from modules.devices.good_we import bat from modules.devices.good_we import counter from modules.devices.good_we import inverter -from modules.devices.good_we.config import (GoodWe, GoodWeBatSetup, GoodWeConfiguration, GoodWeCounterSetup, - GoodWeInverterSetup) +from modules.devices.good_we.config import GoodWe, GoodWeBatSetup, GoodWeCounterSetup, GoodWeInverterSetup log = logging.getLogger(__name__) good_we_component_classes = Union[bat.GoodWeBat, counter.GoodWeCounter, inverter.GoodWeInverter] -class Device(AbstractDevice): - COMPONENT_TYPE_TO_CLASS = { - "bat": bat.GoodWeBat, - "counter": counter.GoodWeCounter, - "inverter": inverter.GoodWeInverter - } - - def __init__(self, device_config: Union[Dict, GoodWe]) -> None: - self.components = {} # type: Dict[str, good_we_component_classes] - try: - self.device_config = dataclass_from_dict(GoodWe, device_config) - self.client = modbus.ModbusTcpClient_( - self.device_config.configuration.ip_address, self.device_config.configuration.port) - except Exception: - log.exception("Fehler im Modul " + self.device_config.name) - - def add_component(self, - component_config: Union[Dict, GoodWeBatSetup, GoodWeCounterSetup, GoodWeInverterSetup]) -> None: - if isinstance(component_config, Dict): - component_type = component_config["type"] - else: - component_type = component_config.type - component_config = dataclass_from_dict(COMPONENT_TYPE_TO_MODULE[ - component_type].component_descriptor.configuration_factory, - component_config) - if component_type in self.COMPONENT_TYPE_TO_CLASS: - self.components["component" + str(component_config.id)] = (self.COMPONENT_TYPE_TO_CLASS[component_type]( - self.device_config.configuration.modbus_id, component_config, self.client)) - else: - raise Exception( - "illegal component type " + component_type + ". Allowed values: " + - ','.join(self.COMPONENT_TYPE_TO_CLASS.keys()) - ) - - def update(self) -> None: - log.debug("Start device reading " + str(self.components)) - if self.components: - for component in self.components: - # Auch wenn bei einer Komponente ein Fehler auftritt, sollen alle anderen noch ausgelesen werden. - with SingleComponentUpdateContext(self.components[component].fault_state): - self.components[component].update() - else: - log.warning( - self.device_config.name + - ": Es konnten keine Werte gelesen werden, da noch keine Komponenten konfiguriert wurden." - ) - - -COMPONENT_TYPE_TO_MODULE = { - "bat": bat, - "counter": counter, - "inverter": inverter -} - - -def read_legacy(component_type: str, ip_address: str, id: int, num: Optional[int]) -> None: - device_config = GoodWe(configuration=GoodWeConfiguration(ip_address=ip_address, modbus_id=id)) - dev = Device(device_config) - if component_type in COMPONENT_TYPE_TO_MODULE: - component_config = COMPONENT_TYPE_TO_MODULE[component_type].component_descriptor.configuration_factory() - else: - raise Exception( - "illegal component type " + component_type + ". Allowed values: " + - ','.join(COMPONENT_TYPE_TO_MODULE.keys()) - ) - component_config.id = num - dev.add_component(component_config) - - log.debug('GoodWe IP-Adresse: ' + ip_address) - log.debug('GoodWe ID: ' + str(id)) - - dev.update() - dev.client.close() - - -def main(argv: List[str]): - run_using_positional_cli_args(read_legacy, argv) +def create_device(device_config: GoodWe): + def create_bat_component(component_config: GoodWeBatSetup): + return bat.GoodWeBat(device_config.configuration.modbus_id, + device_config.configuration.version, device_config.configuration.firmware, + component_config, client) + + def create_counter_component(component_config: GoodWeCounterSetup): + return counter.GoodWeCounter(device_config.id, device_config.configuration.modbus_id, + device_config.configuration.version, device_config.configuration.firmware, + component_config, client) + + def create_inverter_component(component_config: GoodWeInverterSetup): + return inverter.GoodWeInverter(device_config.configuration.modbus_id, + device_config.configuration.version, device_config.configuration.firmware, + component_config, client) + + def update_components(components: Iterable[good_we_component_classes]): + with client: + for component in components: + with SingleComponentUpdateContext(component.fault_state): + component.update() + + try: + client = modbus.ModbusTcpClient_(device_config.configuration.ip_address, device_config.configuration.port) + except Exception: + log.exception("Fehler in create_device") + return ConfigurableDevice( + device_config=device_config, + component_factory=ComponentFactoryByType( + bat=create_bat_component, + counter=create_counter_component, + inverter=create_inverter_component, + ), + component_updater=MultiComponentUpdater(update_components) + ) device_descriptor = DeviceDescriptor(configuration_factory=GoodWe) diff --git a/packages/modules/devices/good_we/inverter.py b/packages/modules/devices/good_we/inverter.py index 9b01b97629..c2de34fc1a 100644 --- a/packages/modules/devices/good_we/inverter.py +++ b/packages/modules/devices/good_we/inverter.py @@ -9,14 +9,19 @@ from modules.common.modbus import ModbusDataType from modules.common.store import get_inverter_value_store from modules.devices.good_we.config import GoodWeInverterSetup +from modules.devices.good_we.version import GoodWeVersion class GoodWeInverter: def __init__(self, modbus_id: int, + version: GoodWeVersion, + firmware: int, component_config: Union[Dict, GoodWeInverterSetup], tcp_client: modbus.ModbusTcpClient_) -> None: self.__modbus_id = modbus_id + self.version = version + self.firmware = firmware self.component_config = dataclass_from_dict(GoodWeInverterSetup, component_config) self.__tcp_client = tcp_client self.store = get_inverter_value_store(self.component_config.id) @@ -24,8 +29,9 @@ def __init__(self, def update(self) -> None: with self.__tcp_client: - power = sum([self.__tcp_client.read_holding_registers(reg, ModbusDataType.UINT_32, - unit=self.__modbus_id) for reg in [35105, 35109, 35113, 35117]]) * -1 + power = sum([self.__tcp_client.read_holding_registers( + reg, ModbusDataType.INT_32, unit=self.__modbus_id) + for reg in [35105, 35109, 35113, 35117]]) * -1 exported = self.__tcp_client.read_holding_registers( 35191, ModbusDataType.UINT_32, unit=self.__modbus_id) * 100 diff --git a/packages/modules/devices/good_we/version.py b/packages/modules/devices/good_we/version.py new file mode 100644 index 0000000000..35ce8df2dd --- /dev/null +++ b/packages/modules/devices/good_we/version.py @@ -0,0 +1,6 @@ +from enum import Enum + + +class GoodWeVersion(Enum): + V_1_7 = "v_1_7" + V_1_1 = "v_1_1" diff --git a/packages/modules/devices/huawei/config.py b/packages/modules/devices/huawei/config.py index 4c197e24a7..335aefe933 100644 --- a/packages/modules/devices/huawei/config.py +++ b/packages/modules/devices/huawei/config.py @@ -12,7 +12,7 @@ def __init__(self, modbus_id: int = 1, ip_address: Optional[str] = None, port: i class Huawei: def __init__(self, - name: str = "Huawei", + name: str = "Huawei Hybrid Wechselrichter", type: str = "huawei", id: int = 0, configuration: HuaweiConfiguration = None) -> None: diff --git a/packages/modules/devices/huawei_smartlogger/config.py b/packages/modules/devices/huawei_smartlogger/config.py index a0d5c08baa..e295ae5a9f 100644 --- a/packages/modules/devices/huawei_smartlogger/config.py +++ b/packages/modules/devices/huawei_smartlogger/config.py @@ -13,7 +13,7 @@ def __init__(self, ip_address: Optional[str] = None, port: int = 502): @auto_str class Huawei_Smartlogger: def __init__(self, - name: str = "Huawei_Smartlogger", + name: str = "Huawei Smartlogger", type: str = "huawei_smartlogger", id: int = 0, configuration: Huawei_SmartloggerConfiguration = None) -> None: diff --git a/packages/modules/devices/huawei_smartlogger/device.py b/packages/modules/devices/huawei_smartlogger/device.py index 9eb3b157d6..34f8aa058e 100644 --- a/packages/modules/devices/huawei_smartlogger/device.py +++ b/packages/modules/devices/huawei_smartlogger/device.py @@ -1,11 +1,11 @@ #!/usr/bin/env python3 import logging -from typing import Optional, Union, List, Dict -from dataclass_utils import dataclass_from_dict -from helpermodules.cli import run_using_positional_cli_args -from modules.common.abstract_device import AbstractDevice, DeviceDescriptor +from typing import Iterable, Union + +from modules.common.abstract_device import DeviceDescriptor from modules.common.component_context import SingleComponentUpdateContext from modules.common import modbus +from modules.common.configurable_device import ComponentFactoryByType, ConfigurableDevice, MultiComponentUpdater from modules.devices.huawei_smartlogger import counter from modules.devices.huawei_smartlogger import inverter from modules.devices.huawei_smartlogger import bat @@ -21,91 +21,35 @@ inverter.Huawei_SmartloggerInverter] -class Device(AbstractDevice): - COMPONENT_TYPE_TO_CLASS = { - "bat": bat.Huawei_SmartloggerBat, - "counter": counter.Huawei_SmartloggerCounter, - "inverter": inverter.Huawei_SmartloggerInverter - } - - def __init__(self, device_config: Union[Dict, Huawei_Smartlogger]) -> None: - self.components = {} # type: Dict[str, huawei_smartlogger_component_classes] - try: - self.device_config = dataclass_from_dict(Huawei_Smartlogger, device_config) - ip_address = self.device_config.configuration.ip_address - self.port = self.device_config.configuration.port - self.client = modbus.ModbusTcpClient_(ip_address, self.device_config.configuration.port) - self.client.connect() - except Exception: - log.exception("Fehler im Modul "+self.device_config.name) - - def add_component(self, component_config: Union[Dict, - Huawei_SmartloggerBatSetup, - Huawei_SmartloggerCounterSetup, - Huawei_SmartloggerInverterSetup]) -> None: - if isinstance(component_config, Dict): - component_type = component_config["type"] - else: - component_type = component_config.type - component_config = dataclass_from_dict(COMPONENT_TYPE_TO_MODULE[ - component_type].component_descriptor.configuration_factory, component_config) - if component_type in self.COMPONENT_TYPE_TO_CLASS: - self.components["component"+str(component_config.id)] = ( - self.COMPONENT_TYPE_TO_CLASS[component_type](self.device_config.id, component_config, self.client)) - else: - raise Exception( - "illegal component type " + component_type + ". Allowed values: " + - ','.join(self.COMPONENT_TYPE_TO_CLASS.keys()) - ) - - def update(self) -> None: - log.debug("Start device reading " + str(self.components)) - if self.components: - for component in self.components: - # Auch wenn bei einer Komponente ein Fehler auftritt, sollen alle anderen noch ausgelesen werden. - with SingleComponentUpdateContext(self.components[component].fault_state): - self.components[component].update() - else: - log.warning( - self.device_config.name + - ": Es konnten keine Werte gelesen werden, da noch keine Komponenten konfiguriert wurden." - ) - - -COMPONENT_TYPE_TO_MODULE = { - "bat": bat, - "counter": counter, - "inverter": inverter -} - - -def read_legacy(component_type: str, - ip_address: str, - modbus_id: Optional[int] = 1, - num: Optional[int] = None) -> None: - - device_config = Huawei_Smartlogger() - device_config.configuration.ip_address = ip_address - dev = Device(device_config) - - if component_type in COMPONENT_TYPE_TO_MODULE: - component_config = COMPONENT_TYPE_TO_MODULE[component_type].component_descriptor.configuration_factory() - else: - raise Exception( - "illegal component type " + component_type + ". Allowed values: " + - ','.join(COMPONENT_TYPE_TO_MODULE.keys()) - ) - component_config.id = num - component_config.configuration.modbus_id = modbus_id - dev.add_component(component_config) - - log.debug('Huawei Smartlogger IP-Adresse: ' + ip_address) - log.debug('Huawei Device Modbus-ID: ' + str(modbus_id)) - dev.update() - - -def main(argv: List[str]): - run_using_positional_cli_args(read_legacy, argv) +def create_device(device_config: Huawei_Smartlogger): + def create_bat_component(component_config: Huawei_SmartloggerBatSetup): + return bat.Huawei_SmartloggerBat(device_config.id, component_config, client) + + def create_counter_component(component_config: Huawei_SmartloggerCounterSetup): + return counter.Huawei_SmartloggerCounter(device_config.id, component_config, client) + + def create_inverter_component(component_config: Huawei_SmartloggerInverterSetup): + return inverter.Huawei_SmartloggerInverter(device_config.id, component_config, client) + + def update_components(components: Iterable[huawei_smartlogger_component_classes]): + with client: + for component in components: + with SingleComponentUpdateContext(component.fault_state): + component.update() + + try: + client = modbus.ModbusTcpClient_(device_config.configuration.ip_address, device_config.configuration.port) + except Exception: + log.exception("Fehler in create_device") + return ConfigurableDevice( + device_config=device_config, + component_factory=ComponentFactoryByType( + bat=create_bat_component, + counter=create_counter_component, + inverter=create_inverter_component, + ), + component_updater=MultiComponentUpdater(update_components) + ) device_descriptor = DeviceDescriptor(configuration_factory=Huawei_Smartlogger) diff --git a/packages/modules/devices/janitza/device.py b/packages/modules/devices/janitza/device.py index 59e0e69fc2..e3aef00be8 100644 --- a/packages/modules/devices/janitza/device.py +++ b/packages/modules/devices/janitza/device.py @@ -1,11 +1,10 @@ #!/usr/bin/env python3 import logging -from typing import Dict, Optional, List, Union +from typing import Iterable -from dataclass_utils import dataclass_from_dict -from helpermodules.cli import run_using_positional_cli_args +from modules.common.configurable_device import ComponentFactoryByType, ConfigurableDevice, MultiComponentUpdater from modules.common import modbus -from modules.common.abstract_device import AbstractDevice, DeviceDescriptor +from modules.common.abstract_device import DeviceDescriptor from modules.common.component_context import SingleComponentUpdateContext from modules.devices.janitza import counter from modules.devices.janitza.config import Janitza, JanitzaCounterSetup @@ -13,77 +12,28 @@ log = logging.getLogger(__name__) -class Device(AbstractDevice): - COMPONENT_TYPE_TO_CLASS = { - "counter": counter.JanitzaCounter - } - - def __init__(self, device_config: Union[Dict, Janitza]) -> None: - self.components = {} # type: Dict[str, counter.JanitzaCounter] - try: - self.device_config = dataclass_from_dict(Janitza, device_config) - self.client = modbus.ModbusTcpClient_( - self.device_config.configuration.ip_address, self.device_config.configuration.port) - except Exception: - log.exception("Fehler im Modul "+self.device_config.name) - - def add_component(self, component_config: Union[Dict, JanitzaCounterSetup]) -> None: - if isinstance(component_config, Dict): - component_type = component_config["type"] - else: - component_type = component_config.type - component_config = dataclass_from_dict(COMPONENT_TYPE_TO_MODULE[ - component_type].component_descriptor.configuration_factory, component_config) - if component_type in self.COMPONENT_TYPE_TO_CLASS: - self.components["component"+str(component_config.id)] = (self.COMPONENT_TYPE_TO_CLASS[component_type]( - self.device_config.id, component_config, self.client, - self.device_config.configuration.modbus_id)) - else: - raise Exception( - "illegal component type " + component_type + ". Allowed values: " + - ','.join(self.COMPONENT_TYPE_TO_CLASS.keys()) - ) - - def update(self) -> None: - log.debug("Start device reading " + str(self.components)) - if self.components: - for component in self.components: - # Auch wenn bei einer Komponente ein Fehler auftritt, sollen alle anderen noch ausgelesen werden. - with SingleComponentUpdateContext(self.components[component].fault_state): - self.components[component].update() - else: - log.warning( - self.device_config.name + - ": Es konnten keine Werte gelesen werden, da noch keine Komponenten konfiguriert wurden." - ) - - -COMPONENT_TYPE_TO_MODULE = { - "counter": counter -} - - -def read_legacy(component_type: str, ip_address: str, num: Optional[int] = None) -> None: - device_config = Janitza() - device_config.configuration.ip_address = ip_address - dev = Device(device_config) - if component_type in COMPONENT_TYPE_TO_MODULE: - component_config = COMPONENT_TYPE_TO_MODULE[component_type].component_descriptor.configuration_factory() - else: - raise Exception( - "illegal component type " + component_type + ". Allowed values: " + - ','.join(COMPONENT_TYPE_TO_MODULE.keys()) - ) - component_config.id = num - dev.add_component(component_config) - - log.debug('Janitza IP-Adresse: ' + ip_address) - - dev.update() - - -def main(argv: List[str]): - run_using_positional_cli_args(read_legacy, argv) +def create_device(device_config: Janitza): + def create_counter_component(component_config: JanitzaCounterSetup): + return counter.JanitzaCounter(device_config.id, component_config, client, + device_config.configuration.modbus_id) + + def update_components(components: Iterable[counter.JanitzaCounter]): + with client: + for component in components: + with SingleComponentUpdateContext(component.fault_state): + component.update() + + try: + client = modbus.ModbusTcpClient_(device_config.configuration.ip_address, device_config.configuration.port) + except Exception: + log.exception("Fehler in create_device") + return ConfigurableDevice( + device_config=device_config, + component_factory=ComponentFactoryByType( + counter=create_counter_component, + ), + component_updater=MultiComponentUpdater(update_components) + ) device_descriptor = DeviceDescriptor(configuration_factory=Janitza) diff --git a/packages/modules/devices/kostal_piko/device.py b/packages/modules/devices/kostal_piko/device.py index 20f18f1d10..567d6f26cd 100644 --- a/packages/modules/devices/kostal_piko/device.py +++ b/packages/modules/devices/kostal_piko/device.py @@ -1,136 +1,30 @@ #!/usr/bin/env python3 import logging -from typing import Dict, Union, Optional, List -from dataclass_utils import dataclass_from_dict -from helpermodules.cli import run_using_positional_cli_args -from modules.devices.byd.config import BYD, BYDBatSetup, BYDConfiguration -from modules.devices.byd.device import Device as BYDDevice -from modules.common.abstract_device import AbstractDevice, DeviceDescriptor -from modules.common.component_context import SingleComponentUpdateContext -from modules.common.component_state import CounterState -from modules.common.simcount import sim_count -from modules.common.store import get_counter_value_store +from modules.common.configurable_device import ComponentFactoryByType, ConfigurableDevice, IndependentComponentUpdater +from modules.common.abstract_device import DeviceDescriptor from modules.devices.kostal_piko import counter from modules.devices.kostal_piko import inverter -from modules.devices.kostal_piko.config import (KostalPiko, - KostalPikoConfiguration, - KostalPikoCounterSetup, KostalPikoInverterConfiguration, - KostalPikoInverterSetup) +from modules.devices.kostal_piko.config import KostalPiko, KostalPikoCounterSetup, KostalPikoInverterSetup log = logging.getLogger(__name__) -kostal_piko_component_classes = Union[counter.KostalPikoCounter, inverter.KostalPikoInverter] +def create_device(device_config: KostalPiko): + def create_counter_component(component_config: KostalPikoCounterSetup): + return counter.KostalPikoCounter(device_config.id, component_config, device_config.configuration.ip_address) + def create_inverter_component(component_config: KostalPikoInverterSetup): + return inverter.KostalPikoInverter(device_config.id, component_config, device_config.configuration.ip_address) -class Device(AbstractDevice): - COMPONENT_TYPE_TO_CLASS = { - "counter": counter.KostalPikoCounter, - "inverter": inverter.KostalPikoInverter - } - - def __init__(self, device_config: Union[Dict, KostalPiko]) -> None: - self.components = {} # type: Dict[str, kostal_piko_component_classes] - try: - self.device_config = dataclass_from_dict(KostalPiko, device_config) - except Exception: - log.exception("Fehler im Modul "+self.device_config.name) - - def add_component(self, component_config: Union[Dict, KostalPikoCounterSetup, KostalPikoInverterSetup]) -> None: - if isinstance(component_config, Dict): - component_type = component_config["type"] - else: - component_type = component_config.type - component_config = dataclass_from_dict(COMPONENT_TYPE_TO_MODULE[ - component_type].component_descriptor.configuration_factory, component_config) - if component_type in self.COMPONENT_TYPE_TO_CLASS: - self.components["component"+str(component_config.id)] = (self.COMPONENT_TYPE_TO_CLASS[component_type]( - self.device_config.id, component_config, self.device_config.configuration.ip_address)) - else: - raise Exception( - "illegal component type " + component_type + ". Allowed values: " + - ','.join(self.COMPONENT_TYPE_TO_CLASS.keys()) - ) - - def update(self) -> None: - log.debug("Start device reading " + str(self.components)) - if self.components: - for component in self.components: - # Auch wenn bei einer Komponente ein Fehler auftritt, sollen alle anderen noch ausgelesen werden. - with SingleComponentUpdateContext(self.components[component].fault_state): - self.components[component].update() - else: - log.warning( - self.device_config.name + - ": Es konnten keine Werte gelesen werden, da noch keine Komponenten konfiguriert wurden." - ) - - -COMPONENT_TYPE_TO_MODULE = { - "counter": counter, - "inverter": inverter -} - - -def read_legacy(component_type: str, - address: str, - bat_module: str, - bat_ip: str, - bat_username: str, - bat_password: str, - num: Optional[int] = None) -> None: - dev = Device(KostalPiko(configuration=KostalPikoConfiguration(ip_address=address))) - if component_type in COMPONENT_TYPE_TO_MODULE: - component_config = COMPONENT_TYPE_TO_MODULE[component_type].component_descriptor.configuration_factory() - else: - raise Exception( - "illegal component type " + component_type + ". Allowed values: " + - ','.join(COMPONENT_TYPE_TO_MODULE.keys()) - ) - component_config.id = num - if isinstance(component_config, KostalPikoInverterSetup) and bat_module != "none": - component_config.configuration.bat_configured = True - dev.add_component(component_config) - - log.debug('KostalPiko IP-Adresse: ' + address) - log.debug('KostalPiko Speicher: ' + bat_module) - - if isinstance(component_config, KostalPikoInverterSetup): - dev.update() - elif isinstance(component_config, KostalPikoCounterSetup): - with SingleComponentUpdateContext(dev.components["componentNone"].component_info): - home_consumption, powers = dev.components["componentNone"].get_values() - if bat_module == "speicher_bydhv": - bat_power = _get_byd_bat_power(bat_ip, bat_username, bat_password, 1) - home_consumption += bat_power - - dev.add_component(KostalPikoInverterSetup( - id=1, configuration=KostalPikoInverterConfiguration(bat_configured=True))) - inverter_power, _ = dev.components["component"+str(1)].update() - - power = home_consumption + inverter_power - imported, exported = sim_count(power, prefix="bezug") - counter_state = CounterState( - imported=imported, - exported=exported, - power=power, - powers=powers - ) - get_counter_value_store(None).set(counter_state) - - -def _get_byd_bat_power(bat_ip: str, bat_username: str, bat_password: str, num: int) -> float: - bat_dev = BYDDevice(BYD(configuration=BYDConfiguration(user=bat_username, - password=bat_password, - ip_address=bat_ip))) - bat_dev.add_component(BYDBatSetup(id=num)) - bat_power, _ = bat_dev.components["component"+str(num)].get_values() - return bat_power - - -def main(argv: List[str]): - run_using_positional_cli_args(read_legacy, argv) + return ConfigurableDevice( + device_config=device_config, + component_factory=ComponentFactoryByType( + counter=create_counter_component, + inverter=create_inverter_component, + ), + component_updater=IndependentComponentUpdater(lambda component: component.update()) + ) device_descriptor = DeviceDescriptor(configuration_factory=KostalPiko) diff --git a/packages/modules/devices/lg/device.py b/packages/modules/devices/lg/device.py index 0a55eac6ab..93a01be397 100644 --- a/packages/modules/devices/lg/device.py +++ b/packages/modules/devices/lg/device.py @@ -1,146 +1,75 @@ #!/usr/bin/env python3 import json import logging -import os -from typing import Dict, Union, Optional, List +from typing import Dict, Iterable, Union from requests import HTTPError, Session -from dataclass_utils import dataclass_from_dict -from helpermodules.cli import run_using_positional_cli_args from modules.common import req -from modules.common.abstract_device import AbstractDevice, DeviceDescriptor -from modules.common.component_context import MultiComponentUpdateContext -from modules.devices.lg.config import LG, LgBatSetup, LgConfiguration, LgCounterSetup, LgInverterSetup -from modules.devices.lg import bat, counter, inverter +from modules.common.abstract_device import DeviceDescriptor +from modules.common.configurable_device import ComponentFactoryByType, ConfigurableDevice, MultiComponentUpdater +from modules.devices.lg.bat import LgBat +from modules.devices.lg.config import LG, LgBatSetup, LgCounterSetup, LgInverterSetup +from modules.devices.lg.counter import LgCounter +from modules.devices.lg.inverter import LgInverter log = logging.getLogger(__name__) -lg_component_classes = Union[bat.LgBat, counter.LgCounter, inverter.LgInverter] - - -class Device(AbstractDevice): - """Beispiel JSON-Objekte liegen im Ordner lgessv1/JSON-Beispiele.txt - lg_ess_url: IP/URL des LG ESS V1.0 - lg_ess_pass: Passwort, um sich in den LG ESS V1.0 einzuloggen. - Das Passwort ist standardmäßig die Registrierungsnummer, - die sich auf dem PCS (dem Hybridwechselrichter und - Batteriemanagementsystem) befindet (Aufkleber!). Alter- - nativ findet man die Registrierungsnummer in der App unter - dem Menüpunkt "Systeminformationen". - Mit der Registrierungsnummer kann man sich dann in der - Rolle "installer" einloggen.""" - COMPONENT_TYPE_TO_CLASS = { - "bat": bat.LgBat, - "counter": counter.LgCounter, - "inverter": inverter.LgInverter - } - - def __init__(self, device_config: Union[Dict, LG]) -> None: - self.components = {} # type: Dict[str, lg_component_classes] - self.session_key = " " - try: - self.device_config = dataclass_from_dict(LG, device_config) - except Exception: - log.exception("Fehler im Modul "+self.device_config.name) - - def add_component(self, component_config: Union[Dict, LgBatSetup, LgCounterSetup, LgInverterSetup]) -> None: - if isinstance(component_config, Dict): - component_type = component_config["type"] - else: - component_type = component_config.type - component_config = dataclass_from_dict(COMPONENT_TYPE_TO_MODULE[ - component_type].component_descriptor.configuration_factory, component_config) - if component_type in self.COMPONENT_TYPE_TO_CLASS: - self.components["component"+str(component_config.id)] = (self.COMPONENT_TYPE_TO_CLASS[component_type]( - self.device_config.id, - component_config)) - else: - raise Exception( - "illegal component type " + component_type + ". Allowed values: " + - ','.join(self.COMPONENT_TYPE_TO_CLASS.keys()) - ) - - def update(self) -> None: - log.debug("Start device reading " + str(self.components)) - if self.components: - with MultiComponentUpdateContext(self.components): - session = req.get_http_session() - try: - response = self._request_data(session) - except HTTPError: - self._update_session_key(session) - response = self._request_data(session) - - for component in self.components: - self.components[component].update(response) - else: - log.warning( - self.device_config.name + - ": Es konnten keine Werte gelesen werden, da noch keine Komponenten konfiguriert wurden." - ) - - def _update_session_key(self, session: Session): - try: - headers = {'Content-Type': 'application/json', } - data = json.dumps({"password": self.device_config.configuration.password}) - response = session.put("https://"+self.device_config.configuration.ip_address+'/v1/login', headers=headers, - data=data, verify=False, timeout=5).json() - self.session_key = response["auth_key"] - except (HTTPError, KeyError) as e: - e.args += ("login failed! check password!", ) - raise e - - def _request_data(self, session: Session) -> Dict: +def _update_session_key(session: Session, ip_address: str, password: str) -> str: + try: headers = {'Content-Type': 'application/json', } - data = json.dumps({"auth_key": self.session_key}) - return session.post("https://"+self.device_config.configuration.ip_address + "/v1/user/essinfo/home", - headers=headers, - data=data, - verify=False, - timeout=5).json() - - -COMPONENT_TYPE_TO_MODULE = { - "bat": bat, - "counter": counter, - "inverter": inverter -} - - -def read_legacy(component_type: str, ip: str, password: str, num: Optional[int] = None) -> None: - dev = Device(LG(configuration=LgConfiguration(ip_address=ip, password=password))) - - if os.path.isfile("/var/www/html/openWB/ramdisk/ess_session_key"): - with open("/var/www/html/openWB/ramdisk/ess_session_key", "r") as f: - # erste Zeile ohne Zeilenumbruch lesen - old_session_key = f.readline().strip() - dev.session_key = old_session_key - else: - old_session_key = dev.session_key - - if component_type in COMPONENT_TYPE_TO_MODULE: - component_config = COMPONENT_TYPE_TO_MODULE[component_type].component_descriptor.configuration_factory() - else: - raise Exception( - "illegal component type " + component_type + ". Allowed values: " + - ','.join(COMPONENT_TYPE_TO_MODULE.keys()) - ) - if component_type == "bat" or component_type == "counter": - num = None - component_config.id = num - dev.add_component(component_config) - log.debug('LG ESS V1.0 IP: ' + ip) - log.debug('LG ESS V1.0 password: ' + password) - dev.update() - - if dev.session_key != old_session_key: - with open("/var/www/html/openWB/ramdisk/ess_session_key", "w") as f: - f.write(str(dev.session_key)) - - -def main(argv: List[str]): - run_using_positional_cli_args(read_legacy, argv) + data = json.dumps({"password": password}) + response = session.put(f"https://{ip_address}/v1/login", headers=headers, + data=data, verify=False, timeout=5).json() + return response["auth_key"] + except (HTTPError, KeyError) as e: + e.args += ("login failed! check password!", ) + raise e + + +def _request_data(session: Session, session_key: str, ip_address: str) -> Dict: + headers = {'Content-Type': 'application/json', } + data = json.dumps({"auth_key": session_key}) + return session.post(f"https://{ip_address}/v1/user/essinfo/home", + headers=headers, + data=data, + verify=False, + timeout=5).json() + + +def create_device(device_config: LG): + def create_bat_component(component_config: LgBatSetup): + return LgBat(device_config.id, component_config) + + def create_counter_component(component_config: LgCounterSetup): + return LgCounter(device_config.id, component_config) + + def create_inverter_component(component_config: LgInverterSetup): + return LgInverter(device_config.id, component_config) + + def update_components(components: Iterable[Union[LgBat, LgCounter, LgInverter]]): + nonlocal session_key + session = req.get_http_session() + try: + response = _request_data(session, session_key, device_config.configuration.ip_address) + except HTTPError: + session_key = _update_session_key( + session, device_config.configuration.ip_address, device_config.configuration.password) + response = _request_data(session, session_key, device_config.configuration.ip_address) + + for component in components: + component.update(response) + + session_key = " " + return ConfigurableDevice( + device_config=device_config, + component_factory=ComponentFactoryByType( + bat=create_bat_component, + counter=create_counter_component, + inverter=create_inverter_component, + ), + component_updater=MultiComponentUpdater(update_components) + ) device_descriptor = DeviceDescriptor(configuration_factory=LG) diff --git a/packages/modules/devices/lg/lg_test.py b/packages/modules/devices/lg/lg_test.py index 5bea372f85..58808a0f8a 100644 --- a/packages/modules/devices/lg/lg_test.py +++ b/packages/modules/devices/lg/lg_test.py @@ -3,7 +3,11 @@ from unittest.mock import Mock from modules.common.component_state import BatState, CounterState, InverterState -from modules.devices.lg import bat, counter, device, inverter +from modules.common.configurable_device import ConfigurableDevice +from modules.devices.lg import bat, counter, inverter +from modules.devices.lg import device +from modules.devices.lg.device import create_device + from modules.devices.lg.config import LG, LgConfiguration from test_utils.mock_ramdisk import MockRamdisk @@ -14,9 +18,10 @@ def mock_ramdisk(monkeypatch): @pytest.fixture -def dev() -> device.Device: - dev = device.Device(LG(configuration=LgConfiguration(ip_address=API_URL, password="some password"))) - dev.session_key = "67567d76-0c83-11ea-8a59-d84fb802005a" +def dev(monkeypatch) -> ConfigurableDevice: + dev = create_device(LG(configuration=LgConfiguration(ip_address=API_URL, password="some password"))) + mock_session_key = Mock(return_value="67567d76-0c83-11ea-8a59-d84fb802005a") + monkeypatch.setattr(device, "_update_session_key", mock_session_key) return dev @@ -38,7 +43,7 @@ def assert_inverter_state_correct(state: InverterState): assert state.exported == 200 -def test_valid_login(monkeypatch, dev: device.Device): +def test_valid_login(monkeypatch, dev: ConfigurableDevice): # setup mock_bat_value_store = Mock() monkeypatch.setattr(bat, "get_bat_value_store", Mock(return_value=mock_bat_value_store)) @@ -46,7 +51,7 @@ def test_valid_login(monkeypatch, dev: device.Device): monkeypatch.setattr(counter, "get_counter_value_store", Mock(return_value=mock_counter_value_store)) mock_inverter_value_store = Mock() monkeypatch.setattr(inverter, "get_inverter_value_store", Mock(return_value=mock_inverter_value_store)) - monkeypatch.setattr(device.Device, "_request_data", Mock(return_value=sample_auth_key_valid)) + monkeypatch.setattr(device, "_request_data", Mock(return_value=sample_auth_key_valid)) component_config = bat.component_descriptor.configuration_factory() component_config.id = None dev.add_component(component_config) @@ -66,7 +71,7 @@ def test_valid_login(monkeypatch, dev: device.Device): assert_inverter_state_correct(mock_inverter_value_store.set.call_args[0][0]) -def test_update_session_key(monkeypatch, dev: device.Device): +def test_update_session_key(monkeypatch, dev: ConfigurableDevice): # setup mock_bat_value_store = Mock() monkeypatch.setattr(bat, "get_bat_value_store", Mock(return_value=mock_bat_value_store)) @@ -74,8 +79,7 @@ def test_update_session_key(monkeypatch, dev: device.Device): monkeypatch.setattr(counter, "get_counter_value_store", Mock(return_value=mock_counter_value_store)) mock_inverter_value_store = Mock() monkeypatch.setattr(inverter, "get_inverter_value_store", Mock(return_value=mock_inverter_value_store)) - monkeypatch.setattr(device.Device, "_update_session_key", Mock()) - monkeypatch.setattr(device.Device, "_request_data", Mock( + monkeypatch.setattr(device, "_request_data", Mock( side_effect=[HTTPError, sample_auth_key_valid])) component_config = bat.component_descriptor.configuration_factory() component_config.id = None diff --git a/packages/modules/devices/mqtt/device.py b/packages/modules/devices/mqtt/device.py index dfbe0dc6fd..5bf377ed2b 100644 --- a/packages/modules/devices/mqtt/device.py +++ b/packages/modules/devices/mqtt/device.py @@ -1,55 +1,37 @@ #!/usr/bin/env python3 -from typing import Dict, Union +from typing import Iterable, Union import logging -from dataclass_utils import dataclass_from_dict -from modules.common.abstract_device import AbstractDevice, DeviceDescriptor -from modules.common.component_context import MultiComponentUpdateContext +from modules.common.abstract_device import DeviceDescriptor +from modules.common.configurable_device import ComponentFactoryByType, ConfigurableDevice, MultiComponentUpdater from modules.devices.mqtt import bat, counter, inverter from modules.devices.mqtt.config import Mqtt, MqttBatSetup, MqttCounterSetup, MqttInverterSetup log = logging.getLogger(__name__) -class Device(AbstractDevice): - COMPONENT_TYPE_TO_CLASS = { - "bat": bat.MqttBat, - "counter": counter.MqttCounter, - "inverter": inverter.MqttInverter - } - COMPONENT_TYPE_TO_MODULE = { - "bat": bat, - "counter": counter, - "inverter": inverter - } - - def __init__(self, device_config: Union[Dict, Mqtt]) -> None: - self.components = {} - try: - self.device_config = dataclass_from_dict(Mqtt, device_config) - except Exception: - log.exception("Fehler im Modul " + self.device_config.name) - - def add_component(self, component_config: Union[Dict, MqttBatSetup, MqttCounterSetup, MqttInverterSetup]) -> None: - if isinstance(component_config, Dict): - component_type = component_config["type"] - else: - component_type = component_config.type - component_config = dataclass_from_dict(self.COMPONENT_TYPE_TO_MODULE[ - component_type].component_descriptor.configuration_factory, component_config) - if component_type in self.COMPONENT_TYPE_TO_CLASS: - self.components["component"+str(component_config.id) - ] = (self.COMPONENT_TYPE_TO_CLASS[component_type](component_config)) - - def update(self) -> None: - if self.components: - with MultiComponentUpdateContext(self.components): - log.debug("MQTT-Module müssen nicht ausgelesen werden.") - else: - log.warning( - self.device_config.name + - ": Es konnten keine Werte gelesen werden, da noch keine Komponenten konfiguriert wurden." - ) +def create_device(device_config: Mqtt): + def create_bat_component(component_config: MqttBatSetup): + return bat.MqttBat(component_config) + + def create_counter_component(component_config: MqttCounterSetup): + return counter.MqttCounter(component_config) + + def create_inverter_component(component_config: MqttInverterSetup): + return inverter.MqttInverter(component_config) + + def update_components(components: Iterable[Union[bat.MqttBat, counter.MqttCounter, inverter.MqttInverter]]): + log.debug("MQTT-Module müssen nicht ausgelesen werden.") + + return ConfigurableDevice( + device_config=device_config, + component_factory=ComponentFactoryByType( + bat=create_bat_component, + counter=create_counter_component, + inverter=create_inverter_component, + ), + component_updater=MultiComponentUpdater(update_components) + ) device_descriptor = DeviceDescriptor(configuration_factory=Mqtt) diff --git a/packages/modules/devices/mtec/bat.py b/packages/modules/devices/mtec/bat.py new file mode 100644 index 0000000000..35caf4ccc7 --- /dev/null +++ b/packages/modules/devices/mtec/bat.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 +import logging +from dataclass_utils import dataclass_from_dict +from modules.common.component_state import BatState +from modules.common.component_type import ComponentDescriptor +from modules.common.fault_state import ComponentInfo, FaultState +from modules.common.modbus import ModbusDataType, ModbusTcpClient_ +from modules.common.simcount import SimCounter +from modules.common.store import get_bat_value_store +from modules.devices.mtec.config import MTecBatSetup + +log = logging.getLogger(__name__) + + +class MTecBat: + def __init__(self, device_id: int, component_config: MTecBatSetup) -> None: + self.component_config = dataclass_from_dict(MTecBatSetup, component_config) + self.store = get_bat_value_store(self.component_config.id) + self.fault_state = FaultState(ComponentInfo.from_component_config(self.component_config)) + self.__device_id = device_id + self.sim_counter = SimCounter(self.__device_id, self.component_config.id, prefix="speicher") + + def update(self, client: ModbusTcpClient_, generation: int) -> None: + unit = self.component_config.configuration.modbus_id + generation = self.component_config.configuration.generation + + if generation == 2: + power = client.read_holding_registers(40258, ModbusDataType.INT_32, unit=unit) * -1 + # soc unit 0.01% + soc = client.read_holding_registers(43000, ModbusDataType.UINT_16, unit=unit) / 100 + else: + power = client.read_holding_registers(30258, ModbusDataType.INT_32, unit=unit) * -1 + soc = client.read_holding_registers(33000, ModbusDataType.UINT_16, unit=unit) / 100 + imported, exported = self.sim_counter.sim_count(power) + + bat_state = BatState( + power=power, + soc=soc, + imported=imported, + exported=exported + ) + self.store.set(bat_state) + + +component_descriptor = ComponentDescriptor(configuration_factory=MTecBatSetup) diff --git a/packages/modules/devices/mtec/config.py b/packages/modules/devices/mtec/config.py new file mode 100644 index 0000000000..d8c693d0a1 --- /dev/null +++ b/packages/modules/devices/mtec/config.py @@ -0,0 +1,73 @@ +from typing import Optional +from helpermodules.auto_str import auto_str + +from modules.common.component_setup import ComponentSetup + + +class MTecConfiguration: + def __init__(self, + ip_address: Optional[str] = None, + port: int = 502): + self.ip_address = ip_address + self.port = port + + +class MTec: + def __init__(self, + name: str = "M-Tec", + type: str = "mtec", + id: int = 0, + configuration: MTecConfiguration = None) -> None: + self.name = name + self.type = type + self.id = id + self.configuration = configuration or MTecConfiguration() + + +@auto_str +class MTecBatConfiguration: + def __init__(self, modbus_id: int = 247, generation: int = 2): + self.modbus_id = modbus_id + self.generation = generation + + +@auto_str +class MTecBatSetup(ComponentSetup[MTecBatConfiguration]): + def __init__(self, + name: str = "M-Tec Speicher", + type: str = "bat", + id: int = 0, + configuration: MTecBatConfiguration = None) -> None: + super().__init__(name, type, id, configuration or MTecBatConfiguration()) + + +@auto_str +class MTecCounterConfiguration: + def __init__(self, modbus_id: int = 247): + self.modbus_id = modbus_id + + +@auto_str +class MTecCounterSetup(ComponentSetup[MTecCounterConfiguration]): + def __init__(self, + name: str = "M-Tec Zähler", + type: str = "counter", + id: int = 0, + configuration: MTecCounterConfiguration = None) -> None: + super().__init__(name, type, id, configuration or MTecCounterConfiguration()) + + +@auto_str +class MTecInverterConfiguration: + def __init__(self, modbus_id: int = 247): + self.modbus_id = modbus_id + + +@auto_str +class MTecInverterSetup(ComponentSetup[MTecInverterConfiguration]): + def __init__(self, + name: str = "M-Tec Wechselrichter", + type: str = "inverter", + id: int = 0, + configuration: MTecInverterConfiguration = None) -> None: + super().__init__(name, type, id, configuration or MTecInverterConfiguration()) diff --git a/packages/modules/devices/mtec/counter.py b/packages/modules/devices/mtec/counter.py new file mode 100644 index 0000000000..c711d8ef82 --- /dev/null +++ b/packages/modules/devices/mtec/counter.py @@ -0,0 +1,34 @@ +#!/usr/bin/env python3 +from dataclass_utils import dataclass_from_dict +from modules.common.component_state import CounterState +from modules.common.component_type import ComponentDescriptor +from modules.common.fault_state import ComponentInfo, FaultState +from modules.common.modbus import ModbusDataType, ModbusTcpClient_ +from modules.common.simcount import SimCounter +from modules.common.store import get_counter_value_store +from modules.devices.mtec.config import MTecCounterSetup + + +class MTecCounter: + def __init__(self, device_id: int, component_config: MTecCounterSetup) -> None: + self.component_config = dataclass_from_dict(MTecCounterSetup, component_config) + self.store = get_counter_value_store(self.component_config.id) + self.fault_state = FaultState(ComponentInfo.from_component_config(self.component_config)) + self.__device_id = device_id + self.sim_counter = SimCounter(self.__device_id, self.component_config.id, prefix="bezug") + + def update(self, client: ModbusTcpClient_): + unit = self.component_config.configuration.modbus_id + + power = client.read_holding_registers(11000, ModbusDataType.INT_32, unit=unit) + imported, exported = self.sim_counter.sim_count(power) + + counter_state = CounterState( + imported=imported, + exported=exported, + power=power + ) + self.store.set(counter_state) + + +component_descriptor = ComponentDescriptor(configuration_factory=MTecCounterSetup) diff --git a/packages/modules/devices/mtec/device.py b/packages/modules/devices/mtec/device.py new file mode 100644 index 0000000000..3a68c9460a --- /dev/null +++ b/packages/modules/devices/mtec/device.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python3 +import logging +from typing import Iterable, Union + +from modules.common.abstract_device import DeviceDescriptor +from modules.common.component_context import SingleComponentUpdateContext +from modules.common.configurable_device import ConfigurableDevice, ComponentFactoryByType, MultiComponentUpdater +from modules.common.modbus import ModbusTcpClient_ +from modules.devices.mtec.bat import MTecBat +from modules.devices.mtec.counter import MTecCounter +from modules.devices.mtec.inverter import MTecInverter +from modules.devices.mtec.config import MTec, MTecBatSetup, MTecCounterSetup, MTecInverterSetup + +log = logging.getLogger(__name__) + + +def create_device(device_config: MTec): + def create_bat_component(component_config: MTecBatSetup): + return MTecBat(device_config.id, component_config) + + def create_counter_component(component_config: MTecCounterSetup): + return MTecCounter(device_config.id, component_config) + + def create_inverter_component(component_config: MTecInverterSetup): + return MTecInverter(device_config.id, component_config) + + def update_components(components: Iterable[Union[MTecBat, MTecCounter, MTecInverter]]): + with client as c: + for component in components: + with SingleComponentUpdateContext(component.fault_state): + component.update(c) + + try: + client = ModbusTcpClient_(device_config.configuration.ip_address, device_config.configuration.port) + except Exception: + log.exception("Fehler in create_device") + return ConfigurableDevice( + device_config=device_config, + component_factory=ComponentFactoryByType( + bat=create_bat_component, + counter=create_counter_component, + inverter=create_inverter_component, + ), + component_updater=MultiComponentUpdater(update_components) + ) + + +device_descriptor = DeviceDescriptor(configuration_factory=MTec) diff --git a/packages/modules/devices/mtec/inverter.py b/packages/modules/devices/mtec/inverter.py new file mode 100644 index 0000000000..57fb56f136 --- /dev/null +++ b/packages/modules/devices/mtec/inverter.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python3 +from typing import Dict, Union + +from dataclass_utils import dataclass_from_dict +from modules.common.component_state import InverterState +from modules.common.component_type import ComponentDescriptor +from modules.common.fault_state import ComponentInfo, FaultState +from modules.common.modbus import ModbusDataType, ModbusTcpClient_ +from modules.common.simcount import SimCounter +from modules.common.store import get_inverter_value_store +from modules.devices.mtec.config import MTecInverterSetup + + +class MTecInverter: + def __init__(self, device_id: int, component_config: Union[Dict, MTecInverterSetup]) -> None: + self.component_config = dataclass_from_dict(MTecInverterSetup, component_config) + self.store = get_inverter_value_store(self.component_config.id) + self.fault_state = FaultState(ComponentInfo.from_component_config(self.component_config)) + self.__device_id = device_id + self.sim_counter = SimCounter(self.__device_id, self.component_config.id, prefix="pv") + + def update(self, client: ModbusTcpClient_) -> None: + unit = self.component_config.configuration.modbus_id + + power = client.read_holding_registers(11028, ModbusDataType.UINT_32, unit=unit) * -1 + _, exported = self.sim_counter.sim_count(power) + + inverter_state = InverterState( + power=power, + exported=exported, + ) + self.store.set(inverter_state) + + +component_descriptor = ComponentDescriptor(configuration_factory=MTecInverterSetup) diff --git a/packages/modules/devices/opendtu/config.py b/packages/modules/devices/opendtu/config.py index 73602c42ae..84a9f82fad 100644 --- a/packages/modules/devices/opendtu/config.py +++ b/packages/modules/devices/opendtu/config.py @@ -13,7 +13,7 @@ def __init__(self, url: Optional[str] = None): @auto_str class OpenDTU(Json): def __init__(self, - name: str = "OpenDTU", + name: str = "Hoymiles über openDTU", type: str = "opendtu", id: int = 0, configuration: OpenDTUConfiguration = None) -> None: @@ -29,7 +29,7 @@ def __init__(self): @auto_str class OpenDTUInverterSetup(JsonInverterSetup): def __init__(self, - name: str = "Hoymiles Wechselrichter", + name: str = "Hoymiles Wechselrichter über openDTU", type: str = "inverter", id: int = 0, configuration: JsonInverterConfiguration = None) -> None: diff --git a/packages/modules/devices/openwb_evu_kit/counter.py b/packages/modules/devices/openwb_evu_kit/counter.py index 394989dc92..37414ad649 100644 --- a/packages/modules/devices/openwb_evu_kit/counter.py +++ b/packages/modules/devices/openwb_evu_kit/counter.py @@ -22,6 +22,8 @@ def __init__(self, id = 2 elif version == 2: id = 115 + elif version == 3: + id = 105 else: raise ValueError("Version " + str(version) + " unbekannt.") diff --git a/packages/modules/devices/openwb_flex/config.py b/packages/modules/devices/openwb_flex/config.py index 0294b1854e..b377b10c8b 100644 --- a/packages/modules/devices/openwb_flex/config.py +++ b/packages/modules/devices/openwb_flex/config.py @@ -62,7 +62,7 @@ def __init__(self, id: int = 115, type: str = "sdm630"): class ConsumptionCounterFlexSetup(ComponentSetup[ConsumptionCounterFlexConfiguration]): def __init__(self, - name: str = "openWB Verbrauchszähler flex", + name: str = "openWB Bezugszähler ohne Einspeisung flex", type: str = "consumption_counter", id: int = 0, configuration: ConsumptionCounterFlexConfiguration = None) -> None: diff --git a/packages/modules/devices/openwb_flex/counter.py b/packages/modules/devices/openwb_flex/counter.py index 021b822def..9ede1cd642 100644 --- a/packages/modules/devices/openwb_flex/counter.py +++ b/packages/modules/devices/openwb_flex/counter.py @@ -8,6 +8,7 @@ from modules.common.fault_state import ComponentInfo, FaultState from modules.common.lovato import Lovato from modules.common.mpm3pm import Mpm3pm +from modules.common.b23 import B23 from modules.common.simcount import SimCounter from modules.common.store import get_counter_value_store from modules.devices.openwb_flex.config import EvuKitFlexSetup @@ -39,13 +40,13 @@ def update(self): frequency = self.__client.get_frequency() power_factors = self.__client.get_power_factors() - if isinstance(self.__client, Mpm3pm): + if isinstance(self.__client, Mpm3pm or B23): imported = self.__client.get_imported() exported = self.__client.get_exported() else: currents = self.__client.get_currents() - if isinstance(self.__client, Mpm3pm): + if isinstance(self.__client, Mpm3pm or B23): currents = [powers[i] / voltages[i] for i in range(3)] else: if isinstance(self.__client, Lovato): diff --git a/packages/modules/devices/openwb_flex/versions.py b/packages/modules/devices/openwb_flex/versions.py index f5a62ce8b0..3e532b7657 100644 --- a/packages/modules/devices/openwb_flex/versions.py +++ b/packages/modules/devices/openwb_flex/versions.py @@ -6,13 +6,15 @@ def kit_counter_version_factory( - version: int) -> Type[Union[mpm3pm.Mpm3pm, lovato.Lovato, sdm.Sdm630]]: + version: int) -> Type[Union[mpm3pm.Mpm3pm, lovato.Lovato, sdm.Sdm630, b23.B23]]: if version == 0: return mpm3pm.Mpm3pm elif version == 1: return lovato.Lovato elif version == 2: return sdm.Sdm630 + elif version == 3: + return b23.B23 else: raise ValueError("Version "+str(version) + " unbekannt.") diff --git a/packages/modules/devices/powerdog/device.py b/packages/modules/devices/powerdog/device.py index a0a1935814..ee027fd814 100644 --- a/packages/modules/devices/powerdog/device.py +++ b/packages/modules/devices/powerdog/device.py @@ -1,112 +1,64 @@ #!/usr/bin/env python3 import logging -from typing import Dict, Union, Optional, List +from typing import Iterable, Union -from dataclass_utils import dataclass_from_dict -from helpermodules.cli import run_using_positional_cli_args from modules.common import modbus -from modules.common.abstract_device import AbstractDevice, DeviceDescriptor -from modules.common.component_context import MultiComponentUpdateContext, SingleComponentUpdateContext -from modules.devices.powerdog import counter -from modules.devices.powerdog import inverter -from modules.devices.powerdog.config import Powerdog, PowerdogConfiguration, PowerdogCounterSetup, PowerdogInverterSetup +from modules.common.abstract_device import DeviceDescriptor +from modules.common.component_context import SingleComponentUpdateContext +from modules.common.configurable_device import ComponentFactoryByType, ConfigurableDevice, MultiComponentUpdater +from modules.devices.powerdog.config import Powerdog, PowerdogCounterSetup, PowerdogInverterSetup +from modules.devices.powerdog.counter import PowerdogCounter +from modules.devices.powerdog.inverter import PowerdogInverter log = logging.getLogger(__name__) -class Device(AbstractDevice): - COMPONENT_TYPE_TO_CLASS = { - "counter": counter.PowerdogCounter, - "inverter": inverter.PowerdogInverter - } +def create_device(device_config: Powerdog): + def create_counter_component(component_config: PowerdogCounterSetup): + return PowerdogCounter(device_config.id, component_config, client, device_config.configuration.modbus_id) - def __init__(self, device_config: Union[Dict, Powerdog]) -> None: - self.components = {} # type: Dict[str, Union[counter.PowerdogCounter, inverter.PowerdogInverter]] - try: - self.device_config = dataclass_from_dict(Powerdog, device_config) - self.client = modbus.ModbusTcpClient_( - self.device_config.configuration.ip_address, self.device_config.configuration.port) - except Exception: - log.exception("Fehler im Modul "+self.device_config.name) + def create_inverter_component(component_config: PowerdogInverterSetup): + return PowerdogInverter(device_config.id, component_config, client, device_config.configuration.modbus_id) - def add_component(self, component_config: Union[Dict, PowerdogCounterSetup, PowerdogInverterSetup]) -> None: - if isinstance(component_config, Dict): - component_type = component_config["type"] - else: - component_type = component_config.type - component_config = dataclass_from_dict(COMPONENT_TYPE_TO_MODULE[ - component_type].component_descriptor.configuration_factory, component_config) - if component_type in self.COMPONENT_TYPE_TO_CLASS: - self.components["component"+str(component_config.id)] = ( - self.COMPONENT_TYPE_TO_CLASS[component_type]( - self.device_config.id, component_config, self.client, - self.device_config.configuration.modbus_id)) - else: - raise Exception( - "illegal component type " + component_type + ". Allowed values: " + - ','.join(self.COMPONENT_TYPE_TO_CLASS.keys()) - ) - - def update(self) -> None: - log.debug("Start device reading " + str(self.components)) - with MultiComponentUpdateContext(self.components): - if len(self.components) == 1: - for component in self.components: - if isinstance(self.components[component], inverter.PowerdogInverter): - with SingleComponentUpdateContext(self.components[component].fault_state): - self.components[component].update() + def update_components(components: Iterable[Union[PowerdogCounter, PowerdogInverter]]): + with client: + if len(components) == 1: + for component in components: + if isinstance(components[component], PowerdogInverter): + with SingleComponentUpdateContext(components[component].fault_state): + components[component].update() else: raise Exception( "Wenn ein EVU-Zähler konfiguriert wurde, muss immer auch ein WR konfiguriert sein.") - elif len(self.components) == 2: - for component in self.components: - if isinstance(self.components[component], inverter.PowerdogInverter): - inverter_power = self.components[component].update() + elif len(components) == 2: + for component in components: + if isinstance(components[component], PowerdogInverter): + inverter_power = components[component].update() break else: inverter_power = 0 - for component in self.components: - if isinstance(self.components[component], counter.PowerdogCounter): - self.components[component].update(inverter_power) + for component in components: + if isinstance(components[component], PowerdogCounter): + components[component].update(inverter_power) else: log.warning( - self.device_config.name + + device_config.name + ": Es konnten keine Werte gelesen werden, da noch keine oder zu viele Komponenten konfiguriert " + "wurden." ) - -COMPONENT_TYPE_TO_MODULE = { - "counter": counter, - "inverter": inverter -} - - -def read_legacy(component_type: str, ip_address: str, num: Optional[int] = None) -> None: - dev = Device(Powerdog(configuration=PowerdogConfiguration(ip_address=ip_address))) - if component_type in COMPONENT_TYPE_TO_MODULE: - component_config = COMPONENT_TYPE_TO_MODULE[component_type].component_descriptor.configuration_factory() - else: - raise Exception( - "illegal component type " + component_type + ". Allowed values: " + - ','.join(COMPONENT_TYPE_TO_MODULE.keys()) - ) - component_config.id = num - dev.add_component(component_config) - - # Wenn der EVU-Zähler ausgelesen werden soll, wird auch noch der Inverter benötigt. - if component_type in COMPONENT_TYPE_TO_MODULE and component_type == "counter": - inverter_config = PowerdogInverterSetup() - inverter_config.id = 1 - dev.add_component(inverter_config) - - log.debug('Powerdog IP-Adresse: ' + ip_address) - - dev.update() - - -def main(argv: List[str]): - run_using_positional_cli_args(read_legacy, argv) + try: + client = modbus.ModbusTcpClient_(device_config.configuration.ip_address, device_config.configuration.port) + except Exception: + log.exception("Fehler in create_device") + return ConfigurableDevice( + device_config=device_config, + component_factory=ComponentFactoryByType( + counter=create_counter_component, + inverter=create_inverter_component, + ), + component_updater=MultiComponentUpdater(update_components) + ) device_descriptor = DeviceDescriptor(configuration_factory=Powerdog) diff --git a/packages/modules/devices/qcells/device.py b/packages/modules/devices/qcells/device.py index 45efcf8f51..b2abe736c5 100644 --- a/packages/modules/devices/qcells/device.py +++ b/packages/modules/devices/qcells/device.py @@ -28,10 +28,15 @@ def create_inverter_component(component_config: QCellsInverterSetup): device_config.configuration.modbus_id) def update_components(components: Iterable[Union[QCellsBat, QCellsCounter, QCellsInverter]]): - with client as c: + if client.is_socket_open() is False: + client.connect() + try: for component in components: with SingleComponentUpdateContext(component.fault_state): - component.update(c) + component.update(client) + except Exception as e: + client.close() + raise e try: client = ModbusTcpClient_(device_config.configuration.ip_address, device_config.configuration.port) diff --git a/packages/modules/devices/saxpower/device.py b/packages/modules/devices/saxpower/device.py index 12e59c1734..727be7ed67 100644 --- a/packages/modules/devices/saxpower/device.py +++ b/packages/modules/devices/saxpower/device.py @@ -1,88 +1,38 @@ #!/usr/bin/env python3 import logging -from typing import Dict, List, Union +from typing import Iterable -from dataclass_utils import dataclass_from_dict -from helpermodules.cli import run_using_positional_cli_args from modules.common import modbus -from modules.common.abstract_device import AbstractDevice, DeviceDescriptor +from modules.common.abstract_device import DeviceDescriptor from modules.common.component_context import SingleComponentUpdateContext -from modules.devices.saxpower import bat +from modules.common.configurable_device import ComponentFactoryByType, ConfigurableDevice, MultiComponentUpdater +from modules.devices.saxpower.bat import SaxpowerBat from modules.devices.saxpower.config import Saxpower, SaxpowerBatSetup log = logging.getLogger(__name__) -class Device(AbstractDevice): - COMPONENT_TYPE_TO_CLASS = { - "bat": bat.SaxpowerBat - } - - def __init__(self, device_config: Union[Dict, Saxpower]) -> None: - self.components = {} # type: Dict[str, bat.SaxpowerBat] - try: - self.device_config = dataclass_from_dict(Saxpower, device_config) - self.client = modbus.ModbusTcpClient_( - self.device_config.configuration.ip_address, self.device_config.configuration.port) - except Exception: - log.exception("Fehler im Modul "+self.device_config.name) - - def add_component(self, component_config: Union[Dict, SaxpowerBatSetup]) -> None: - if isinstance(component_config, Dict): - component_type = component_config["type"] - else: - component_type = component_config.type - component_config = dataclass_from_dict(COMPONENT_TYPE_TO_MODULE[ - component_type].component_descriptor.configuration_factory, component_config) - if component_type in self.COMPONENT_TYPE_TO_CLASS: - self.components["component"+str(component_config.id)] = (self.COMPONENT_TYPE_TO_CLASS[component_type]( - self.device_config.id, component_config, self.client, self.device_config.configuration.modbus_id)) - else: - raise Exception( - "illegal component type " + component_type + ". Allowed values: " + - ','.join(self.COMPONENT_TYPE_TO_CLASS.keys()) - ) - - def update(self) -> None: - log.debug("Start device reading " + str(self.components)) - if self.components: - for component in self.components: - # Auch wenn bei einer Komponente ein Fehler auftritt, sollen alle anderen noch ausgelesen werden. - with SingleComponentUpdateContext(self.components[component].fault_state): - self.components[component].update() - else: - log.warning( - self.device_config.name + - ": Es konnten keine Werte gelesen werden, da noch keine Komponenten konfiguriert wurden." - ) - - -COMPONENT_TYPE_TO_MODULE = { - "bat": bat -} - - -def read_legacy(component_type: str, ip_address: str) -> None: - device_config = Saxpower() - device_config.configuration.ip_address = ip_address - dev = Device(device_config) - if component_type in COMPONENT_TYPE_TO_MODULE: - component_config = COMPONENT_TYPE_TO_MODULE[component_type].component_descriptor.configuration_factory() - else: - raise Exception( - "illegal component type " + component_type + ". Allowed values: " + - ','.join(COMPONENT_TYPE_TO_MODULE.keys()) - ) - component_config.id = None - dev.add_component(component_config) - - log.debug('Saxpower IP-Adresse: ' + ip_address) - - dev.update() - - -def main(argv: List[str]): - run_using_positional_cli_args(read_legacy, argv) +def create_device(device_config: Saxpower): + def create_bat_component(component_config: SaxpowerBatSetup): + return SaxpowerBat(device_config.id, component_config, client, device_config.configuration.modbus_id) + + def update_components(components: Iterable[SaxpowerBat]): + with client: + for component in components: + with SingleComponentUpdateContext(component.fault_state): + component.update() + + try: + client = modbus.ModbusTcpClient_(device_config.configuration.ip_address, device_config.configuration.port) + except Exception: + log.exception("Fehler in create_device") + return ConfigurableDevice( + device_config=device_config, + component_factory=ComponentFactoryByType( + bat=create_bat_component, + ), + component_updater=MultiComponentUpdater(update_components) + ) device_descriptor = DeviceDescriptor(configuration_factory=Saxpower) diff --git a/packages/modules/devices/shelly/bat.py b/packages/modules/devices/shelly/bat.py new file mode 100644 index 0000000000..5372b30cf5 --- /dev/null +++ b/packages/modules/devices/shelly/bat.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python3 +import logging +from typing import Optional +from modules.common import req +from modules.common.component_state import BatState +from modules.common.component_type import ComponentDescriptor +from modules.common.fault_state import ComponentInfo, FaultState +from modules.common.store import get_bat_value_store +from modules.common.simcount._simcounter import SimCounter +from modules.devices.shelly.config import ShellyBatSetup + +log = logging.getLogger(__name__) + + +class ShellyBat: + + def __init__(self, + device_id: int, + component_config: ShellyBatSetup, + address: str, + generation: Optional[int]) -> None: + self.component_config = component_config + self.sim_counter = SimCounter(device_id, self.component_config.id, prefix="speicher") + self.store = get_bat_value_store(self.component_config.id) + self.fault_state = FaultState(ComponentInfo.from_component_config(self.component_config)) + self.address = address + self.generation = generation + + def total_power_from_shelly(self) -> int: + total = 0 + if self.generation == 1: + status_url = "http://" + self.address + "/status" + else: + status_url = "http://" + self.address + "/rpc/Shelly.GetStatus" + status = req.get_http_session().get(status_url, timeout=3).json() + try: + if self.generation == 1: + meters = status['emeters'] # shelly3EM + for meter in meters: + total = total + meter['power'] + else: + total = status['em:0']['total_act_power'] # shelly Pro3EM + except KeyError: + log.exception("unsupported shelly device?") + finally: + return int(total) + + def update(self) -> None: + bat = self.total_power_from_shelly() * -1 + imported, exported = self.sim_counter.sim_count(bat) + bat_state = BatState( + power=bat, + imported=imported, + exported=exported + ) + self.store.set(bat_state) + + +component_descriptor = ComponentDescriptor(configuration_factory=ShellyBatSetup) diff --git a/packages/modules/devices/shelly/config.py b/packages/modules/devices/shelly/config.py index a831ee9403..db5005aba3 100644 --- a/packages/modules/devices/shelly/config.py +++ b/packages/modules/devices/shelly/config.py @@ -23,6 +23,22 @@ def __init__(self, self.configuration = configuration or ShellyConfiguration() +@auto_str +class ShellyCounterConfiguration: + def __init__(self) -> None: + pass + + +@auto_str +class ShellyCounterSetup(ComponentSetup[ShellyCounterConfiguration]): + def __init__(self, + name: str = "Shelly Zähler", + type: str = "counter", + id: int = 0, + configuration: ShellyCounterConfiguration = None) -> None: + super().__init__(name, type, id, configuration or ShellyCounterConfiguration()) + + @auto_str class ShellyInverterConfiguration: def __init__(self) -> None: @@ -37,3 +53,19 @@ def __init__(self, id: int = 0, configuration: ShellyInverterConfiguration = None) -> None: super().__init__(name, type, id, configuration or ShellyInverterConfiguration()) + + +@auto_str +class ShellyBatConfiguration: + def __init__(self) -> None: + pass + + +@auto_str +class ShellyBatSetup(ComponentSetup[ShellyBatConfiguration]): + def __init__(self, + name: str = "Shelly Speicher", + type: str = "bat", + id: int = 0, + configuration: ShellyBatConfiguration = None) -> None: + super().__init__(name, type, id, configuration or ShellyBatConfiguration()) diff --git a/packages/modules/devices/shelly/counter.py b/packages/modules/devices/shelly/counter.py new file mode 100644 index 0000000000..f348fb0917 --- /dev/null +++ b/packages/modules/devices/shelly/counter.py @@ -0,0 +1,72 @@ +#!/usr/bin/env python3 +import logging +from typing import Optional +from modules.common import req +from modules.common.component_state import CounterState +from modules.common.component_type import ComponentDescriptor +from modules.common.fault_state import ComponentInfo, FaultState +from modules.common.store import get_counter_value_store +from modules.common.simcount._simcounter import SimCounter +from modules.devices.shelly.config import ShellyCounterSetup + +log = logging.getLogger(__name__) + + +class ShellyCounter: + + def __init__(self, + device_id: int, + component_config: ShellyCounterSetup, + address: str, + generation: Optional[int]) -> None: + self.component_config = component_config + self.sim_counter = SimCounter(device_id, self.component_config.id, prefix="bezug") + self.store = get_counter_value_store(self.component_config.id) + self.fault_state = FaultState(ComponentInfo.from_component_config(self.component_config)) + self.address = address + self.generation = generation + + def update(self) -> None: + power = 0 + if self.generation == 1: + status_url = "http://" + self.address + "/status" + else: + status_url = "http://" + self.address + "/rpc/Shelly.GetStatus" + status = req.get_http_session().get(status_url, timeout=3).json() + try: + if self.generation == 1: # shelly3EM + meters = status['emeters'] + # shelly3EM has three meters: + for meter in meters: + power = power + meter['power'] + power = power * -1 + + voltages = [status['emeters'][i]['voltage'] for i in range(0, 3)] + currents = [status['emeters'][i]['current'] for i in range(0, 3)] + powers = [status['emeters'][i]['power'] for i in range(0, 3)] + power_factors = [status['emeters'][i]['pf'] for i in range(0, 3)] + imported, exported = self.sim_counter.sim_count(power) + else: + # shelly Pro3EM + voltages = [status['em:0'][f'{i}_voltage'] for i in 'abc'] + currents = [status['em:0'][f'{i}_current'] for i in 'abc'] + powers = [status['em:0'][f'{i}_act_power'] for i in 'abc'] + power_factors = [status['em:0'][f'{i}_pf'] for i in 'abc'] + power = status['em:0']['total_act_power'] * -1 + imported, exported = self.sim_counter.sim_count(power) + + counter_state = CounterState( + voltages=voltages, + currents=currents, + powers=powers, + power_factors=power_factors, + imported=imported, + exported=exported, + power=power + ) + self.store.set(counter_state) + except KeyError: + log.exception("unsupported shelly device?") + + +component_descriptor = ComponentDescriptor(configuration_factory=ShellyCounterSetup) diff --git a/packages/modules/devices/shelly/device.py b/packages/modules/devices/shelly/device.py index 04be22c494..b2639df22f 100644 --- a/packages/modules/devices/shelly/device.py +++ b/packages/modules/devices/shelly/device.py @@ -8,8 +8,12 @@ from modules.common.abstract_device import DeviceDescriptor from modules.common.configurable_device import (ConfigurableDevice, ComponentFactoryByType, IndependentComponentUpdater) from modules.devices.shelly.inverter import ShellyInverter +from modules.devices.shelly.bat import ShellyBat +from modules.devices.shelly.counter import ShellyCounter from modules.devices.shelly.config import Shelly, ShellyConfiguration from modules.devices.shelly.config import ShellyInverterSetup, ShellyInverterConfiguration +from modules.devices.shelly.config import ShellyBatSetup +from modules.devices.shelly.config import ShellyCounterSetup log = logging.getLogger(__name__) @@ -25,17 +29,27 @@ def get_device_generation(address: str) -> int: def create_device(device_config: Shelly) -> ConfigurableDevice: + def create_counter_component(component_config: ShellyCounterSetup) -> ShellyCounter: + return ShellyCounter(device_config.id, component_config, device_config.configuration.ip_address, + device_config.configuration.generation) + def create_inverter_component(component_config: ShellyInverterSetup) -> ShellyInverter: return ShellyInverter(device_config.id, component_config, device_config.configuration.ip_address, device_config.configuration.generation) + def create_bat_component(component_config: ShellyBatSetup) -> ShellyBat: + return ShellyBat(device_config.id, component_config, device_config.configuration.ip_address, + device_config.configuration.generation) + if device_config.configuration.generation is None and device_config.configuration.ip_address is not None: device_config.configuration.generation = get_device_generation(device_config.configuration.ip_address) return ConfigurableDevice( device_config=device_config, component_factory=ComponentFactoryByType( - inverter=create_inverter_component + counter=create_counter_component, + inverter=create_inverter_component, + bat=create_bat_component ), component_updater=IndependentComponentUpdater(lambda component: component.update()) ) diff --git a/packages/modules/devices/siemens/device.py b/packages/modules/devices/siemens/device.py index 03f4ca2b4a..720c87dbe6 100644 --- a/packages/modules/devices/siemens/device.py +++ b/packages/modules/devices/siemens/device.py @@ -1,99 +1,51 @@ #!/usr/bin/env python3 import logging -from typing import Dict, Union, Optional, List +from typing import Iterable, Union -from dataclass_utils import dataclass_from_dict -from helpermodules.cli import run_using_positional_cli_args from modules.common import modbus -from modules.common.abstract_device import AbstractDevice, DeviceDescriptor +from modules.common.abstract_device import DeviceDescriptor from modules.common.component_context import SingleComponentUpdateContext -from modules.devices.siemens import bat, counter, inverter +from modules.common.configurable_device import ComponentFactoryByType, ConfigurableDevice, MultiComponentUpdater +from modules.devices.siemens.bat import SiemensBat from modules.devices.siemens.config import Siemens, SiemensBatSetup, SiemensCounterSetup, SiemensInverterSetup +from modules.devices.siemens.counter import SiemensCounter +from modules.devices.siemens.inverter import SiemensInverter log = logging.getLogger(__name__) -siemens_component_classes = Union[bat.SiemensBat, counter.SiemensCounter, inverter.SiemensInverter] +siemens_component_classes = Union[SiemensBat, SiemensCounter, SiemensInverter] -class Device(AbstractDevice): - COMPONENT_TYPE_TO_CLASS = { - "bat": bat.SiemensBat, - "counter": counter.SiemensCounter, - "inverter": inverter.SiemensInverter - } +def create_device(device_config: Siemens): + def create_bat_component(component_config: SiemensBatSetup): + return SiemensBat(device_config.id, component_config, client, device_config.configuration.modbus_id) - def __init__(self, device_config: Union[Dict, Siemens]) -> None: - self.components = {} # type: Dict[str, siemens_component_classes] - try: - self.device_config = dataclass_from_dict(Siemens, device_config) - self.client = modbus.ModbusTcpClient_( - self.device_config.configuration.ip_address, self.device_config.configuration.port) - except Exception: - log.exception("Fehler im Modul "+self.device_config.name) + def create_counter_component(component_config: SiemensCounterSetup): + return SiemensCounter(device_config.id, component_config, client, device_config.configuration.modbus_id) - def add_component(self, component_config: Union[Dict, - SiemensBatSetup, - SiemensCounterSetup, - SiemensInverterSetup]) -> None: - if isinstance(component_config, Dict): - component_type = component_config["type"] - else: - component_type = component_config.type - component_config = dataclass_from_dict(COMPONENT_TYPE_TO_MODULE[ - component_type].component_descriptor.configuration_factory, component_config) - if component_type in self.COMPONENT_TYPE_TO_CLASS: - self.components["component"+str(component_config.id)] = (self.COMPONENT_TYPE_TO_CLASS[component_type]( - self.device_config.id, component_config, self.client, - self.device_config.configuration.modbus_id)) - else: - raise Exception( - "illegal component type " + component_type + ". Allowed values: " + - ','.join(self.COMPONENT_TYPE_TO_CLASS.keys()) - ) + def create_inverter_component(component_config: SiemensInverterSetup): + return SiemensInverter(device_config.id, component_config, client, device_config.configuration.modbus_id) - def update(self) -> None: - log.debug("Start device reading" + str(self.components)) - if self.components: - for component in self.components: - # Auch wenn bei einer Komponente ein Fehler auftritt, sollen alle anderen noch ausgelesen werden. - with SingleComponentUpdateContext(self.components[component].fault_state): - self.components[component].update() - else: - log.warning( - self.device_config.name + - ": Es konnten keine Werte gelesen werden, da noch keine Komponenten konfiguriert wurden." - ) + def update_components(components: Iterable[siemens_component_classes]): + with client: + for component in components: + with SingleComponentUpdateContext(component.fault_state): + component.update() - -COMPONENT_TYPE_TO_MODULE = { - "bat": bat, - "counter": counter, - "inverter": inverter -} - - -def read_legacy(component_type: str, ip_address: str, num: Optional[int] = None) -> None: - device_config = Siemens() - device_config.configuration.ip_address = ip_address - dev = Device(device_config) - if component_type in COMPONENT_TYPE_TO_MODULE: - component_config = COMPONENT_TYPE_TO_MODULE[component_type].component_descriptor.configuration_factory() - else: - raise Exception( - "illegal component type " + component_type + ". Allowed values: " + - ','.join(COMPONENT_TYPE_TO_MODULE.keys()) - ) - component_config.id = num - dev.add_component(component_config) - - log.debug('Siemens IP-Adresse: ' + ip_address) - - dev.update() - - -def main(argv: List[str]): - run_using_positional_cli_args(read_legacy, argv) + try: + client = modbus.ModbusTcpClient_(device_config.configuration.ip_address, device_config.configuration.port) + except Exception: + log.exception("Fehler in create_device") + return ConfigurableDevice( + device_config=device_config, + component_factory=ComponentFactoryByType( + bat=create_bat_component, + counter=create_counter_component, + inverter=create_inverter_component, + ), + component_updater=MultiComponentUpdater(update_components) + ) device_descriptor = DeviceDescriptor(configuration_factory=Siemens) diff --git a/packages/modules/devices/sma_shm/config.py b/packages/modules/devices/sma_shm/config.py index 2b1a69f270..0e2bd2021f 100644 --- a/packages/modules/devices/sma_shm/config.py +++ b/packages/modules/devices/sma_shm/config.py @@ -8,7 +8,7 @@ def __init__(self): class Speedwire: def __init__(self, - name: str = "SMA Sunny Home Manager 2.0", + name: str = "SMA Sunny Home Manager 2.0, Energy Meter", type: str = "sma_shm", id: int = 0, configuration: SpeedwireComponentConfiguration = None) -> None: @@ -25,7 +25,7 @@ def __init__(self, serials: int = None): class SmaHomeManagerCounterSetup(ComponentSetup[SmaHomeManagerCounterConfiguration]): def __init__(self, - name: str = "SMA Sunny Home Manager 2.0 Zähler", + name: str = "SMA Sunny Home Manager 2.0, Energy Meter Zähler", type: str = "counter", id: int = 0, configuration: SmaHomeManagerCounterConfiguration = None) -> None: @@ -39,7 +39,7 @@ def __init__(self, serials: int = None): class SmaHomeManagerInverterSetup(ComponentSetup[SmaHomeManagerInverterConfiguration]): def __init__(self, - name: str = "SMA Sunny Home Manager 2.0 Wechselrichter", + name: str = "SMA Sunny Home Manager 2.0, Energy Meter Wechselrichter", type: str = "inverter", id: int = 0, configuration: SmaHomeManagerInverterConfiguration = None) -> None: diff --git a/packages/modules/devices/sma_sunny_boy/counter.py b/packages/modules/devices/sma_sunny_boy/counter.py index 57bdfec0db..f258193944 100644 --- a/packages/modules/devices/sma_sunny_boy/counter.py +++ b/packages/modules/devices/sma_sunny_boy/counter.py @@ -34,7 +34,8 @@ def update(self): else: power = exp * -1 - imported, exported = self.sim_counter.sim_count(power) + imported = self.__tcp_client.read_holding_registers(30581, ModbusDataType.UINT_32, unit=unit) + exported = self.__tcp_client.read_holding_registers(30583, ModbusDataType.UINT_32, unit=unit) counter_state = CounterState( imported=imported, diff --git a/packages/modules/devices/sma_sunny_boy/device.py b/packages/modules/devices/sma_sunny_boy/device.py index ba544bb8b8..1fdede73da 100644 --- a/packages/modules/devices/sma_sunny_boy/device.py +++ b/packages/modules/devices/sma_sunny_boy/device.py @@ -7,7 +7,7 @@ from helpermodules.cli import run_using_positional_cli_args from modules.common import modbus from modules.common.abstract_device import AbstractDevice, DeviceDescriptor -from modules.common.component_context import SingleComponentUpdateContext +from modules.common.component_context import MultiComponentUpdateContext, SingleComponentUpdateContext from modules.common.component_state import InverterState from modules.common.store import get_inverter_value_store from modules.devices.sma_sunny_boy import bat, bat_smart_energy, counter, inverter @@ -71,11 +71,13 @@ def add_component(self, component_config: Union[Dict, def update(self) -> None: log.debug("Start device reading " + str(self.components)) if self.components: - with self.client: - for component in self.components.values(): - # Auch wenn bei einer Komponente ein Fehler auftritt, sollen alle anderen noch ausgelesen werden. - with SingleComponentUpdateContext(component.fault_state): - component.update() + with MultiComponentUpdateContext(self.components): + with self.client: + for component in self.components.values(): + # Auch wenn bei einer Komponente ein Fehler auftritt, sollen alle anderen noch ausgelesen + # werden. + with SingleComponentUpdateContext(component.fault_state): + component.update() else: log.warning( self.device_config.name + diff --git a/packages/modules/devices/sma_sunny_boy/inverter.py b/packages/modules/devices/sma_sunny_boy/inverter.py index 6b89593639..22a6f6a9c0 100644 --- a/packages/modules/devices/sma_sunny_boy/inverter.py +++ b/packages/modules/devices/sma_sunny_boy/inverter.py @@ -18,6 +18,7 @@ class SmaSunnyBoyInverter: SMA_INT32_NAN = -0x80000000 # SMA uses this value to represent NaN + SMA_UINT_64_NAN = 0x100000000 SMA_NAN = -0xC000 def __init__(self, @@ -66,6 +67,8 @@ def read(self) -> InverterState: raise ValueError("Unbekannte Version "+str(self.component_config.configuration.version)) if power_total == self.SMA_INT32_NAN or power_total == self.SMA_NAN: power_total = 0 + if dc_power == self.SMA_UINT_64_NAN: + dc_power = 0 inverter_state = InverterState( power=power_total * -1, diff --git a/packages/modules/display_themes/cards/config.py b/packages/modules/display_themes/cards/config.py index da17e3c042..0d4e528838 100644 --- a/packages/modules/display_themes/cards/config.py +++ b/packages/modules/display_themes/cards/config.py @@ -15,7 +15,10 @@ def __init__(self, enable_dashboard_card_battery_sum: bool = True, enable_dashboard_card_inverter_sum: bool = True, enable_dashboard_card_charge_point_sum: bool = True, + enable_dashboard_card_vehicles: bool = True, + enable_energy_flow_view: bool = True, enable_charge_points_view: bool = True, + simple_charge_point_view: bool = False, enable_status_view: bool = True) -> None: # display lock settings self.lock_changes = lock_changes @@ -27,8 +30,12 @@ def __init__(self, self.enable_dashboard_card_battery_sum = enable_dashboard_card_battery_sum self.enable_dashboard_card_inverter_sum = enable_dashboard_card_inverter_sum self.enable_dashboard_card_charge_point_sum = enable_dashboard_card_charge_point_sum + self.enable_dashboard_card_vehicles = enable_dashboard_card_vehicles + # energy flow settings + self.enable_energy_flow_view = enable_energy_flow_view # charge point settings self.enable_charge_points_view = enable_charge_points_view + self.simple_charge_point_view = simple_charge_point_view # state settings self.enable_status_view = enable_status_view diff --git a/packages/modules/display_themes/cards/source/.eslintrc.cjs b/packages/modules/display_themes/cards/source/.eslintrc.cjs deleted file mode 100644 index dba6d34e8e..0000000000 --- a/packages/modules/display_themes/cards/source/.eslintrc.cjs +++ /dev/null @@ -1,14 +0,0 @@ -/* eslint-env node */ -require("@rushstack/eslint-patch/modern-module-resolution"); - -module.exports = { - root: true, - extends: [ - "plugin:vue/vue3-essential", - "eslint:recommended", - "@vue/eslint-config-prettier", - ], - parserOptions: { - ecmaVersion: "latest", - }, -}; diff --git a/packages/modules/display_themes/cards/source/eslint.config.js b/packages/modules/display_themes/cards/source/eslint.config.js new file mode 100644 index 0000000000..75e73d5160 --- /dev/null +++ b/packages/modules/display_themes/cards/source/eslint.config.js @@ -0,0 +1,13 @@ +import js from "@eslint/js" +import pluginVue from "eslint-plugin-vue" + +export default [ + js.configs.recommended, + ...pluginVue.configs['flat/recommended'], + { + files: ["**/*.{vue,js,jsx,cjs,mjs}"], + languageOptions: { + ecmaVersion: "latest", + }, + } +] diff --git a/packages/modules/display_themes/cards/source/package-lock.json b/packages/modules/display_themes/cards/source/package-lock.json index eb8a1ea378..a98ccbb98a 100644 --- a/packages/modules/display_themes/cards/source/package-lock.json +++ b/packages/modules/display_themes/cards/source/package-lock.json @@ -8,50 +8,54 @@ "name": "openwb-display-cards", "version": "0.0.0", "dependencies": { - "@fortawesome/fontawesome-svg-core": "^6.2.1", - "@fortawesome/free-regular-svg-icons": "^6.2.1", - "@fortawesome/free-solid-svg-icons": "^6.2.1", - "@fortawesome/vue-fontawesome": "^3.0.2", - "@inkline/inkline": "^3.2.0", + "@fortawesome/fontawesome-svg-core": "^6.5.2", + "@fortawesome/free-regular-svg-icons": "^6.5.2", + "@fortawesome/free-solid-svg-icons": "^6.5.2", + "@fortawesome/vue-fontawesome": "^3.0.8", + "@inkline/inkline": "^3.2.2", "buffer": "^6.0.3", "events": "^3.3.0", - "mqtt": "^5.3.3", + "mqtt": "^5.8.0", "node-stdlib-browser": "^1.2.0", - "pinia": "^2.0.28", - "vue": "^3.4.19", - "vue-router": "^4.1.6" + "pinia": "^2.1.7", + "vue": "^3.4.31", + "vue-router": "^4.4.0" }, "devDependencies": { - "@rushstack/eslint-patch": "^1.1.4", - "@vitejs/plugin-vue": "^5.0.4", + "@rushstack/eslint-patch": "^1.10.3", + "@vitejs/plugin-vue": "^5.0.5", "@vue/eslint-config-prettier": "^9.0.0", - "@vue/test-utils": "^2.2.4", - "eslint": "^8.30.0", - "eslint-plugin-vue": "^9.3.0", - "jsdom": "^24.0.0", - "postcss": "^8.4.35", - "postcss-preset-env": "^9.0.0", - "prettier": "^3.1.1", + "@vue/test-utils": "^2.4.6", + "eslint": "^9.6.0", + "eslint-plugin-vue": "^9.27.0", + "jsdom": "^24.1.0", + "postcss": "^8.4.39", + "postcss-preset-env": "^9.6.0", + "prettier": "^3.3.2", "rollup-plugin-polyfill-node": "^0.13.0", - "sass": "^1.57.1", - "vite": "^5.0.12", - "vite-plugin-node-polyfills": "^0.21.0", - "vitest": "^1.0.4" + "sass": "^1.77.7", + "vite": "^5.3.3", + "vite-plugin-node-polyfills": "^0.22.0", + "vitest": "^2.0.2" } }, - "node_modules/@aashutoshrathi/word-wrap": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", - "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, "engines": { - "node": ">=0.10.0" + "node": ">=6.0.0" } }, "node_modules/@babel/parser": { - "version": "7.23.9", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.9.tgz", - "integrity": "sha512-9tcKgqKbs3xGJ+NtKF2ndOBBLVwPjl1SHxPQkd36r3Dlirw3xWUeGaTbqr7uGZcTaxkVNwc+03SVP7aCdWrTlA==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.7.tgz", + "integrity": "sha512-9uUYRm6OqQrCqQdG1iCBwBPZgN8ciDBro2nIOFaiRz1/BCxaI7CNvQbDHvsArAC7Tw9Hda/B3U+6ui9u4HWXPw==", "bin": { "parser": "bin/babel-parser.js" }, @@ -60,9 +64,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.23.9", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.9.tgz", - "integrity": "sha512-0CX6F+BI2s9dkUqr08KFrAIZgNFj75rdBU/DjCyYLIaV/quFjkk6T+EJ2LkZHyZTbEV4L5p97mNkUsHl2wLFAw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.7.tgz", + "integrity": "sha512-UwgBRMjJP+xv857DCngvqXI3Iq6J4v0wXmwc6sapg+zyhbwmQX67LUEFrkK5tbyJ30jGuG3ZvWpBiB9LCy1kWw==", "dependencies": { "regenerator-runtime": "^0.14.0" }, @@ -71,9 +75,9 @@ } }, "node_modules/@csstools/cascade-layer-name-parser": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/@csstools/cascade-layer-name-parser/-/cascade-layer-name-parser-1.0.7.tgz", - "integrity": "sha512-9J4aMRJ7A2WRjaRLvsMeWrL69FmEuijtiW1XlK/sG+V0UJiHVYUyvj9mY4WAXfU/hGIiGOgL8e0jJcRyaZTjDQ==", + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/@csstools/cascade-layer-name-parser/-/cascade-layer-name-parser-1.0.13.tgz", + "integrity": "sha512-MX0yLTwtZzr82sQ0zOjqimpZbzjMaK/h2pmlrLK7DCzlmiZLYFpoO94WmN1akRVo6ll/TdpHb53vihHLUMyvng==", "dev": true, "funding": [ { @@ -89,14 +93,14 @@ "node": "^14 || ^16 || >=18" }, "peerDependencies": { - "@csstools/css-parser-algorithms": "^2.5.0", - "@csstools/css-tokenizer": "^2.2.3" + "@csstools/css-parser-algorithms": "^2.7.1", + "@csstools/css-tokenizer": "^2.4.1" } }, "node_modules/@csstools/color-helpers": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-4.0.0.tgz", - "integrity": "sha512-wjyXB22/h2OvxAr3jldPB7R7kjTUEzopvjitS8jWtyd8fN6xJ8vy1HnHu0ZNfEkqpBJgQ76Q+sBDshWcMvTa/w==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-4.2.1.tgz", + "integrity": "sha512-CEypeeykO9AN7JWkr1OEOQb0HRzZlPWGwV0Ya6DuVgFdDi6g3ma/cPZ5ZPZM4AWQikDpq/0llnGGlIL+j8afzw==", "dev": true, "funding": [ { @@ -113,9 +117,9 @@ } }, "node_modules/@csstools/css-calc": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-1.1.6.tgz", - "integrity": "sha512-YHPAuFg5iA4qZGzMzvrQwzkvJpesXXyIUyaONflQrjtHB+BcFFbgltJkIkb31dMGO4SE9iZFA4HYpdk7+hnYew==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-1.2.4.tgz", + "integrity": "sha512-tfOuvUQeo7Hz+FcuOd3LfXVp+342pnWUJ7D2y8NUpu1Ww6xnTbHLpz018/y6rtbHifJ3iIEf9ttxXd8KG7nL0Q==", "dev": true, "funding": [ { @@ -131,14 +135,14 @@ "node": "^14 || ^16 || >=18" }, "peerDependencies": { - "@csstools/css-parser-algorithms": "^2.5.0", - "@csstools/css-tokenizer": "^2.2.3" + "@csstools/css-parser-algorithms": "^2.7.1", + "@csstools/css-tokenizer": "^2.4.1" } }, "node_modules/@csstools/css-color-parser": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-1.5.1.tgz", - "integrity": "sha512-x+SajGB2paGrTjPOUorGi8iCztF008YMKXTn+XzGVDBEIVJ/W1121pPerpneJYGOe1m6zWLPLnzOPaznmQxKFw==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-2.0.4.tgz", + "integrity": "sha512-yUb0mk/k2yVNcQvRmd9uikpu6D0aamFJGgU++5d0lng6ucaJkhKyhDCQCj9rVuQYntvFQKqyU6UfTPQWU2UkXQ==", "dev": true, "funding": [ { @@ -151,21 +155,21 @@ } ], "dependencies": { - "@csstools/color-helpers": "^4.0.0", - "@csstools/css-calc": "^1.1.6" + "@csstools/color-helpers": "^4.2.1", + "@csstools/css-calc": "^1.2.4" }, "engines": { "node": "^14 || ^16 || >=18" }, "peerDependencies": { - "@csstools/css-parser-algorithms": "^2.5.0", - "@csstools/css-tokenizer": "^2.2.3" + "@csstools/css-parser-algorithms": "^2.7.1", + "@csstools/css-tokenizer": "^2.4.1" } }, "node_modules/@csstools/css-parser-algorithms": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-2.5.0.tgz", - "integrity": "sha512-abypo6m9re3clXA00eu5syw+oaPHbJTPapu9C4pzNsJ4hdZDzushT50Zhu+iIYXgEe1CxnRMn7ngsbV+MLrlpQ==", + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-2.7.1.tgz", + "integrity": "sha512-2SJS42gxmACHgikc1WGesXLIT8d/q2l0UFM7TaEeIzdFCE/FPMtTiizcPGGJtlPo2xuQzY09OhrLTzRxqJqwGw==", "dev": true, "funding": [ { @@ -181,13 +185,13 @@ "node": "^14 || ^16 || >=18" }, "peerDependencies": { - "@csstools/css-tokenizer": "^2.2.3" + "@csstools/css-tokenizer": "^2.4.1" } }, "node_modules/@csstools/css-tokenizer": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-2.2.3.tgz", - "integrity": "sha512-pp//EvZ9dUmGuGtG1p+n17gTHEOqu9jO+FiCUjNN3BDmyhdA2Jq9QsVeR7K8/2QCK17HSsioPlTW9ZkzoWb3Lg==", + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-2.4.1.tgz", + "integrity": "sha512-eQ9DIktFJBhGjioABJRtUucoWR2mwllurfnM8LuNGAqX3ViZXaUchqk+1s7jjtkFiT9ySdACsFEA3etErkALUg==", "dev": true, "funding": [ { @@ -204,9 +208,9 @@ } }, "node_modules/@csstools/media-query-list-parser": { - "version": "2.1.7", - "resolved": "https://registry.npmjs.org/@csstools/media-query-list-parser/-/media-query-list-parser-2.1.7.tgz", - "integrity": "sha512-lHPKJDkPUECsyAvD60joYfDmp8UERYxHGkFfyLJFTVK/ERJe0sVlIFLXU5XFxdjNDTerp5L4KeaKG+Z5S94qxQ==", + "version": "2.1.13", + "resolved": "https://registry.npmjs.org/@csstools/media-query-list-parser/-/media-query-list-parser-2.1.13.tgz", + "integrity": "sha512-XaHr+16KRU9Gf8XLi3q8kDlI18d5vzKSKCY510Vrtc9iNR0NJzbY9hhTmwhzYZj/ZwGL4VmB3TA9hJW0Um2qFA==", "dev": true, "funding": [ { @@ -222,14 +226,14 @@ "node": "^14 || ^16 || >=18" }, "peerDependencies": { - "@csstools/css-parser-algorithms": "^2.5.0", - "@csstools/css-tokenizer": "^2.2.3" + "@csstools/css-parser-algorithms": "^2.7.1", + "@csstools/css-tokenizer": "^2.4.1" } }, "node_modules/@csstools/postcss-cascade-layers": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@csstools/postcss-cascade-layers/-/postcss-cascade-layers-4.0.2.tgz", - "integrity": "sha512-PqM+jvg5T2tB4FHX+akrMGNWAygLupD4FNUjcv4PSvtVuWZ6ISxuo37m4jFGU7Jg3rCfloGzKd0+xfr5Ec3vZQ==", + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@csstools/postcss-cascade-layers/-/postcss-cascade-layers-4.0.6.tgz", + "integrity": "sha512-Xt00qGAQyqAODFiFEJNkTpSUz5VfYqnDLECdlA/Vv17nl/OIV5QfTRHGAXrBGG5YcJyHpJ+GF9gF/RZvOQz4oA==", "dev": true, "funding": [ { @@ -242,7 +246,7 @@ } ], "dependencies": { - "@csstools/selector-specificity": "^3.0.1", + "@csstools/selector-specificity": "^3.1.1", "postcss-selector-parser": "^6.0.13" }, "engines": { @@ -253,9 +257,9 @@ } }, "node_modules/@csstools/postcss-color-function": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/@csstools/postcss-color-function/-/postcss-color-function-3.0.9.tgz", - "integrity": "sha512-6Hbkw/4k73UH121l4LG+LNLKSvrfHqk3GHHH0A6/iFlD0xGmsWAr80Jd0VqXjfYbUTOGmJTOMMoxv3jvNxt1uw==", + "version": "3.0.19", + "resolved": "https://registry.npmjs.org/@csstools/postcss-color-function/-/postcss-color-function-3.0.19.tgz", + "integrity": "sha512-d1OHEXyYGe21G3q88LezWWx31ImEDdmINNDy0LyLNN9ChgN2bPxoubUPiHf9KmwypBMaHmNcMuA/WZOKdZk/Lg==", "dev": true, "funding": [ { @@ -268,10 +272,11 @@ } ], "dependencies": { - "@csstools/css-color-parser": "^1.5.1", - "@csstools/css-parser-algorithms": "^2.5.0", - "@csstools/css-tokenizer": "^2.2.3", - "@csstools/postcss-progressive-custom-properties": "^3.0.3" + "@csstools/css-color-parser": "^2.0.4", + "@csstools/css-parser-algorithms": "^2.7.1", + "@csstools/css-tokenizer": "^2.4.1", + "@csstools/postcss-progressive-custom-properties": "^3.3.0", + "@csstools/utilities": "^1.0.0" }, "engines": { "node": "^14 || ^16 || >=18" @@ -281,9 +286,38 @@ } }, "node_modules/@csstools/postcss-color-mix-function": { - "version": "2.0.9", - "resolved": "https://registry.npmjs.org/@csstools/postcss-color-mix-function/-/postcss-color-mix-function-2.0.9.tgz", - "integrity": "sha512-fs1SOWJ/44DQSsDeJP+rxAkP2MYkCg6K4ZB8qJwFku2EjurgCAPiPZJvC6w94T1hBBinJwuMfT9qvvvniXyVgw==", + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/@csstools/postcss-color-mix-function/-/postcss-color-mix-function-2.0.19.tgz", + "integrity": "sha512-mLvQlMX+keRYr16AuvuV8WYKUwF+D0DiCqlBdvhQ0KYEtcQl9/is9Ssg7RcIys8x0jIn2h1zstS4izckdZj9wg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "dependencies": { + "@csstools/css-color-parser": "^2.0.4", + "@csstools/css-parser-algorithms": "^2.7.1", + "@csstools/css-tokenizer": "^2.4.1", + "@csstools/postcss-progressive-custom-properties": "^3.3.0", + "@csstools/utilities": "^1.0.0" + }, + "engines": { + "node": "^14 || ^16 || >=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-content-alt-text": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-content-alt-text/-/postcss-content-alt-text-1.0.0.tgz", + "integrity": "sha512-SkHdj7EMM/57GVvSxSELpUg7zb5eAndBeuvGwFzYtU06/QXJ/h9fuK7wO5suteJzGhm3GDF/EWPCdWV2h1IGHQ==", "dev": true, "funding": [ { @@ -296,10 +330,10 @@ } ], "dependencies": { - "@csstools/css-color-parser": "^1.5.1", - "@csstools/css-parser-algorithms": "^2.5.0", - "@csstools/css-tokenizer": "^2.2.3", - "@csstools/postcss-progressive-custom-properties": "^3.0.3" + "@csstools/css-parser-algorithms": "^2.7.1", + "@csstools/css-tokenizer": "^2.4.1", + "@csstools/postcss-progressive-custom-properties": "^3.3.0", + "@csstools/utilities": "^1.0.0" }, "engines": { "node": "^14 || ^16 || >=18" @@ -309,9 +343,9 @@ } }, "node_modules/@csstools/postcss-exponential-functions": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@csstools/postcss-exponential-functions/-/postcss-exponential-functions-1.0.3.tgz", - "integrity": "sha512-IfGtEg3eC4b8Nd/kPgO3SxgKb33YwhHVsL0eJ3UYihx6fzzAiZwNbWmVW9MZTQjZ5GacgKxa4iAHikGvpwuIjw==", + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@csstools/postcss-exponential-functions/-/postcss-exponential-functions-1.0.9.tgz", + "integrity": "sha512-x1Avr15mMeuX7Z5RJUl7DmjhUtg+Amn5DZRD0fQ2TlTFTcJS8U1oxXQ9e5mA62S2RJgUU6db20CRoJyDvae2EQ==", "dev": true, "funding": [ { @@ -324,9 +358,9 @@ } ], "dependencies": { - "@csstools/css-calc": "^1.1.6", - "@csstools/css-parser-algorithms": "^2.5.0", - "@csstools/css-tokenizer": "^2.2.3" + "@csstools/css-calc": "^1.2.4", + "@csstools/css-parser-algorithms": "^2.7.1", + "@csstools/css-tokenizer": "^2.4.1" }, "engines": { "node": "^14 || ^16 || >=18" @@ -336,9 +370,9 @@ } }, "node_modules/@csstools/postcss-font-format-keywords": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@csstools/postcss-font-format-keywords/-/postcss-font-format-keywords-3.0.1.tgz", - "integrity": "sha512-D1lcG2sfotTq6yBEOMV3myFxJLT10F3DLYZJMbiny5YToqzHWodZen8WId3UTimm0mEHitXqAUNL5jdd6RzVdA==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@csstools/postcss-font-format-keywords/-/postcss-font-format-keywords-3.0.2.tgz", + "integrity": "sha512-E0xz2sjm4AMCkXLCFvI/lyl4XO6aN1NCSMMVEOngFDJ+k2rDwfr6NDjWljk1li42jiLNChVX+YFnmfGCigZKXw==", "dev": true, "funding": [ { @@ -351,6 +385,7 @@ } ], "dependencies": { + "@csstools/utilities": "^1.0.0", "postcss-value-parser": "^4.2.0" }, "engines": { @@ -361,9 +396,9 @@ } }, "node_modules/@csstools/postcss-gamut-mapping": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@csstools/postcss-gamut-mapping/-/postcss-gamut-mapping-1.0.2.tgz", - "integrity": "sha512-zf9KHGM2PTuJEm4ZYg4DTmzCir38EbZBzlMPMbA4jbhLDqXHkqwnQ+Z5+UNrU8y6seVu5B4vzZmZarTFQwe+Ig==", + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@csstools/postcss-gamut-mapping/-/postcss-gamut-mapping-1.0.11.tgz", + "integrity": "sha512-KrHGsUPXRYxboXmJ9wiU/RzDM7y/5uIefLWKFSc36Pok7fxiPyvkSHO51kh+RLZS1W5hbqw9qaa6+tKpTSxa5g==", "dev": true, "funding": [ { @@ -376,9 +411,9 @@ } ], "dependencies": { - "@csstools/css-color-parser": "^1.5.1", - "@csstools/css-parser-algorithms": "^2.5.0", - "@csstools/css-tokenizer": "^2.2.3" + "@csstools/css-color-parser": "^2.0.4", + "@csstools/css-parser-algorithms": "^2.7.1", + "@csstools/css-tokenizer": "^2.4.1" }, "engines": { "node": "^14 || ^16 || >=18" @@ -388,9 +423,9 @@ } }, "node_modules/@csstools/postcss-gradients-interpolation-method": { - "version": "4.0.9", - "resolved": "https://registry.npmjs.org/@csstools/postcss-gradients-interpolation-method/-/postcss-gradients-interpolation-method-4.0.9.tgz", - "integrity": "sha512-PSqR6QH7h3ggOl8TsoH73kbwYTKVQjAJauGg6nDKwaGfi5IL5StV//ehrv1C7HuPsHixMTc9YoAuuv1ocT20EQ==", + "version": "4.0.20", + "resolved": "https://registry.npmjs.org/@csstools/postcss-gradients-interpolation-method/-/postcss-gradients-interpolation-method-4.0.20.tgz", + "integrity": "sha512-ZFl2JBHano6R20KB5ZrB8KdPM2pVK0u+/3cGQ2T8VubJq982I2LSOvQ4/VtxkAXjkPkk1rXt4AD1ni7UjTZ1Og==", "dev": true, "funding": [ { @@ -403,10 +438,11 @@ } ], "dependencies": { - "@csstools/css-color-parser": "^1.5.1", - "@csstools/css-parser-algorithms": "^2.5.0", - "@csstools/css-tokenizer": "^2.2.3", - "@csstools/postcss-progressive-custom-properties": "^3.0.3" + "@csstools/css-color-parser": "^2.0.4", + "@csstools/css-parser-algorithms": "^2.7.1", + "@csstools/css-tokenizer": "^2.4.1", + "@csstools/postcss-progressive-custom-properties": "^3.3.0", + "@csstools/utilities": "^1.0.0" }, "engines": { "node": "^14 || ^16 || >=18" @@ -416,9 +452,9 @@ } }, "node_modules/@csstools/postcss-hwb-function": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/@csstools/postcss-hwb-function/-/postcss-hwb-function-3.0.8.tgz", - "integrity": "sha512-CRQEG372Hivmt17rm/Ho22hBQI9K/a6grzGQ21Zwc7dyspmyG0ibmPIW8hn15vJmXqWGeNq7S+L2b8/OrU7O5A==", + "version": "3.0.18", + "resolved": "https://registry.npmjs.org/@csstools/postcss-hwb-function/-/postcss-hwb-function-3.0.18.tgz", + "integrity": "sha512-3ifnLltR5C7zrJ+g18caxkvSRnu9jBBXCYgnBznRjxm6gQJGnnCO9H6toHfywNdNr/qkiVf2dymERPQLDnjLRQ==", "dev": true, "funding": [ { @@ -431,9 +467,11 @@ } ], "dependencies": { - "@csstools/css-color-parser": "^1.5.1", - "@csstools/css-parser-algorithms": "^2.5.0", - "@csstools/css-tokenizer": "^2.2.3" + "@csstools/css-color-parser": "^2.0.4", + "@csstools/css-parser-algorithms": "^2.7.1", + "@csstools/css-tokenizer": "^2.4.1", + "@csstools/postcss-progressive-custom-properties": "^3.3.0", + "@csstools/utilities": "^1.0.0" }, "engines": { "node": "^14 || ^16 || >=18" @@ -443,9 +481,9 @@ } }, "node_modules/@csstools/postcss-ic-unit": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@csstools/postcss-ic-unit/-/postcss-ic-unit-3.0.3.tgz", - "integrity": "sha512-MpcmIL0/uMm/cFWh5V/9nbKKJ7jRr2qTYW5Q6zoE6HZ6uzOBJr2KRERv5/x8xzEBQ1MthDT7iP1EBp9luSQy7g==", + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@csstools/postcss-ic-unit/-/postcss-ic-unit-3.0.7.tgz", + "integrity": "sha512-YoaNHH2wNZD+c+rHV02l4xQuDpfR8MaL7hD45iJyr+USwvr0LOheeytJ6rq8FN6hXBmEeoJBeXXgGmM8fkhH4g==", "dev": true, "funding": [ { @@ -458,7 +496,8 @@ } ], "dependencies": { - "@csstools/postcss-progressive-custom-properties": "^3.0.3", + "@csstools/postcss-progressive-custom-properties": "^3.3.0", + "@csstools/utilities": "^1.0.0", "postcss-value-parser": "^4.2.0" }, "engines": { @@ -491,9 +530,9 @@ } }, "node_modules/@csstools/postcss-is-pseudo-class": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@csstools/postcss-is-pseudo-class/-/postcss-is-pseudo-class-4.0.4.tgz", - "integrity": "sha512-vTVO/uZixpTVAOQt3qZRUFJ/K1L03OfNkeJ8sFNDVNdVy/zW0h1L5WT7HIPMDUkvSrxQkFaCCybTZkUP7UESlQ==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/@csstools/postcss-is-pseudo-class/-/postcss-is-pseudo-class-4.0.8.tgz", + "integrity": "sha512-0aj591yGlq5Qac+plaWCbn5cpjs5Sh0daovYUKJUOMjIp70prGH/XPLp7QjxtbFXz3CTvb0H9a35dpEuIuUi3Q==", "dev": true, "funding": [ { @@ -506,7 +545,7 @@ } ], "dependencies": { - "@csstools/selector-specificity": "^3.0.1", + "@csstools/selector-specificity": "^3.1.1", "postcss-selector-parser": "^6.0.13" }, "engines": { @@ -516,6 +555,34 @@ "postcss": "^8.4" } }, + "node_modules/@csstools/postcss-light-dark-function": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@csstools/postcss-light-dark-function/-/postcss-light-dark-function-1.0.8.tgz", + "integrity": "sha512-x0UtpCyVnERsplUeoaY6nEtp1HxTf4lJjoK/ULEm40DraqFfUdUSt76yoOyX5rGY6eeOUOkurHyYlFHVKv/pew==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "dependencies": { + "@csstools/css-parser-algorithms": "^2.7.1", + "@csstools/css-tokenizer": "^2.4.1", + "@csstools/postcss-progressive-custom-properties": "^3.3.0", + "@csstools/utilities": "^1.0.0" + }, + "engines": { + "node": "^14 || ^16 || >=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, "node_modules/@csstools/postcss-logical-float-and-clear": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@csstools/postcss-logical-float-and-clear/-/postcss-logical-float-and-clear-2.0.1.tgz", @@ -608,9 +675,9 @@ } }, "node_modules/@csstools/postcss-logical-viewport-units": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@csstools/postcss-logical-viewport-units/-/postcss-logical-viewport-units-2.0.5.tgz", - "integrity": "sha512-2fjSamKN635DSW6fEoyNd2Bkpv3FVblUpgk5cpghIgPW1aDHZE2SYfZK5xQALvjMYZVjfqsD5EbXA7uDVBQVQA==", + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@csstools/postcss-logical-viewport-units/-/postcss-logical-viewport-units-2.0.11.tgz", + "integrity": "sha512-ElITMOGcjQtvouxjd90WmJRIw1J7KMP+M+O87HaVtlgOOlDt1uEPeTeii8qKGe2AiedEp0XOGIo9lidbiU2Ogg==", "dev": true, "funding": [ { @@ -623,7 +690,8 @@ } ], "dependencies": { - "@csstools/css-tokenizer": "^2.2.3" + "@csstools/css-tokenizer": "^2.4.1", + "@csstools/utilities": "^1.0.0" }, "engines": { "node": "^14 || ^16 || >=18" @@ -633,9 +701,9 @@ } }, "node_modules/@csstools/postcss-media-minmax": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@csstools/postcss-media-minmax/-/postcss-media-minmax-1.1.2.tgz", - "integrity": "sha512-7qTRTJxW96u2yiEaTep1+8nto1O/rEDacewKqH+Riq5E6EsHTOmGHxkB4Se5Ic5xgDC4I05lLZxzzxnlnSypxA==", + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@csstools/postcss-media-minmax/-/postcss-media-minmax-1.1.8.tgz", + "integrity": "sha512-KYQCal2i7XPNtHAUxCECdrC7tuxIWQCW+s8eMYs5r5PaAiVTeKwlrkRS096PFgojdNCmHeG0Cb7njtuNswNf+w==", "dev": true, "funding": [ { @@ -648,10 +716,10 @@ } ], "dependencies": { - "@csstools/css-calc": "^1.1.6", - "@csstools/css-parser-algorithms": "^2.5.0", - "@csstools/css-tokenizer": "^2.2.3", - "@csstools/media-query-list-parser": "^2.1.7" + "@csstools/css-calc": "^1.2.4", + "@csstools/css-parser-algorithms": "^2.7.1", + "@csstools/css-tokenizer": "^2.4.1", + "@csstools/media-query-list-parser": "^2.1.13" }, "engines": { "node": "^14 || ^16 || >=18" @@ -661,9 +729,9 @@ } }, "node_modules/@csstools/postcss-media-queries-aspect-ratio-number-values": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@csstools/postcss-media-queries-aspect-ratio-number-values/-/postcss-media-queries-aspect-ratio-number-values-2.0.5.tgz", - "integrity": "sha512-XHMPasWYPWa9XaUHXU6Iq0RLfoAI+nvGTPj51hOizNsHaAyFiq2SL4JvF1DU8lM6B70+HVzKM09Isbyrr755Bw==", + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@csstools/postcss-media-queries-aspect-ratio-number-values/-/postcss-media-queries-aspect-ratio-number-values-2.0.11.tgz", + "integrity": "sha512-YD6jrib20GRGQcnOu49VJjoAnQ/4249liuz7vTpy/JfgqQ1Dlc5eD4HPUMNLOw9CWey9E6Etxwf/xc/ZF8fECA==", "dev": true, "funding": [ { @@ -676,9 +744,9 @@ } ], "dependencies": { - "@csstools/css-parser-algorithms": "^2.5.0", - "@csstools/css-tokenizer": "^2.2.3", - "@csstools/media-query-list-parser": "^2.1.7" + "@csstools/css-parser-algorithms": "^2.7.1", + "@csstools/css-tokenizer": "^2.4.1", + "@csstools/media-query-list-parser": "^2.1.13" }, "engines": { "node": "^14 || ^16 || >=18" @@ -688,9 +756,9 @@ } }, "node_modules/@csstools/postcss-nested-calc": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@csstools/postcss-nested-calc/-/postcss-nested-calc-3.0.1.tgz", - "integrity": "sha512-bwwababZpWRm0ByHaWBxTsDGTMhZKmtUNl3Wt0Eom8AY7ORgXx5qF9SSk1vEFrCi+HOfJT6M6W5KPgzXuQNRwQ==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@csstools/postcss-nested-calc/-/postcss-nested-calc-3.0.2.tgz", + "integrity": "sha512-ySUmPyawiHSmBW/VI44+IObcKH0v88LqFe0d09Sb3w4B1qjkaROc6d5IA3ll9kjD46IIX/dbO5bwFN/swyoyZA==", "dev": true, "funding": [ { @@ -703,6 +771,7 @@ } ], "dependencies": { + "@csstools/utilities": "^1.0.0", "postcss-value-parser": "^4.2.0" }, "engines": { @@ -738,9 +807,9 @@ } }, "node_modules/@csstools/postcss-oklab-function": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/@csstools/postcss-oklab-function/-/postcss-oklab-function-3.0.9.tgz", - "integrity": "sha512-l639gpcBfL3ogJe+og1M5FixQn8iGX8+29V7VtTSCUB37VzpzOC05URfde7INIdiJT65DkHzgdJ64/QeYggU8A==", + "version": "3.0.19", + "resolved": "https://registry.npmjs.org/@csstools/postcss-oklab-function/-/postcss-oklab-function-3.0.19.tgz", + "integrity": "sha512-e3JxXmxjU3jpU7TzZrsNqSX4OHByRC3XjItV3Ieo/JEQmLg5rdOL4lkv/1vp27gXemzfNt44F42k/pn0FpE21Q==", "dev": true, "funding": [ { @@ -753,10 +822,11 @@ } ], "dependencies": { - "@csstools/css-color-parser": "^1.5.1", - "@csstools/css-parser-algorithms": "^2.5.0", - "@csstools/css-tokenizer": "^2.2.3", - "@csstools/postcss-progressive-custom-properties": "^3.0.3" + "@csstools/css-color-parser": "^2.0.4", + "@csstools/css-parser-algorithms": "^2.7.1", + "@csstools/css-tokenizer": "^2.4.1", + "@csstools/postcss-progressive-custom-properties": "^3.3.0", + "@csstools/utilities": "^1.0.0" }, "engines": { "node": "^14 || ^16 || >=18" @@ -766,9 +836,9 @@ } }, "node_modules/@csstools/postcss-progressive-custom-properties": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@csstools/postcss-progressive-custom-properties/-/postcss-progressive-custom-properties-3.0.3.tgz", - "integrity": "sha512-WipTVh6JTMQfeIrzDV4wEPsV9NTzMK2jwXxyH6CGBktuWdivHnkioP/smp1x/0QDPQyx7NTS14RB+GV3zZZYEw==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-progressive-custom-properties/-/postcss-progressive-custom-properties-3.3.0.tgz", + "integrity": "sha512-W2oV01phnILaRGYPmGFlL2MT/OgYjQDrL9sFlbdikMFi6oQkFki9B86XqEWR7HCsTZFVq7dbzr/o71B75TKkGg==", "dev": true, "funding": [ { @@ -791,9 +861,9 @@ } }, "node_modules/@csstools/postcss-relative-color-syntax": { - "version": "2.0.9", - "resolved": "https://registry.npmjs.org/@csstools/postcss-relative-color-syntax/-/postcss-relative-color-syntax-2.0.9.tgz", - "integrity": "sha512-2UoaRd2iIuzUGtYgteN5fJ0s+OfCiV7PvCnw8MCh3om8+SeVinfG8D5sqBOvImxFVfrp6k60XF5RFlH6oc//fg==", + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/@csstools/postcss-relative-color-syntax/-/postcss-relative-color-syntax-2.0.19.tgz", + "integrity": "sha512-MxUMSNvio1WwuS6WRLlQuv6nNPXwIWUFzBBAvL/tBdWfiKjiJnAa6eSSN5gtaacSqUkQ/Ce5Z1OzLRfeaWhADA==", "dev": true, "funding": [ { @@ -806,10 +876,11 @@ } ], "dependencies": { - "@csstools/css-color-parser": "^1.5.1", - "@csstools/css-parser-algorithms": "^2.5.0", - "@csstools/css-tokenizer": "^2.2.3", - "@csstools/postcss-progressive-custom-properties": "^3.0.3" + "@csstools/css-color-parser": "^2.0.4", + "@csstools/css-parser-algorithms": "^2.7.1", + "@csstools/css-tokenizer": "^2.4.1", + "@csstools/postcss-progressive-custom-properties": "^3.3.0", + "@csstools/utilities": "^1.0.0" }, "engines": { "node": "^14 || ^16 || >=18" @@ -844,9 +915,9 @@ } }, "node_modules/@csstools/postcss-stepped-value-functions": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@csstools/postcss-stepped-value-functions/-/postcss-stepped-value-functions-3.0.4.tgz", - "integrity": "sha512-gyNQ2YaOVXPqLR737XtReRPVu7DGKBr9JBDLoiH1T+N1ggV3r4HotRCOC1l6rxVC0zOuU1KiOzUn9Z5W838/rg==", + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@csstools/postcss-stepped-value-functions/-/postcss-stepped-value-functions-3.0.10.tgz", + "integrity": "sha512-MZwo0D0TYrQhT5FQzMqfy/nGZ28D1iFtpN7Su1ck5BPHS95+/Y5O9S4kEvo76f2YOsqwYcT8ZGehSI1TnzuX2g==", "dev": true, "funding": [ { @@ -859,9 +930,9 @@ } ], "dependencies": { - "@csstools/css-calc": "^1.1.6", - "@csstools/css-parser-algorithms": "^2.5.0", - "@csstools/css-tokenizer": "^2.2.3" + "@csstools/css-calc": "^1.2.4", + "@csstools/css-parser-algorithms": "^2.7.1", + "@csstools/css-tokenizer": "^2.4.1" }, "engines": { "node": "^14 || ^16 || >=18" @@ -871,9 +942,9 @@ } }, "node_modules/@csstools/postcss-text-decoration-shorthand": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@csstools/postcss-text-decoration-shorthand/-/postcss-text-decoration-shorthand-3.0.4.tgz", - "integrity": "sha512-yUZmbnUemgQmja7SpOZeU45+P49wNEgQguRdyTktFkZsHf7Gof+ZIYfvF6Cm+LsU1PwSupy4yUeEKKjX5+k6cQ==", + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@csstools/postcss-text-decoration-shorthand/-/postcss-text-decoration-shorthand-3.0.7.tgz", + "integrity": "sha512-+cptcsM5r45jntU6VjotnkC9GteFR7BQBfZ5oW7inLCxj7AfLGAzMbZ60hKTP13AULVZBdxky0P8um0IBfLHVA==", "dev": true, "funding": [ { @@ -886,7 +957,7 @@ } ], "dependencies": { - "@csstools/color-helpers": "^4.0.0", + "@csstools/color-helpers": "^4.2.1", "postcss-value-parser": "^4.2.0" }, "engines": { @@ -897,9 +968,9 @@ } }, "node_modules/@csstools/postcss-trigonometric-functions": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@csstools/postcss-trigonometric-functions/-/postcss-trigonometric-functions-3.0.4.tgz", - "integrity": "sha512-qj4Cxth6c38iNYzfJJWAxt8jsLrZaMVmbfGDDLOlI2YJeZoC3A5Su6/Kr7oXaPFRuspUu+4EQHngOktqVHWfVg==", + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@csstools/postcss-trigonometric-functions/-/postcss-trigonometric-functions-3.0.10.tgz", + "integrity": "sha512-G9G8moTc2wiad61nY5HfvxLiM/myX0aYK4s1x8MQlPH29WDPxHQM7ghGgvv2qf2xH+rrXhztOmjGHJj4jsEqXw==", "dev": true, "funding": [ { @@ -912,9 +983,9 @@ } ], "dependencies": { - "@csstools/css-calc": "^1.1.6", - "@csstools/css-parser-algorithms": "^2.5.0", - "@csstools/css-tokenizer": "^2.2.3" + "@csstools/css-calc": "^1.2.4", + "@csstools/css-parser-algorithms": "^2.7.1", + "@csstools/css-tokenizer": "^2.4.1" }, "engines": { "node": "^14 || ^16 || >=18" @@ -945,10 +1016,32 @@ "postcss": "^8.4" } }, + "node_modules/@csstools/selector-resolve-nested": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@csstools/selector-resolve-nested/-/selector-resolve-nested-1.1.0.tgz", + "integrity": "sha512-uWvSaeRcHyeNenKg8tp17EVDRkpflmdyvbE0DHo6D/GdBb6PDnCYYU6gRpXhtICMGMcahQmj2zGxwFM/WC8hCg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "engines": { + "node": "^14 || ^16 || >=18" + }, + "peerDependencies": { + "postcss-selector-parser": "^6.0.13" + } + }, "node_modules/@csstools/selector-specificity": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-3.0.1.tgz", - "integrity": "sha512-NPljRHkq4a14YzZ3YD406uaxh7s0g6eAq3L9aLOWywoqe8PkYamAvtsh7KNX6c++ihDrJ0RiU+/z7rGnhlZ5ww==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-3.1.1.tgz", + "integrity": "sha512-a7cxGcJ2wIlMFLlh8z2ONm+715QkPHiyJcxwQlKOz/03GPw1COpfhcmC9wm4xlZfp//jWHNNMwzjtqHXVWU9KA==", "dev": true, "funding": [ { @@ -967,10 +1060,32 @@ "postcss-selector-parser": "^6.0.13" } }, + "node_modules/@csstools/utilities": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@csstools/utilities/-/utilities-1.0.0.tgz", + "integrity": "sha512-tAgvZQe/t2mlvpNosA4+CkMiZ2azISW5WPAcdSalZlEjQvUfghHxfQcrCiK/7/CrfAWVxyM88kGFYO82heIGDg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "engines": { + "node": "^14 || ^16 || >=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz", - "integrity": "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", "cpu": [ "ppc64" ], @@ -984,9 +1099,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.12.tgz", - "integrity": "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", "cpu": [ "arm" ], @@ -1000,9 +1115,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz", - "integrity": "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", "cpu": [ "arm64" ], @@ -1016,9 +1131,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.12.tgz", - "integrity": "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", "cpu": [ "x64" ], @@ -1032,9 +1147,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz", - "integrity": "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", "cpu": [ "arm64" ], @@ -1048,9 +1163,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz", - "integrity": "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", "cpu": [ "x64" ], @@ -1064,9 +1179,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz", - "integrity": "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", "cpu": [ "arm64" ], @@ -1080,9 +1195,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz", - "integrity": "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", "cpu": [ "x64" ], @@ -1096,9 +1211,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz", - "integrity": "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", "cpu": [ "arm" ], @@ -1112,9 +1227,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz", - "integrity": "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", "cpu": [ "arm64" ], @@ -1128,9 +1243,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz", - "integrity": "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", "cpu": [ "ia32" ], @@ -1144,9 +1259,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz", - "integrity": "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", "cpu": [ "loong64" ], @@ -1160,9 +1275,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz", - "integrity": "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", "cpu": [ "mips64el" ], @@ -1176,9 +1291,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz", - "integrity": "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", "cpu": [ "ppc64" ], @@ -1192,9 +1307,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz", - "integrity": "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", "cpu": [ "riscv64" ], @@ -1208,9 +1323,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz", - "integrity": "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", "cpu": [ "s390x" ], @@ -1224,9 +1339,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz", - "integrity": "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", "cpu": [ "x64" ], @@ -1240,9 +1355,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz", - "integrity": "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", "cpu": [ "x64" ], @@ -1256,9 +1371,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz", - "integrity": "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", "cpu": [ "x64" ], @@ -1272,9 +1387,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz", - "integrity": "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", "cpu": [ "x64" ], @@ -1288,9 +1403,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz", - "integrity": "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", "cpu": [ "arm64" ], @@ -1304,9 +1419,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz", - "integrity": "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", "cpu": [ "ia32" ], @@ -1320,9 +1435,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz", - "integrity": "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", "cpu": [ "x64" ], @@ -1350,25 +1465,51 @@ "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/@eslint-community/regexpp": { - "version": "4.10.0", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", - "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==", + "version": "4.11.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.0.tgz", + "integrity": "sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A==", "dev": true, "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, + "node_modules/@eslint/config-array": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.17.0.tgz", + "integrity": "sha512-A68TBu6/1mHHuc5YJL0U0VVeGNiklLAL6rRmhTCP2B5XjWLMnrX+HkO+IAXyHvks5cyyY1jjK5ITPQ1HGS2EVA==", + "dev": true, + "dependencies": { + "@eslint/object-schema": "^2.1.4", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, "node_modules/@eslint/eslintrc": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", - "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.1.0.tgz", + "integrity": "sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==", "dev": true, "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", - "espree": "^9.6.0", - "globals": "^13.19.0", + "espree": "^10.0.1", + "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", @@ -1376,89 +1517,84 @@ "strip-json-comments": "^3.1.1" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" } }, "node_modules/@eslint/js": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.56.0.tgz", - "integrity": "sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A==", + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.6.0.tgz", + "integrity": "sha512-D9B0/3vNg44ZeWbYMpBoXqNP4j6eQD5vNwIlGAuFRRzK/WtT/jvDQW3Bi9kkf3PMDMlM7Yi+73VLUsn5bJcl8A==", "dev": true, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.4.tgz", + "integrity": "sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@fortawesome/fontawesome-common-types": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.5.1.tgz", - "integrity": "sha512-GkWzv+L6d2bI5f/Vk6ikJ9xtl7dfXtoRu3YGE6nq0p/FFqA1ebMOAWg3XgRyb0I6LYyYkiAo+3/KrwuBp8xG7A==", + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.5.2.tgz", + "integrity": "sha512-gBxPg3aVO6J0kpfHNILc+NMhXnqHumFxOmjYCFfOiLZfwhnnfhtsdA2hfJlDnj+8PjAs6kKQPenOTKj3Rf7zHw==", "hasInstallScript": true, "engines": { "node": ">=6" } }, "node_modules/@fortawesome/fontawesome-svg-core": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.5.1.tgz", - "integrity": "sha512-MfRCYlQPXoLlpem+egxjfkEuP9UQswTrlCOsknus/NcMoblTH2g0jPrapbcIb04KGA7E2GZxbAccGZfWoYgsrQ==", + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.5.2.tgz", + "integrity": "sha512-5CdaCBGl8Rh9ohNdxeeTMxIj8oc3KNBgIeLMvJosBMdslK/UnEB8rzyDRrbKdL1kDweqBPo4GT9wvnakHWucZw==", "hasInstallScript": true, "dependencies": { - "@fortawesome/fontawesome-common-types": "6.5.1" + "@fortawesome/fontawesome-common-types": "6.5.2" }, "engines": { "node": ">=6" } }, "node_modules/@fortawesome/free-regular-svg-icons": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.5.1.tgz", - "integrity": "sha512-m6ShXn+wvqEU69wSP84coxLbNl7sGVZb+Ca+XZq6k30SzuP3X4TfPqtycgUh9ASwlNh5OfQCd8pDIWxl+O+LlQ==", + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.5.2.tgz", + "integrity": "sha512-iabw/f5f8Uy2nTRtJ13XZTS1O5+t+anvlamJ3zJGLEVE2pKsAWhPv2lq01uQlfgCX7VaveT3EVs515cCN9jRbw==", "hasInstallScript": true, "dependencies": { - "@fortawesome/fontawesome-common-types": "6.5.1" + "@fortawesome/fontawesome-common-types": "6.5.2" }, "engines": { "node": ">=6" } }, "node_modules/@fortawesome/free-solid-svg-icons": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.5.1.tgz", - "integrity": "sha512-S1PPfU3mIJa59biTtXJz1oI0+KAXW6bkAb31XKhxdxtuXDiUIFsih4JR1v5BbxY7hVHsD1RKq+jRkVRaf773NQ==", + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.5.2.tgz", + "integrity": "sha512-QWFZYXFE7O1Gr1dTIp+D6UcFUF0qElOnZptpi7PBUMylJh+vFmIedVe1Ir6RM1t2tEQLLSV1k7bR4o92M+uqlw==", "hasInstallScript": true, "dependencies": { - "@fortawesome/fontawesome-common-types": "6.5.1" + "@fortawesome/fontawesome-common-types": "6.5.2" }, "engines": { "node": ">=6" } }, "node_modules/@fortawesome/vue-fontawesome": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/@fortawesome/vue-fontawesome/-/vue-fontawesome-3.0.6.tgz", - "integrity": "sha512-akrL7lTroyNpPkoHtvK2UpsMzJr6jXdHaQ0YdcwqDsB8jdwlpNHZYijpOUd9KJsARr+VB3WXY4EyObepqJ4ytQ==", + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@fortawesome/vue-fontawesome/-/vue-fontawesome-3.0.8.tgz", + "integrity": "sha512-yyHHAj4G8pQIDfaIsMvQpwKMboIZtcHTUvPqXjOHyldh1O1vZfH4W03VDPv5RvI9P6DLTzJQlmVgj9wCf7c2Fw==", "peerDependencies": { "@fortawesome/fontawesome-svg-core": "~1 || ~6", "vue": ">= 3.0.0 < 4" } }, - "node_modules/@humanwhocodes/config-array": { - "version": "0.11.14", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", - "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", - "dev": true, - "dependencies": { - "@humanwhocodes/object-schema": "^2.0.2", - "debug": "^4.3.1", - "minimatch": "^3.0.5" - }, - "engines": { - "node": ">=10.10.0" - } - }, "node_modules/@humanwhocodes/module-importer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", @@ -1472,11 +1608,18 @@ "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@humanwhocodes/object-schema": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz", - "integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==", - "dev": true + "node_modules/@humanwhocodes/retry": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.0.tgz", + "integrity": "sha512-d2CGZR2o7fS6sWB7DG/3a95bGKQyHMACZ5aW8qGkkqQpUoZV6C0X7Pc7l4ZNMZkfNBf4VWNe9E1jRsf0G146Ew==", + "dev": true, + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } }, "node_modules/@inkline/inkline": { "version": "3.2.2", @@ -1537,22 +1680,52 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, - "node_modules/@jest/schemas": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", - "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", "dev": true, "dependencies": { - "@sinclair/typebox": "^0.27.8" + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true, + "engines": { + "node": ">=6.0.0" } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.15", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", @@ -1671,9 +1844,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.9.6.tgz", - "integrity": "sha512-MVNXSSYN6QXOulbHpLMKYi60ppyO13W9my1qogeiAqtjb2yR4LSmfU2+POvDkLzhjYLXz9Rf9+9a3zFHW1Lecg==", + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.18.1.tgz", + "integrity": "sha512-lncuC4aHicncmbORnx+dUaAgzee9cm/PbIqgWz1PpXuwc+sa1Ct83tnqUDy/GFKleLiN7ZIeytM6KJ4cAn1SxA==", "cpu": [ "arm" ], @@ -1684,9 +1857,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.9.6.tgz", - "integrity": "sha512-T14aNLpqJ5wzKNf5jEDpv5zgyIqcpn1MlwCrUXLrwoADr2RkWA0vOWP4XxbO9aiO3dvMCQICZdKeDrFl7UMClw==", + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.18.1.tgz", + "integrity": "sha512-F/tkdw0WSs4ojqz5Ovrw5r9odqzFjb5LIgHdHZG65dFI1lWTWRVy32KDJLKRISHgJvqUeUhdIvy43fX41znyDg==", "cpu": [ "arm64" ], @@ -1697,9 +1870,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.9.6.tgz", - "integrity": "sha512-CqNNAyhRkTbo8VVZ5R85X73H3R5NX9ONnKbXuHisGWC0qRbTTxnF1U4V9NafzJbgGM0sHZpdO83pLPzq8uOZFw==", + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.18.1.tgz", + "integrity": "sha512-vk+ma8iC1ebje/ahpxpnrfVQJibTMyHdWpOGZ3JpQ7Mgn/3QNHmPq7YwjZbIE7km73dH5M1e6MRRsnEBW7v5CQ==", "cpu": [ "arm64" ], @@ -1710,9 +1883,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.9.6.tgz", - "integrity": "sha512-zRDtdJuRvA1dc9Mp6BWYqAsU5oeLixdfUvkTHuiYOHwqYuQ4YgSmi6+/lPvSsqc/I0Omw3DdICx4Tfacdzmhog==", + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.18.1.tgz", + "integrity": "sha512-IgpzXKauRe1Tafcej9STjSSuG0Ghu/xGYH+qG6JwsAUxXrnkvNHcq/NL6nz1+jzvWAnQkuAJ4uIwGB48K9OCGA==", "cpu": [ "x64" ], @@ -1723,9 +1896,22 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.9.6.tgz", - "integrity": "sha512-oNk8YXDDnNyG4qlNb6is1ojTOGL/tRhbbKeE/YuccItzerEZT68Z9gHrY3ROh7axDc974+zYAPxK5SH0j/G+QQ==", + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.18.1.tgz", + "integrity": "sha512-P9bSiAUnSSM7EmyRK+e5wgpqai86QOSv8BwvkGjLwYuOpaeomiZWifEos517CwbG+aZl1T4clSE1YqqH2JRs+g==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.18.1.tgz", + "integrity": "sha512-5RnjpACoxtS+aWOI1dURKno11d7krfpGDEn19jI8BuWmSBbUC4ytIADfROM1FZrFhQPSoP+KEa3NlEScznBTyQ==", "cpu": [ "arm" ], @@ -1736,9 +1922,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.9.6.tgz", - "integrity": "sha512-Z3O60yxPtuCYobrtzjo0wlmvDdx2qZfeAWTyfOjEDqd08kthDKexLpV97KfAeUXPosENKd8uyJMRDfFMxcYkDQ==", + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.18.1.tgz", + "integrity": "sha512-8mwmGD668m8WaGbthrEYZ9CBmPug2QPGWxhJxh/vCgBjro5o96gL04WLlg5BA233OCWLqERy4YUzX3bJGXaJgQ==", "cpu": [ "arm64" ], @@ -1749,9 +1935,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.9.6.tgz", - "integrity": "sha512-gpiG0qQJNdYEVad+1iAsGAbgAnZ8j07FapmnIAQgODKcOTjLEWM9sRb+MbQyVsYCnA0Im6M6QIq6ax7liws6eQ==", + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.18.1.tgz", + "integrity": "sha512-dJX9u4r4bqInMGOAQoGYdwDP8lQiisWb9et+T84l2WXk41yEej8v2iGKodmdKimT8cTAYt0jFb+UEBxnPkbXEQ==", "cpu": [ "arm64" ], @@ -1761,10 +1947,23 @@ "linux" ] }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.18.1.tgz", + "integrity": "sha512-V72cXdTl4EI0x6FNmho4D502sy7ed+LuVW6Ym8aI6DRQ9hQZdp5sj0a2usYOlqvFBNKQnLQGwmYnujo2HvjCxQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.9.6.tgz", - "integrity": "sha512-+uCOcvVmFUYvVDr27aiyun9WgZk0tXe7ThuzoUTAukZJOwS5MrGbmSlNOhx1j80GdpqbOty05XqSl5w4dQvcOA==", + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.18.1.tgz", + "integrity": "sha512-f+pJih7sxoKmbjghrM2RkWo2WHUW8UbfxIQiWo5yeCaCM0TveMEuAzKJte4QskBp1TIinpnRcxkquY+4WuY/tg==", "cpu": [ "riscv64" ], @@ -1774,10 +1973,23 @@ "linux" ] }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.18.1.tgz", + "integrity": "sha512-qb1hMMT3Fr/Qz1OKovCuUM11MUNLUuHeBC2DPPAWUYYUAOFWaxInaTwTQmc7Fl5La7DShTEpmYwgdt2hG+4TEg==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.9.6.tgz", - "integrity": "sha512-HUNqM32dGzfBKuaDUBqFB7tP6VMN74eLZ33Q9Y1TBqRDn+qDonkAUyKWwF9BR9unV7QUzffLnz9GrnKvMqC/fw==", + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.18.1.tgz", + "integrity": "sha512-7O5u/p6oKUFYjRbZkL2FLbwsyoJAjyeXHCU3O4ndvzg2OFO2GinFPSJFGbiwFDaCFc+k7gs9CF243PwdPQFh5g==", "cpu": [ "x64" ], @@ -1788,9 +2000,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.9.6.tgz", - "integrity": "sha512-ch7M+9Tr5R4FK40FHQk8VnML0Szi2KRujUgHXd/HjuH9ifH72GUmw6lStZBo3c3GB82vHa0ZoUfjfcM7JiiMrQ==", + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.18.1.tgz", + "integrity": "sha512-pDLkYITdYrH/9Cv/Vlj8HppDuLMDUBmgsM0+N+xLtFd18aXgM9Nyqupb/Uw+HeidhfYg2lD6CXvz6CjoVOaKjQ==", "cpu": [ "x64" ], @@ -1801,9 +2013,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.9.6.tgz", - "integrity": "sha512-VD6qnR99dhmTQ1mJhIzXsRcTBvTjbfbGGwKAHcu+52cVl15AC/kplkhxzW/uT0Xl62Y/meBKDZvoJSJN+vTeGA==", + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.18.1.tgz", + "integrity": "sha512-W2ZNI323O/8pJdBGil1oCauuCzmVd9lDmWBBqxYZcOqWD6aWqJtVBQ1dFrF4dYpZPks6F+xCZHfzG5hYlSHZ6g==", "cpu": [ "arm64" ], @@ -1814,9 +2026,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.9.6.tgz", - "integrity": "sha512-J9AFDq/xiRI58eR2NIDfyVmTYGyIZmRcvcAoJ48oDld/NTR8wyiPUu2X/v1navJ+N/FGg68LEbX3Ejd6l8B7MQ==", + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.18.1.tgz", + "integrity": "sha512-ELfEX1/+eGZYMaCIbK4jqLxO1gyTSOIlZr6pbC4SRYFaSIDVKOnZNMdoZ+ON0mrFDp4+H5MhwNC1H/AhE3zQLg==", "cpu": [ "ia32" ], @@ -1827,9 +2039,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.9.6.tgz", - "integrity": "sha512-jqzNLhNDvIZOrt69Ce4UjGRpXJBzhUBzawMwnaDAwyHriki3XollsewxWzOzz+4yOFDkuJHtTsZFwMxhYJWmLQ==", + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.18.1.tgz", + "integrity": "sha512-yjk2MAkQmoaPYCSu35RLJ62+dz358nE83VfTePJRp8CG7aMg25mEJYpXFiD+NcevhX8LxD5OP5tktPXnXN7GDw==", "cpu": [ "x64" ], @@ -1840,15 +2052,9 @@ ] }, "node_modules/@rushstack/eslint-patch": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.7.2.tgz", - "integrity": "sha512-RbhOOTCNoCrbfkRyoXODZp75MlpiHMgbE5MEBZAnnnLyQNgrigEj4p0lzsMDyc1zVsJDLrivB58tgg3emX0eEA==", - "dev": true - }, - "node_modules/@sinclair/typebox": { - "version": "0.27.8", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", - "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.10.3.tgz", + "integrity": "sha512-qC/xYId4NMebE6w/V33Fh9gWxLgURiNYgVNObbJl2LZv0GUUItCcCqC5axQSwRaAgaxl2mELq1rMzlswaQ0Zxg==", "dev": true }, "node_modules/@types/estree": { @@ -1858,17 +2064,17 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.11.16", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.16.tgz", - "integrity": "sha512-gKb0enTmRCzXSSUJDq6/sPcqrfCv2mkkG6Jt/clpn5eiCbKTY+SgZUxo+p8ZKMof5dCp9vHQUAB7wOUTod22wQ==", + "version": "20.14.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.10.tgz", + "integrity": "sha512-MdiXf+nDuMvY0gJKxyfZ7/6UFsETO7mGKF54MVD/ekJS6HdFtpZFBgrh6Pseu64XTb2MLyFPlbW6hj8HYRQNOQ==", "dependencies": { "undici-types": "~5.26.4" } }, "node_modules/@types/readable-stream": { - "version": "4.0.10", - "resolved": "https://registry.npmjs.org/@types/readable-stream/-/readable-stream-4.0.10.tgz", - "integrity": "sha512-AbUKBjcC8SHmImNi4yK2bbjogQlkFSg7shZCcicxPQapniOlajG8GCc39lvXzCWX4lLRRs7DM3VAeSlqmEVZUA==", + "version": "4.0.15", + "resolved": "https://registry.npmjs.org/@types/readable-stream/-/readable-stream-4.0.15.tgz", + "integrity": "sha512-oAZ3kw+kJFkEqyh7xORZOku1YAKvsFTogRY8kVl4vHpEKiDkfnSA/My8haRE7fvmix5Zyy+1pwzOi7yycGLBJw==", "dependencies": { "@types/node": "*", "safe-buffer": "~5.1.1" @@ -1882,16 +2088,10 @@ "@types/node": "*" } }, - "node_modules/@ungap/structured-clone": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", - "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", - "dev": true - }, "node_modules/@vitejs/plugin-vue": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.0.4.tgz", - "integrity": "sha512-WS3hevEszI6CEVEx28F8RjTX97k3KsrcY6kvTg7+Whm5y3oYvcqzVeGCU3hxSAn4uY2CLCkeokkGKpoctccilQ==", + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.0.5.tgz", + "integrity": "sha512-LOjm7XeIimLBZyzinBQ6OSm3UBCNVCpLkxGC0oWmm2YPzVZoxMsdvNVimLTBzpAnR9hl/yn1SHGuRfe6/Td9rQ==", "dev": true, "engines": { "node": "^18.0.0 || >=20.0.0" @@ -1902,96 +2102,81 @@ } }, "node_modules/@vitest/expect": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.2.2.tgz", - "integrity": "sha512-3jpcdPAD7LwHUUiT2pZTj2U82I2Tcgg2oVPvKxhn6mDI2On6tfvPQTjAI4628GUGDZrCm4Zna9iQHm5cEexOAg==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.0.2.tgz", + "integrity": "sha512-nKAvxBYqcDugYZ4nJvnm5OR8eDJdgWjk4XM9owQKUjzW70q0icGV2HVnQOyYsp906xJaBDUXw0+9EHw2T8e0mQ==", "dev": true, "dependencies": { - "@vitest/spy": "1.2.2", - "@vitest/utils": "1.2.2", - "chai": "^4.3.10" + "@vitest/spy": "2.0.2", + "@vitest/utils": "2.0.2", + "chai": "^5.1.1", + "tinyrainbow": "^1.2.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, - "node_modules/@vitest/runner": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.2.2.tgz", - "integrity": "sha512-JctG7QZ4LSDXr5CsUweFgcpEvrcxOV1Gft7uHrvkQ+fsAVylmWQvnaAr/HDp3LAH1fztGMQZugIheTWjaGzYIg==", + "node_modules/@vitest/pretty-format": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.0.2.tgz", + "integrity": "sha512-SBCyOXfGVvddRd9r2PwoVR0fonQjh9BMIcBMlSzbcNwFfGr6ZhOhvBzurjvi2F4ryut2HcqiFhNeDVGwru8tLg==", "dev": true, "dependencies": { - "@vitest/utils": "1.2.2", - "p-limit": "^5.0.0", - "pathe": "^1.1.1" + "tinyrainbow": "^1.2.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, - "node_modules/@vitest/runner/node_modules/p-limit": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-5.0.0.tgz", - "integrity": "sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==", + "node_modules/@vitest/runner": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.0.2.tgz", + "integrity": "sha512-OCh437Vi8Wdbif1e0OvQcbfM3sW4s2lpmOjAE7qfLrpzJX2M7J1IQlNvEcb/fu6kaIB9n9n35wS0G2Q3en5kHg==", "dev": true, "dependencies": { - "yocto-queue": "^1.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@vitest/runner/node_modules/yocto-queue": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz", - "integrity": "sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==", - "dev": true, - "engines": { - "node": ">=12.20" + "@vitest/utils": "2.0.2", + "pathe": "^1.1.2" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/snapshot": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.2.2.tgz", - "integrity": "sha512-SmGY4saEw1+bwE1th6S/cZmPxz/Q4JWsl7LvbQIky2tKE35US4gd0Mjzqfr84/4OD0tikGWaWdMja/nWL5NIPA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.0.2.tgz", + "integrity": "sha512-Yc2ewhhZhx+0f9cSUdfzPRcsM6PhIb+S43wxE7OG0kTxqgqzo8tHkXFuFlndXeDMp09G3sY/X5OAo/RfYydf1g==", "dev": true, "dependencies": { - "magic-string": "^0.30.5", - "pathe": "^1.1.1", - "pretty-format": "^29.7.0" + "@vitest/pretty-format": "2.0.2", + "magic-string": "^0.30.10", + "pathe": "^1.1.2" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/spy": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.2.2.tgz", - "integrity": "sha512-k9Gcahssw8d7X3pSLq3e3XEu/0L78mUkCjivUqCQeXJm9clfXR/Td8+AP+VC1O6fKPIDLcHDTAmBOINVuv6+7g==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.0.2.tgz", + "integrity": "sha512-MgwJ4AZtCgqyp2d7WcQVE8aNG5vQ9zu9qMPYQHjsld/QVsrvg78beNrXdO4HYkP0lDahCO3P4F27aagIag+SGQ==", "dev": true, "dependencies": { - "tinyspy": "^2.2.0" + "tinyspy": "^3.0.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/utils": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.2.2.tgz", - "integrity": "sha512-WKITBHLsBHlpjnDQahr+XK6RE7MiAsgrIkr0pGhQ9ygoxBfUeG0lUG5iLlzqjmKSlBv3+j5EGsriBzh+C3Tq9g==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.0.2.tgz", + "integrity": "sha512-pxCY1v7kmOCWYWjzc0zfjGTA3Wmn8PKnlPvSrsA643P1NHl1fOyXj2Q9SaNlrlFE+ivCsxM80Ov3AR82RmHCWQ==", "dev": true, "dependencies": { - "diff-sequences": "^29.6.3", + "@vitest/pretty-format": "2.0.2", "estree-walker": "^3.0.3", - "loupe": "^2.3.7", - "pretty-format": "^29.7.0" + "loupe": "^3.1.1", + "tinyrainbow": "^1.2.0" }, "funding": { "url": "https://opencollective.com/vitest" @@ -2007,55 +2192,55 @@ } }, "node_modules/@vue/compiler-core": { - "version": "3.4.19", - "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.4.19.tgz", - "integrity": "sha512-gj81785z0JNzRcU0Mq98E56e4ltO1yf8k5PQ+tV/7YHnbZkrM0fyFyuttnN8ngJZjbpofWE/m4qjKBiLl8Ju4w==", + "version": "3.4.31", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.4.31.tgz", + "integrity": "sha512-skOiodXWTV3DxfDhB4rOf3OGalpITLlgCeOwb+Y9GJpfQ8ErigdBUHomBzvG78JoVE8MJoQsb+qhZiHfKeNeEg==", "dependencies": { - "@babel/parser": "^7.23.9", - "@vue/shared": "3.4.19", + "@babel/parser": "^7.24.7", + "@vue/shared": "3.4.31", "entities": "^4.5.0", "estree-walker": "^2.0.2", - "source-map-js": "^1.0.2" + "source-map-js": "^1.2.0" } }, "node_modules/@vue/compiler-dom": { - "version": "3.4.19", - "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.4.19.tgz", - "integrity": "sha512-vm6+cogWrshjqEHTzIDCp72DKtea8Ry/QVpQRYoyTIg9k7QZDX6D8+HGURjtmatfgM8xgCFtJJaOlCaRYRK3QA==", + "version": "3.4.31", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.4.31.tgz", + "integrity": "sha512-wK424WMXsG1IGMyDGyLqB+TbmEBFM78hIsOJ9QwUVLGrcSk0ak6zYty7Pj8ftm7nEtdU/DGQxAXp0/lM/2cEpQ==", "dependencies": { - "@vue/compiler-core": "3.4.19", - "@vue/shared": "3.4.19" + "@vue/compiler-core": "3.4.31", + "@vue/shared": "3.4.31" } }, "node_modules/@vue/compiler-sfc": { - "version": "3.4.19", - "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.4.19.tgz", - "integrity": "sha512-LQ3U4SN0DlvV0xhr1lUsgLCYlwQfUfetyPxkKYu7dkfvx7g3ojrGAkw0AERLOKYXuAGnqFsEuytkdcComei3Yg==", - "dependencies": { - "@babel/parser": "^7.23.9", - "@vue/compiler-core": "3.4.19", - "@vue/compiler-dom": "3.4.19", - "@vue/compiler-ssr": "3.4.19", - "@vue/shared": "3.4.19", + "version": "3.4.31", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.4.31.tgz", + "integrity": "sha512-einJxqEw8IIJxzmnxmJBuK2usI+lJonl53foq+9etB2HAzlPjAS/wa7r0uUpXw5ByX3/0uswVSrjNb17vJm1kQ==", + "dependencies": { + "@babel/parser": "^7.24.7", + "@vue/compiler-core": "3.4.31", + "@vue/compiler-dom": "3.4.31", + "@vue/compiler-ssr": "3.4.31", + "@vue/shared": "3.4.31", "estree-walker": "^2.0.2", - "magic-string": "^0.30.6", - "postcss": "^8.4.33", - "source-map-js": "^1.0.2" + "magic-string": "^0.30.10", + "postcss": "^8.4.38", + "source-map-js": "^1.2.0" } }, "node_modules/@vue/compiler-ssr": { - "version": "3.4.19", - "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.4.19.tgz", - "integrity": "sha512-P0PLKC4+u4OMJ8sinba/5Z/iDT84uMRRlrWzadgLA69opCpI1gG4N55qDSC+dedwq2fJtzmGald05LWR5TFfLw==", + "version": "3.4.31", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.4.31.tgz", + "integrity": "sha512-RtefmITAje3fJ8FSg1gwgDhdKhZVntIVbwupdyZDSifZTRMiWxWehAOTCc8/KZDnBOcYQ4/9VWxsTbd3wT0hAA==", "dependencies": { - "@vue/compiler-dom": "3.4.19", - "@vue/shared": "3.4.19" + "@vue/compiler-dom": "3.4.31", + "@vue/shared": "3.4.31" } }, "node_modules/@vue/devtools-api": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.5.1.tgz", - "integrity": "sha512-+KpckaAQyfbvshdDW5xQylLni1asvNSGme1JFs8I1+/H5pHEhqUKMEQD/qn3Nx5+/nycBq11qAEi8lk+LXI2dA==" + "version": "6.6.3", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.3.tgz", + "integrity": "sha512-0MiMsFma/HqA6g3KLKn+AGpL1kgKhFWszC9U29NfpWK5LE7bjeXxySWJrOJ77hBz+TBrBQ7o4QJqbPbqbs8rJw==" }, "node_modules/@vue/eslint-config-prettier": { "version": "9.0.0", @@ -2072,66 +2257,58 @@ } }, "node_modules/@vue/reactivity": { - "version": "3.4.19", - "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.4.19.tgz", - "integrity": "sha512-+VcwrQvLZgEclGZRHx4O2XhyEEcKaBi50WbxdVItEezUf4fqRh838Ix6amWTdX0CNb/b6t3Gkz3eOebfcSt+UA==", + "version": "3.4.31", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.4.31.tgz", + "integrity": "sha512-VGkTani8SOoVkZNds1PfJ/T1SlAIOf8E58PGAhIOUDYPC4GAmFA2u/E14TDAFcf3vVDKunc4QqCe/SHr8xC65Q==", "dependencies": { - "@vue/shared": "3.4.19" + "@vue/shared": "3.4.31" } }, "node_modules/@vue/runtime-core": { - "version": "3.4.19", - "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.4.19.tgz", - "integrity": "sha512-/Z3tFwOrerJB/oyutmJGoYbuoadphDcJAd5jOuJE86THNZji9pYjZroQ2NFsZkTxOq0GJbb+s2kxTYToDiyZzw==", + "version": "3.4.31", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.4.31.tgz", + "integrity": "sha512-LDkztxeUPazxG/p8c5JDDKPfkCDBkkiNLVNf7XZIUnJ+66GVGkP+TIh34+8LtPisZ+HMWl2zqhIw0xN5MwU1cw==", "dependencies": { - "@vue/reactivity": "3.4.19", - "@vue/shared": "3.4.19" + "@vue/reactivity": "3.4.31", + "@vue/shared": "3.4.31" } }, "node_modules/@vue/runtime-dom": { - "version": "3.4.19", - "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.4.19.tgz", - "integrity": "sha512-IyZzIDqfNCF0OyZOauL+F4yzjMPN2rPd8nhqPP2N1lBn3kYqJpPHHru+83Rkvo2lHz5mW+rEeIMEF9qY3PB94g==", + "version": "3.4.31", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.4.31.tgz", + "integrity": "sha512-2Auws3mB7+lHhTFCg8E9ZWopA6Q6L455EcU7bzcQ4x6Dn4cCPuqj6S2oBZgN2a8vJRS/LSYYxwFFq2Hlx3Fsaw==", "dependencies": { - "@vue/runtime-core": "3.4.19", - "@vue/shared": "3.4.19", + "@vue/reactivity": "3.4.31", + "@vue/runtime-core": "3.4.31", + "@vue/shared": "3.4.31", "csstype": "^3.1.3" } }, "node_modules/@vue/server-renderer": { - "version": "3.4.19", - "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.4.19.tgz", - "integrity": "sha512-eAj2p0c429RZyyhtMRnttjcSToch+kTWxFPHlzGMkR28ZbF1PDlTcmGmlDxccBuqNd9iOQ7xPRPAGgPVj+YpQw==", + "version": "3.4.31", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.4.31.tgz", + "integrity": "sha512-D5BLbdvrlR9PE3by9GaUp1gQXlCNadIZytMIb8H2h3FMWJd4oUfkUTEH2wAr3qxoRz25uxbTcbqd3WKlm9EHQA==", "dependencies": { - "@vue/compiler-ssr": "3.4.19", - "@vue/shared": "3.4.19" + "@vue/compiler-ssr": "3.4.31", + "@vue/shared": "3.4.31" }, "peerDependencies": { - "vue": "3.4.19" + "vue": "3.4.31" } }, "node_modules/@vue/shared": { - "version": "3.4.19", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.19.tgz", - "integrity": "sha512-/KliRRHMF6LoiThEy+4c1Z4KB/gbPrGjWwJR+crg2otgrf/egKzRaCPvJ51S5oetgsgXLfc4Rm5ZgrKHZrtMSw==" + "version": "3.4.31", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.31.tgz", + "integrity": "sha512-Yp3wtJk//8cO4NItOPpi3QkLExAr/aLBGZMmTtW9WpdwBCJpRM6zj9WgWktXAl8IDIozwNMByT45JP3tO3ACWA==" }, "node_modules/@vue/test-utils": { - "version": "2.4.4", - "resolved": "https://registry.npmjs.org/@vue/test-utils/-/test-utils-2.4.4.tgz", - "integrity": "sha512-8jkRxz8pNhClAf4Co4ZrpAoFISdvT3nuSkUlY6Ys6rmTpw3DMWG/X3mw3gQ7QJzgCZO9f+zuE2kW57fi09MW7Q==", + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/@vue/test-utils/-/test-utils-2.4.6.tgz", + "integrity": "sha512-FMxEjOpYNYiFe0GkaHsnJPXFHxQ6m4t8vI/ElPGpMWxZKpmRvQ33OIrvRXemy6yha03RxhOlQuy+gZMC3CQSow==", "dev": true, "dependencies": { "js-beautify": "^1.14.9", - "vue-component-type-helpers": "^1.8.21" - }, - "peerDependencies": { - "@vue/server-renderer": "^3.0.1", - "vue": "^3.0.1" - }, - "peerDependenciesMeta": { - "@vue/server-renderer": { - "optional": true - } + "vue-component-type-helpers": "^2.0.0" } }, "node_modules/abbrev": { @@ -2155,9 +2332,9 @@ } }, "node_modules/acorn": { - "version": "8.11.3", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", - "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "version": "8.12.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", + "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", "dev": true, "bin": { "acorn": "bin/acorn" @@ -2175,19 +2352,10 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, - "node_modules/acorn-walk": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz", - "integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==", - "dev": true, - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/agent-base": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.0.tgz", - "integrity": "sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", "dev": true, "dependencies": { "debug": "^4.3.4" @@ -2256,14 +2424,13 @@ "dev": true }, "node_modules/asn1.js": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz", - "integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==", + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz", + "integrity": "sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==", "dependencies": { "bn.js": "^4.0.0", "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0", - "safer-buffer": "^2.1.0" + "minimalistic-assert": "^1.0.0" } }, "node_modules/asn1.js/node_modules/bn.js": { @@ -2284,12 +2451,12 @@ } }, "node_modules/assertion-error": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", - "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", "dev": true, "engines": { - "node": "*" + "node": ">=12" } }, "node_modules/asynckit": { @@ -2299,9 +2466,9 @@ "dev": true }, "node_modules/autoprefixer": { - "version": "10.4.17", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.17.tgz", - "integrity": "sha512-/cpVNRLSfhOtcGflT13P2794gVSgmPgTR+erw5ifnMLZb0UnSlkK4tquLmkd3BhA+nLo5tX8Cu0upUsGKvKbmg==", + "version": "10.4.19", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.19.tgz", + "integrity": "sha512-BaENR2+zBZ8xXhM4pUaKUxlVdxZ0EZhjvbopwnXmxRUfqDmwSpC2lAi/QXvx7NRdPCo1WKEcEF6mV64si1z4Ew==", "dev": true, "funding": [ { @@ -2318,8 +2485,8 @@ } ], "dependencies": { - "browserslist": "^4.22.2", - "caniuse-lite": "^1.0.30001578", + "browserslist": "^4.23.0", + "caniuse-lite": "^1.0.30001599", "fraction.js": "^4.3.7", "normalize-range": "^0.1.2", "picocolors": "^1.0.0", @@ -2336,9 +2503,12 @@ } }, "node_modules/available-typed-arrays": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.6.tgz", - "integrity": "sha512-j1QzY8iPNPG4o4xmO3ptzpRxTciqD3MgEHtifP/YnJpIo58Xu+ne4BejlbkuaLfXn/nz6HFiw29bLpj2PNMdGg==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, "engines": { "node": ">= 0.4" }, @@ -2372,19 +2542,23 @@ ] }, "node_modules/binary-extensions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", - "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", "dev": true, "engines": { "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/bl": { - "version": "6.0.10", - "resolved": "https://registry.npmjs.org/bl/-/bl-6.0.10.tgz", - "integrity": "sha512-F14DFhDZfxtVm2FY0k9kG2lWAwzZkO9+jX3Ytuoy/V0E1/5LBuBzzQHXAjqpxXEDIpmTPZZf5GVIGPQcLxFpaA==", + "version": "6.0.14", + "resolved": "https://registry.npmjs.org/bl/-/bl-6.0.14.tgz", + "integrity": "sha512-TJfbvGdL7KFGxTsEbsED7avqpFdY56q9IW0/aiytyheJzxST/+Io6cx/4Qx0K2/u0BPRDs65mjaQzYvMZeNocQ==", "dependencies": { + "@types/readable-stream": "^4.0.0", "buffer": "^6.0.3", "inherits": "^2.0.4", "readable-stream": "^4.2.0" @@ -2412,12 +2586,12 @@ } }, "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" @@ -2480,37 +2654,44 @@ } }, "node_modules/browserify-sign": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.2.tgz", - "integrity": "sha512-1rudGyeYY42Dk6texmv7c4VcQ0EsvVbLwZkA+AQB7SxvXxmcD93jcHie8bzecJ+ChDlmAm2Qyu0+Ccg5uhZXCg==", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.3.tgz", + "integrity": "sha512-JWCZW6SKhfhjJxO8Tyiiy+XYB7cqd2S5/+WeYHsKdNKFlCBhKbblba1A/HN/90YwtxKc8tCErjffZl++UNmGiw==", "dependencies": { "bn.js": "^5.2.1", "browserify-rsa": "^4.1.0", "create-hash": "^1.2.0", "create-hmac": "^1.1.7", - "elliptic": "^6.5.4", + "elliptic": "^6.5.5", + "hash-base": "~3.0", "inherits": "^2.0.4", - "parse-asn1": "^5.1.6", - "readable-stream": "^3.6.2", + "parse-asn1": "^5.1.7", + "readable-stream": "^2.3.8", "safe-buffer": "^5.2.1" }, "engines": { - "node": ">= 4" + "node": ">= 0.12" } }, "node_modules/browserify-sign/node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" } }, + "node_modules/browserify-sign/node_modules/readable-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, "node_modules/browserify-sign/node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -2530,6 +2711,19 @@ } ] }, + "node_modules/browserify-sign/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/browserify-sign/node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, "node_modules/browserify-zlib": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz", @@ -2539,9 +2733,9 @@ } }, "node_modules/browserslist": { - "version": "4.22.3", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.3.tgz", - "integrity": "sha512-UAp55yfwNv0klWNapjs/ktHoguxuQNGnOzxYmfnXIS+8AsRDZkSDxg7R1AX3GKzn078SBI5dzwzj/Yx0Or0e3A==", + "version": "4.23.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.2.tgz", + "integrity": "sha512-qkqSyistMYdxAcw+CzbZwlBy8AGmS/eEWs+sEV5TnLRGDOL+C5M2EnH6tlZyg0YoAxGJAFKh61En9BR941GnHA==", "dev": true, "funding": [ { @@ -2558,10 +2752,10 @@ } ], "dependencies": { - "caniuse-lite": "^1.0.30001580", - "electron-to-chromium": "^1.4.648", + "caniuse-lite": "^1.0.30001640", + "electron-to-chromium": "^1.4.820", "node-releases": "^2.0.14", - "update-browserslist-db": "^1.0.13" + "update-browserslist-db": "^1.1.0" }, "bin": { "browserslist": "cli.js" @@ -2618,13 +2812,18 @@ } }, "node_modules/call-bind": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", - "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.1", - "set-function-length": "^1.1.1" + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -2640,9 +2839,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001583", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001583.tgz", - "integrity": "sha512-acWTYaha8xfhA/Du/z4sNZjHUWjkiuoAi2LM+T/aL+kemKQgPT1xBb/YKjlQ0Qo8gvbHsGNplrEJ+9G3gL7i4Q==", + "version": "1.0.30001641", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001641.tgz", + "integrity": "sha512-Phv5thgl67bHYo1TtMY/MurjkHhV4EDaCosezRXgZ8jzA/Ub+wjxAvbGvjoFENStinwi5kCyOYV3mi5tOGykwA==", "dev": true, "funding": [ { @@ -2660,21 +2859,19 @@ ] }, "node_modules/chai": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.4.1.tgz", - "integrity": "sha512-13sOfMv2+DWduEU+/xbun3LScLoqN17nBeTLUsmDfKdoiC1fr0n9PU4guu4AhRcOVFk/sW8LyZWHuhWtQZiF+g==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.1.1.tgz", + "integrity": "sha512-pT1ZgP8rPNqUgieVaEY+ryQr6Q4HXNg8Ei9UnLUrjN4IA7dvQC5JB+/kxVcPNDHyBcc/26CXPkbNzq3qwrOEKA==", "dev": true, "dependencies": { - "assertion-error": "^1.1.0", - "check-error": "^1.0.3", - "deep-eql": "^4.1.3", - "get-func-name": "^2.0.2", - "loupe": "^2.3.6", - "pathval": "^1.1.1", - "type-detect": "^4.0.8" + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" }, "engines": { - "node": ">=4" + "node": ">=12" } }, "node_modules/chalk": { @@ -2694,28 +2891,19 @@ } }, "node_modules/check-error": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", - "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", + "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", "dev": true, - "dependencies": { - "get-func-name": "^2.0.2" - }, "engines": { - "node": "*" + "node": ">= 16" } }, "node_modules/chokidar": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", - "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ], "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", @@ -2728,6 +2916,9 @@ "engines": { "node": ">= 8.10.0" }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, "optionalDependencies": { "fsevents": "~2.3.2" } @@ -2850,6 +3041,11 @@ "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz", "integrity": "sha512-xFxOwqIzR/e1k1gLiWEophSCMqXcwVHIH7akf7b/vxcUeGunlj3hvZaaqxwHsTgn+IndtkQJgSztIDWeumWJDQ==" }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" + }, "node_modules/create-ecdh": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.4.tgz", @@ -2930,9 +3126,9 @@ } }, "node_modules/css-blank-pseudo": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/css-blank-pseudo/-/css-blank-pseudo-6.0.1.tgz", - "integrity": "sha512-goSnEITByxTzU4Oh5oJZrEWudxTqk7L6IXj1UW69pO6Hv0UdX+Vsrt02FFu5DweRh2bLu6WpX/+zsQCu5O1gKw==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/css-blank-pseudo/-/css-blank-pseudo-6.0.2.tgz", + "integrity": "sha512-J/6m+lsqpKPqWHOifAFtKFeGLOzw3jR92rxQcwRUfA/eTuZzKfKlxOmYDx2+tqOPQAueNvBiY8WhAeHu5qNmTg==", "dev": true, "funding": [ { @@ -2955,9 +3151,9 @@ } }, "node_modules/css-has-pseudo": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/css-has-pseudo/-/css-has-pseudo-6.0.1.tgz", - "integrity": "sha512-WwoVKqNxApfEI7dWFyaHoeFCcUPD+lPyjL6lNpRUNX7IyIUuVpawOTwwA5D0ZR6V2xQZonNPVj8kEcxzEaAQfQ==", + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/css-has-pseudo/-/css-has-pseudo-6.0.5.tgz", + "integrity": "sha512-ZTv6RlvJJZKp32jPYnAJVhowDCrRrHUTAxsYSuUPBEDJjzws6neMnzkRblxtgmv1RgcV5dhH2gn7E3wA9Wt6lw==", "dev": true, "funding": [ { @@ -2970,7 +3166,7 @@ } ], "dependencies": { - "@csstools/selector-specificity": "^3.0.1", + "@csstools/selector-specificity": "^3.1.1", "postcss-selector-parser": "^6.0.13", "postcss-value-parser": "^4.2.0" }, @@ -3004,9 +3200,9 @@ } }, "node_modules/cssdb": { - "version": "7.10.0", - "resolved": "https://registry.npmjs.org/cssdb/-/cssdb-7.10.0.tgz", - "integrity": "sha512-yGZ5tmA57gWh/uvdQBHs45wwFY0IBh3ypABk5sEubPBPSzXzkNgsWReqx7gdx6uhC+QoFBe+V8JwBB9/hQ6cIA==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/cssdb/-/cssdb-8.1.0.tgz", + "integrity": "sha512-BQN57lfS4dYt2iL0LgyrlDbefZKEtUyrO8rbzrbGrqBk6OoyNTQLF+porY9DrpDBjLo4NEvj2IJttC7vf3x+Ew==", "dev": true, "funding": [ { @@ -3043,6 +3239,12 @@ "node": ">=18" } }, + "node_modules/cssstyle/node_modules/rrweb-cssom": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.6.0.tgz", + "integrity": "sha512-APM0Gt1KoXBz0iIkkdB/kfvGOwC4UuJFeG/c+yV7wSc7q96cG/kJ0HiYCnzivD9SB53cLV1MlHFNfOuPaadYSw==", + "dev": true + }, "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", @@ -3062,9 +3264,9 @@ } }, "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", + "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", "dependencies": { "ms": "2.1.2" }, @@ -3084,13 +3286,10 @@ "dev": true }, "node_modules/deep-eql": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz", - "integrity": "sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", "dev": true, - "dependencies": { - "type-detect": "^4.0.0" - }, "engines": { "node": ">=6" } @@ -3102,16 +3301,19 @@ "dev": true }, "node_modules/define-data-property": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz", - "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", "dependencies": { - "get-intrinsic": "^1.2.1", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.0" + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" }, "engines": { "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/define-properties": { @@ -3148,15 +3350,6 @@ "minimalistic-assert": "^1.0.0" } }, - "node_modules/diff-sequences": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", - "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", - "dev": true, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, "node_modules/diffie-hellman": { "version": "5.0.3", "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", @@ -3172,18 +3365,6 @@ "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" }, - "node_modules/doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "dev": true, - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/domain-browser": { "version": "4.23.0", "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-4.23.0.tgz", @@ -3244,15 +3425,15 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.4.656", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.656.tgz", - "integrity": "sha512-9AQB5eFTHyR3Gvt2t/NwR0le2jBSUNwCnMbUCejFWHD+so4tH40/dRLgoE+jxlPeWS43XJewyvCv+I8LPMl49Q==", + "version": "1.4.825", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.825.tgz", + "integrity": "sha512-OCcF+LwdgFGcsYPYC5keEEFC2XT0gBhrYbeGzHCx7i9qRFbzO/AqTmc/C/1xNhJj+JA7rzlN7mpBuStshh96Cg==", "dev": true }, "node_modules/elliptic": { - "version": "6.5.4", - "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz", - "integrity": "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==", + "version": "6.5.5", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.5.tgz", + "integrity": "sha512-7EjbcmUm17NQFu4Pmgmq2olYMj8nwMnpcddByChSUjArp8F5DQWcIcpriwO4ZToLNAJig0yiyjswfyGNje/ixw==", "dependencies": { "bn.js": "^4.11.9", "brorand": "^1.1.0", @@ -3285,18 +3466,29 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/es-errors": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.1.0.tgz", - "integrity": "sha512-ka/z/Hxav2YGgkzSwOp1ugbUk6fgIX5gI69PfRHCvODD+LuVOnV1jHPBWXBNPZqX0O900p2I+IdM9sEbac0BNA==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", "engines": { "node": ">= 0.4" } }, "node_modules/esbuild": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz", - "integrity": "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", "dev": true, "hasInstallScript": true, "bin": { @@ -3306,35 +3498,35 @@ "node": ">=12" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.19.12", - "@esbuild/android-arm": "0.19.12", - "@esbuild/android-arm64": "0.19.12", - "@esbuild/android-x64": "0.19.12", - "@esbuild/darwin-arm64": "0.19.12", - "@esbuild/darwin-x64": "0.19.12", - "@esbuild/freebsd-arm64": "0.19.12", - "@esbuild/freebsd-x64": "0.19.12", - "@esbuild/linux-arm": "0.19.12", - "@esbuild/linux-arm64": "0.19.12", - "@esbuild/linux-ia32": "0.19.12", - "@esbuild/linux-loong64": "0.19.12", - "@esbuild/linux-mips64el": "0.19.12", - "@esbuild/linux-ppc64": "0.19.12", - "@esbuild/linux-riscv64": "0.19.12", - "@esbuild/linux-s390x": "0.19.12", - "@esbuild/linux-x64": "0.19.12", - "@esbuild/netbsd-x64": "0.19.12", - "@esbuild/openbsd-x64": "0.19.12", - "@esbuild/sunos-x64": "0.19.12", - "@esbuild/win32-arm64": "0.19.12", - "@esbuild/win32-ia32": "0.19.12", - "@esbuild/win32-x64": "0.19.12" + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" } }, "node_modules/escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", + "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", "dev": true, "engines": { "node": ">=6" @@ -3353,41 +3545,37 @@ } }, "node_modules/eslint": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.56.0.tgz", - "integrity": "sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ==", + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.6.0.tgz", + "integrity": "sha512-ElQkdLMEEqQNM9Njff+2Y4q2afHk7JpkPvrd7Xh7xefwgQynqPxwf55J7di9+MEibWUGdNjFF9ITG9Pck5M84w==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.56.0", - "@humanwhocodes/config-array": "^0.11.13", + "@eslint/config-array": "^0.17.0", + "@eslint/eslintrc": "^3.1.0", + "@eslint/js": "9.6.0", "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.3.0", "@nodelib/fs.walk": "^1.2.8", - "@ungap/structured-clone": "^1.2.0", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", "debug": "^4.3.2", - "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.2", - "eslint-visitor-keys": "^3.4.3", - "espree": "^9.6.1", - "esquery": "^1.4.2", + "eslint-scope": "^8.0.1", + "eslint-visitor-keys": "^4.0.0", + "espree": "^10.1.0", + "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", + "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "graphemer": "^1.4.0", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "is-path-inside": "^3.0.3", - "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.4.1", "lodash.merge": "^4.6.2", @@ -3401,10 +3589,10 @@ "eslint": "bin/eslint.js" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://opencollective.com/eslint" + "url": "https://eslint.org/donate" } }, "node_modules/eslint-config-prettier": { @@ -3450,75 +3638,91 @@ } }, "node_modules/eslint-plugin-vue": { - "version": "9.21.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.21.1.tgz", - "integrity": "sha512-XVtI7z39yOVBFJyi8Ljbn7kY9yHzznKXL02qQYn+ta63Iy4A9JFBw6o4OSB9hyD2++tVT+su9kQqetUyCCwhjw==", + "version": "9.27.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.27.0.tgz", + "integrity": "sha512-5Dw3yxEyuBSXTzT5/Ge1X5kIkRTQ3nvBn/VwPwInNiZBSJOO/timWMUaflONnFBzU6NhB68lxnCda7ULV5N7LA==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", + "globals": "^13.24.0", "natural-compare": "^1.4.0", "nth-check": "^2.1.1", - "postcss-selector-parser": "^6.0.13", - "semver": "^7.5.4", - "vue-eslint-parser": "^9.4.2", + "postcss-selector-parser": "^6.0.15", + "semver": "^7.6.0", + "vue-eslint-parser": "^9.4.3", "xml-name-validator": "^4.0.0" }, "engines": { "node": "^14.17.0 || >=16.0.0" }, "peerDependencies": { - "eslint": "^6.2.0 || ^7.0.0 || ^8.0.0" + "eslint": "^6.2.0 || ^7.0.0 || ^8.0.0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-vue/node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/eslint-scope": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", - "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.0.1.tgz", + "integrity": "sha512-pL8XjgP4ZOmmwfFE8mEhSxA7ZY4C+LWyqjQ3o4yWkkmD0qcMT9kkW3zWHOczhWcjTSgqycYAgwSlXvZltv65og==", "dev": true, "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" } }, "node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz", + "integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==", "dev": true, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" } }, "node_modules/espree": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", - "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.1.0.tgz", + "integrity": "sha512-M1M6CpiE6ffoigIOWYO9UDP8TMUw9kqb21tf+08IgDYjCsOvCuDt4jQcZmoYxx+w7zlKw9/N0KXfto+I8/FrXA==", "dev": true, "dependencies": { - "acorn": "^8.9.0", + "acorn": "^8.12.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" + "eslint-visitor-keys": "^4.0.0" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" } }, "node_modules/esquery": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", - "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", "dev": true, "dependencies": { "estraverse": "^5.1.0" @@ -3647,30 +3851,30 @@ } }, "node_modules/fastq": { - "version": "1.17.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.0.tgz", - "integrity": "sha512-zGygtijUMT7jnk3h26kUms3BkSDp4IfIKjmnqI2tvx6nuBfiF1UqOxbnLfzdv+apBy+53oaImsKtMw/xYbW+1w==", + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", "dev": true, "dependencies": { "reusify": "^1.0.4" } }, "node_modules/file-entry-cache": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", - "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", "dev": true, "dependencies": { - "flat-cache": "^3.0.4" + "flat-cache": "^4.0.0" }, "engines": { - "node": "^10.12.0 || >=12.0.0" + "node": ">=16.0.0" } }, "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, "dependencies": { "to-regex-range": "^5.0.1" @@ -3695,23 +3899,22 @@ } }, "node_modules/flat-cache": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", - "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", "dev": true, "dependencies": { "flatted": "^3.2.9", - "keyv": "^4.5.3", - "rimraf": "^3.0.2" + "keyv": "^4.5.4" }, "engines": { - "node": "^10.12.0 || >=12.0.0" + "node": ">=16" } }, "node_modules/flatted": { - "version": "3.2.9", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz", - "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", + "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", "dev": true }, "node_modules/for-each": { @@ -3723,9 +3926,9 @@ } }, "node_modules/foreground-child": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", - "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.2.1.tgz", + "integrity": "sha512-PXUUyLqrR2XCWICfv6ukppP96sdFwWbNEnfEMt7jNsISjMsvaLNinAHNDYyvkyU+SZG2BTSbT5NjG+vZslfGTA==", "dev": true, "dependencies": { "cross-spawn": "^7.0.0", @@ -3765,12 +3968,6 @@ "url": "https://github.com/sponsors/rawify" } }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true - }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -3803,11 +4000,11 @@ } }, "node_modules/get-intrinsic": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.3.tgz", - "integrity": "sha512-JIcZczvcMVE7AUOP+X72bh8HqHBRxFdz5PDHYtNG/lE3yk9b3KZBJlwFcTyPYjg3L4RLLmZJzvjxhaZVapxFrQ==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", "dependencies": { - "es-errors": "^1.0.0", + "es-errors": "^1.3.0", "function-bind": "^1.1.2", "has-proto": "^1.0.1", "has-symbols": "^1.0.3", @@ -3833,23 +4030,21 @@ } }, "node_modules/glob": { - "version": "10.3.10", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", - "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", "dev": true, "dependencies": { "foreground-child": "^3.1.0", - "jackspeak": "^2.3.5", - "minimatch": "^9.0.1", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", - "path-scurry": "^1.10.1" + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, "funding": { "url": "https://github.com/sponsors/isaacs" } @@ -3876,9 +4071,9 @@ } }, "node_modules/glob/node_modules/minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "dev": true, "dependencies": { "brace-expansion": "^2.0.1" @@ -3891,15 +4086,12 @@ } }, "node_modules/globals": { - "version": "13.24.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", - "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", "dev": true, - "dependencies": { - "type-fest": "^0.20.2" - }, "engines": { - "node": ">=8" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -3916,12 +4108,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/graphemer": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true - }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -3932,20 +4118,20 @@ } }, "node_modules/has-property-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", - "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", "dependencies": { - "get-intrinsic": "^1.2.2" + "es-define-property": "^1.0.0" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/has-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", - "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", "engines": { "node": ">= 0.4" }, @@ -3979,50 +4165,17 @@ } }, "node_modules/hash-base": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.0.tgz", - "integrity": "sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.4.tgz", + "integrity": "sha512-EeeoJKjTyt868liAlVmcv2ZsUfGHlE3Q+BICOXcZiwN3osr5Q/zFGYmTJpoIzuaSTAwndFy+GqhEwlU4L3j4Ow==", "dependencies": { - "inherits": "^2.0.4", - "readable-stream": "^3.6.0", - "safe-buffer": "^5.2.0" + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" }, "engines": { "node": ">=4" } }, - "node_modules/hash-base/node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/hash-base/node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, "node_modules/hash.js": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", @@ -4033,9 +4186,9 @@ } }, "node_modules/hasown": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", - "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "dependencies": { "function-bind": "^1.1.2" }, @@ -4071,9 +4224,9 @@ } }, "node_modules/http-proxy-agent": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.0.tgz", - "integrity": "sha512-+ZT+iBxVUQ1asugqnD6oWoRiS25AkjNfG085dKJGtGxkdwLQrMKU5wJr2bOOFAXzKcTuqq+7fZlTMgG3SRfIYQ==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", "dev": true, "dependencies": { "agent-base": "^7.1.0", @@ -4089,9 +4242,9 @@ "integrity": "sha512-J+FkSdyD+0mA0N+81tMotaRMfSL9SGi+xpD3T6YApKsc3bGSXJlfXri3VyFOeYkfLRQisDk1W+jIFFKBeUBbBg==" }, "node_modules/https-proxy-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.2.tgz", - "integrity": "sha512-NmLNjm6ucYwtcUmL7JQC1ZQ57LmHP4lT15FQ8D61nak1rO6DH+fz5qNK2Ap5UN4ZapYICE3/0KodcLYSPsPbaA==", + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", + "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", "dev": true, "dependencies": { "agent-base": "^7.0.2", @@ -4151,9 +4304,9 @@ } }, "node_modules/immutable": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.5.tgz", - "integrity": "sha512-8eabxkth9gZatlwl5TBuJnCsoTADlL6ftEr7A4qgdaTsPyreilDSnUk57SO+jfKcNtxPa22U5KK6DSeAYhpBJw==", + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.6.tgz", + "integrity": "sha512-Ju0+lEMyzMVZarkTn/gqRpdqd5dOPaz1mCZ0SH3JV6iFw81PldE/PEB1hWVEA288HPt4WXW8O7AWxB10M+03QQ==", "dev": true }, "node_modules/import-fresh": { @@ -4181,16 +4334,6 @@ "node": ">=0.8.19" } }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "dev": true, - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -4241,11 +4384,14 @@ } }, "node_modules/is-core-module": { - "version": "2.13.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", - "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.14.0.tgz", + "integrity": "sha512-a5dFJih5ZLYlRtDc0dZWP7RiKr6xIKzmn/oAYCDvdLThadVgyJwlaoQPmRtMSpz+rk0OGAgIu+TcM9HUF0fk1A==", "dependencies": { - "hasown": "^2.0.0" + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -4360,6 +4506,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -4375,16 +4526,13 @@ } }, "node_modules/jackspeak": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", - "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==", + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", "dev": true, "dependencies": { "@isaacs/cliui": "^8.0.2" }, - "engines": { - "node": ">=14" - }, "funding": { "url": "https://github.com/sponsors/isaacs" }, @@ -4393,14 +4541,15 @@ } }, "node_modules/js-beautify": { - "version": "1.14.11", - "resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.14.11.tgz", - "integrity": "sha512-rPogWqAfoYh1Ryqqh2agUpVfbxAhbjuN1SmU86dskQUKouRiggUTCO4+2ym9UPXllc2WAp0J+T5qxn7Um3lCdw==", + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.15.1.tgz", + "integrity": "sha512-ESjNzSlt/sWE8sciZH8kBF8BPlwXPwhR6pWKAw8bw4Bwj+iZcnKW6ONWUutJ7eObuBZQpiIb8S7OYspWrKt7rA==", "dev": true, "dependencies": { "config-chain": "^1.1.13", - "editorconfig": "^1.0.3", + "editorconfig": "^1.0.4", "glob": "^10.3.3", + "js-cookie": "^3.0.5", "nopt": "^7.2.0" }, "bin": { @@ -4412,6 +4561,15 @@ "node": ">=14" } }, + "node_modules/js-cookie": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz", + "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==", + "dev": true, + "engines": { + "node": ">=14" + } + }, "node_modules/js-sdsl": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.3.0.tgz", @@ -4434,9 +4592,9 @@ } }, "node_modules/jsdom": { - "version": "24.0.0", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-24.0.0.tgz", - "integrity": "sha512-UDS2NayCvmXSXVP6mpTj+73JnNQadZlr9N68189xib2tx5Mls7swlTNao26IoHv46BZJFvXygyRtyXd1feAk1A==", + "version": "24.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-24.1.0.tgz", + "integrity": "sha512-6gpM7pRXCwIOKxX47cgOyvyQDN/Eh0f1MeKySBV2xGdKtqJBLj8P25eY3EVCWo2mglDDzozR2r2MW4T+JiNUZA==", "dev": true, "dependencies": { "cssstyle": "^4.0.1", @@ -4444,21 +4602,21 @@ "decimal.js": "^10.4.3", "form-data": "^4.0.0", "html-encoding-sniffer": "^4.0.0", - "http-proxy-agent": "^7.0.0", - "https-proxy-agent": "^7.0.2", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.4", "is-potential-custom-element-name": "^1.0.1", - "nwsapi": "^2.2.7", + "nwsapi": "^2.2.10", "parse5": "^7.1.2", - "rrweb-cssom": "^0.6.0", + "rrweb-cssom": "^0.7.0", "saxes": "^6.0.0", "symbol-tree": "^3.2.4", - "tough-cookie": "^4.1.3", + "tough-cookie": "^4.1.4", "w3c-xmlserializer": "^5.0.0", "webidl-conversions": "^7.0.0", "whatwg-encoding": "^3.1.1", "whatwg-mimetype": "^4.0.0", "whatwg-url": "^14.0.0", - "ws": "^8.16.0", + "ws": "^8.17.0", "xml-name-validator": "^5.0.0" }, "engines": { @@ -4500,12 +4658,6 @@ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true }, - "node_modules/jsonc-parser": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.1.tgz", - "integrity": "sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA==", - "dev": true - }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -4528,22 +4680,6 @@ "node": ">= 0.8.0" } }, - "node_modules/local-pkg": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.5.0.tgz", - "integrity": "sha512-ok6z3qlYyCDS4ZEU27HaU6x/xZa9Whf8jD4ptH5UZTQYZVYeb9bnZ3ojVhiJNLiXK1Hfc0GNbLXcmZ5plLDDBg==", - "dev": true, - "dependencies": { - "mlly": "^1.4.2", - "pkg-types": "^1.0.3" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -4571,31 +4707,25 @@ "dev": true }, "node_modules/loupe": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", - "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.1.tgz", + "integrity": "sha512-edNu/8D5MKVfGVFRhFf8aAxiTM6Wumfz5XsaatSxlD3w4R1d/WEKUTydCdPGbl9K7QG/Ca3GnDV2sIKIpXRQcw==", "dev": true, "dependencies": { "get-func-name": "^2.0.1" } }, "node_modules/lru-cache": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz", - "integrity": "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==", - "engines": { - "node": "14 || >=16.14" - } + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==" }, "node_modules/magic-string": { - "version": "0.30.6", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.6.tgz", - "integrity": "sha512-n62qCLbPjNjyo+owKtveQxZFZTBm+Ms6YoGD23Wew6Vw337PElFNifQpknPruVRQV57kVShPnLGo9vWxVhpPvA==", + "version": "0.30.10", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.10.tgz", + "integrity": "sha512-iIRwTIf0QKV3UAnYK4PU8uiEc4SRh5jX0mwpIwETPpHdhVM4f53RSwS/vXvN1JhGX+Cs7B8qIq3d6AH49O5fAQ==", "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" - }, - "engines": { - "node": ">=12" } }, "node_modules/md5.js": { @@ -4695,30 +4825,18 @@ } }, "node_modules/minipass": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", - "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", "dev": true, "engines": { "node": ">=16 || 14 >=14.17" } }, - "node_modules/mlly": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.5.0.tgz", - "integrity": "sha512-NPVQvAY1xr1QoVeG0cy8yUYC7FQcOx6evl/RjT1wL5FvzPnzOysoqB/jmx/DhssT2dYa8nxECLAaFI/+gVLhDQ==", - "dev": true, - "dependencies": { - "acorn": "^8.11.3", - "pathe": "^1.1.2", - "pkg-types": "^1.0.3", - "ufo": "^1.3.2" - } - }, "node_modules/mqtt": { - "version": "5.3.5", - "resolved": "https://registry.npmjs.org/mqtt/-/mqtt-5.3.5.tgz", - "integrity": "sha512-xd7qt/LEM721U6yHQcqjlaAKXL1Fsqf/MXq6C2WPi/6OXK2jdSzL1eZ7ZUgjea7IY2yFLRWD5LNdT1mL0arPoA==", + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/mqtt/-/mqtt-5.8.0.tgz", + "integrity": "sha512-/+H04mv6goy6K5gHMNH3uS0icBzXapS+4uUf4yZyQWXi72APPZNb81bQhvkm99poEQettXVT8XETB0mPxl5Wjg==", "dependencies": { "@types/readable-stream": "^4.0.5", "@types/ws": "^8.5.9", @@ -4735,8 +4853,8 @@ "reinterval": "^1.1.0", "rfdc": "^1.3.0", "split2": "^4.2.0", - "worker-timers": "^7.0.78", - "ws": "^8.14.2" + "worker-timers": "^7.1.4", + "ws": "^8.17.1" }, "bin": { "mqtt": "build/bin/mqtt.js", @@ -4865,9 +4983,9 @@ } }, "node_modules/nopt": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-7.2.0.tgz", - "integrity": "sha512-CVDtwCdhYIvnAzFoJ6NJ6dX3oga9/HyciQDnG1vQDjSLMeKLJ4A93ZqYKDrgYSr1FBY5/hMYC+2VCi24pgpkGA==", + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-7.2.1.tgz", + "integrity": "sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==", "dev": true, "dependencies": { "abbrev": "^2.0.0" @@ -4898,9 +5016,9 @@ } }, "node_modules/npm-run-path": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.2.0.tgz", - "integrity": "sha512-W4/tgAXFqFA0iL7fk0+uQ3g7wkL8xJmx3XdK0VGb4cHW//eZTtKGvFBBoRKVTpY7n6ze4NL9ly7rgXcHufqXKg==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", + "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", "dev": true, "dependencies": { "path-key": "^4.0.0" @@ -4946,26 +5064,29 @@ } }, "node_modules/nwsapi": { - "version": "2.2.7", - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.7.tgz", - "integrity": "sha512-ub5E4+FBPKwAZx0UwIQOjYWGHTEq5sPqHQNRN8Z9e4A7u3Tj1weLJsL59yH9vmvqEtBHaOmT6cYQKIZOxp35FQ==", + "version": "2.2.12", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.12.tgz", + "integrity": "sha512-qXDmcVlZV4XRtKFzddidpfVP4oMSGhga+xdMc25mv8kaLUHtgzCDhUxkrN8exkGdTlLNaXj7CV3GtON7zuGZ+w==", "dev": true }, "node_modules/object-inspect": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", - "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", + "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/object-is": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.5.tgz", - "integrity": "sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3" + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz", + "integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1" }, "engines": { "node": ">= 0.4" @@ -4999,15 +5120,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, - "dependencies": { - "wrappy": "1" - } - }, "node_modules/onetime": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", @@ -5024,17 +5136,17 @@ } }, "node_modules/optionator": { - "version": "0.9.3", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", - "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", "dev": true, "dependencies": { - "@aashutoshrathi/word-wrap": "^1.2.3", "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", - "type-check": "^0.4.0" + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" }, "engines": { "node": ">= 0.8.0" @@ -5073,6 +5185,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz", + "integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==", + "dev": true + }, "node_modules/pako": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", @@ -5091,17 +5209,40 @@ } }, "node_modules/parse-asn1": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.6.tgz", - "integrity": "sha512-RnZRo1EPU6JBnra2vGHj0yhp6ebyjBZpmUCLHWiFhxlzvBCCpAuZ7elsBp1PVAbQN0/04VD/19rfzlBSwLstMw==", + "version": "5.1.7", + "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.7.tgz", + "integrity": "sha512-CTM5kuWR3sx9IFamcl5ErfPl6ea/N8IYwiJ+vpeB2g+1iknv7zBl5uPwbMbRVznRVbrNY6lGuDoE5b30grmbqg==", "dependencies": { - "asn1.js": "^5.2.0", - "browserify-aes": "^1.0.0", - "evp_bytestokey": "^1.0.0", - "pbkdf2": "^3.0.3", - "safe-buffer": "^5.1.1" + "asn1.js": "^4.10.1", + "browserify-aes": "^1.2.0", + "evp_bytestokey": "^1.0.3", + "hash-base": "~3.0", + "pbkdf2": "^3.1.2", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.10" } }, + "node_modules/parse-asn1/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/parse5": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", @@ -5127,15 +5268,6 @@ "node": ">=8" } }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -5151,16 +5283,16 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" }, "node_modules/path-scurry": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.1.tgz", - "integrity": "sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==", + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", "dev": true, "dependencies": { - "lru-cache": "^9.1.1 || ^10.0.0", + "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": ">=16 || 14 >=14.18" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -5173,12 +5305,12 @@ "dev": true }, "node_modules/pathval": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", - "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz", + "integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==", "dev": true, "engines": { - "node": "*" + "node": ">= 14.16" } }, "node_modules/pbkdf2": { @@ -5197,9 +5329,9 @@ } }, "node_modules/picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", + "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==" }, "node_modules/picomatch": { "version": "2.3.1", @@ -5239,9 +5371,9 @@ } }, "node_modules/pinia/node_modules/vue-demi": { - "version": "0.14.7", - "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.7.tgz", - "integrity": "sha512-EOG8KXDQNwkJILkx/gPcoL/7vH+hORoBaKgGe+6W7VFMvCYJfmF2dGbvgDroVnI8LU7/kTu8mbjRZGBU1z9NTA==", + "version": "0.14.8", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.8.tgz", + "integrity": "sha512-Uuqnk9YE9SsWeReYqK2alDI5YzciATE0r2SkA6iMAtuXvNTMNACJLJEXNXaEy94ECuBe4Sk6RzRU80kjdbIo1Q==", "hasInstallScript": true, "bin": { "vue-demi-fix": "bin/vue-demi-fix.js", @@ -5274,21 +5406,18 @@ "node": ">=10" } }, - "node_modules/pkg-types": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.0.3.tgz", - "integrity": "sha512-nN7pYi0AQqJnoLPC9eHFQ8AcyaixBUOwvqc5TDnIKCMEE6I0y8P7OKA7fPexsXGCGxQDl/cmrLAp26LhcwxZ4A==", - "dev": true, - "dependencies": { - "jsonc-parser": "^3.2.0", - "mlly": "^1.2.0", - "pathe": "^1.1.0" + "node_modules/possible-typed-array-names": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", + "integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==", + "engines": { + "node": ">= 0.4" } }, "node_modules/postcss": { - "version": "8.4.35", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.35.tgz", - "integrity": "sha512-u5U8qYpBCpN13BsiEB0CbR1Hhh4Gc0zLFuedrHJKMctHCHAGrMdG0PRM/KErzAL3CU6/eckEtmHNB3x6e3c0vA==", + "version": "8.4.39", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.39.tgz", + "integrity": "sha512-0vzE+lAiG7hZl1/9I8yzKLx3aR9Xbof3fBHKunvMfOCYAtMhrsnccJY2iTURb9EZd5+pLuiNV9/c/GZJOHsgIw==", "funding": [ { "type": "opencollective", @@ -5305,28 +5434,34 @@ ], "dependencies": { "nanoid": "^3.3.7", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" + "picocolors": "^1.0.1", + "source-map-js": "^1.2.0" }, "engines": { "node": "^10 || ^12 || >=14" } }, "node_modules/postcss-attribute-case-insensitive": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/postcss-attribute-case-insensitive/-/postcss-attribute-case-insensitive-6.0.2.tgz", - "integrity": "sha512-IRuCwwAAQbgaLhxQdQcIIK0dCVXg3XDUnzgKD8iwdiYdwU4rMWRWyl/W9/0nA4ihVpq5pyALiHB2veBJ0292pw==", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/postcss-attribute-case-insensitive/-/postcss-attribute-case-insensitive-6.0.3.tgz", + "integrity": "sha512-KHkmCILThWBRtg+Jn1owTnHPnFit4OkqS+eKiGEOPIGke54DCeYGJ6r0Fx/HjfE9M9kznApCLcU0DvnPchazMQ==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], "dependencies": { - "postcss-selector-parser": "^6.0.10" + "postcss-selector-parser": "^6.0.13" }, "engines": { "node": "^14 || ^16 || >=18" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, "peerDependencies": { "postcss": "^8.4" } @@ -5347,9 +5482,9 @@ } }, "node_modules/postcss-color-functional-notation": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/postcss-color-functional-notation/-/postcss-color-functional-notation-6.0.4.tgz", - "integrity": "sha512-YBzfVvVUNR4U3N0imzU1NPKCuwxzfHJkEP6imJxzsJ8LozRKeej9mWmg9Ef1ovJdb0xrGTRVzUxgTrMun5iw/Q==", + "version": "6.0.14", + "resolved": "https://registry.npmjs.org/postcss-color-functional-notation/-/postcss-color-functional-notation-6.0.14.tgz", + "integrity": "sha512-dNUX+UH4dAozZ8uMHZ3CtCNYw8fyFAmqqdcyxMr7PEdM9jLXV19YscoYO0F25KqZYhmtWKQ+4tKrIZQrwzwg7A==", "dev": true, "funding": [ { @@ -5362,10 +5497,11 @@ } ], "dependencies": { - "@csstools/css-color-parser": "^1.5.1", - "@csstools/css-parser-algorithms": "^2.5.0", - "@csstools/css-tokenizer": "^2.2.3", - "@csstools/postcss-progressive-custom-properties": "^3.0.3" + "@csstools/css-color-parser": "^2.0.4", + "@csstools/css-parser-algorithms": "^2.7.1", + "@csstools/css-tokenizer": "^2.4.1", + "@csstools/postcss-progressive-custom-properties": "^3.3.0", + "@csstools/utilities": "^1.0.0" }, "engines": { "node": "^14 || ^16 || >=18" @@ -5375,9 +5511,9 @@ } }, "node_modules/postcss-color-hex-alpha": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/postcss-color-hex-alpha/-/postcss-color-hex-alpha-9.0.3.tgz", - "integrity": "sha512-7sEHU4tAS6htlxun8AB9LDrCXoljxaC34tFVRlYKcvO+18r5fvGiXgv5bQzN40+4gXLCyWSMRK5FK31244WcCA==", + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/postcss-color-hex-alpha/-/postcss-color-hex-alpha-9.0.4.tgz", + "integrity": "sha512-XQZm4q4fNFqVCYMGPiBjcqDhuG7Ey2xrl99AnDJMyr5eDASsAGalndVgHZF8i97VFNy1GQeZc4q2ydagGmhelQ==", "dev": true, "funding": [ { @@ -5390,6 +5526,7 @@ } ], "dependencies": { + "@csstools/utilities": "^1.0.0", "postcss-value-parser": "^4.2.0" }, "engines": { @@ -5400,9 +5537,9 @@ } }, "node_modules/postcss-color-rebeccapurple": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/postcss-color-rebeccapurple/-/postcss-color-rebeccapurple-9.0.2.tgz", - "integrity": "sha512-f+RDEAPW2m8UbJWkSpRfV+QxhSaQhDMihI75DVGJJh4oRIoegjheeRtINFJum9D8BqGJcvD4GLjggTvCwZ4zuA==", + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/postcss-color-rebeccapurple/-/postcss-color-rebeccapurple-9.0.3.tgz", + "integrity": "sha512-ruBqzEFDYHrcVq3FnW3XHgwRqVMrtEPLBtD7K2YmsLKVc2jbkxzzNEctJKsPCpDZ+LeMHLKRDoSShVefGc+CkQ==", "dev": true, "funding": [ { @@ -5415,6 +5552,7 @@ } ], "dependencies": { + "@csstools/utilities": "^1.0.0", "postcss-value-parser": "^4.2.0" }, "engines": { @@ -5425,9 +5563,9 @@ } }, "node_modules/postcss-custom-media": { - "version": "10.0.2", - "resolved": "https://registry.npmjs.org/postcss-custom-media/-/postcss-custom-media-10.0.2.tgz", - "integrity": "sha512-zcEFNRmDm2fZvTPdI1pIW3W//UruMcLosmMiCdpQnrCsTRzWlKQPYMa1ud9auL0BmrryKK1+JjIGn19K0UjO/w==", + "version": "10.0.8", + "resolved": "https://registry.npmjs.org/postcss-custom-media/-/postcss-custom-media-10.0.8.tgz", + "integrity": "sha512-V1KgPcmvlGdxTel4/CyQtBJEFhMVpEmRGFrnVtgfGIHj5PJX9vO36eFBxKBeJn+aCDTed70cc+98Mz3J/uVdGQ==", "dev": true, "funding": [ { @@ -5440,10 +5578,10 @@ } ], "dependencies": { - "@csstools/cascade-layer-name-parser": "^1.0.5", - "@csstools/css-parser-algorithms": "^2.3.2", - "@csstools/css-tokenizer": "^2.2.1", - "@csstools/media-query-list-parser": "^2.1.5" + "@csstools/cascade-layer-name-parser": "^1.0.13", + "@csstools/css-parser-algorithms": "^2.7.1", + "@csstools/css-tokenizer": "^2.4.1", + "@csstools/media-query-list-parser": "^2.1.13" }, "engines": { "node": "^14 || ^16 || >=18" @@ -5453,9 +5591,9 @@ } }, "node_modules/postcss-custom-properties": { - "version": "13.3.4", - "resolved": "https://registry.npmjs.org/postcss-custom-properties/-/postcss-custom-properties-13.3.4.tgz", - "integrity": "sha512-9YN0gg9sG3OH+Z9xBrp2PWRb+O4msw+5Sbp3ZgqrblrwKspXVQe5zr5sVqi43gJGwW/Rv1A483PRQUzQOEewvA==", + "version": "13.3.12", + "resolved": "https://registry.npmjs.org/postcss-custom-properties/-/postcss-custom-properties-13.3.12.tgz", + "integrity": "sha512-oPn/OVqONB2ZLNqN185LDyaVByELAA/u3l2CS2TS16x2j2XsmV4kd8U49+TMxmUsEU9d8fB/I10E6U7kB0L1BA==", "dev": true, "funding": [ { @@ -5468,9 +5606,10 @@ } ], "dependencies": { - "@csstools/cascade-layer-name-parser": "^1.0.7", - "@csstools/css-parser-algorithms": "^2.5.0", - "@csstools/css-tokenizer": "^2.2.3", + "@csstools/cascade-layer-name-parser": "^1.0.13", + "@csstools/css-parser-algorithms": "^2.7.1", + "@csstools/css-tokenizer": "^2.4.1", + "@csstools/utilities": "^1.0.0", "postcss-value-parser": "^4.2.0" }, "engines": { @@ -5481,9 +5620,9 @@ } }, "node_modules/postcss-custom-selectors": { - "version": "7.1.6", - "resolved": "https://registry.npmjs.org/postcss-custom-selectors/-/postcss-custom-selectors-7.1.6.tgz", - "integrity": "sha512-svsjWRaxqL3vAzv71dV0/65P24/FB8TbPX+lWyyf9SZ7aZm4S4NhCn7N3Bg+Z5sZunG3FS8xQ80LrCU9hb37cw==", + "version": "7.1.12", + "resolved": "https://registry.npmjs.org/postcss-custom-selectors/-/postcss-custom-selectors-7.1.12.tgz", + "integrity": "sha512-ctIoprBMJwByYMGjXG0F7IT2iMF2hnamQ+aWZETyBM0aAlyaYdVZTeUkk8RB+9h9wP+NdN3f01lfvKl2ZSqC0g==", "dev": true, "funding": [ { @@ -5496,10 +5635,10 @@ } ], "dependencies": { - "@csstools/cascade-layer-name-parser": "^1.0.5", - "@csstools/css-parser-algorithms": "^2.3.2", - "@csstools/css-tokenizer": "^2.2.1", - "postcss-selector-parser": "^6.0.13" + "@csstools/cascade-layer-name-parser": "^1.0.13", + "@csstools/css-parser-algorithms": "^2.7.1", + "@csstools/css-tokenizer": "^2.4.1", + "postcss-selector-parser": "^6.1.0" }, "engines": { "node": "^14 || ^16 || >=18" @@ -5534,9 +5673,9 @@ } }, "node_modules/postcss-double-position-gradients": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/postcss-double-position-gradients/-/postcss-double-position-gradients-5.0.3.tgz", - "integrity": "sha512-QKYpwmaSm6HcdS0ndAuWSNNMv78R1oSySoh3mYBmctHWr2KWcwPJVakdOyU4lvFVW0GRu9wfIQwGeM4p3xU9ow==", + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/postcss-double-position-gradients/-/postcss-double-position-gradients-5.0.7.tgz", + "integrity": "sha512-1xEhjV9u1s4l3iP5lRt1zvMjI/ya8492o9l/ivcxHhkO3nOz16moC4JpMxDUGrOs4R3hX+KWT7gKoV842cwRgg==", "dev": true, "funding": [ { @@ -5549,7 +5688,8 @@ } ], "dependencies": { - "@csstools/postcss-progressive-custom-properties": "^3.0.3", + "@csstools/postcss-progressive-custom-properties": "^3.3.0", + "@csstools/utilities": "^1.0.0", "postcss-value-parser": "^4.2.0" }, "engines": { @@ -5641,9 +5781,9 @@ } }, "node_modules/postcss-image-set-function": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/postcss-image-set-function/-/postcss-image-set-function-6.0.2.tgz", - "integrity": "sha512-/O1xwqpJiz/apxGQi7UUfv1xUcorvkHZfvCYHPpRxxZj2WvjD0rg0+/+c+u5/Do5CpUg3XvfYxMrhcnjW1ArDQ==", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/postcss-image-set-function/-/postcss-image-set-function-6.0.3.tgz", + "integrity": "sha512-i2bXrBYzfbRzFnm+pVuxVePSTCRiNmlfssGI4H0tJQvDue+yywXwUxe68VyzXs7cGtMaH6MCLY6IbCShrSroCw==", "dev": true, "funding": [ { @@ -5656,6 +5796,7 @@ } ], "dependencies": { + "@csstools/utilities": "^1.0.0", "postcss-value-parser": "^4.2.0" }, "engines": { @@ -5666,9 +5807,9 @@ } }, "node_modules/postcss-lab-function": { - "version": "6.0.9", - "resolved": "https://registry.npmjs.org/postcss-lab-function/-/postcss-lab-function-6.0.9.tgz", - "integrity": "sha512-PKFAVTBEWJYsoSTD7Kp/OzeiMsXaLX39Pv75XgUyF5VrbMfeTw+JqCGsvDP3dPhclh6BemdCFHcjXBG9gO4UCg==", + "version": "6.0.19", + "resolved": "https://registry.npmjs.org/postcss-lab-function/-/postcss-lab-function-6.0.19.tgz", + "integrity": "sha512-vwln/mgvFrotJuGV8GFhpAOu9iGf3pvTBr6dLPDmUcqVD5OsQpEFyQMAFTxSxWXGEzBj6ld4pZ/9GDfEpXvo0g==", "dev": true, "funding": [ { @@ -5681,10 +5822,11 @@ } ], "dependencies": { - "@csstools/css-color-parser": "^1.5.1", - "@csstools/css-parser-algorithms": "^2.5.0", - "@csstools/css-tokenizer": "^2.2.3", - "@csstools/postcss-progressive-custom-properties": "^3.0.3" + "@csstools/css-color-parser": "^2.0.4", + "@csstools/css-parser-algorithms": "^2.7.1", + "@csstools/css-tokenizer": "^2.4.1", + "@csstools/postcss-progressive-custom-properties": "^3.3.0", + "@csstools/utilities": "^1.0.0" }, "engines": { "node": "^14 || ^16 || >=18" @@ -5719,9 +5861,9 @@ } }, "node_modules/postcss-nesting": { - "version": "12.0.2", - "resolved": "https://registry.npmjs.org/postcss-nesting/-/postcss-nesting-12.0.2.tgz", - "integrity": "sha512-63PpJHSeNs93S3ZUIyi+7kKx4JqOIEJ6QYtG3x+0qA4J03+4n0iwsyA1GAHyWxsHYljQS4/4ZK1o2sMi70b5wQ==", + "version": "12.1.5", + "resolved": "https://registry.npmjs.org/postcss-nesting/-/postcss-nesting-12.1.5.tgz", + "integrity": "sha512-N1NgI1PDCiAGWPTYrwqm8wpjv0bgDmkYHH72pNsqTCv9CObxjxftdYu6AKtGN+pnJa7FQjMm3v4sp8QJbFsYdQ==", "dev": true, "funding": [ { @@ -5734,8 +5876,9 @@ } ], "dependencies": { - "@csstools/selector-specificity": "^3.0.1", - "postcss-selector-parser": "^6.0.13" + "@csstools/selector-resolve-nested": "^1.1.0", + "@csstools/selector-specificity": "^3.1.1", + "postcss-selector-parser": "^6.1.0" }, "engines": { "node": "^14 || ^16 || >=18" @@ -5826,9 +5969,9 @@ } }, "node_modules/postcss-preset-env": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/postcss-preset-env/-/postcss-preset-env-9.3.0.tgz", - "integrity": "sha512-ycw6doPrqV6QxDCtgiyGDef61bEfiSc59HGM4gOw/wxQxmKnhuEery61oOC/5ViENz/ycpRsuhTexs1kUBTvVw==", + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/postcss-preset-env/-/postcss-preset-env-9.6.0.tgz", + "integrity": "sha512-Lxfk4RYjUdwPCYkc321QMdgtdCP34AeI94z+/8kVmqnTIlD4bMRQeGcMZgwz8BxHrzQiFXYIR5d7k/9JMs2MEA==", "dev": true, "funding": [ { @@ -5841,66 +5984,67 @@ } ], "dependencies": { - "@csstools/postcss-cascade-layers": "^4.0.1", - "@csstools/postcss-color-function": "^3.0.7", - "@csstools/postcss-color-mix-function": "^2.0.7", - "@csstools/postcss-exponential-functions": "^1.0.1", - "@csstools/postcss-font-format-keywords": "^3.0.0", - "@csstools/postcss-gamut-mapping": "^1.0.0", - "@csstools/postcss-gradients-interpolation-method": "^4.0.7", - "@csstools/postcss-hwb-function": "^3.0.6", - "@csstools/postcss-ic-unit": "^3.0.2", - "@csstools/postcss-initial": "^1.0.0", - "@csstools/postcss-is-pseudo-class": "^4.0.3", - "@csstools/postcss-logical-float-and-clear": "^2.0.0", - "@csstools/postcss-logical-overflow": "^1.0.0", - "@csstools/postcss-logical-overscroll-behavior": "^1.0.0", - "@csstools/postcss-logical-resize": "^2.0.0", - "@csstools/postcss-logical-viewport-units": "^2.0.3", - "@csstools/postcss-media-minmax": "^1.1.0", - "@csstools/postcss-media-queries-aspect-ratio-number-values": "^2.0.3", - "@csstools/postcss-nested-calc": "^3.0.0", - "@csstools/postcss-normalize-display-values": "^3.0.1", - "@csstools/postcss-oklab-function": "^3.0.7", - "@csstools/postcss-progressive-custom-properties": "^3.0.2", - "@csstools/postcss-relative-color-syntax": "^2.0.7", - "@csstools/postcss-scope-pseudo-class": "^3.0.0", - "@csstools/postcss-stepped-value-functions": "^3.0.2", - "@csstools/postcss-text-decoration-shorthand": "^3.0.3", - "@csstools/postcss-trigonometric-functions": "^3.0.2", - "@csstools/postcss-unset-value": "^3.0.0", - "autoprefixer": "^10.4.16", - "browserslist": "^4.22.1", - "css-blank-pseudo": "^6.0.0", - "css-has-pseudo": "^6.0.0", - "css-prefers-color-scheme": "^9.0.0", - "cssdb": "^7.9.0", - "postcss-attribute-case-insensitive": "^6.0.2", + "@csstools/postcss-cascade-layers": "^4.0.6", + "@csstools/postcss-color-function": "^3.0.19", + "@csstools/postcss-color-mix-function": "^2.0.19", + "@csstools/postcss-content-alt-text": "^1.0.0", + "@csstools/postcss-exponential-functions": "^1.0.9", + "@csstools/postcss-font-format-keywords": "^3.0.2", + "@csstools/postcss-gamut-mapping": "^1.0.11", + "@csstools/postcss-gradients-interpolation-method": "^4.0.20", + "@csstools/postcss-hwb-function": "^3.0.18", + "@csstools/postcss-ic-unit": "^3.0.7", + "@csstools/postcss-initial": "^1.0.1", + "@csstools/postcss-is-pseudo-class": "^4.0.8", + "@csstools/postcss-light-dark-function": "^1.0.8", + "@csstools/postcss-logical-float-and-clear": "^2.0.1", + "@csstools/postcss-logical-overflow": "^1.0.1", + "@csstools/postcss-logical-overscroll-behavior": "^1.0.1", + "@csstools/postcss-logical-resize": "^2.0.1", + "@csstools/postcss-logical-viewport-units": "^2.0.11", + "@csstools/postcss-media-minmax": "^1.1.8", + "@csstools/postcss-media-queries-aspect-ratio-number-values": "^2.0.11", + "@csstools/postcss-nested-calc": "^3.0.2", + "@csstools/postcss-normalize-display-values": "^3.0.2", + "@csstools/postcss-oklab-function": "^3.0.19", + "@csstools/postcss-progressive-custom-properties": "^3.3.0", + "@csstools/postcss-relative-color-syntax": "^2.0.19", + "@csstools/postcss-scope-pseudo-class": "^3.0.1", + "@csstools/postcss-stepped-value-functions": "^3.0.10", + "@csstools/postcss-text-decoration-shorthand": "^3.0.7", + "@csstools/postcss-trigonometric-functions": "^3.0.10", + "@csstools/postcss-unset-value": "^3.0.1", + "autoprefixer": "^10.4.19", + "browserslist": "^4.23.1", + "css-blank-pseudo": "^6.0.2", + "css-has-pseudo": "^6.0.5", + "css-prefers-color-scheme": "^9.0.1", + "cssdb": "^8.1.0", + "postcss-attribute-case-insensitive": "^6.0.3", "postcss-clamp": "^4.1.0", - "postcss-color-functional-notation": "^6.0.2", - "postcss-color-hex-alpha": "^9.0.2", - "postcss-color-rebeccapurple": "^9.0.1", - "postcss-custom-media": "^10.0.2", - "postcss-custom-properties": "^13.3.2", - "postcss-custom-selectors": "^7.1.6", - "postcss-dir-pseudo-class": "^8.0.0", - "postcss-double-position-gradients": "^5.0.2", - "postcss-focus-visible": "^9.0.0", - "postcss-focus-within": "^8.0.0", + "postcss-color-functional-notation": "^6.0.14", + "postcss-color-hex-alpha": "^9.0.4", + "postcss-color-rebeccapurple": "^9.0.3", + "postcss-custom-media": "^10.0.8", + "postcss-custom-properties": "^13.3.12", + "postcss-custom-selectors": "^7.1.12", + "postcss-dir-pseudo-class": "^8.0.1", + "postcss-double-position-gradients": "^5.0.7", + "postcss-focus-visible": "^9.0.1", + "postcss-focus-within": "^8.0.1", "postcss-font-variant": "^5.0.0", - "postcss-gap-properties": "^5.0.0", - "postcss-image-set-function": "^6.0.1", - "postcss-lab-function": "^6.0.7", - "postcss-logical": "^7.0.0", - "postcss-nesting": "^12.0.1", + "postcss-gap-properties": "^5.0.1", + "postcss-image-set-function": "^6.0.3", + "postcss-lab-function": "^6.0.19", + "postcss-logical": "^7.0.1", + "postcss-nesting": "^12.1.5", "postcss-opacity-percentage": "^2.0.0", - "postcss-overflow-shorthand": "^5.0.0", + "postcss-overflow-shorthand": "^5.0.1", "postcss-page-break": "^3.0.4", - "postcss-place": "^9.0.0", - "postcss-pseudo-class-any-link": "^9.0.0", + "postcss-place": "^9.0.1", + "postcss-pseudo-class-any-link": "^9.0.2", "postcss-replace-overflow-wrap": "^4.0.0", - "postcss-selector-not": "^7.0.1", - "postcss-value-parser": "^4.2.0" + "postcss-selector-not": "^7.0.2" }, "engines": { "node": "^14 || ^16 || >=18" @@ -5910,9 +6054,9 @@ } }, "node_modules/postcss-pseudo-class-any-link": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/postcss-pseudo-class-any-link/-/postcss-pseudo-class-any-link-9.0.1.tgz", - "integrity": "sha512-cKYGGZ9yzUZi+dZd7XT2M8iSDfo+T2Ctbpiizf89uBTBfIpZpjvTavzIJXpCReMVXSKROqzpxClNu6fz4DHM0Q==", + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/postcss-pseudo-class-any-link/-/postcss-pseudo-class-any-link-9.0.2.tgz", + "integrity": "sha512-HFSsxIqQ9nA27ahyfH37cRWGk3SYyQLpk0LiWw/UGMV4VKT5YG2ONee4Pz/oFesnK0dn2AjcyequDbIjKJgB0g==", "dev": true, "funding": [ { @@ -5944,28 +6088,34 @@ } }, "node_modules/postcss-selector-not": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/postcss-selector-not/-/postcss-selector-not-7.0.1.tgz", - "integrity": "sha512-1zT5C27b/zeJhchN7fP0kBr16Cc61mu7Si9uWWLoA3Px/D9tIJPKchJCkUH3tPO5D0pCFmGeApAv8XpXBQJ8SQ==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/postcss-selector-not/-/postcss-selector-not-7.0.2.tgz", + "integrity": "sha512-/SSxf/90Obye49VZIfc0ls4H0P6i6V1iHv0pzZH8SdgvZOPFkF37ef1r5cyWcMflJSFJ5bfuoluTnFnBBFiuSA==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], "dependencies": { - "postcss-selector-parser": "^6.0.10" + "postcss-selector-parser": "^6.0.13" }, "engines": { "node": "^14 || ^16 || >=18" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, "peerDependencies": { "postcss": "^8.4" } }, "node_modules/postcss-selector-parser": { - "version": "6.0.15", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.15.tgz", - "integrity": "sha512-rEYkQOMUCEMhsKbK66tbEU9QVIxbhN18YiniAwA7XQYTVBqrBy+P2p5JcdqsHgKM2zWylp8d7J6eszocfds5Sw==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.0.tgz", + "integrity": "sha512-UMz42UD0UY0EApS0ZL9o1XnLhSTtvvvLe5Dc2H2O56fvRZi+KulDyf5ctDhhtYJBGKStV2FL1fy6253cmLgqVQ==", "dev": true, "dependencies": { "cssesc": "^3.0.0", @@ -5991,9 +6141,9 @@ } }, "node_modules/prettier": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz", - "integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.2.tgz", + "integrity": "sha512-rAVeHYMcv8ATV5d508CFdn+8/pHPpXeIid1DdrPwXnaAdH7cqjVbpJaT5eq4yRAFU/lsbwYwSF/n5iNrdJHPQA==", "dev": true, "bin": { "prettier": "bin/prettier.cjs" @@ -6017,32 +6167,6 @@ "node": ">=6.0.0" } }, - "node_modules/pretty-format": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", - "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", - "dev": true, - "dependencies": { - "@jest/schemas": "^29.6.3", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/pretty-format/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, "node_modules/process": { "version": "0.11.10", "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", @@ -6092,11 +6216,11 @@ "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==" }, "node_modules/qs": { - "version": "6.11.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.2.tgz", - "integrity": "sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==", + "version": "6.12.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.12.3.tgz", + "integrity": "sha512-AWJm14H1vVaO/iNZ4/hO+HyaTehuy9nRqVdkTqlJt0HWvBiBIEXFmb4C0DGeYo3Xes9rrEW+TxHsaigCbN5ICQ==", "dependencies": { - "side-channel": "^1.0.4" + "side-channel": "^1.0.6" }, "engines": { "node": ">=0.6" @@ -6156,12 +6280,6 @@ "safe-buffer": "^5.1.0" } }, - "node_modules/react-is": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", - "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", - "dev": true - }, "node_modules/readable-stream": { "version": "4.5.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz", @@ -6241,44 +6359,9 @@ } }, "node_modules/rfdc": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.1.tgz", - "integrity": "sha512-r5a3l5HzYlIC68TpmYKlxWjmOP6wiPJ1vWv2HeLhNsRZMrCkxeqxiHlQ21oXmQ4F3SiryXBHhAD7JZqvOJjFmg==" - }, - "node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/rimraf/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dev": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==" }, "node_modules/ripemd160": { "version": "2.0.2", @@ -6290,9 +6373,9 @@ } }, "node_modules/rollup": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.9.6.tgz", - "integrity": "sha512-05lzkCS2uASX0CiLFybYfVkwNbKZG5NFQ6Go0VWyogFTXXbR039UVsegViTntkk4OglHBdF54ccApXRRuXRbsg==", + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.18.1.tgz", + "integrity": "sha512-Elx2UT8lzxxOXMpy5HWQGZqkrQOtrVDDa/bm9l10+U4rQnVzbL/LgZ4NOM1MPIDyHk69W4InuYDF5dzRh4Kw1A==", "dev": true, "dependencies": { "@types/estree": "1.0.5" @@ -6305,19 +6388,22 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.9.6", - "@rollup/rollup-android-arm64": "4.9.6", - "@rollup/rollup-darwin-arm64": "4.9.6", - "@rollup/rollup-darwin-x64": "4.9.6", - "@rollup/rollup-linux-arm-gnueabihf": "4.9.6", - "@rollup/rollup-linux-arm64-gnu": "4.9.6", - "@rollup/rollup-linux-arm64-musl": "4.9.6", - "@rollup/rollup-linux-riscv64-gnu": "4.9.6", - "@rollup/rollup-linux-x64-gnu": "4.9.6", - "@rollup/rollup-linux-x64-musl": "4.9.6", - "@rollup/rollup-win32-arm64-msvc": "4.9.6", - "@rollup/rollup-win32-ia32-msvc": "4.9.6", - "@rollup/rollup-win32-x64-msvc": "4.9.6", + "@rollup/rollup-android-arm-eabi": "4.18.1", + "@rollup/rollup-android-arm64": "4.18.1", + "@rollup/rollup-darwin-arm64": "4.18.1", + "@rollup/rollup-darwin-x64": "4.18.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.18.1", + "@rollup/rollup-linux-arm-musleabihf": "4.18.1", + "@rollup/rollup-linux-arm64-gnu": "4.18.1", + "@rollup/rollup-linux-arm64-musl": "4.18.1", + "@rollup/rollup-linux-powerpc64le-gnu": "4.18.1", + "@rollup/rollup-linux-riscv64-gnu": "4.18.1", + "@rollup/rollup-linux-s390x-gnu": "4.18.1", + "@rollup/rollup-linux-x64-gnu": "4.18.1", + "@rollup/rollup-linux-x64-musl": "4.18.1", + "@rollup/rollup-win32-arm64-msvc": "4.18.1", + "@rollup/rollup-win32-ia32-msvc": "4.18.1", + "@rollup/rollup-win32-x64-msvc": "4.18.1", "fsevents": "~2.3.2" } }, @@ -6334,9 +6420,9 @@ } }, "node_modules/rrweb-cssom": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.6.0.tgz", - "integrity": "sha512-APM0Gt1KoXBz0iIkkdB/kfvGOwC4UuJFeG/c+yV7wSc7q96cG/kJ0HiYCnzivD9SB53cLV1MlHFNfOuPaadYSw==", + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.7.1.tgz", + "integrity": "sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==", "dev": true }, "node_modules/run-parallel": { @@ -6370,12 +6456,13 @@ "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true }, "node_modules/sass": { - "version": "1.70.0", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.70.0.tgz", - "integrity": "sha512-uUxNQ3zAHeAx5nRFskBnrWzDUJrrvpCPD5FNAoRvTi0WwremlheES3tg+56PaVtCs5QDRX5CBLxxKMDJMEa1WQ==", + "version": "1.77.7", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.77.7.tgz", + "integrity": "sha512-9ywH75cO+rLjbrZ6en3Gp8qAMwPGBapFtlsMJoDTkcMU/bSe5a6cjKVUn5Jr4Gzg5GbP3HE8cm+02pLCgcoMow==", "dev": true, "dependencies": { "chokidar": ">=3.0.0 <4.0.0", @@ -6402,13 +6489,10 @@ } }, "node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, "bin": { "semver": "bin/semver.js" }, @@ -6416,28 +6500,17 @@ "node": ">=10" } }, - "node_modules/semver/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/set-function-length": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.0.tgz", - "integrity": "sha512-4DBHDoyHlM1IRPGYcoxexgh67y4ueR53FKV1yyxwFMY7aCqcN/38M1+SwZ/qJQ8iLv7+ck385ot4CcisOAPT9w==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", "dependencies": { - "define-data-property": "^1.1.1", + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.2", + "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.1" + "has-property-descriptors": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -6482,13 +6555,17 @@ } }, "node_modules/side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", "dependencies": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -6513,9 +6590,9 @@ } }, "node_modules/source-map-js": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", - "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", + "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", "engines": { "node": ">=0.10.0" } @@ -6727,18 +6804,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/strip-literal": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-1.3.0.tgz", - "integrity": "sha512-PugKzOsyXpArk0yWmUwqOZecSO0GH0bPoctLcqNDH9J04pVW3lflYE0ujElBGTloevcxF5MofAOZ7C5l2b+wLg==", - "dev": true, - "dependencies": { - "acorn": "^8.10.0" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -6802,24 +6867,33 @@ } }, "node_modules/tinybench": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.6.0.tgz", - "integrity": "sha512-N8hW3PG/3aOoZAN5V/NSAEDz0ZixDSSt5b/a05iqtpgfLWMSVuCo7w0k2vVvEjdrIoeGqZzweX2WlyioNIHchA==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.8.0.tgz", + "integrity": "sha512-1/eK7zUnIklz4JUUlL+658n58XO2hHLQfSk1Zf2LKieUjxidN16eKFEoDEfjHc3ohofSSqK3X5yO6VGb6iW8Lw==", "dev": true }, "node_modules/tinypool": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.8.2.tgz", - "integrity": "sha512-SUszKYe5wgsxnNOVlBYO6IC+8VGWdVGZWAqUxp3UErNBtptZvWbwyUOyzNL59zigz2rCA92QiL3wvG+JDSdJdQ==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.0.0.tgz", + "integrity": "sha512-KIKExllK7jp3uvrNtvRBYBWBOAXSX8ZvoaD8T+7KB/QHIuoJW3Pmr60zucywjAlMb5TeXUkcs/MWeWLu0qvuAQ==", + "dev": true, + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", + "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", "dev": true, "engines": { "node": ">=14.0.0" } }, "node_modules/tinyspy": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.2.0.tgz", - "integrity": "sha512-d2eda04AN/cPOR89F7Xv5bK/jrQEhmcLFe6HFldoeO9AJtps+fqEnh486vnT/8y4bw38pSyxDcTCAq+Ks2aJTg==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.0.tgz", + "integrity": "sha512-q5nmENpTHgiPVd1cJDDc9cVoYN5x4vCvwT3FMilvKPKneCBZAxn2YWQjDF0UMcE9k0Cay1gBiDfTMU0g+mPMQA==", "dev": true, "engines": { "node": ">=14.0.0" @@ -6838,9 +6912,9 @@ } }, "node_modules/tough-cookie": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz", - "integrity": "sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", + "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", "dev": true, "dependencies": { "psl": "^1.1.33", @@ -6883,9 +6957,9 @@ } }, "node_modules/tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", + "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==" }, "node_modules/tty-browserify": { "version": "0.0.1", @@ -6904,15 +6978,6 @@ "node": ">= 0.8.0" } }, - "node_modules/type-detect": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/type-fest": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", @@ -6930,12 +6995,6 @@ "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==" }, - "node_modules/ufo": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.3.2.tgz", - "integrity": "sha512-o+ORpgGwaYQXgqGDwd+hkS4PuZ3QnmqMMxRuajK/a38L6fTpcE5GPIfrf+L/KemFzfUpeUQc1rRS1iDBozvnFA==", - "dev": true - }, "node_modules/undici-types": { "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", @@ -6951,9 +7010,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.0.13", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", - "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz", + "integrity": "sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==", "dev": true, "funding": [ { @@ -6970,8 +7029,8 @@ } ], "dependencies": { - "escalade": "^3.1.1", - "picocolors": "^1.0.0" + "escalade": "^3.1.2", + "picocolors": "^1.0.1" }, "bin": { "update-browserslist-db": "cli.js" @@ -7035,14 +7094,14 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, "node_modules/vite": { - "version": "5.0.12", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.0.12.tgz", - "integrity": "sha512-4hsnEkG3q0N4Tzf1+t6NdN9dg/L3BM+q8SWgbSPnJvrgH2kgdyzfVJwbR1ic69/4uMJJ/3dqDZZE5/WwqW8U1w==", + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.3.3.tgz", + "integrity": "sha512-NPQdeCU0Dv2z5fu+ULotpuq5yfCS1BzKUIPhNbP3YBfAMGJXbt2nS+sbTFu+qchaqWTD+H3JK++nRwr6XIcp6A==", "dev": true, "dependencies": { - "esbuild": "^0.19.3", - "postcss": "^8.4.32", - "rollup": "^4.2.0" + "esbuild": "^0.21.3", + "postcss": "^8.4.39", + "rollup": "^4.13.0" }, "bin": { "vite": "bin/vite.js" @@ -7090,15 +7149,15 @@ } }, "node_modules/vite-node": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.2.2.tgz", - "integrity": "sha512-1as4rDTgVWJO3n1uHmUYqq7nsFgINQ9u+mRcXpjeOMJUmviqNKjcZB7UfRZrlM7MjYXMKpuWp5oGkjaFLnjawg==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.0.2.tgz", + "integrity": "sha512-w4vkSz1Wo+NIQg8pjlEn0jQbcM/0D+xVaYjhw3cvarTanLLBh54oNiRbsT8PNK5GfuST0IlVXjsNRoNlqvY/fw==", "dev": true, "dependencies": { "cac": "^6.7.14", - "debug": "^4.3.4", - "pathe": "^1.1.1", - "picocolors": "^1.0.0", + "debug": "^4.3.5", + "pathe": "^1.1.2", + "tinyrainbow": "^1.2.0", "vite": "^5.0.0" }, "bin": { @@ -7112,9 +7171,9 @@ } }, "node_modules/vite-plugin-node-polyfills": { - "version": "0.21.0", - "resolved": "https://registry.npmjs.org/vite-plugin-node-polyfills/-/vite-plugin-node-polyfills-0.21.0.tgz", - "integrity": "sha512-Sk4DiKnmxN8E0vhgEhzLudfJQfaT8k4/gJ25xvUPG54KjLJ6HAmDKbr4rzDD/QWEY+Lwg80KE85fGYBQihEPQA==", + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/vite-plugin-node-polyfills/-/vite-plugin-node-polyfills-0.22.0.tgz", + "integrity": "sha512-F+G3LjiGbG8QpbH9bZ//GSBr9i1InSTkaulfUHFa9jkLqVGORFBoqc2A/Yu5Mmh1kNAbiAeKeK+6aaQUf3x0JA==", "dev": true, "dependencies": { "@rollup/plugin-inject": "^5.0.5", @@ -7128,31 +7187,29 @@ } }, "node_modules/vitest": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.2.2.tgz", - "integrity": "sha512-d5Ouvrnms3GD9USIK36KG8OZ5bEvKEkITFtnGv56HFaSlbItJuYr7hv2Lkn903+AvRAgSixiamozUVfORUekjw==", - "dev": true, - "dependencies": { - "@vitest/expect": "1.2.2", - "@vitest/runner": "1.2.2", - "@vitest/snapshot": "1.2.2", - "@vitest/spy": "1.2.2", - "@vitest/utils": "1.2.2", - "acorn-walk": "^8.3.2", - "cac": "^6.7.14", - "chai": "^4.3.10", - "debug": "^4.3.4", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.0.2.tgz", + "integrity": "sha512-WlpZ9neRIjNBIOQwBYfBSr0+of5ZCbxT2TVGKW4Lv0c8+srCFIiRdsP7U009t8mMn821HQ4XKgkx5dVWpyoyLw==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.3.0", + "@vitest/expect": "2.0.2", + "@vitest/pretty-format": "^2.0.2", + "@vitest/runner": "2.0.2", + "@vitest/snapshot": "2.0.2", + "@vitest/spy": "2.0.2", + "@vitest/utils": "2.0.2", + "chai": "^5.1.1", + "debug": "^4.3.5", "execa": "^8.0.1", - "local-pkg": "^0.5.0", - "magic-string": "^0.30.5", - "pathe": "^1.1.1", - "picocolors": "^1.0.0", - "std-env": "^3.5.0", - "strip-literal": "^1.3.0", - "tinybench": "^2.5.1", - "tinypool": "^0.8.2", + "magic-string": "^0.30.10", + "pathe": "^1.1.2", + "std-env": "^3.7.0", + "tinybench": "^2.8.0", + "tinypool": "^1.0.0", + "tinyrainbow": "^1.2.0", "vite": "^5.0.0", - "vite-node": "1.2.2", + "vite-node": "2.0.2", "why-is-node-running": "^2.2.2" }, "bin": { @@ -7167,8 +7224,8 @@ "peerDependencies": { "@edge-runtime/vm": "*", "@types/node": "^18.0.0 || >=20.0.0", - "@vitest/browser": "^1.0.0", - "@vitest/ui": "^1.0.0", + "@vitest/browser": "2.0.2", + "@vitest/ui": "2.0.2", "happy-dom": "*", "jsdom": "*" }, @@ -7199,15 +7256,15 @@ "integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==" }, "node_modules/vue": { - "version": "3.4.19", - "resolved": "https://registry.npmjs.org/vue/-/vue-3.4.19.tgz", - "integrity": "sha512-W/7Fc9KUkajFU8dBeDluM4sRGc/aa4YJnOYck8dkjgZoXtVsn3OeTGni66FV1l3+nvPA7VBFYtPioaGKUmEADw==", + "version": "3.4.31", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.4.31.tgz", + "integrity": "sha512-njqRrOy7W3YLAlVqSKpBebtZpDVg21FPoaq1I7f/+qqBThK9ChAIjkRWgeP6Eat+8C+iia4P3OYqpATP21BCoQ==", "dependencies": { - "@vue/compiler-dom": "3.4.19", - "@vue/compiler-sfc": "3.4.19", - "@vue/runtime-dom": "3.4.19", - "@vue/server-renderer": "3.4.19", - "@vue/shared": "3.4.19" + "@vue/compiler-dom": "3.4.31", + "@vue/compiler-sfc": "3.4.31", + "@vue/runtime-dom": "3.4.31", + "@vue/server-renderer": "3.4.31", + "@vue/shared": "3.4.31" }, "peerDependencies": { "typescript": "*" @@ -7219,15 +7276,15 @@ } }, "node_modules/vue-component-type-helpers": { - "version": "1.8.27", - "resolved": "https://registry.npmjs.org/vue-component-type-helpers/-/vue-component-type-helpers-1.8.27.tgz", - "integrity": "sha512-0vOfAtI67UjeO1G6UiX5Kd76CqaQ67wrRZiOe7UAb9Jm6GzlUr/fC7CV90XfwapJRjpCMaZFhv1V0ajWRmE9Dg==", + "version": "2.0.26", + "resolved": "https://registry.npmjs.org/vue-component-type-helpers/-/vue-component-type-helpers-2.0.26.tgz", + "integrity": "sha512-sO9qQ8oC520SW6kqlls0iqDak53gsTVSrYylajgjmkt1c0vcgjsGSy1KzlDrbEx8pm02IEYhlUkU5hCYf8rwtg==", "dev": true }, "node_modules/vue-eslint-parser": { - "version": "9.4.2", - "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-9.4.2.tgz", - "integrity": "sha512-Ry9oiGmCAK91HrKMtCrKFWmSFWvYkpGglCeFAIqDdr9zdXmMMpJOmUJS7WWsW7fX81h6mwHmUZCQQ1E0PkSwYQ==", + "version": "9.4.3", + "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-9.4.3.tgz", + "integrity": "sha512-2rYRLWlIpaiN8xbPiDyXZXRgLGOtWxERV7ND5fFAv5qo1D2N9Fu9MNajBNc6o13lZ+24DAWCkQCvj4klgmcITg==", "dev": true, "dependencies": { "debug": "^4.3.4", @@ -7248,12 +7305,57 @@ "eslint": ">=6.0.0" } }, + "node_modules/vue-eslint-parser/node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/vue-eslint-parser/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/vue-eslint-parser/node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/vue-router": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.2.5.tgz", - "integrity": "sha512-DIUpKcyg4+PTQKfFPX88UWhlagBEBEfJ5A8XDXRJLUnZOvcpMF8o/dnL90vpVkGaPbjvXazV/rC1qBKrZlFugw==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.4.0.tgz", + "integrity": "sha512-HB+t2p611aIZraV2aPSRNXf0Z/oLZFrlygJm+sZbdJaW6lcFqEDQwnzUBXn+DApw+/QzDU/I9TeWx9izEjTmsA==", "dependencies": { - "@vue/devtools-api": "^6.5.0" + "@vue/devtools-api": "^6.5.1" }, "funding": { "url": "https://github.com/sponsors/posva" @@ -7342,15 +7444,15 @@ } }, "node_modules/which-typed-array": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.14.tgz", - "integrity": "sha512-VnXFiIW8yNn9kIHN88xvZ4yOWchftKDsRJ8fEPacX/wl1lOvBrhsJ/OeJCXq7B0AaijRuqgzSKalJoPk+D8MPg==", + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.15.tgz", + "integrity": "sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==", "dependencies": { - "available-typed-arrays": "^1.0.6", - "call-bind": "^1.0.5", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", "for-each": "^0.3.3", "gopd": "^1.0.1", - "has-tostringtag": "^1.0.1" + "has-tostringtag": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -7360,9 +7462,9 @@ } }, "node_modules/why-is-node-running": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.2.2.tgz", - "integrity": "sha512-6tSwToZxTOcotxHeA+qGCq1mVzKR3CwcJGmVcY+QE8SHy6TnpFnh8PAvPNHYr7EcuVeG0QSMxtYCuO1ta/G/oA==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", "dev": true, "dependencies": { "siginfo": "^2.0.0", @@ -7375,34 +7477,43 @@ "node": ">=8" } }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/worker-timers": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/worker-timers/-/worker-timers-7.1.1.tgz", - "integrity": "sha512-axtq83GwPqYwkQmQmei2abQ9cT7oSwmLw4lQCZ9VmMH9g4t4kuEF1Gw+tdnIJGHCiZ2QPDnr/+307bYx6tynLA==", + "version": "7.1.8", + "resolved": "https://registry.npmjs.org/worker-timers/-/worker-timers-7.1.8.tgz", + "integrity": "sha512-R54psRKYVLuzff7c1OTFcq/4Hue5Vlz4bFtNEIarpSiCYhpifHU3aIQI29S84o1j87ePCYqbmEJPqwBTf+3sfw==", "dependencies": { - "@babel/runtime": "^7.23.8", + "@babel/runtime": "^7.24.5", "tslib": "^2.6.2", - "worker-timers-broker": "^6.1.1", - "worker-timers-worker": "^7.0.65" + "worker-timers-broker": "^6.1.8", + "worker-timers-worker": "^7.0.71" } }, "node_modules/worker-timers-broker": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/worker-timers-broker/-/worker-timers-broker-6.1.1.tgz", - "integrity": "sha512-CTlDnkXAewtYvw5gOwVIc6UuIPcNHJrqWxBMhZbCWOmadvl20nPs9beAsXlaTEwW3G2KBpuKiSgkhBkhl3mxDA==", + "version": "6.1.8", + "resolved": "https://registry.npmjs.org/worker-timers-broker/-/worker-timers-broker-6.1.8.tgz", + "integrity": "sha512-FUCJu9jlK3A8WqLTKXM9E6kAmI/dR1vAJ8dHYLMisLNB/n3GuaFIjJ7pn16ZcD1zCOf7P6H62lWIEBi+yz/zQQ==", "dependencies": { - "@babel/runtime": "^7.23.8", + "@babel/runtime": "^7.24.5", "fast-unique-numbers": "^8.0.13", "tslib": "^2.6.2", - "worker-timers-worker": "^7.0.65" + "worker-timers-worker": "^7.0.71" } }, "node_modules/worker-timers-worker": { - "version": "7.0.65", - "resolved": "https://registry.npmjs.org/worker-timers-worker/-/worker-timers-worker-7.0.65.tgz", - "integrity": "sha512-Dl4nGONr8A8Fr+vQnH7Ee+o2iB480S1fBcyJYqnMyMwGRVyQZLZU+o91vbMvU1vHqiryRQmjXzzMYlh86wx+YQ==", + "version": "7.0.71", + "resolved": "https://registry.npmjs.org/worker-timers-worker/-/worker-timers-worker-7.0.71.tgz", + "integrity": "sha512-ks/5YKwZsto1c2vmljroppOKCivB/ma97g9y77MAAz2TBBjPPgpoOiS1qYQKIgvGTr2QYPT3XhJWIB6Rj2MVPQ==", "dependencies": { - "@babel/runtime": "^7.23.8", + "@babel/runtime": "^7.24.5", "tslib": "^2.6.2" } }, @@ -7500,16 +7611,10 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true - }, "node_modules/ws": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.16.0.tgz", - "integrity": "sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", "engines": { "node": ">=10.0.0" }, @@ -7549,12 +7654,6 @@ "node": ">=0.4" } }, - "node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/packages/modules/display_themes/cards/source/package.json b/packages/modules/display_themes/cards/source/package.json index 6bd4b7fe7a..17352cc6bd 100644 --- a/packages/modules/display_themes/cards/source/package.json +++ b/packages/modules/display_themes/cards/source/package.json @@ -8,38 +8,38 @@ "build": "vite build --out-dir=../web/ --emptyOutDir", "preview": "vite preview --host", "test:unit": "vitest run --environment jsdom --root src/", - "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --fix --ignore-path .gitignore", + "lint": "eslint . --fix", "build-dev": "vite build --mode=development --out-dir=../web/ --emptyOutDir" }, "dependencies": { - "@fortawesome/fontawesome-svg-core": "^6.2.1", - "@fortawesome/free-regular-svg-icons": "^6.2.1", - "@fortawesome/free-solid-svg-icons": "^6.2.1", - "@fortawesome/vue-fontawesome": "^3.0.2", - "@inkline/inkline": "^3.2.0", + "@fortawesome/fontawesome-svg-core": "^6.5.2", + "@fortawesome/free-regular-svg-icons": "^6.5.2", + "@fortawesome/free-solid-svg-icons": "^6.5.2", + "@fortawesome/vue-fontawesome": "^3.0.8", + "@inkline/inkline": "^3.2.2", "buffer": "^6.0.3", "events": "^3.3.0", - "mqtt": "^5.3.3", + "mqtt": "^5.8.0", "node-stdlib-browser": "^1.2.0", - "pinia": "^2.0.28", - "vue": "^3.4.19", - "vue-router": "^4.1.6" + "pinia": "^2.1.7", + "vue": "^3.4.31", + "vue-router": "^4.4.0" }, "devDependencies": { - "@rushstack/eslint-patch": "^1.1.4", - "@vitejs/plugin-vue": "^5.0.4", + "@rushstack/eslint-patch": "^1.10.3", + "@vitejs/plugin-vue": "^5.0.5", "@vue/eslint-config-prettier": "^9.0.0", - "@vue/test-utils": "^2.2.4", - "eslint": "^8.30.0", - "eslint-plugin-vue": "^9.3.0", - "jsdom": "^24.0.0", - "postcss": "^8.4.35", - "postcss-preset-env": "^9.0.0", - "prettier": "^3.1.1", + "@vue/test-utils": "^2.4.6", + "eslint": "^9.6.0", + "eslint-plugin-vue": "^9.27.0", + "jsdom": "^24.1.0", + "postcss": "^8.4.39", + "postcss-preset-env": "^9.6.0", + "prettier": "^3.3.2", "rollup-plugin-polyfill-node": "^0.13.0", - "sass": "^1.57.1", - "vite": "^5.0.12", - "vite-plugin-node-polyfills": "^0.21.0", - "vitest": "^1.0.4" + "sass": "^1.77.7", + "vite": "^5.3.3", + "vite-plugin-node-polyfills": "^0.22.0", + "vitest": "^2.0.2" } } diff --git a/packages/modules/display_themes/cards/source/public/icons/icon_Data.md b/packages/modules/display_themes/cards/source/public/icons/icon_Data.md new file mode 100644 index 0000000000..e0977a0983 --- /dev/null +++ b/packages/modules/display_themes/cards/source/public/icons/icon_Data.md @@ -0,0 +1,3 @@ + +Herkunft der Icons: +https://www.veryicon.com/ \ No newline at end of file diff --git a/packages/modules/display_themes/cards/source/public/icons/owbBattery.svg b/packages/modules/display_themes/cards/source/public/icons/owbBattery.svg new file mode 100644 index 0000000000..dd1fc748d3 --- /dev/null +++ b/packages/modules/display_themes/cards/source/public/icons/owbBattery.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/modules/display_themes/cards/source/public/icons/owbChargePoint.svg b/packages/modules/display_themes/cards/source/public/icons/owbChargePoint.svg new file mode 100644 index 0000000000..29cca3f93c --- /dev/null +++ b/packages/modules/display_themes/cards/source/public/icons/owbChargePoint.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/modules/display_themes/cards/source/public/icons/owbGrid.svg b/packages/modules/display_themes/cards/source/public/icons/owbGrid.svg new file mode 100644 index 0000000000..e098048efa --- /dev/null +++ b/packages/modules/display_themes/cards/source/public/icons/owbGrid.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/modules/display_themes/cards/source/public/icons/owbHouse.svg b/packages/modules/display_themes/cards/source/public/icons/owbHouse.svg new file mode 100644 index 0000000000..412dc8613b --- /dev/null +++ b/packages/modules/display_themes/cards/source/public/icons/owbHouse.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/modules/display_themes/cards/source/public/icons/owbPV.svg b/packages/modules/display_themes/cards/source/public/icons/owbPV.svg new file mode 100644 index 0000000000..becf88bc97 --- /dev/null +++ b/packages/modules/display_themes/cards/source/public/icons/owbPV.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/modules/display_themes/cards/source/public/icons/owbVehicle.svg b/packages/modules/display_themes/cards/source/public/icons/owbVehicle.svg new file mode 100644 index 0000000000..e721de12f4 --- /dev/null +++ b/packages/modules/display_themes/cards/source/public/icons/owbVehicle.svg @@ -0,0 +1,39 @@ + + + + + + diff --git a/packages/modules/display_themes/cards/source/src/App.vue b/packages/modules/display_themes/cards/source/src/App.vue index 6b8c375c93..92f135b11f 100644 --- a/packages/modules/display_themes/cards/source/src/App.vue +++ b/packages/modules/display_themes/cards/source/src/App.vue @@ -9,7 +9,7 @@ import LockNavItem from "@/components/LockNavItem.vue"; import { useMqttStore } from "@/stores/mqtt.js"; export default { - name: "openwbDisplayCardsApp", + name: "OpenwbDisplayCardsApp", components: { RouterView, DateTime, @@ -41,7 +41,6 @@ export default { "openWB/chargepoint/+/get/plug_state", "openWB/chargepoint/+/get/power", "openWB/chargepoint/+/get/rfid", - "openWB/chargepoint/+/set/change_ev_permitted", "openWB/chargepoint/+/set/current", "openWB/chargepoint/+/set/manual_lock", "openWB/chargepoint/+/set/log", @@ -76,6 +75,30 @@ export default { ); }, }, + created() { + this.createConnection(); + }, + mounted() { + // parse and add url parameters to store + let uri = window.location.search; + if (uri != "") { + console.debug("search", uri); + let params = new URLSearchParams(uri); + params.forEach((value, key) => { + this.mqttStore.updateSetting(key, parseInt(value)); + }); + } + // subscribe our topics + this.doSubscribe(this.mqttTopicsToSubscribe); + // timer for chart data + this.chartInterval = setInterval(this.mqttStore.updateChartData, 5000); + }, + beforeUnmount() { + // unsubscribe our topics + this.doUnsubscribe(this.mqttTopicsToSubscribe); + // clear timer for chart data + clearInterval(this.chartInterval); + }, methods: { /** * Establishes a connection to the configured broker @@ -105,7 +128,7 @@ export default { try { myPayload = JSON.parse(message.toString()); } catch (error) { - console.debug("Json parsing failed, fallback to string: ", topic); + console.debug("Json parsing failed, fallback to string: ", topic, error); myPayload = message.toString(); } this.mqttStore.addTopic(topic, myPayload); @@ -197,37 +220,16 @@ export default { }); }, }, - created() { - this.createConnection(); - }, - mounted() { - // parse and add url parameters to store - let uri = window.location.search; - if (uri != "") { - console.debug("search", uri); - let params = new URLSearchParams(uri); - params.forEach((value, key) => { - this.mqttStore.updateSetting(key, parseInt(value)); - }); - } - // subscribe our topics - this.doSubscribe(this.mqttTopicsToSubscribe); - // timer for chart data - this.chartInterval = setInterval(this.mqttStore.updateChartData, 5000); - }, - beforeUnmount() { - // unsubscribe our topics - this.doUnsubscribe(this.mqttTopicsToSubscribe); - // clear timer for chart data - clearInterval(this.chartInterval); - }, }; diff --git a/packages/modules/display_themes/cards/source/src/components/ChargePointCodeButton.vue b/packages/modules/display_themes/cards/source/src/components/ChargePointCodeButton.vue index a5c8743b94..77fb540fae 100644 --- a/packages/modules/display_themes/cards/source/src/components/ChargePointCodeButton.vue +++ b/packages/modules/display_themes/cards/source/src/components/ChargePointCodeButton.vue @@ -11,6 +11,10 @@ import CodeInputModal from "./CodeInputModal.vue"; export default { name: "ChargePointCodeButton", + components: { + FontAwesomeIcon, + CodeInputModal, + }, props: { chargePointId: { type: Number, required: true }, }, @@ -22,10 +26,6 @@ export default { code: "", }; }, - components: { - FontAwesomeIcon, - CodeInputModal, - }, computed: { tagState() { return this.mqttStore.getChargepointTagState(this.chargePointId); @@ -68,11 +68,11 @@ export default { diff --git a/packages/modules/display_themes/cards/source/src/components/ChargePointLockButton.vue b/packages/modules/display_themes/cards/source/src/components/ChargePointLockButton.vue index d9bedbf0c2..2d9fc3f86a 100644 --- a/packages/modules/display_themes/cards/source/src/components/ChargePointLockButton.vue +++ b/packages/modules/display_themes/cards/source/src/components/ChargePointLockButton.vue @@ -13,6 +13,7 @@ library.add(fasLock, fasLockOpen); export default { name: "ChargePointLockButton", + components: { FontAwesomeIcon }, props: { chargePointId: { required: true, type: Number }, changesLocked: { required: false, type: Boolean, default: false }, @@ -22,7 +23,6 @@ export default { mqttStore: useMqttStore(), }; }, - components: { FontAwesomeIcon }, computed: { locked() { return this.mqttStore.getChargePointManualLock(this.chargePointId); @@ -56,7 +56,11 @@ export default {